/*-------------------------------------------------------------------------------------------------------------------------- C ve Sistem Programcıları Derneği UNIX/Linux Sistem Programlama Kursunda Yapılan Örnekler ve Özet Notlar Eğitmen: Kaan ASLAN Bu notlar Kaan ASLAN tarafından oluşturulmuştur. Kaynak belirtmek koşulu ile her türlü alıntı yapılabilir. (Notları sabit genişlikli font kullanan programlama editörleri ile açınız.) (Editörünüzün "Line Wrapping" özelliğini pasif hale getiriniz.) Son Güncelleme: 22/11/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 1. Ders 22/10/2022 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Merhaba UNIX/Linux Programı ---------------------------------------------------------------------------------------------------------------------------*/ #include int main(void) { printf("Hello UNIX/Linux System Programming...\n"); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- 4. Ders 05/11/2022 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux dünyasında komut satırı argümanlarının oluşturulması için geniş bir kesim tarafından kullanılan geleneksel bir biçim vardır. Bu biçime "GNU biçimi" de denilmektedir. Biz de kursumuzda UNIX/Linux dünyasında yazacağımız programlarda bu geleneği kullanacağız. GNU stilinde komut satırı argümanları üçe ayrılmaktadır: 1) Argümansız seçenekler 2) Argümanlı seçenekler 3) Seçeneksiz argümanlar Argümansız seçenekler "-" karakterine yapışık tek bir harften oluşmaktadır. Harflerde büyük harf - küçük harf duyarlılığı (case sensitivity) dikkate alınmaktadır. Örneğin: $ ls -l -i /usr/include Burada -l ve -i argümansız seçeneklerdir. /usr/include argümanının bu seçeneklerle hiçbir ilgisi yoktur. Argümansız seçenekler tek bir karakterden oluşturulduğu için birleştirilebilmektedir. Örneğin: $ ls -li Buradaki -li aslında -l -i ile tamamen aynı anlamdadır. Genel olarak GNU stilinde seçenekler arasındaki sıranın bir önemi yoktur. Yani örneğin: $ ls -l -i ile $ ls -i -l arasında bir farklılık yoktur. Argümanlı seçeneklerde bir seçeneğin yanında o seçenekle ilişkili bir argüman da bulunur. Örneğin: $ gcc -o sample sample.c Burada -o seçeneği seçeneği tek başına kullanılmaz. Hedef dosyanın ismi seçeneğin argümanını oluşturmaktadır. O halde buradaki -o seçeneği tipik olarak argümanlı seçeneğe bir örnektir. Argüman seçeneklerin birleştirilmesi tavsiye edilmez. Ancak birleştirme yapılabilmektedir. Örneğin: $ gcc -co sample.o sample.c Bu yazım biçimini pek çok program kabul etse de biz tavsiye etmiyoruz. Buradaki argümanların aşağıdaki gibi belirtilmesi daha uygundur: $ gcc -c -o sample.o sample.c Programlar, argümanlı seçeneklerde seçeneğin argümanı hiç boşluk karakterleriyle ayrılmasa bile bunu kabul edebilmektedir. Örneğin: $ gcc -osample sample.c Burada -o argümanlı seçenek olduğu için onu başka bir seçenek izleyemeyeceğinden dolayı "sample" -o seçeneğinin argümanı olarak ele alınmaktadır. Seçeneklerle ilgisi olmayan argümanlara "seçeneksiz argüman" denilmektedir. Örneğin: $ gcc -o sample sample.c Burada "sample.c" argümanı herhangi bir seçenekle ilgili değildir. Örneğin: $ cp x.txt y.txt Buradaki "x.txt" ve "y.txt" argümanları da seçeneklerle ilgili değildir. Seçeneksiz argümanların sonda bulunması gerekmez. Örneğin: $ gcc sample.c -o sample ---------------------------------------------------------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------------------------------------------------------- Eskiden yalnızca tek karakterden oluşan kısa seçenekler kullanılıyordu. Ancak daha sonraları bu kısa seçeneklerin yetersiz kaldığı ve okunabilirliği bozduğu gerekçesiyle uzun seçenekler de kullanılmaya başlanmıştır. POSIX standartları uzun seçenekleri desteklememektedir. Ancak UNIX/Linux dünyasında yaygın biçimde kullanılmaktadır. Uzun seçenekler "--" öneki ile başlatılmaktadır. Örneğin: prog --count -a -b --length 100 Uzun seçenekler de argümanlı ve argümansız olabilmektedir. Yukarıdaki örnekte "--count" argümansız uzun seçenek, "-a" ve "-b" argümansız seçenekler ve "--length 100" ise argümanlı uzun seçenektir. Uzun seçeneklerde "isteğe bağlı argüman (optional argument)" denilen özel bir argüman da kullanılmaktadır. İsmi üzerinde "isteğe bağlı argüman" uzun seçeneklerin yanında verilip verilmemesi isteğe bağlı olan argümanlardır. Uzun seçeneklerin isteğe bağlı argümanları "=" sentaksı ile yapışık bir biçimde belirtilmektedir. Örneğin: prog --size=512 Burada --size uzun seçeneğinin argümanı isteğe bağlıdır. Yani bu uzun seçenek argümansız da aşağıdaki gibi kullanılabilirdi: prog --size Günümüzde genel olarak programlar kısa seçenekleri de uzun seçenekleri de bir arada kullanmaktadır. Programcılar bazı kısa seçeneklerin alternatif uzun seçeneklerini oluşturabilmektedir. Yukarıda da belirttiğimiz gibi POSIX standartları uzun seçenekleri desteklememektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux dünyasında kullanılan komut satırı argümanlarını parse etmek için getopt ve getopt_long isimli iki fonksiyon bulundurulmuştur. getopt fonksiyonu bir POSIX fonksiyonudur. Ancak bu fonksiyon uzun seçenekleri parse etmemektedir. getopt_long ise uzun seçenekleri de parse eden getopt fonksiyonunun daha gelişmiş bir biçimidir. Ancak getopt_long bir POSIX fonksiyonu değildir. Ancak libc kütüphanesinde bulunmaktadır. Bu fonksiyonlar Windows sistemlerinde hazır bir biçimde herhangi bir kütüphanede bulunmamaktadır. Zaten yukarıda da belirttiğimiz gibi Windows sistemlerindeki komut satırı argüman stili UNIX/Linux sistemlerindekinden farklıdır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- getopt fonksiyonunun prototipi şöyledir: #include int getopt(int argc, char * const argv[], const char *optstring); getopt fonksiyonunun ilk iki parametresi main fonksiyonunun argc ve argv parametreleri gibidir. Yani programcı main fonksiyonunun bu parametrelerini getopt fonksiyonuna geçirir. Fonksiyonun üçüncü parametresinde kısa seçenekler belirtilmektedir. Bu parametre bir yazı biçiminde girilir. Bu yazıdaki her bir karakter bir kısa seçeneği belirtir. Bir karakterin yanında ':' karakteri varsa bu ':' karakterinin solundaki seçeneğin argümanlı bir seçenek olduğunu belirtmektedir. Örneğin "ab:c" burada -a, -b ve -c seçenekleri belirtilmiştir. Ancak -b seçeneğinin bir argümanı da vardır. getopt fonksiyonu bir kez çağrılmaz. Bir döngü içerisinde çağrılmalıdır. Çünkü fonksiyon her çağrıldığında bir kısa seçeneği bulmaktadır. Fonksiyon bütün kısa seçenekleri bulduktan sonra artık bulacak bir seçenek kalmadığında -1 değerine geri dönmektedir. O halde fonksiyonun çağrılma kalıbı şöyle olmalıdır: int result; ... while ((result = getopt(argc, argv, "ab:c")) != -1) { ... } getopt, her kısa seçeneği bulduğunda o kısa seçeneğe ilişkin karakterle (yani o karakterin sayısal karşılığı ile) geri dönmektedir. O halde bizim getopt fonksiyonunun geri dönüş değerini switch içerisinde ele almamız gerekir: while ((result = getopt(argc, argv, "ab:c")) != -1) { switch (result) { case 'a': ... break; case 'b': ... break; case 'c': ... break; } } getopt fonksiyonu, olmayan (yani üçüncü parametresinde belirtilmeyen) bir kısa seçenekle karşılaştığında ya da argümanı olması gerektiği halde girilmemiş bir kısa seçenekle karşılaştığında '?' özel değerine geri dönmektedir. Programcının switch deyimine bu case bölümünü ekleyerek bu durumu da değerlendirmesi uygun olur. Örneğin: while ((result = getopt(argc, argv, "ab:c")) != -1) { switch (result) { case 'a': ... break; case 'b': ... break; case 'c': ... break; case '?': ... break; } } getopt fonksiyonunun kullandığı dört global değişken vardır. Bu global değişkenler kütüphanenin içerisinde tanımlanmıştır. Bunları biz extern bildirimi ile kullanabiliriz. Ancak bunların extern bildirimleri zaten dosyası içerisinde yapılmış durumdadır: extern int opterr; extern int optopt; extern int optind; extern char *optarg; Default durumda, getopt fonksiyonu geçersiz bir seçenekle (yani üçüncü parametresinde belirtilmeyen bir seçenekle) karşılaştığında stderr dosyasına (ekranda çıkacaktır) kendisi hata mesajını yazdırmaktadır. Programcılar genellikle bunu istemezler. getopt fonksiyonunun geçersiz seçenekler için hata mesajını yazdırması opterr değişkenine 0 değeri atanarak sağlanabilir. Yani opterr değişkeni sıfır dışı bir değerdeyse (default durum) fonksiyon mesajı stderr dosyasına kendisi de yazar, sıfır değerindeyse fonksiyon hata mesajını stderr dosyasına yazmaz. getopt fonksiyonu geçersiz bir seçenekle ya da argümanı girilmemiş argümanlı bir seçenekle karşılaştığında '?' geri dönmekle birlikte aynı zamanda optopt global değişkenine geçersiz seçeneğin karakter karşılığını yerleştirmektedir. Böylece programcı daha yeterli bir mesaj verebilmektedir. Örneğin: opterr = 0; while ((result = getopt(argc, argv, "ab:c")) != -1) { switch (result) { case 'a': printf("-a given...\n"); break; case 'b': printf("-b given...\n"); break; case 'c': printf("-c given...\n"); break; case '?': if (optopt == 'b') fprintf(stderr, "-b option given without argument!...\n"); else fprintf(stderr, "invalid option: -%c\n", optopt); break; } } Argümanlı bir kısa seçenek bulunduğunda getopt fonksiyonu, optarg global değişkenini o kısa seçeneğin argümanını gösterecek biçimde set eder. Ancak optarg, yeni bir argümanlı kısa seçenek bulunduğunda bu kez onun argümanını gösterecek biçimde set edilmektedir. Yani programcı argümanlı kısa seçeneği bulduğu anda optarg değişkenine başvurmalı gerekirse onu başka bir göstericide saklamalıdır. Pekiyi seçeneksiz argümanları nasıl edebiliriz? Seçeneksiz argümanlar argv dizisinin herhangi bir yerine bulunuyor olabilir. İşte getopt fonksiyonu her zaman seçeneksiz argümanları girildiği sırada argv dizisinin sonuna taşır ve onların başladığı indeksi de optind global değişkeninin göstermesini sağlar. O halde programcı getopt ile işini bitirdikten sonra (yani while döngüsünden çıktıktan sonra) optind indeksinden argc indeksine kadar ilerleyerek tüm seçeneksiz argümanları elde edebilmektedir. Örneğin: ./sample -a ali -b veli selami -c Burada "ali" ve "selami" seçeneksiz argümanlardır. getopt bu argv dizisini şu halde getirmektedir: ./sample -a -b veli -c ali selami Şimdi burada optind indeksi artık "ali" argümanının başladığı indeksi belirtecektir. Onun ötesindeki tüm argümanlar seçeneksiz argümanlardır. Bu argümanları while döngüsünün dışında şöyle yazdırabiliriz: for (int i = optind; i < argc; ++i) puts(argv[i]); Programcının girilmiş olan seçenekleri saklayıp programın ilerleyen aşamalarında bunları kullanması gerekebilmektedir. Bunun için şöyle bir kalıp önerilebilir: - Her seçenek için bir flag değişkeni tutulur. Bu flag değişkenlerine başlangıçta 0 atanır. - Her argümanlı seçenek için bir gösterici tutulur. - Her seçenekle karşılaşıldığında flag değişkenine 1 atanarak o seçeneğin kullanıldığı kaydedilir. - Argümanlı seçeneklerle karşılaşıldığında onların argümanları göstericilerde saklanır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- getopt fonksiyonun kullanımına ilişkin tipik bir kalıp aşağıda verilmiştir. Aşağıdaki örnekte -a, -b, -d argümansız seçenekler, -c ve -e ise argümanlı seçeneklerdir. Bu kalıbı kendi programlarınızda da kullanabilirsiniz. Bu örnekte ayrıştırma işleminde bir hata oluştuğunda programın devam etmemesini isteriz. Ancak tüm hataların rapor edilmesi de gerekmektedir. Bunun için bir flag değişkeninden faydalanılabilir. O flag değişkeni hata durumunda set edilir. Çıkışta kontrol edilip duruma göre program sonlandırılır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(int argc, char *argv[]) { int result; int a_flag, b_flag, c_flag, d_flag, e_flag, err_flag; char *c_arg, *e_arg; a_flag = b_flag = c_flag = d_flag = e_flag = err_flag = 0; opterr = 0; while ((result = getopt(argc, argv, "abc:de:")) != -1) { switch (result) { case 'a': a_flag = 1; break; case 'b': b_flag = 1; break; case 'c': c_flag = 1; c_arg = optarg; break; case 'd': d_flag = 1; break; case 'e': e_flag = 1; e_arg = optarg; break; case '?': if (optopt == 'c' || optopt == 'e') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (a_flag) printf("-a option given\n"); if (b_flag) printf("-b option given\n"); if (c_flag) printf("-c option given with argument \"%s\"\n", c_arg); if (d_flag) printf("-d option given\n"); if (e_flag) printf("-e option given with argument \"%s\"\n", e_arg); if (optind != argc) printf("Arguments without option:\n"); for (int i = optind; i < argc; ++i) puts(argv[i]); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- getopt fonksiyonun kullanımına bir örnek. Bu örnekte disp isimli program şu komut satırı argümanlarını almaktadır: -x (display hex) -o (display octal) -t (display text) -n (number of character per line) Burada -x, -o ve -t seçeneklerinden yalnızca bir tanesi kullanılabilmektedir. Eğer hiçbir seçenek kullanılmazsa default durum "-t" biçimindedir. -n seçeneği yalnızca hex ve octal görüntülemede kullanılabilmektedir. Bu seçenek de belirtilmezse sanki "-n 16" gibi bir belirleme yapıldığı varsayılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #define DEFAULT_LINE_CHAR 16 bool disp_text(FILE *f); bool disp_hex(FILE *f, int n_arg); bool disp_octal(FILE *f, int n_arg); int check_number(const char *str); int main(int argc, char *argv[]) { int result; int t_flag, o_flag, x_flag, n_flag, err_flag; int n_arg; FILE *f; t_flag = o_flag = x_flag = n_flag = err_flag = 0; n_arg = DEFAULT_LINE_CHAR; opterr = 0; while ((result = getopt(argc, argv, "toxn:")) != -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 '?': if (optopt == 'n') fprintf(stderr, "-%c option given without argument!...\n", optopt); else fprintf(stderr, "invalid option: -%c\n", optopt); err_flag = 1; break; } } if (err_flag) exit(EXIT_FAILURE); if (t_flag + o_flag + x_flag > 1) { fprintf(stderr, "only one of -[tox] option may be specified!...\n"); exit(EXIT_FAILURE); } if (t_flag + o_flag + x_flag == 0) t_flag = 1; if (t_flag && n_flag) { fprintf(stderr, "-n option cannot be used with -t option!...\n"); exit(EXIT_FAILURE); } if (argc - optind == 0) { fprintf(stderr, "file must be specified!...\n"); exit(EXIT_FAILURE); } if (argc - optind > 1) { fprintf(stderr, "too many files specified!...\n"); exit(EXIT_FAILURE); } 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) result = disp_text(f); else if (x_flag) result = disp_hex(f, n_arg); else if (o_flag) result = disp_octal(f, n_arg); if (!result) { fprintf(stderr, "cannot read file: %s\n", argv[optind]); exit(EXIT_FAILURE); } fclose(f); return 0; } bool disp_text(FILE *f) { int ch; while ((ch = fgetc(f)) != EOF) putchar(ch); return feof(f); } bool disp_hex(FILE *f, int n_arg) { 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); } putchar('\n'); return feof(f); } bool disp_octal(FILE *f, int n_arg) { 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'); } putchar('\n'); return feof(f); } int check_number(const char *str) { const char *temp; int result; while (isspace(*str)) ++str; temp = str; while (isdigit(*str)) ++str; if (*str != '\0') return -1; result = atoi(temp); if (!result) return -1; return result; } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte mycalc isimli bir program yazılmıştır. Program iki komut satırı argümanı ile aldığı değerler üzerinde dört işlem yapmaktadır. Aşağıdaki seçeneklere sahiptir: -a: Toplama işlemi -m: Çarpma işlemi -d: Bölme işlemi -s: Çıkartma işlemi -D msg: Çıktının başında "msg: " kısmını ekler ---------------------------------------------------------------------------------------------------------------------------*/ /* mycalc.c */ #include #include #include int main(int argc, char *argv[]) { int result; int a_flag, m_flag, M_flag, d_flag, s_flag, err_flag; char *M_arg; double arg1, arg2, calc_result; a_flag = m_flag = M_flag = d_flag = s_flag = err_flag = 0; opterr = 0; while ((result = getopt(argc, argv, "amM:ds")) != -1) { switch (result) { case 'a': a_flag = 1; break; case 'm': m_flag = 1; break; case 'M': M_flag = 1; M_arg = optarg; break; case 'd': d_flag = 1; break; case 's': s_flag = 1; break; case '?': if (optopt == 'M') fprintf(stderr, "-M option must have an argument!\n"); else fprintf(stderr, "invalid option: -%c\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (a_flag + m_flag + d_flag + s_flag > 1) { fprintf(stderr, "only one option must be specified!\n"); exit(EXIT_FAILURE); } if (a_flag + m_flag + d_flag + s_flag == 0) { fprintf(stderr, "at least one of -amds options must be specified\n"); exit(EXIT_FAILURE); } if (argc - optind != 2) { fprintf(stderr, "two number must be specified!\n"); exit(EXIT_FAILURE); } arg1 = atof(argv[optind]); arg2 = atof(argv[optind + 1]); if (a_flag) calc_result = arg1 + arg2; else if (m_flag) calc_result = arg1 * arg2; else if (d_flag) calc_result = arg1 / arg2; else calc_result = arg1 - arg2; if (M_flag) printf("%s: %f\n", M_arg, calc_result); else printf("%f\n", calc_result); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi komut satırında uzun seçenek kullanımı POSIX standartlarında yoktur. Ancak Linux gibi pek çok sistemdeki çeşitli yardımcı programlar uzun seçenekleri desteklemektedir. Programlarda bazı kısa seçeneklerin eşdeğer uzun seçenekleri bulunmaktadır. Bazı uzun seçeneklerin ise kısa seçenek eşdeğeri bulunmamaktadır. Bazı kısa seçeneklerin de uzun seçenek eşdeğerleri yoktur. Uzun seçenekleri parse etmek için getopt_long isimli fonksiyon kullanılmaktadır. Uzun seçenekler POSIX standartlarında olmadığına göre getopt_long fonksiyonu da bir POSIX fonksiyonu değildir. Ancak GNU'nun glibc kütüphanesinde bir eklenti biçiminde bulunmaktadır. getopt_long fonksiyonu işlevsel olarak getopt fonksiyonunu kapsamaktadır. Ancak fonksiyonun kullanımı biraz daha zordur. Fonksiyonun prototipi şöyledir: #include int getopt_long(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex); Fonksiyonun birinci ve ikinci parametrelerine, main fonksiyonundan alınan argc ve argv parametreleri geçirilir. Fonksiyonun üçüncü parametresi yine kısa seçeneklerin belirtildiği yazının adresini almaktadır. Yani fonksiyonun ilk üç parametresi tamamen getopt fonksiyonu ile aynıdır. Fonksiyonun dördüncü parametresi uzun seçeneklerin belirtildiği struct option türünden bir yapı dizisinin adresini almaktadır. Her uzun seçenek struct option türünden bir nesneyle ifade edilmektedir. struct option yapısı şöyle bildirilmiştir: struct option { const char *name; int has_arg; int *flag; int val; }; Fonksiyon bu yapı dizisinin bittiğini nasıl anlayacaktır? İşte yapı dizisinin son elemanına ilişkin yapı nesnesinin tüm elemanları 0'larla doldurulmalıdır. (0 sabitinin göstericiler söz konusu olduğunda NULL adres anlamına geldiğini de anımsayınız.) struct option yapısının name elemanı uzun seçeneğin ismini belirtmektedir. Yapının has_arg elemanı üç değerden birini alabilir: no_argument (0) required_argument (1) optional_argument (2) Bu eleman uzun seçeneğin argüman alıp almadığını belirtmektedir. Yapının flag ve val elemanları birbirleriyle ilişkilidir. Yapının val elemanı uzun seçenek bulunduğunda bunun hangi sayısal değerle ifade edileceğini belirtir. İşte bu flag elemanına int bir nesnenin adresi geçilirse bu durumda uzun seçenek bulunduğunda bu val değeri bu int nesneye yerleştirilir. getopt_long ise bu durumda 0 değeri ile geri döner. Ancak bu flag göstericisine NULL adres de geçilebilir. Bu durumda getopt_long uzun seçenek bulunduğunda val elemanındaki değeri geri dönüş değeri olarak verir. Örneğin: struct option options[] = { {"count", required_argument, NULL, 'c'}, {0, 0, 0, 0} }; Burada uzun seçenek "--count" biçimindedir. Bir argümanla kullanılmak zorundadır. Bu uzun seçenek bulunduğunda flag parametresi NULL adres geçildiği için getopt_long fonksiyonu 'c' değeri ile geri dönecektir. Örneğin: int count_flag; ... struct option options[] = { {"count", required_argument, &count_flag, 1}, {0, 0, 0, 0} }; Burada artık uzun seçenek bulunduğunda getopt_long fonksiyonu 0 ile geri dönecek ancak 1 değeri count_flag nesnesine yerleştirilecektir. getopt_long fonksiyonunun son parametresi uzun seçenek bulunduğunda o uzun seçeneğin option dizisindeki kaçıncı indeksli uzun seçenek olduğunu anlamak için kullanılmaktadır. Burada belirtilen adresteki nesneye uzun seçeneğin option dizisi içerisindeki indeks numarası yerleştirilmektedir. Ancak bu bilgiye genellikle gereksinim duyulmamaktadır. Bu parametre NULL geçilebilir. Bu durumda böyle bir yerleştirme yapılmaz. Bu durumda getopt_long fonksiyonunun geri dönüş değeri beş biçimden biri olabilir: 1) Fonksiyon bir kısa seçenek bulmuştur. Kısa seçeneğin karakter koduyla geri döner. 2) Fonksiyon bir uzun seçenek bulmuştur ve option yapısının flag elemanında NULL adres vardır. Bu durumda fonksiyon option yapısının val elemanındaki değerle geri döner. 3) Fonksiyon bir uzun seçenek bulmuştur ve option yapısının flag elemanında NULL adres yoktur. Bu durumda fonksiyon val değerini bu adrese yerleştirir ve 0 değeri ile geri döner. 4) Fonksiyon geçersiz (yani olmayan) bir kısa ya da uzun seçenekle karşılaşmıştır ya da argümanlı bir kısa seçenek ya da uzun seçeneğin argümanı girilmemiştir. Bu durumda fonksiyon '?' karakterinin değeriyle geri döner. 5) Parse edecek argüman kalmamıştır fonksiyon -1 ile geri döner. getopt fonksiyonundaki yardımcı global değişkenlerin aynısı burada da kullanılmaktadır: opterr: Hata mesajının fonksiyon tarafından stderr dosyasına basılıp basılmayacağını belirtir. optarg: Argümanlı bir kısa ya da uzun seçenekte argümanı belirtmektedir. Eğer "isteğe bağlı argümanlı" bir uzun seçenek bulunmuşsa ve bu uzun seçenek için argüman girilmemişse optarg nesnesine NULL adres yerleştirilmektedir. optind: Bu değişken yine seçeneksiz argümanların başladığı indeksi belirtmektedir. optopt: Bu değişken geçersiz bir uzun ya da kısa seçenek girildiğinde hatanın nedenini belirtmektedir. getopt_long geçersiz bir seçenekle karşılaştığında '?' geri dönmekle birlikte optopt değişkenini şu biçimlerde set etmektedir: 1) Eğer fonksiyon argümanlı bir kısa seçenek bulduğu halde argüman girilmemişse o argümanlı kısa seçeneğin karakter karşılığını optopt değişkenine yerleştirir. 2) Eğer fonksiyon argümanlı bir uzun seçenek bulduğu halde argüman girilmemişse o argümanlı uzun seçeneğin option yapısındaki val değerini optopt değişkenine yerleştirmektedir. 3) Eğer fonksiyon geçersiz bir kısa seçenekle karşılaşmışsa bu durumda optopt geçersiz kısa seçeneğin karakter karşılığına geri döner. 4) Eğer fonksiyon geçersiz bir uzun seçenekle karşılaşmışsa bu durumda optopt değişkenine 0 değeri yerleştirilmektedir. Maalesef getopt_long olmayan bir uzun seçenek girildiğinde bunu bize vermemektedir. Ancak GNU'nun getopt_long gerçekleştirimine bakıldığında bu geçersiz uzun seçeneğin argv dizisinin "optind - 1" indeksinde olduğu görülmektedir. Yani bu geçersiz uzun seçeneğe argv[optind - 1] ifadesi ile erişilebilmektedir. Ancak bu durum glibc dokümanlarında belirtilmemiştir. Bu nedenle bu özelliğin kullanılması uygun değildir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 5. Ders 06/11/2022 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekteki komut satırı argümanları şunlardır: -a -b -c ya da --count --verbose ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(int argc, char *argv[]) { int a_flag, b_flag, c_flag, verbose_flag; int err_flag; char *c_arg; int result; struct option options[] = { {"count", required_argument, NULL, 'c'}, {"verbose", no_argument, &verbose_flag, 1}, {0, 0, 0, 0} }; a_flag = b_flag = c_flag = verbose_flag = err_flag = 0; opterr = 0; while ((result = getopt_long(argc, argv, "abc:", options, NULL)) != -1) { switch (result) { case 'a': a_flag = 1; break; case 'b': b_flag = 1; break; case 'c': c_flag = 1; c_arg = optarg; break; case '?': if (optopt == 'c') fprintf(stderr, "option -c or --count without argument!...\n"); else if (optopt != 0) fprintf(stderr, "invalid option: -%c\n", optopt); else fprintf(stderr, "invalid long option!...\n"); /* fprintf(stderr, "invalid long option: %s\n", argv[optind - 1]); */ err_flag = 1; break; } } if (err_flag) exit(EXIT_FAILURE); if (a_flag) printf("-a option given\n"); if (b_flag) printf("-b option given\n"); if (c_flag) printf("-c or --count option given with argument \"%s\"\n", c_arg); if (verbose_flag) printf("--verbose given\n"); if (optind != argc) { printf("Arguments without options"); for (int i = optind; i < argc; ++i) printf("%s\n", argv[i]); } return 0; } /*-------------------------------------------------------------------------------------------------------------------------- getopt_long fonksiyonun kullanımına diğer bir örnekte aşağıda verilmiştir. Aşağıda programın komut satırı argümanları şunlardır: -a -b -c -h ya da --help --count --line[=] ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(int argc, char *argv[]) { int result; int a_flag, b_flag, c_flag, h_flag, count_flag, line_flag; char *b_arg, *count_arg, *line_arg; int err_flag; int i; struct option options[] = { {"help", no_argument, &h_flag, 1}, {"count", required_argument, NULL, 2}, {"line", optional_argument, NULL, 3}, {0, 0, 0, 0 }, }; a_flag = b_flag = c_flag = h_flag = count_flag = line_flag = 0; err_flag = 0; opterr = 0; while ((result = getopt_long(argc, argv, "ab:ch", options, NULL)) != -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 'h': h_flag = 1; break; case 2: /* --count */ count_flag = 1; count_arg = optarg; break; case 3: /* --line */ line_flag = 1; line_arg = optarg; break; case '?': if (optopt == 'b') fprintf(stderr, "-b option must have an argument!...\n"); else if (optopt == 2) fprintf(stderr, "argument must be specified with --count option\n"); else if (optopt != 0) fprintf(stderr, "invalid option: -%c\n", optopt); else fprintf(stderr, "invalid long option!...\n"); err_flag = 1; break; } } if (err_flag) exit(EXIT_FAILURE); if (a_flag) printf("-a option given...\n"); if (b_flag) printf("-b option given with argument \"%s\"...\n", b_arg); if (c_flag) printf("-c option given...\n"); if (h_flag) printf("-h or --help option given...\n"); if (count_flag) printf("--count option specified with \"%s\"...\n", count_arg); if (line_flag) { if (line_arg != NULL) printf("--line option given with optional argument \"%s\"\n", line_arg); else printf("--line option given without optional argument...\n"); } if (optind != argc) { printf("Arguments without options:\n"); for (i = optind; i < argc; ++i) printf("%s\n", argv[i]); } return 0; } /*-------------------------------------------------------------------------------------------------------------------------- getopt_long fonksiyonun kullanılmasına başka bir örnek. Bu örnekteki seçenekler şöyledir: -a: argümansız kısa seçenek -b: argümanlı kısa seçenek --all: argümansız uzun seçenek --length: argümanlı uzun seçenek --number: isteğe bağlı argümanlı uzun seçenek ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(int argc, char *argv[]) { int result; struct option options[] = { {"all", no_argument, NULL, 1}, {"length", required_argument, NULL, 2}, {"number", optional_argument, NULL, 3}, {0, 0, 0, 0}, }; int a_flag, b_flag, all_flag, length_flag, number_flag, err_flag; char *b_arg, *length_arg, *number_arg; a_flag = b_flag = all_flag = length_flag = number_flag = err_flag = 0; opterr = 0; while ((result = getopt_long(argc, argv, "ab:", options, NULL)) != -1) { switch (result) { case 'a': a_flag = 1; break; case 'b': b_flag = 1; b_arg = optarg; break; case 1: all_flag = 1; break; case 2: length_flag = 1; length_arg = optarg; break; case 3: number_flag = 1; number_arg = optarg; break; case '?': if (optopt == 'b') fprintf(stderr, "-b option without argument!\n"); else if (optopt == 2) fprintf(stderr, "--length option without argument!\n"); else if (optopt != 0) fprintf(stderr, "invalid option: -%c\n", optopt); else fprintf(stderr, "invalid long option!\n"); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (a_flag) printf("-a option given\n"); if (b_flag) printf("-b option given with argument \"%s\"\n", b_arg); if (all_flag) printf("--all option given\n"); if (length_flag) printf("--length option given with argument \"%s\"\n", length_arg); if (number_flag) if (number_arg != NULL) printf("--number option given with argument \"%s\"\n", number_arg); else printf("--number option given without argument\n"); if (optind != argc) printf("Arguments without options:\n"); for (int i = optind; i < argc; ++i) puts(argv[i]); return 0; } /* test girişi: ./sample --all --length 100 --number=300 -a ali veli selami Çıktısı şöyledir: -a option given --all option given --length option given with argument "100" --number option given with argument "300" Arguments without options: ali veli selami */ /*-------------------------------------------------------------------------------------------------------------------------- getopt_long fonksiyonunda struct option yapısındaki flag elemanına NULL adres yerine int bir nesnenin adresi geçirilirse bu durumda getopt_long bu uzun seçenek girildiğinde doğrudan yapının val elemanındaki değeri bu nesneye yerleştirir ve 0 ile geri döner. Böylece programcı isterse argümansız uzun seçenekleri switch içerisinde işlemeden doğrudan onun bayrağına set işlemi yapabilir. Ayrıca programlarda kısa seçeneklerin uzun seçenek eşdeğerleri de bulunabilmektedir. Bunu sağlamanın en kolay yolu uzun seçeneğe ilişkin struct option yapısındaki val elemanına kısa seçeneğe ilişkin karakter kodunu girmektir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(int argc, char *argv[]) { int result; int a_flag, b_flag, all_flag, length_flag, number_flag, err_flag; char *b_arg, *length_arg, *number_arg; struct option options[] = { {"all", no_argument, &all_flag, 1}, {"length", required_argument, NULL, 'l'}, {"number", optional_argument, NULL, 3}, {0, 0, 0, 0}, }; a_flag = b_flag = all_flag = length_flag = number_flag = err_flag = 0; opterr = 0; while ((result = getopt_long(argc, argv, "ab:l:", options, NULL)) != -1) { switch (result) { case 'b': b_flag = 1; b_arg = optarg; break; case 1: all_flag = 1; break; case 'l': length_flag = 1; length_arg = optarg; break; case 3: number_flag = 1; number_arg = optarg; break; case '?': if (optopt == 'b') fprintf(stderr, "-b option without argument!\n"); else if (optopt == 2) fprintf(stderr, "--length option without argument!\n"); else if (optopt != 0) fprintf(stderr, "invalid option: -%c\n", optopt); else fprintf(stderr, "invalid long option!\n"); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (a_flag) printf("-a option given\n"); if (b_flag) printf("-b option given with argument \"%s\"\n", b_arg); if (all_flag) printf("--all option given\n"); if (length_flag) printf("--length option given with argument \"%s\"\n", length_arg); if (number_flag) if (number_arg != NULL) printf("--number option given with argument \"%s\"\n", number_arg); else printf("--number option given without argument\n"); if (optind != argc) printf("Arguments without options:\n"); for (int i = optind; i < argc; ++i) puts(argv[i]); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- 6. Ders 12/11/2022 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir kullanıcı ile login olunduğunda login programı /etc/passwd dosyasında belirtilen programı çalıştırır. Biz istersek bu programı değiştirip kendi istediğimiz bir programın çalıştırılmasını sağlayabiliriz. Kendi programımız myshell isimli program olsun ve onu /bin dizinine kopyalamış olalım. /etc/passwd dosyasının içeriğini şöyle değiştirebiliriz: ali:x:1002:1001::/home/ali:/bin/myshell ---------------------------------------------------------------------------------------------------------------------------*/ /* myshell.c */ #include #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 typedef struct tagCMD { char *name; void (*proc)(void); } CMD; void parse_cmd_line(char *cmdline); void dir_proc(void); void clear_proc(void); void pwd_proc(void); char *g_params[MAX_CMD_PARAMS]; int g_nparams; CMD g_cmds[] = { {"dir", dir_proc}, {"clear", clear_proc}, {"pwd", pwd_proc}, {NULL, NULL} }; int main(void) { char cmdline[MAX_CMD_LINE]; char *str; int i; for (;;) { printf("CSD>"); if (fgets(cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(cmdline); if (g_nparams == 0) continue; if (!strcmp(g_params[0], "exit")) break; for (i = 0; g_cmds[i].name != NULL; ++i) if (!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if (g_cmds[i].name == NULL) printf("bad command: %s\n", g_params[0]); } return 0; } void parse_cmd_line(char *cmdline) { char *str; g_nparams = 0; for (str = strtok(cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) g_params[g_nparams++] = str; } void dir_proc(void) { printf("dir command executing...\n"); } void clear_proc(void) { system("clear"); } void pwd_proc(void) { char cwd[4096]; if (g_nparams > 1) { printf("pwd command must be used withoud argument!...\n"); return; } getcwd(cwd, 4096); printf("%s\n", cwd); } /*-------------------------------------------------------------------------------------------------------------------------- 7. Ders 13/11/2022 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir hata değerinin yazısını elde etmek için strerror fonksiyonu kullanılabilir. Fonksiyon bizden EXXX biçimindeki hata kodunu parametre olarak alır, bize statik düzeyde tahsis edilmiş hata yazısının adresini verir. Biz de POSIX fonksiyonu başarısız olduğunda errno değerini bu biçimde yazıya dönüştürüp rapor edebiliriz. Aşağıda buna bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include int main(void) { int fd; if ((fd = open("xxx.txt", O_RDONLY)) == -1) { fprintf(stderr, "open failed: %s\n", strerror(errno)); exit(EXIT_FAILURE); } printf("success\n"); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- strerror fonksiyonu ile alınan error yazısı default durumda İngilizce'dir. POSIX standartlarına göre bu yazının içeriği locale'in LC_MESSAGES kategorisine göre ayarlanmaktadır. Dolayısıyla eğer mesajları Türkçe bastırmak istiyorsanız LC_MESSAGES kategorisine ilişkin locale'i setlocale fonksiyonu ile değiştirmelisiniz. Tabii genel olarak tüm kategorilerin değiştirilmesi yoluna gidilmektedir. Türkçe UNICODE UTF-8 locale'i "tr_TR.UTF-8" ile temsil edilmektedir. Dolayısıyla bu işlemi şöyle yapabilirsiniz: if (setlocale(LC_ALL, "tr_TR.UTF-8") == NULL) { fprintf(stderr, "cannot set locale!...\n"); exit(EXIT_FAILURE); } ---------------------------------------------------------------------------------------------------------------------------*/ #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); } puts(strerror(EPERM)); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- POSIX fonksiyonlarında oluşan hatayı rapor etmek için perror isimli daha pratik kullanımı olan bir POSIX fonksiyonu (aynı zamanda standart C fonksiyonudur) bulundurulmuştur. Fonksiyonun prototipi şöyledir: #include void perror(const char *str); Fonksiyon argüman olarak girilen yazıyı stderr dosyasına yazdırır. Sonra hemen yanına ':' karakterini ve bir SPACE karakterini basar ve sonra da o andaki errno değerinin yazısını yazdırır. İmleci aşağı satırın başına geçirir. Fonksiyon aşağıdaki gibi yazılabilir: void perror(const char *str) { fprintf(stderr, "%s: %s\n", str, strerror(errno)); } ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(void) { int fd; if ((fd = open("xxx.txt", O_RDONLY)) == -1) { perror("open"); exit(EXIT_FAILURE); } printf("success\n"); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Biz kursumuzda, bir POSIX fonksiyonu başarısız olduğunda genellikle (ancak her zaman değil) programımızı sonlandıracağız. Bu durumda daha az tuşa basmak için bir exit_sys isimli "sarma (wrapper)" fonksiyondan faydalanacağız. Bu fonksiyon önce perror fonksiyonu ile hatayı stderr dosyasına yazdıracak sonra da exit fonksiyonu ile program sonlandıracaktır: void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("xxx.txt", O_RDONLY)) == -1) exit_sys("open"); printf("success\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bazı programcılar yukarıdaki exit_sys fonksiyonunu printf fonksiyonuna benzetmektedir. (Örneğin Stevens "Advanced Programming in the UNIX Environment)" kitabında böyle bir sarma fonksiyon kullanmıştır. Böyle bir sarma fonksiyona örnek şu olabilir: void exit_sys(const char *format, ...) { va_list ap; va_start(ap, format); vfprintf(stderr, format, ap); fprintf(stderr, ": %s\n", strerror(errno)); va_end(ap); exit(EXIT_FAILURE); } ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void exit_sys(const char *format, ...); int main(void) { int fd; char path[] = "xxx.txt"; if ((fd = open(path, O_RDONLY)) == -1) exit_sys("open (%s)", path); printf("success\n"); return 0; } void exit_sys(const char *format, ...) { va_list ap; va_start(ap, format); vfprintf(stderr, format, ap); fprintf(stderr, ": %s\n", strerror(errno)); va_end(ap); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- C standartlarında errno değeri çok kısıtlı bir biçimde kullanılmıştır. Yani C standartlarında pek az fonksiyon errno değişkenini set etmektedir. Ancak standartlar çeşitli standart C fonksiyonlarının errno değişkenini derleyiciye bağlı olarak set edebileceğini belirtmektedir. POSIX standartlarına göre her standart C fonksiyonu aynı zamanda bir POSIX fonksiyonu olarak ele alınmaktadır. Standart C fonksiyonları aynı zamanda errno değişkenini de set etmektedir. Örneğin biz fopen fonksiyonu ile bir dosyayı açmak istesek fopen başarısız olduğunda UNIX/Linux sistemleri errno değerini uygun biçimde set edebilmektedir. Böylece biz standart C fonksiyonlarındaki hata mesajlarını da aşağıdaki gibi yazdırabilmekteyiz: if ((f = fopen("test.dat", "r")) == NULL) exit_sys("fopen"); Ya da örneğin: if ((p = malloc(SIZE)) == NULL) exit_sys("malloc"); Her ne kadar standart C fonksiyonları UNIX/Linux sistemlerinde errno değişkenini set ediyorsa da biz standart C uyumunu korumak için kursumuzda standart C fonksiyonlarında set edilen errno değişkenini kullanmayacağız. Örneğin: if ((f = fopen("test.dat", "r")) == NULL) { fprintf(stderr, "cannot open file!...\n"); exit(EXIT_FAILURE); } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında errno değişkeni Linux'ta çekirdek tarafından set edilen bir değişken değildir. errno değişkeni tamamen user mode'daki POSIX kütüphanesi tarafından set edilmektedir. Tipik olarak Linux çekirdeğinde bir sistem fonksiyonu başarısız olduğunda negatif errno değerine geri dönmektedir. Sistem fonksiyonunu çağıran POSIX fonksiyonu bu negatif errno değerini pozitife dönüştürerek errno değişkenini set etmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde her dosyanın bir kullanıcı id'si (user id) ve grup id'si (group id) bulunmaktadır. Bu sistemlerde tüm dosyalar "open" isimli bir POSIX fonksiyonu tarafından yaratılmaktadır. Bir dosyanın kullanıcı id'si onu yaratan prosesin etkin kullanıcı id'si olarak set edilmektedir. Dosyanın grup id'si ise iki seçenekten biri olarak set edilebilmektedir. Bazı sistemler dosyanın grup id'sini onu yaratan prosesin etkin grup id'si olarak set etmektedir. Bu biçim klasik AT&T UNIX sistemlerinin uyguladığı biçimdir. Linux böyle davranmaktadır. İkinci seçenek BSD sistemlerinde olduğu gibi dosyanın grup id'sinin onun içinde bulunduğu dizinin grup id'si olarak set edilmesidir. POSIX standartları her iki durumu da geçerli kabul etmektedir. Linux sistemlerinde "mount parametreleriyle" BSD tarzı davranış istenirse oluşturulabilmektedir. Aynı zamanda Linux sistemlerinde "dosyanın içinde bulunduğu dizinde set group id" bayrağı set edilerek de aynı etki oluşturulabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 8. Ders 20/11/2022 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dosya üzerinde işlem yapmak isteyen proses erişme biçimini de (okumak için mi, yazmak için mi, hem okuyup hem yazmak için mi, yoksa dosyadaki kodu çalıştırmak için mi) belirtmektedir. Bu durumda işletim sistemi sırasıyla şu kontrolleri yapmaktadır (bu işlemler else-if biçiminde sıralanmıştır): 1) Eğer işlem yapmak isteyen prosesin etkin kullanıcı id'si (etkin grup id'sinin burada önemi yoktur) 0 ise işlem yapmak isteyen proses yetkili kullanıcının bir prosesidir. Bu tür proseslere "root prosesler" ya da "super user prosesler" ya da "öncelikli (priviledged) prosesler" denilmektedir. Bu durumda işletim sistemi yapılmak istenen işlem ne olursa olsun bu işleme onay verir. 2) Eğer işlem yapmak isteyen prosesin etkin kullanıcı id'si (effective user id) dosyanın kullanıcı id'si ile aynıysa bu durumda "dosyanın sahibinin dosya üzerinde işlem yaptığı gibi mantıksal bir çıkarım" yapılmaktadır. Yapılmak istenen işlem ile dosyanın sahiplik (owner) erişim bilgileri karşılaştırılır. Eğer bu erişim bilgileri işlemi destekliyorsa işleme onay verilir. Değilse işlem başarısızlıkla sonuçlanır. 3) Eğer işlem yapmak isteyen prosesin etkin grup id'si (effective group id) ya da "ek grup (supplemantary groups)" id'lerinden biri dosyanın grup id'si ile aynıysa bu durumda "dosya ile aynı grupta bulunan bir kullanıcının dosya üzerinde işlem yaptığı gibi mantıksal bir çıkarım" yapılmaktadır. Yapılmak istenen işlem ile dosyanın grupluk (group) erişim bilgileri karşılaştırılır. Eğer bu erişim bilgileri işlemi destekliyorsa işleme onay verilir. Değilse işlem başarısızlıkla sonuçlanır. 4) İşlem yapmak isteyen proses herhangi bir proses ise bu durumda yapılmak istenen işlem ile dosyanın "diğer (other)" erişim bilgileri karşılaştırılır. Eğer bu erişim bilgileri işlemi destekliyorsa işleme onay verilir. Değilse işlem başarısızlıkla sonuçlanır. Örneğin aşağıdaki gibi bir dosya söz konusu olsun: -rw-r--r-- 1 kaan study 20 Kas 13 13:54 test.txt Dosyaya erişim yapmak isteyen proses, "okuma ve yazma amaçlı" erişim yapmak istesin. Eğer prosesin etkin kullanıcı id'si 0 ise bu işlem onaylanacaktır. Eğer prosesin etkin kullanıcı id'si "kaan" ise bu işlem yine onaylanacaktır. Ancak prosesin etkin grup id'si ya da ek grup id'lerinden biri study ise işlem onaylanmayacaktır. Çünkü erişim hakları gruptaki üyelere yalnızca okuma izni vermektedir. Benzer biçimde prosesin etkin kullanıcı id'si ya da etkin grup id'si (ve ek grup id'leri) burada belirtilenlerin dışında ise yine prosese bu işlem için onay verilmeyecektir. Yukarıdaki maddeler else-if biçiminde düşünülmelidir. Örneğin dosya aşağıdaki gibi olsun: -r--rw-r-- 1 kaan study 20 Kas 13 13:54 test.txt Burada dosyanın sahibi (yani etkin kullanıcı id'si dosyanın kullanıcı id'si ile aynı olan proses) dosya üzerinde yazma yapamayacaktır. Ancak aynı grupta olan prosesler bunu yapabilecektir. Tabii bu biçimdeki erişim hakları mantıksal olarak tuhaf ve anlamsızdır. Yani dosyanın sahibine verilmeyen bir hakkın gruba ya da diğerlerine verilmesi normal bir durum değildir. Çalıştırılabilir bir dosya 'x' hakkı ile temsil edilmiştir. Bu durumda biz bir program dosyasının başkaları tarafından çalıştırılması engelleyebiliriz. Örneğin: -rwxr--r-- 1 kaan study 16816 Kas 13 13:49 sample Burada dosyanın sahibi (ve tabii root kullanıcısı) bu dosyayı çalıştırabilir. Ancak diğer kullanıcılar bu dosyayı çalıştıramazlar. Örneğin: -rw-r--r-- 1 kaan study 16816 Kas 13 13:49 sample Burada artık root kullanıcısı da dosyayı çalıştıramaz. root kullanıcısının dosyayı çalıştırabilmesi için sahiplik, grupluk ya da diğer erişim bilgilerinin en az birinde 'x' hakkının belirtilmiş olması gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- POSIX standartlarında erişim mekanizması üzerinde açıklamalar yapılırken "root önceliği" ya da "prosesin etkin kullanıcı id'sinin 0 olması" gibi bir anlatım uygulanmamıştır. Onun yerine POSIX standartlarında "appropriate privileges" terimi kullanılmıştır. Çünkü bir POSIX sistemi "ya hep ya hiç" biçiminde tasarlanmak zorunda değildir. Gerçekten de örneğin Linux sistemlerinde "capability" denilen bir özellik bulunmaktadır. Bu "capability" sayesinde bir prosesin etkin kullanıcı id'si 0 olmamasına karşın o proses belirlenen bazı şeyleri yapabilir duruma getirilebilmektedir. İşte POSIX standartlarındaki "appropriate privileges" terimi bunu anlatmaktadır. Yani buradaki "appropriate privileges" terimi "prosesin etkin kullanıcı id'si 0 ya da 0 olmasa da prosesin bu işlemi yapabilme yeteneğinin" olduğunu belirtmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Prosesin çalışma dizini getcwd isimli POSIX fonksiyonuyla elde edilebilmektedir. Fonksiyonun prototipi şöyledir: #include char *getcwd(char *buf, size_t size); Fonksiyonun birinci parametresi yol ifadesinin yerleştirileceği dizinin adresini, ikinci parametresi ise bu dizinin null karakter dahil olmak üzere uzunluğunu almaktadır. Fonksiyon başarı durumunda birinci parametresiyle belirtilen adresin aynısına, başarısızlık durumunda NULL adrese geri dönmektedir. Fonksiyonun ikinci parametresinde belirtilen uzunluk eğer yol ifadesini ve null karakteri içerecek büyüklükte değilse fonksiyon başarısız olmaktadır. UNIX/Linux sistemlerinde bir yol ifadesinin maksimum karakter sayısı (null karakter dahil olmak üzere) içerisindeki PATH_MAX sembolik sabitiyle belirtilmiştir. Ancak bu konuda bazı ayrıntılar vardır. Bazı sistemlerde bu PATH_MAX sembolik sabiti tanımlı değildir. Dolayısıyla bazı sistemlerde maksimum yol ifadesi uzunluğu pathconf denilen özel bir fonksiyon ile elde edilebilmektedir. Linux sistemlerinde dosyası içerisinde PATH_MAX 4096 olarak define edilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { char buf[PATH_MAX]; if (getcwd(buf, PATH_MAX) == NULL) exit_sys("getcwd"); puts(buf); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Prosesin çalışma dizinini chdir isimli POSIX fonksiyonuyla değiştirebiliriz. Fonksiyonun prototipi şöyledir: #include int chdir(const char *path); Fonksiyon yeni çalışma dizinin yol ifadesini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { char buf[PATH_MAX]; if (getcwd(buf, PATH_MAX) == NULL) exit_sys("getcwd"); puts(buf); if (chdir("/usr/bin") == -1) exit_sys("chdir"); if (getcwd(buf, PATH_MAX) == NULL) exit_sys("getcwd"); puts(buf); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Daha önce yazmış olduğumuz kabuk programına cd komutunu aşağıdaki gibi ekleyebiliriz. Bu örnekteki getenv fonksiyonunu henüz görmedik. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 typedef struct tagCMD { char *name; void (*proc)(void); } CMD; void parse_cmd_line(char *cmdline); void dir_proc(void); void clear_proc(void); void pwd_proc(void); void cd_proc(void); void exit_sys(const char *msg); char *g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[PATH_MAX]; CMD g_cmds[] = { {"dir", dir_proc}, {"clear", clear_proc}, {"pwd", pwd_proc}, {"cd", cd_proc}, {NULL, NULL} }; int main(void) { char cmdline[MAX_CMD_LINE]; char *str; int i; if (getcwd(g_cwd, PATH_MAX) == NULL) exit_sys("fatal error (getcwd)"); for (;;) { printf("CSD:%s>", g_cwd); if (fgets(cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(cmdline); if (g_nparams == 0) continue; if (!strcmp(g_params[0], "exit")) break; for (i = 0; g_cmds[i].name != NULL; ++i) if (!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if (g_cmds[i].name == NULL) printf("bad command: %s\n", g_params[0]); } return 0; } void parse_cmd_line(char *cmdline) { char *str; g_nparams = 0; for (str = strtok(cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) g_params[g_nparams++] = str; } void dir_proc(void) { printf("dir command executing...\n"); } void clear_proc(void) { system("clear"); } void pwd_proc(void) { printf("%s\n", g_cwd); } void cd_proc(void) { char *dir; if (g_nparams > 2) { printf("too many arguments!\n"); return; } if (g_nparams == 1) { if ((dir = getenv("HOME")) == NULL) exit_sys("fatal error (getenv"); } else dir = g_params[1]; if (chdir(dir) == -1) { printf("%s\n", strerror(errno)); return; } if (getcwd(g_cwd, PATH_MAX) == NULL) exit_sys("fatal error (getcwd)"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 9. Ders 20/11/2022 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dizinler de işletim sistemi tarafından birer dosyaymış gibi ele alınmaktadır. Gerçekten de dizinleri sanki "içerisinde dosya bilgilerini tutan dosyalar" gibi düşünebiliriz. Dolayısıyla UNIX/Linux sistemlerinde bir dosyayı silmek için, bir dosya yaratmak için, bir dosyanın ismini değiştirmek için prosesin o dizine "w" hakkının olması gerekir. Yukarıda belirttiğimiz üç işlem de aslında dizine yazma yapma anlamına gelmektedir. Yani bizim bir dosyayı silebilmek için dosyaya "w" hakkına sahip olmamız gerekmez, dosyanın içinde bulunduğu dizine "w" hakkına sahip olmamız gerekir. Bir dizin için "r" hakkı demek o dizinin içeriğinin okunabilmesi hakkı demektir. Yani bizim bir dizinin içeriğini elde edebilmemiz (ya da ls gibi bir komutla görüntüleyebilmemiz) için o dizine "r" hakkına sahip olmamız gerekir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dizinlerde "x" hakları farklı bir anlama gelmektedir. İşletim sistemi, bir yol ifadesi verildiğinde yol ifadesinde hedeflenen dizin girişi için bilgileri elde etmek isteyecektir. Örneğin: "/home/kaan/Study/C/sample.c" Burada hedeflenen dosya "sample.c" dosyasıdır. Ancak işletim sistemi bu dosyanın yerini bulabilmek için yol ifadesindeki bileşenlerin üzerinden geçer. Bu işleme "pathname resolution" denilmektedir. İşte "pathname resolution" işleminde dizin geçişleriyle hedefe ulaşılabilmesi için prosesin yol ifadesine ilişkin dizin bileşenlerinin "x" hakkına sahip olması gerekir. Yani dizinlerdeki "x" hakkı "içinden geçilebilirlik" gibi bir anlama gelmektedir. Biz bir dizinimizdeki "x" hakkını kaldırırsak, işletim sistemi "pathname resolution" işleminde başarısız olur. Dolayısıyla "pathname resolution" işleminin başarılı olabilmesi için yol ifadesindeki dizin bileşenlerinin hepsine (son bileşen de dahil olmak üzere) prosesin "x" hakkına sahip olması gerekir. Yukarıdaki örnekte "pathname resolution işleminin" bitirilebilmesi için prosesin "home" dizini "kaan" dizini "Study" dizini ve "C" dizini için "x" hakkına sahip olması gerekir. "x" hakkı bir dizin ağacında bir noktaya duvar örmek için kullanılabilmektedir. mkdir gibi kabuk komutları dizin yaratırken zaten "x" hakkını default durumda vermektedir. Proses id'si 0 olan "root prosesler" her zaman pathname resolution sırasında dizinler içerisinden geçebilirler. "x" hakkı göreli yol ifadelerinde de aynı biçimde uygulanmaktadır. Örneğin biz kendi dizinimizde bulunan "test.txt" dosyasını open ile "test.txt" yol ifadesini vererek açmak isteyelim. Eğer içinde bulunduğumuz dizine "x" hakkına sahip değilsek yine "pathname resolution" işlemi başarısız olacaktır. Başka bir deyişle "test.txt" yol ifadesi sanki "./test.txt" gibi ele alınmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir işletim sisteminin dosyalarla uğraşan kısmına "dosya sistemi (file system)" denilmektedir. Dosya sisteminin iki yönü vardır: Disk ve Bellek. İşletim sistemi dosya organizasyonu için diskte belli bir biçim kullanmaktadır. Ancak bir dosya açıldığında işletim sistemi çekirdek alanı içerisinde bazı veri yapıları oluşturur bu da dosya sisteminin bellek tarafı ile ilgilidir. Pek çok POSIX uyumlu işletim sistemi dosya işlemleri için 5 sistem bulundurmaktadır: - Dosya açmak için gereken sistem fonksiyonu (Linux'ta sys_open) - Dosya kapatmak için gereken sistem fonksiyonu (Linux'ta sys_close) - Dosyadan okuma yapmak için gereken sistem fonksiyonu (Linux'ta sys_read) - Dosyaya yazma yapmak için gereken sistem fonksiyonu (Linux'ta sys_write) - Dosya göstericisini konumlandırmak için gereken sistem fonksiyonu (Linux'ta sys_lseek) Bu 5 sistem fonksiyonunu çağıran 5 POSIX fonksiyonu bulunmaktadır: open, close, read, write ve lseek Biz bir UNIX/Linux sisteminde hangi düzeyde çalışıyor olursak olalım eninde sonunda dosya işlemleri bu 5 POSIX fonksiyonu ile yapılmaktadır. Programlama dili ne olursa olsun durum böyledir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dosya açıldığında işletim sistemi açılacak dosyanın bilgilerini pathname resolution işlemi sonucunda diskte bulur. Dosyanın bilgilerini kernel alanı içerisinde bir alana çeker. Bu alana "dosya nesnesi (file object)" denilmektedir. Buradaki "nesne (object)" terimi tahsis edilmiş yapı alanları için kullanılmaktadır. Dosya nesnesi Linux'un kaynak kodlarında "struct file" ile temsil edilmiştir. İşletim sistemi, bir proses bir dosyayı açtığında açılan dosyayı o proses ile ilişkilendirir. Yani dosya nesnelerine proses kontrol blokları yoluyla erişilmektedir. Güncel Linux çekirdeklerinde bu durum biraz karmaşıktır: task_struct (files) ---> files_struct (fdt) ---> fdtable (fd) ---> file * türünden bir dizi ---> file Linux'ta proses kontrol bloktan dosya nesnesine erişim birkaç yapıdan geçilerek yapılmaktadır. Ancak biz bu durumu şöyle basitleştirerek ifade edebiliriz: "proses kontrol blokta bir eleman bir diziyi göstermektedir. Bu diziye "dosya betimleyici tablosu (file desctiptor table)" denilmektedir. Dosya betimleyici tablosunun her elemanı bir dosya nesnesini göstermektedir. Yani biz yukarıdaki yapıyı aşağıdaki gibi sadeleştirerek kavramsallaştırıyoruz: proses kontrol block ---> betimleyici tablosu --> dosya nesneleri Dosya betimleyici tablosu (file descriptor table) açık dosyalara ilişkin dosya nesnelerinin adreslerini tutan bir gösterici dizisidir. Dosya betimleyici tablosuna proses kontrol bloktan hareketle erişilmektedir. Her prosesin ayrı bir dosya betimleyici tablosu vardır. İşletim sistemi her açılan dosya için bir dosya nesnesi tahsis etmektedir. Aynı dosya ikinci kez açıldığında o dosya için yine yeni bir dosya nesnesi oluşturulur. Dosya göstericisinin konumu da dosya nesnesinin içerisinde saklanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde dosyayı açmak için open isimli POSIX fonksiyonu kullanılmaktadır. (Örneğin fopen standart C fonksiyonu da UNIX/Linux sistemlerinde aslında open fonksiyonunu çağırmaktadır.) Fonksiyonun prototipi şöyledir: #include int open(const char *path, int flags, ...); open fonksiyonu isteğe bağlı (optional) olarak bir üçüncü argüman alabilmektedir. Eğer fonksiyon 3 argümanla çağrılacaksa üçüncü argüman mode_t türünden olmalıdır. Her ne kadar prototipteki "..." "istenildiği kadar argüman girilebilir" anlamına geliyorsa da open ya iki argümanla ya da üç argümanla çağrılmalıdır. open fonksiyonunu daha fazla argümanla çağırmak "tanımsız davranışa (undefined behavior)" yol açmaktadır. Fonksiyonun birinci parametresi açılacak dosyanın yol ifadesini belirtir. İkinci parametre açış bayraklarını (modlarını) belirtmektedir. Bu parametre O_XXX biçiminde isimlendirilmiş sembolik sabitlerin "bit OR" işlemine sokulmasıyla oluşturulur. Açış sırasında aşağıdaki sembolik sabitlerden yalnızca biri belirtilmek zorundadır. O_RDONLY O_WRONLY O_RDWR O_SEARCH (at'li fonksiyonlar için bulundurulmuştur, ileride ele alınacaktır) O_EXEC (fexecve fonksiyonu için bulundurulmuştur, ileride ele alınacaktır) Buradaki O_RDONLY "yalnızca okuma yapma amacıyla", O_WRONLY "yalnızca yazma yapma amacıyla" ve O_RDWR "hem okuma hem de yazma yapma amacıyla" dosyanın açılmak istendiği anlamına gelmektedir. İşletim sistemi, prosesin etkin kullanıcı id'sine ve etkin grup id'sine ve dosyanın kullanıcı ve grup id'lerine bakarak prosesin dosyaya "r", "w" hakkının olup olmadığını kontrol eder. Eğer proses bu hakka sahip değilse open fonksiyonu başarısız olur. Buradaki O_SEARCH bayrağı bazı POSIX fonksiyonlarının "at"li versiyonları için, O_EXEC bayrağı ise "fexecve" fonksiyonu için bulundurulmuştur. Bu bayraklar ileride ele alınacaktır. open fonksiyonu yalnızca olan dosyayı açmak için değil aynı zamanda yeni bir dosya yaratmak için de kullanılmaktadır. O_CREAT bayrağı dosya varsa etkili olmaz. Ancak dosya yoksa dosyanın yaratılmasını sağlar. Yani O_CREAT bayrağı "dosya varsa olanı aç, yoksa yarat ve aç" anlamına gelmektedir. Bir dosya yaratılırken dosyanın erişim haklarını, dosyayı yaratan kişi open fonksiyonun üçüncü parametresinde vermek zorundadır. Yani dosyanın erişim haklarını dosyayı yaratan kişi belirlemektedir. Biz O_CREAT bayrağını açış moduna eklemişsek bu durumda "dosya yaratılabilir" fikri ile erişim haklarını open fonksiyonun üçüncü parametresinde belirtmemiz gerekir. Erişim hakları tüm bitleri sıfır tek biti 1 olan sembolik sabitlerin "bit OR" işlemine sokulmasıyla oluşturulmaktadır. Bu sembolik sabitlerin hepsi S_I öneki başlar. Bunu R, W ya da X harfi izler. Bunu da USR, GRP ya da OTH harfleri izlemektedir. Böylece 9 tane erişim hakkı şöyle isimlendirilmiştir: S_IRUSR S_IWUSR S_IXUSR S_IRGRP S_IWGRP S_IXGRP S_IROTH S_IWOTH S_IXOTH Örneğin S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH erişim hakları "rw-r--r--" anlamına gelmektedir. Ayrıca içerisisinde aşağıdaki sembolik sabitler de bildirilmiştir: S_IRWXU S_IRWXG S_IRWXO Bu sembolik sabitler şöyle oluşturulmuştur: #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 örneğin S_IRWXU|S_IRWXG|S_IRWXO işlemi "rwxrwxrwx" anlamına gelmektedir. Yukarıdaki S_IXXX biçimindeki sembolik sabitlerin değerlerinin eskiden sistemden sisteme değişebileceği dikkate alınmıştır. Bu nedenle POSIX standartları başlarda bu sembolik sabitlerin sayısal değerlerini işletim sistemlerini oluşturanların belirlemesini istemiştir. Ancak daha sonraları (2008 ve sonrasında, SUS 4) bu sembolik sabitlerin değerleri POSIX standartlarında açıkça belirtilmiştir. Dolayısyla programcılar artık bu sembolik sabitleri kullanmak yerine bunların sayısal karşılıklarını da kullanabilir duruma gelmiştir. Ancak eski sistemler dikkate alındığında bunların sayısal karşılıkları yerine yukarıdaki sembolik sabitlerin kullanılması tavsiye edilmektedir. Bu sembolik sabitler aynı zamanda okunabilirliği de artırmaktadır. POSIX standartları belli bir sürümden sonra bu sembolik sabitlerin sayısal değerlerini aşağıdaki gibi belirlemiştir: S_IRWXU 0700 S_IRUSR 0400 S_IWUSR 0200 S_IXUSR 0100 S_IRWXG 070 S_IRGRP 040 S_IWGRP 020 S_IXGRP 010 S_IRWXO 07 S_IROTH 04 S_IWOTH 02 S_IXOTH 01 S_ISUID 04000 S_ISGID 02000 S_ISVTX 01000 Yani belli bir süreden sonra artık rwxrwxrwx biçiminde owner, group ve other bilgilerine ilişkin S_IXXX biçimindeki sembolik sabitler gerçekten yukarıdaki sıraya göre bitleri temsil eder hale gelmiştir. Örneğin S_IWGRP sembolik sabiti 000010000 bitlerinden oluşmaktadır. Bu durumda belli bir süreden sonra örneğin S_IRUSR|S_IWURS|S_IRGRP|S_IROTH bir erişim hakkını biz 0644 octal değeri ile edebiliriz. Bu sembolik sabitlerin binary karşılıklarını da vermek istiyoruz. S_IRUSR 100 000 000 S_IWUSR 010 000 000 S_IXUSR 001 000 000 S_IRGRP 000 100 000 S_IWGRP 000 010 000 S_IXGRP 001 001 000 S_IROTH 000 000 100 S_IWOTH 000 010 010 S_IXOTH 001 001 001 open fonksiyonunda O_CREAT bayrağı belirtilmemişse erişim haklarının girilmesinin hiçbir anlamı yoktur. Kaldı ki O_CREAT bayrağı girildiğinde dosya varsa erişim hakları yine dikkate alınmayacaktır. POSIX sistemlerinde yukarıdaki S_IXXX biçimindeki sembolik sabitler mode_t türüyle temsil edilmiştir. mode_t türü ve bazı başlık dosyalarında sistemi oluşturanların belirlediği bir tamsayı türü olarak typedef edilmiştir. O_TRUNC açış bayrağı "eğer dosya varsa onu sıfırlayarak aç" anlamına gelmektedir. Ancak O_TRUNC ancak yazma modunda açılan dosyalarda kullanılabilmektedir. Yani O_TRUNC bayrağını kullanabilmek için O_WRONLY ya da O_RDWR bayraklarından birinin de belirtilmiş olması gerekmektedir. Örneğin O_WRONLY|O_CREAT|O_TRUNC açış modu "dosya yoksa yarat ancak varsa sıfırlayarak aç" anlamına gelmektedir. O_TRUNC bayrağı için dosyanın yaratılıyor olması gerekmez. O_WRONLY|O_TRUNC geçerli bir açış modudur. Bu durumda dosya yoksa open başarısız olur. Ancak dosya varsa sıfırlanarak açılır. O_APPEND bayrağı yazma işlemlerinin dosyanın sonuna yapılacağı anlamına gelmektedir. Yani bu bayrak kullanılırsa tüm yazma işlemlerinde işletim sistemi dosya göstericisini dosyanın sonuna çekip sonra yazmayı yapmaktadır. Bu açış modu da O_WRONLY ya da O_RDWR için anlamlıdır. Örneğin O_RDWR|O_APPEND burada dosyaya her yazılan sona eklenecektir. Ancak dosyanın herhangi bir yerinden okuma yapılabilecektir. O halde standart C'nin fopen fonksiyonundaki açış modlarının POSIX karşılıkları şöyle oluşturulabilir: Standart C fopen POSIX "w" O_WRONLY|O_CREAT|O_TRUNC "w+" O_RDWR|O_CREAT|O_TRUNC "r" O_RDONLY "r+" O_RDWR "a" O_WRONLY|O_CREAT|O_APPEND "a+" O_RDWR|O_CREAT|O_APPEND O_EXCL bayrağı "exclusive" açım kullanılmaktadır. Bu bayrak O_CREAT ile birlikte kullanılmalıdır. O_CREAT|O_EXCL biçiminde açış modu "dosya yoksa yarat, varsa yaratma başarısız ol" anlamına gelmektedir. O_EXCL bayrağının O_CREAT olmadan kullanılması "tanımsız davranışa" yol açmaktadır. O_DIRECTORY bayrağının tek işlevi açılmak istenen dosya bir dizin dosyası değilse açımın başarısız olmasını sağlamak içindir. open fonksiyonunun diğer açış modları ileride başka konular içerisinde ele alınacaktır. Erişim hakları open fonksiyonu tarafından (yani open fonksiyonunun çağırdığı sistem fonksiyonu tarafından) kontrol edilmektedir. Örneğin biz dosyayı O_RDWR modunda açmak isteyelim bu durumda prosesimizin dosyaya "r" ve "w" haklarına sahip olması gerekir. Eğer prosesimiz dosya için bu haklara sahip değilse open başarısız olur ve errno EACCESS değeri ile set edilir. Burada önemli olan nokta kontrolün en başta open tarafından yapılmasıdır. Yani O_RDWR modunda açma istendiğinde eğer proses bu haklara sahip değilse açma başarılı olup read ya da write fonksiyonlarındaki hatadan dolayı başarısız olma söz konusu değildir. Direkt açmanın kendisi başarısız olmaktadır. open fonksiyonu başarı durumunda int türden "dosya betimleyicisi (file descriptor)" denilen bir değerle geri dönmektedir. Dosya betimleyicisi bir handle olarak diğer fonksiyonlar tarafından istenmektedir. open başarısız olursa -1 ile geri döner ve errno uygun biçimde set edilir. open fonksiyonunun başarısız olması için pek çok neden söz konusudur. Bundan dolayı açma işleminin başarısı kesinlikle test edilmelidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 10. Ders 26/11/2022 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- open fonksiyonu işletim sisteminin dosya açan sistem fonksiyonunu (Linux'ta sys_open) çağırmaktadır. Bu sistem fonksiyonu açılacak dosyaya ilişkin bilgileri diskten bulur ve o bilgileri "dosya nesnesi (file object)" denilen bir yapının içerisine yerleştirir. Dosya nesnesi Linux'un kaynak kodlarında "struct file" türü ile temsil edilmiştir. İşletim sistemi dosya nesnesinin içini doldurduktan sonra dosya betimleyici tablosunda boş bir slot bulur ve o slota dosya nesnesinin adresini yazar. Anımsanacağı gibi dosya betimleyici tablosu dosya nesnelerinin adreslerini tutan bir gösterici dizisi biçiminde organize edilmiştir. Dosya betimleyici tablosunun yeri prosesin kontrol bloğundan hareketle elde edilmektedir. İşte open fonksiyonunun bize geri döndürdüğü dosya betimleyicisi aslında dosya betimleyici tablosunda (yani gösterici dizisinde) bir indeks belirtmektedir. Bir program çalıştığında genellikle dosya betimleyici tablosunun ilk üç betimleyicisi dolu diğerleri boştur. Dosya betimleyici tablosunun 0'ıncı slotu (yani 0 numaralı betimleyici) terminal aygıt sürücüsü için oluşturulmuş dosya nesnesini göstermektedir. Buna stdin dosya betimleyicisi denilmektedir. 1 ve 2 numaralı betimleyiciler yine terminal aygıt sürücüsü oluşturulmuş dosya betimleyicisini göstermektedir. (1 ve 2 numaralı betimleyiciler aynı nesneyi göstermektedir) Bu betimleyicilere de sırasıyla stdout ve stderr denilmektedir. Böylece ilk boş betimleyici genellikle 3 numaralı betimleyici olmaktadır. open fonksiyonun dosya betimleyici tablosunda ilk boş betimleyiciyi vermesi POSIX standartlarında garanti edilmiştir. Her prosesin proses kontrol bloğu ve dolayısıyla dosya betimleyici tablosu birbirinden farklıdır. O halde dosya betimleyicileri kendi prosesinin dosya betimleyici tablosunda bir indeks belirtmektedir. Yani dosya betimleyicileri prosese özgü bir anlama sahiptir. Bu durumda tipik olarak işletim sisteminin dosya açan sistem fonksiyonu sırasıyla şu işlemleri yapmaktadır: 1) Dosya betimleyici tablosunda ilk boş betimleyiciyi bulmaya çalışır. Boş betimleyiciyi bulamazsa başarısız olur ve errno değerini EMFILE ise set eder. 2) Dosya nesnesini tahsis eder ve bunun içini diskten elden ettiği bilgilerle doldurur. Bunun adresini de dosya betimleyici tablosunda ilk boş betimleyiciye ilişkin slota yazar. 3) Dosya betimleyici tablosunda indeks belirten betimleyici ile geri döner. C'nin fopen fonksiyonunda dosya açımı sırasında "text mode", "binary mode" gibi bir kavram vardır. Halbuki işletim sisteminde böyle bir kavram yoktur. İşletim sistemine göre dosya byte'lardan oluşmaktadır. Text mode, binary mode C ve diğer diller tarafından uydurulmuş olan yapay bir kavramdır. Bir proses her open işlemi yaptığında kesinlikle yeni bir dosya nesnesi oluşturulur. Bu durumda bir proses aynı dosyayı aynı biçimde ikinci kez açmış olsa bile dosya aynı dosya nesnesi kullanılmaz. Her iki open iki farklı dosya nesnesinin ve dosya betimleyicisinin oluşmasına yol açmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #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"); printf("file opened: %d\n", fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Açılan her dosyanın kapatılması gerekir. Bir dosyanın kapatılması sırasında işletim sistemi dosyanın açılması sırasında yapılan işlemleri geri almaktadır. Tipik olarak UNIX/Linux sistemlerinde dosya kapatıldığında şunlar yapılmaktadır: 1) Dosya nesnesi eğer onu gösteren tek bir betimleyici varsa yok edilir. 2) Dosya betimleyici tablosundaki betimleyiciye ilişkin slot boşaltılır. İleride de görüleceği gibi dosya betimleyici tablosunda birden fazla betimleyici aynı dosya nesnesini gösteriyor durumda olabilir. Bu durumda işletim sistemi dosya nesnesi içerisinde bir sayaç tutup bu sayacı artırıp eksiltmektedir. Sayaç 0'a geldiğinde nesneyi silmektedir. (Linux'un kaynak kodlarında bu sayaç struct file yapısının f_count elemanında tutulmaktadır.) Bir dosya artık kullanılmayacaksa onu kapatmak iyi bir tekniktir. Çünkü bu sayede: 1) Dosya betimleyici tablosunda gereksiz bir slot tahsis edilmiş durumda olmaz. 2) Dosya nesnesi gereksiz bir biçimde kernel alanı içerisinde yer kaplamaz. Tabii işletim sistemi, proses dosyayı kapatmasa bile proses sonlandırılırken dosya prosesin dosya betimleyici tablosunu inceler ve açık dosyaları bu biçimde kapatır. Yani biz bir dosyayı kapatmasak bile proses bittiğinde dosyalar zaten kapatılmaktadır. Dosyanın kapatılması için close isimli POSIX fonksiyonu bulundurulmuştur. Bu POSIX fonksiyonu doğrudan işletim sisteminin dosyayı kapatan sistem fonksiyonunu çağırmaktadır. close fonksiyonunun prototipi şöyledir: #include int close(int fd); Fonksiyon parametre olarak dosya betimleyicisini alır. close fonksiyonu başarı durumunda 0, başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyonun geri dönüş değeri genellikle kontrol edilmez. ---------------------------------------------------------------------------------------------------------------------------*/ #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"); printf("file opened: %d\n", fd); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- İlk UNIX sistemlerinden beri creat isimli bir fonksiyon da open fonksiyonun bir sarma fonksiyonu biçiminde bulundurulmaktadır. creat fonksiyonu POSIX standartlarında var olan bir fonksiyondur. Fonksiyonun prototipi şöyledir: #include int creat(const char *path, mode_t mode); Fonksiyonun birinci parametresi dosyanın yol ifadesini belirtmektedir. İkinci parametre erişim bilgisini belirtir. Görüldüğü gibi fonksiyonda açış modu belirten flags parametresi yoktur. Çünkü bu parametre O_WRONLY|O_CREAT|O_TRUNC biçiminde alınmaktadır. creat fonksiyonu aşağıdaki gibi yazılmıştır: int creat(const char *path, mode_t mode) { return open(path, O_WRONLY|O_CREAT|O_TRUNC, mode); } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dosyadaki her bir byte'a bir offset numarası karşı getirilmiştir. Buna ilgili byte'ın offset'i denilmektedir. Dosya göstericisi okuma ve yazma işlemlerinin hangi offset'ten itibaren yapılacağını gösteren bir offset belirtmektedir. Okuma ya da yazma miktarı kadar dosya göstericisi otomatik olarak ilerletilmektedir. Dosya ilk açıldığında dosya göstericisi 0 durumundadır. Dosya göstericisinin dosyanın son byte'ından sonraki byte'ı göstermesi durumuna EOF durumu denir. EOF durumunda okuma yapılamaz. Ancak yazma yapılabilir. Bu durumda yazılanlar dosyaya eklenmiş olur. Dosyada araya bir şey eklemek (insert) diye bir kavram yoktur. Dosya boyutunu değiştirmek için dosya göstericisi EOF'a çekilip yazma yapılmalıdır. Dosya göstericisin konumu dosya nesnesi içerisinde saklanmaktadır. (Linux'un kaynak kodlarında "struct file" yapısının f_pos elemanı dosya göstericisinin konumunu tutmaktadır.) Biz aynı dosyayı ikinci kez açmış olsak bile yeni bir dosya nesnesi dolayısıyla yeni bir dosya göstericisi elde etmiş oluruz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dosyadan okuma yapmak için read POSIX fonksiyonu kullanılmaktadır. Pek çok sistemde bu POSIX fonksiyonu doğrudan işletim sisteminin okuma yapan sistem fonksiyonunu (Linux'ta sys_read) çağırmaktadır. read fonksiyonunun prototipi şöyledir: #include ssize_t read(int fd, void *buf, size_t nbyte); Fonksiyonun birinci parametresi okuma işleminin yapılacağı dosya betimleyicisini belirtmektedir. İşletim sistemi, bu betimleyiciden hareketle dosya nesnesine erişmektedir. İkinci parametre bellekteki transfer adresini belirtmektedir. Üçüncü parametre okunacak byte sayısını belirtir. Fonksiyon başarı durumunda okuyabildiği byte ile geri döner. read fonksiyonu ile eğer dosya göstericisinin gösterdiği yerden itibaren dosya sonuna kadar mevcut olan byte miktarından daha fazla byte okunmak istenirse, read fonksiyonu okuyabildiği kadar byte'ı okur ve okuyabildiği byte sayısına geri döner. Dosya göstericisi EOF durumunda ise read hiç okuma yapamayacağı için 0 ile geri dönmektedir. Ancak argümanların yanlış girilmesinde ya da IO hatalarında read başarısız olur ve -1 değerine geri döner. ssize_t ve içerisinde işaretli bir tamsayı türü biçiminde typedef edilmek durumunda olan POSIX'e özgü bir typedef ismidir. read fonksiyonu ile dosyadan 0 byte okunmak istendiğinde read fonksiyonu temel bazı kontrolleri yapar. (Örneğin; dosyanın okuma modunda açılmış olup olmadığı kontrol edilir.) Eğer bu kontrollerde bir sorun çıkarsa -1 değerine geri döner. Eğer bu kontrollerde bir sorun çıkmazsa 0 değerine geri döner ve herhangi bir okuma işlemi yapmaz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[10 + 1]; ssize_t result; if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); buf[result] = '\0'; printf(":%s:\n", buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Şimdi bir dosyayı (örneğimizde içerisinde yazı olan bir dosyayı) dosya sonuna kadar read fonksiyonu ile bir döngü içerisinde okuyalım. Bu tür durumlarda klasik yöntem aşağıdaki gibi bir döngü oluşturmaktır: while ((result = read(fd, buf, BUFSIZE)) > 0) { buf[result] = '\0'; printf("%s", buf); } if (result == -1) exit_sys("read"); Bu döngüden IO hatası oluşunca ya da dosya göstericisi dosyanın sonuna geldiğinde çıkılacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #define BUFSIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[BUFSIZE + 1]; ssize_t result; if ((fd = open("sample.c", O_RDONLY)) == -1) exit_sys("open"); while ((result = read(fd, buf, BUFSIZE)) > 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); } /*-------------------------------------------------------------------------------------------------------------------------- 11. Ders 27/11/2022 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dosyaya yazma yapmak için write isimli POSIX fonksiyonu kullanılmaktadır. Bu fonksiyon da pek çok sistemde doğrudan işletim sisteminin dosyaya yazma yapan sistem fonksiyonunu (Linux'ta sys_write) çağırmaktadır. Prototipi şöyledir: #include ssize_t write(int fd, const void *buf, size_t nbyte); Fonksiyonun birinci parametresi yazma yapılacak dosyaya ilişkin dosya betimleyicisini belirtir. İkinci parametre yazılacak bilgilerin bulunduğu bellek adresidir. Üçüncü parametre yazılacak byte sayısını belirtir. write fonksiyonu başarılı olarak yazılan byte sayısı ile geri dönmektedir. Normal olarak bu değer üçüncü parametrede belirtilen yazılmak istenen byte sayısıdır. Ancak çok seyrek bazı durumlarda (örneğin diskin dolu olması gibi) write talep edilenden daha az byte'ı yazabilir. Bu durumda yazabildiği byte sayısı ile geri döner. write başarısız olursa -1 değerine geri dönmektedir. write fonksiyonu ile dosyaya 0 byte yazılmak istendiğinde gerçek bir yazma yapılmaz. write fonksiyonu bu durumda yazma konusunda gerekli kontrolleri yapar (örneğin dosyanın yazma modunda açılıp açılmadığı gibi). Eğer bu kontrollerde başarısızlık oluşursa write fonksiyonu -1 ile geri döner. Eğer bu kontrollerde başarısızlık oluşmazsa write fonksiyonu 0 ile geri döner. Ancak yukarıda da belirttiğimiz gibi bu durumda gerçek bir yazma yapılmamaktadır. POSIX standartları normal dosyaların dışında (yani "regular" olmayan dosyaların dışında) 0 byte yazma işlemini "unspecified" olarak belirtmiştir. Dolayısıyla ileride göreceğimiz boru gibi dosyalara 0 byte yazıldığında ne olacağı o sisteme bağlı bir durumdur. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[] = "this is a test"; ssize_t result; if ((fd = open("test.txt", O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("open"); if (write(fd, buf, strlen(buf)) == -1) exit_sys("write"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde dosya kopyalama bir döngü içerisinde kaynak dosyadan hedef dosyaya blok blok okuma yazma işlemi ile yapılmaktadır. Ancak bazı UNIX türevi işletim sistemleri dosya kopyalama işlemi için sistem fonksiyonları da bulundurabilmektedir. Örneğin Linux sistemlerinde copy_file_range isimli sistem fonksiyonu doğrudan disk üzerinde blok kopyalaması yoluyla dosya kopyalamasını hiç user mode işlem yapmadan gerçekleştirebilmektedir. Ancak bu işlemin taşınabilir yolu yukarıda belirttiğimiz gibi kaynaktan hedefe aktarım yapmaktır. Pekiyi bu kopyalama işleminde hangi büyüklükte bir tampon kullanılmalıdır? Tipik olarak dosya sistemindeki blok uzunluğu bunun için tercih edilir. stat, fstat, lstat gibi fonksiyonlar bunu bize verirler. Blok uzunlukları 512'nin katları biçimindedir. Aşağıdaki örnekte blok kopyalaması yoluyla dosya kopyalaması yapılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { char buf[BUFFER_SIZE]; int fds, fdd; ssize_t result; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((fds = open(argv[1], O_RDONLY)) == -1) exit_sys(argv[1]); if ((fdd = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys(argv[2]); while ((result = read(fds, buf, BUFFER_SIZE)) > 0) if (write(fdd, buf, result) != result) { fprintf(stderr, "cannot write file!...\n"); exit(EXIT_FAILURE); } if (result == -1) exit_sys("read"); close(fds); close(fdd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- write çok çok seyrek de olsa başarılı olduğu halde talep edilen miktar kadar hedef dosyaya yazamayabilir. Örneğin diskin dolu olması durumunda ya da bir sinyal oluşması durumunda write talep edilen miktar kadar yazma yapamayabilir. Bu tür durumları diğer durumlardan ayırmak için ayrı bir kontrol yapmak gerekebilir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { char buf[BUFFER_SIZE]; int fds, fdd; ssize_t size, result; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((fds = open(argv[1], O_RDONLY)) == -1) exit_sys(argv[1]); if ((fdd = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys(argv[2]); while ((result = read(fds, buf, BUFFER_SIZE)) > 0) { if ((size = write(fdd, buf, result)) == -1) exit_sys("write"); if (size != result) { fprintf(stderr, "cannot write file!...\n"); exit(EXIT_FAILURE); } } if (result == -1) exit_sys("read"); close(fds); close(fdd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- read ve write POSIX fonksiyonları yukarıda da belirttiğimiz gibi dosya göstericisinin gösterdiği yerden itibaren okuma ve yazma işlemlerini yapmaktadır. Bu fonksiyonlar dosya göstericisinin konumunu okunan ya da yazılan miktar kadar ilerletmektedir. İşte read ve write fonksiyonlarının pread ve pwrite biçiminde bir versiyonu da bulunmaktadır. pread ve pwrite fonksiyonları, işlemlerini dosya göstericisinin gösterdiği yerden itibaren değil, parametreleriyle belirtilen offset'ten yapmaktadır. Bu fonksiyonlar dosya göstericisinin konumunu değiştirmezler. Uygulamada pread ve pwrite fonksiyonları seyrek kullanılmaktadır. Örneğin dosyanın farklı yerlerinden sürekli okuma/yazma yapıldığı durumlarda bu fonksiyonlar kullanım kolaylığı sağlayabilmektedir. Fonksiyonların prototipleri şöyledir: #include ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset); ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset); pread ve pwrite fonksiyonlarının read ve write fonksiyonlarından tek farkı offset parametresidir. Bu fonksiyonlar dosya göstericisinin gösterdiği yerden değil, son parametreleriyle belirtilen yerden okuma ve yazma işlemini yaparlar. Fonksiyonların dosya göstericisinin konumunu değiştirmediğine dikkat ediniz. Dosyalara okuma yazma işlemi genellikle ardışıl bir biçimde yapıldığı için bu fonksiyonlar seyrek kullanılmaktadır. Ancak örneğin veritabanı işlemlerinde yukarıda da belirttiğimiz gibi dosyanın farklı offset'lerinden sıkça okuma ve yazmanın yapıldığı durumlarda bu fonksiyonlar tercih edilebilmektedir. pread ve pwrite fonksiyonları da doğrudan ilgili sistem fonksiyonlarını çağırmaktadır. (Linux sistemlerinde sys_pread ve sys_pwrite). Tabii bu işlemler önce dosya gösterisicini saklayıp, sonra konumlandırıp, sonra read/write işlemlerini yapıp, sonra da yeniden dosya göstericisini eski konumuna yerleştirmekle yapılabilir. Ancak pread ve pwrite işlemlerini yapan sistem fonksiyonları bu biçimde değil, daha doğrudan aynı işlemi yapmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dosya göstericisi dosya açıldığında 0'ıncı offset'tedir. Ancak okuma ve yazma yapıldığında okunan ya da yazılan miktar kadar otomatik ilerletilmektedir. Dosya göstericisini belli bir konuma almak için lseek isimli POSIX fonksiyonu kullanılmaktadır. Bu fonksiyon da pek çok işletim sisteminde doğrudan dosyayı konumlandıran sistem fonksiyonunu (Linux'ta sys_lseek) çağırmaktadır. lseek fonksiyonun genel kullanımı fseek standart C fonksiyonuna çok benzemektedir. Fonksiyonun prototipi şöyledir: #include off_t lseek(int fd, off_t offset, int whence); Fonksiyonun birinci parametresi dosya göstericisi konumlandırılacak dosyayaya ilişkin dosya betimleyicisini belirtir. Dosya göstericisi dosya nesnesinin (Linux'ta struct file) içerisinde tutulmaktadır. İkinci parametre konumlandırma offset'ini belirtir. off_t ve içerisinde işaretli bir tamsayı türü biçiminde typedef edilmiş olan bir tür ismidir. Üçüncü parametre konumlandırma orijinini belirtmektedir. Bu üçüncü parametre 0, 1 ya da 2 olarak girilebilir. Tabii sayısal değer girmek yerine yine SEEK_SET (0), SEEK_CUR (1) ve SEEK_END (2) sembolik sabitlerini girebiliriz. Bu sembolik sabitler ve içerisinde de bildirilmiştir. Fonksiyon başarı durumunda konumlandırılan offset'e, başarısızlık durumunda -1 değerine geri dönmektedir. SEEK_SET konumlandırmanın dosyanın başından itibaren yapılacağını, SEEK_CUR konumlandırmanın o anda dosya göstericisinin gösterdiği yerden itibaren yapılacağını ve SEEK_END de konumlandırmanın EOF durumundan itibaren yapılacağını belirtmektedir. En normalk durum SEEK_SET orijininde ikinci parametrenin >= 0, SEEK_END orijininde <= 0 biçiminde girilmesidir. SEEK_CUR orijininde ikinci parametre pozitif ya da negatif girilebilir. Örneğin dosya göstericisini EOF durumuna şöyle konumlandırabiliriz: lseek(fd, 0, SEEK_END); Dosya sistemine de bağlı olarak UNIX/Linux sistemleri dosya göstericisini EOF'un ötesine konumlandırmaya izin verebilmektedir. Bu özel bir durumdur. Bu tür durumlarda dosyaya yazma yapıldığında "dosya delikleri (file holes)" oluşmaktadır. Dosya delikleri konusu ileride ele alınacaktır. Aslında dosya açarken O_APPEND modu atomik bir biçimde her write işleminden önce dosya göstericisini EOF durumuna çekmektedir. Bu nedenle her yazılan dosyanın sonuna eklenmektedir. Aşağıdaki örnekte "test.txt" O_WRONLY modunda açılmış ve dosya göstericisi EOF durumuna çekilerek dosyaya ekleme yapılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[] = "\nthis is a test"; if ((fd = open("test.txt", O_WRONLY)) == -1) exit_sys("open"); lseek(fd, 0, SEEK_END); 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); } /*-------------------------------------------------------------------------------------------------------------------------- Bir C/C++ programcısı olarak UNIX/Linux sistemlerinde dosya işlemleri yapmak için üç seçenek söz konusu olabilir: 1) C'nin ya da C++'ın standart dosya fonksiyonlarınu kullanmak 2) POSIX dosya fonksiyonlarını kullanmak 3) Sistem fonksiyonlarını kullanmak Burada en taşınabilir olan standart C/C++ fonksiyonlarıdır. Dolayısıyla ilk tercih bunlar olmalıdır. Ancak C ve C++'ın standart dosya fonksiyonları spesifik bir sistemin gereksinimini karşılayacak biçimde yazılmamıştır. Bunedenle bazen doğrudan POSIX fonksiyonlarının kullanılması gerekebilmektedir. Genellikle dosya işlemleri yapan sistem fonksiyonlarının kullanılması hiç gerekmez. Çünkü Linux'ta olduğu gibi pek çok UNIX türevi sistemde yukarıda gördüğümüz POSIX fonksiyonları zaten doğrudan sistem fonksiyonlarını çağırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- POSIX standartlarına göre dosyaya yapılan read ve write işlemleri sistem genelinde atomiktir. Yani örneğin iki program aynı anda aynı dosyanın aynı yerine yazma yapsa bile iç içe geçme oluşmaz. Önce birisi yazar daha sonra diğeri yazar. Tabii hangi prosesin önce yazacağını bilemeyiz. Ancak burada önemli lan nokta iç içe geçmenin olmamasıdır. Benzer biçimde bir read ile bir dısyanın bir yerinden n byte okumak istediğimizde başka bir proses aynı dosyanın aynı yerine yazma yaptığında biz ya o prosesin yazdıklarını okuruz ya da onun yazmadan önceki dosya değerlerini okuruz. Yarısı eski yarısı yeni bir bilgi okumayız. Ancak işletim sistemi farklı read ve write çağrılarını bu anlamda senktronize etmemektedir. Yani örneğin biz bir dosyanın belli bir yerine iki farklı write fonksiyonu ile ardışık şeyler yazdığımızı düşünelim. Birinci write işleminden sonra başka bir proses artık orayı değiştirebilir. Dlayısıyla bu anlamda bir iç içe girme durumu oluşabilir. Veritabanı programlarında bu tür durumlarla sık karşılaşılmaktadır. Örneğin veri tabanı programı bir kaydı "data" dosyasına yazıp ona ilişkin indeksleri "index" dosyasına yazıyor olabilir. Bu durumda iki write işlemi söz konusudur. Data dosyasına bilgiler yazıldktan sonra henüz indeks dosyasına bilgi yazılmadan başka bir proses bu iki işlemi hızlı davranarak yaparsa data ve indeks bütünlüğü bozulur. İşletim sisteminin burada bir sorumluluğu yoktur. Bu tarz işlemlerde senkroznizasyon programcılar tarafından sağlanmak zorundadır. Bu tür senktronizasyonlar senkronizasyon nesneleriyle (semaphore gibi, mutex gibi) dosya bütününde yapılabilir. Ancak tüm dosyaya erişimin engellenmesi iyi bir teknik değildir. İşte bu tür durumlar için işletim sistemleri çekirdeğe entegre edilmiş olan "dosya kilitleme (file locking)" mekanizması bulundurmaktadır. Dosya kilitleme tüm dosyayı değil dosyanın belli offset'lerine erişimi engelleme amacındadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz open fonksiyonu ile bir dosya yaratırken yaratacağımız dosyaya verdiğimiz erişim hakları dosyaya tam olarak yansıtılmayabilir. Yani örneğin biz gruba "w" hakkı vermek istesek bile bunu sağlayamayabiliriz. Çünkü belirtilen erişim değerlerini maskeleyen (yani ortadan kaldıran) bir mekanizma vardır. Buna prosin umask değeri denilmektedir. Prosesin umask değeri mode_t türü ile ifade edilir; sahiplik, grupluk ve diğerlik bilgilerini içerir. Bu bilgiler aslında maskeleneck değerleri belirtmektedir. Örneğin prosesin umask değerinin S_IWGRP|S_IWOTH olduğunu varsayalım. Bu umask değeri "biz open fonksiyonu ile bir dosyayı yaratırken grup için ve diğerleri için "w" hakkı versek bile bu hak dosyaya yanısıtılmayacak" anlamına gelmektedir. Eğer prosesin umask değeri 0 ise bu durumda maskelenecek bir şey yoktur dolayısıyla verilen hakların hepsi dosyaya yansıtılır. Prosesin umask değerinin umask olduğunu varsayalım. Dosyaya vermek istediğimiz erişim haklarının da mode olduğunu varsayalım. (Yani mode S_IXXX gibi tek biti 1 olan değerlerin bit düzeyinde OR'lanması ile oluşturumuş değer olsun.) Bu durumda dosyaya yansıtılacak erişim hakları mode & ~umask olacaktır. Yani prosesin umask değerindeki bitler maskelenecek erişim haklarını belirtmektedir. Prosesin başlangıçtaki umask değeri üst prosesten aktarılmaktadır. Örneğin biz kabuktan program çalıştırırken çalıştırdığımız programın umask değeri kabuğun (örneğin bash prosesinin) umask değeri olarak bizim prosesimize geçirilecektir. Kabuğun umask değeri "umask" isimli komutla elde edilebilir. Bu değer genellikle "0022" ya da "0002" gibi bir değerde olacaktır. Buradaki basamaklar octal sayı (sekizlik sistemde sayı) belirtmektedir. Bir octal digit 3 bitle açılmaktadır. Dolayısıyla bu bitler maskelenecek erişim haklarının durumunu belirtir: ? owner group other En yüksek anlamlı octal digit şimdiye kadar görmediğimiz başka haklarla ilgilidir. Bu haklara "set user id", "set group id" ve "sticky" hakları denilmektedir. Ancak diğer 3 octal digit sırasıyla owner, group ve other maskeleme bitlerini belirtmektedir. Kabuk üzerinde umask komutuyla aynı zamanda kabuğun umask değeri de değiştirilebilir. Bu durumda yine değiştirme değerleri octal digitler biçiminde verilmelidir. Örneğin: umask 022 Burada en yüksek anlamlı octal verilmediğine göre 0 kabul edilir. O halde burada belirtilern umask değeri grup için ve diğerleri için "w" hakkını maskeleyecektir. (Zaten pek çok kabulta umask değerin default durumu böyledir.) Bazen programcı umask değerini tamamen sıfırlamak da isteyebilir. Bu işlem şöyle yapılabilir: umask 0 Burada yüksek anlamlı üç octal digit de 0 kabul edilmektedir. Bu durumda artık çalıştırdığımız programda open fonksiyonun tüm erişim hakları dosyalara yansıtılacaktır. Bir proses başlangıçta umask değerini üst prosesten almaktadır. Ancak proses istediği zaman umask isimli POSIX fonksiyonu ile kendi umask değerini değiştirebilmektedir. umask fonksiyonunun prototipi şöyledir: #include mode_t umask(mode_t cmask); Fonksiyon belirtilen değerle prosesin umask değerini set eder ve prosesin eski umask değerine geri döner. Fonksiyon başarısız olamaz. umask fonksiyonu ile kendi prosesimizin umask değerini almak için onu değişltirmemiz gerekir. Bu durumda bu işlem aşağıdaki bir kodla yapılabilir: mode_t mode; mode = umask(0); umask(mode); Tabii programcı umask fonksiyonuna octal digitler girebilir. Ancak sistemlerde bu ocatl digitler tam olarak S_IXXX sembolik sabitlerinin değerlerine karşı gelmeyebilir. Ancak daha önceden de bahsedildiği gibi POSIX standartlarında belli bir zamandan sonra bu S_IXXX sembolik sabitlerinin değerleri açıkça belirtilmiştir. Örneğin: umask(00022); /* Eskiden bu biçimde belirleme taşınabilir değildi, eski sistemlerde dikkat edilmesi gerekir */ umask(S_IWGRP|S_IWOTH); /* Bu biçimde belirleme daha okunabilirdir. */ Aşağıdaki örnekte prosesin umask değeri önce sıfırlanmış, sonra bir dosya yaratılmıştır. open fonksiyonunda verilen erişim hakları artık dosyaya tamamen yansıtılacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; umask(0); if ((fd = open("x.dat", O_WRONLY|O_CREAT, S_IRWXU|S_IRWXG|S_IRWXO)) == -1) exit_sys("open"); printf("success...\n"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 12. Ders 03/12/2022 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki daha önce yapmış olduğumuz shell programına umask komutunu ekliyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 typedef struct tagCMD { char *name; void (*proc)(void); } CMD; void parse_cmd_line(char *cmdline); void dir_proc(void); void clear_proc(void); void pwd_proc(void); void cd_proc(void); void umask_proc(void); int check_umask_arg(const char *str); void exit_sys(const char *msg); char *g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[PATH_MAX]; CMD g_cmds[] = { {"dir", dir_proc}, {"clear", clear_proc}, {"pwd", pwd_proc}, {"cd", cd_proc}, {"umask", umask_proc}, {NULL, NULL} }; int main(void) { char cmdline[MAX_CMD_LINE]; char *str; int i; if (getcwd(g_cwd, PATH_MAX) == NULL) exit_sys("fatal error (getcwd)"); for (;;) { printf("CSD:%s>", g_cwd); if (fgets(cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(cmdline); if (g_nparams == 0) continue; if (!strcmp(g_params[0], "exit")) break; for (i = 0; g_cmds[i].name != NULL; ++i) if (!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if (g_cmds[i].name == NULL) printf("bad command: %s\n", g_params[0]); } return 0; } void parse_cmd_line(char *cmdline) { char *str; g_nparams = 0; for (str = strtok(cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) g_params[g_nparams++] = str; } void dir_proc(void) { printf("dir command executing...\n"); } void clear_proc(void) { system("clear"); } void pwd_proc(void) { printf("%s\n", g_cwd); } void cd_proc(void) { char *dir; if (g_nparams > 2) { printf("too many arguments!\n"); return; } if (g_nparams == 1) { if ((dir = getenv("HOME")) == NULL) exit_sys("fatal error (getenv"); } else dir = g_params[1]; if (chdir(dir) == -1) { printf("%s\n", strerror(errno)); return; } if (getcwd(g_cwd, PATH_MAX) == NULL) exit_sys("fatal error (getcwd)"); } void umask_proc(void) { mode_t mode; int argval; if (g_nparams > 2) { printf("too many arguments in umask command!...\n"); return; } if (g_nparams == 1) { mode = umask(0); umask(mode); printf("%04o\n", (int)mode); return; } if (!check_umask_arg(g_params[1])) { printf("%s octal number out of range!...\n", g_params[1]); return; } sscanf(g_params[1], "%o", &argval); umask(argval); } int check_umask_arg(const char *str) { if (strlen(str) > 4) return 0; for (int i = 0; str[i] != '\0'; ++i) if (str[i] < '0' || str[i] > '7') return 0; return 1; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde open, close, read, wri,te ve lseek fonksiyonlarının yanı sıra pek çok yardımcı dosya fonksiyonları da vardır. Bu yardımcı dosya fonksiyonları dosyalar üzerinde bazı önemli işlemleri yapmaktadır. Bu bölümde bu fonlksiyonların önemli olanlarını tanıtacağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dosyaya ilişkin bilgileri elde etmek için stat, lstat ve fstat isimli üç fonksiyon kullanılmaktadır. Bu fonksiyonlar aslında aynı şeyi yaparlar. Fakat parametrik yapı bakımından ve semantik bakımdan bunların arasında küçük farklılıklar 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 fonksiyonları bir dosyanın bilgilerini elde etmek amacıyla kullanılmaktadır. Örneğin dosyanın erişim hakları, kullanıcı ve grup id'leri, dosyanın uzunluğu, dosyanın tarih zaman bilgileri bu stat fonksiyonlarıyla elde edilmektedir. ls komutu -l seneği ile kullanıldığında aslında dosya bilgilerini bu stat fonksiyonuyla elde edip ekrana yazdırmaktadır. stat fonksiyonlarından en çok kullanılanı stat fonksiyonudur: int stat(const char *path, struct stat *buf); Fonksiyonun birinci parametresi bilgisi elde edilecek dosyanın yol ifadesini belirtmektedir. İkinci parametresi dosya bilgilerinin yerleştirileceği struct stat isimli bir yapı nesnesinin adresini almaktadır. stat isimli yapı içerisinde bildirilmiştir. Fonksiyon başarı durumunda 0, başarısızlık durumunda -1 değerine geri döner. struct stat yapısının elemanları şöyledir: struct stat { dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* Inode number */ mode_t st_mode; /* File type and mode */ nlink_t st_nlink; /* Number of hard links */ uid_t st_uid; /* User ID of owner */ gid_t st_gid; /* Group ID of owner */ dev_t st_rdev; /* Device ID (if special file) */ off_t st_size; /* Total size, in bytes */ blksize_t st_blksize; /* Block size for filesystem I/O */ blkcnt_t st_blocks; /* Number of 512B blocks allocated */ /* Since Linux 2.6, the kernel supports nanosecond precision for the following timestamp fields. For the details before Linux 2.6, see NOTES. */ struct timespec st_atim; /* Time of last access */ struct timespec st_mtim; /* Time of last modification */ struct timespec st_ctim; /* Time of last status change */ #define st_atime st_atim.tv_sec /* Backward compatibility */ #define st_mtime st_mtim.tv_sec #define st_ctime st_ctim.tv_sec }; Yapının st_dev elemanı dosyanın içinde bulunduğu aygıtın aygıt numarasını belirtir. Genellikle programcılar bu bilgiye gereksinim duymazlar. dev_t türü herhangi bir tamsayı türü biçiminde typedef edilebilecek bir tür ismidir. stat fonksiyonları dosya bilgilerini aslında diskten elde etmektedir. UNIX/Linux sistemlerinde kullanılan dosya sistemlerinin disk organşizasyonunda i-node tablosu denilen bir tablo vardır. i-node tablosu i-node elemanlarından oluşmaktadır. Her i-node elemanı bir dosyaya ilişkin bilgileri tutar. İşte bir dosyanın bilgilerinin hangi i-node elemanında olduğu stat yapısının st_ino elemanına yerleştirilmektedir. Dosyanın i-node elemanı i-node tablosunda bir indeks belirtmektedir. Dosyaların i-node numarları ls komutunda -i seçeneği ile gösterilmektedir. ino_t türü işaretsiz olmak koşuluyla herhangi bir tamsayı türü biçiminde typedef edilebilmektedir. Yapının st_mode elemanı dosyanın erişim bilgilerini ve türünü içermektedir. Yine bu elemanın içerisindeki değerler bitler biçiminde oluşturulmuştur. 1 olan bitler ilgili özelliğin olduğunu belirtmektedir. Belli bir erişim hakkının (örneğin S_IWGRP gibi) olup olmadığını anlamak için programcı ilgili bitin set edilip edilmediğine st_mode & S_IXX işlemi ile bakmalıdır. Dosyanın türü de yine aynı elemanın içerisine bitsel olarak kodlanmıştır. Ancak hangi bitlerin hangi türleri belirttiği POSIX standartlarında belirtilmemiştir. Bu durum sistemden sisteme değişebilmektedir. (Anımsanacağı gibi eskiden aynı durum S_IXXX sembolik sabitleri için de geçerliydi. Ancak daha sonra bu sembolik sabitlerinyonları sayısal değerleri yani bit pozisyonları POSIX standartlarında belirlendi.) Dosyanın türünü anlamak için iki yöntem bulunmaktadır. Birincisi içerisindeki S_ISXXX biçimindeki makroları kullanmaktır. Bu makrolar eğer dosya ilgili türdense sıfır dışı bir değer ilgili türden değilse sıfır değerini verir. Makrolar şunlardır: S_ISBLK(m) Blok aygıt sürücü dosyası mı? (ls -l'de 'b' dosya türü) S_ISCHR(m) Karakter aygıt sürücü dosyası mı? (ls -l'de 'c' dosya türü) S_ISDIR(m) Dizin dosyası mı? (ls -l'de 'd' dosya türü) S_ISFIFO(m) Boru dosyası mı? (ls -l'de 'p' dosya türü) S_ISREG(m) Sıradan bir disk dosyası mı? (ls -l'de '-' dosya türü) S_ISLNK(m) Sembolik bağlantı dosyası mı? (ls -l'de 'l' dosya türü) S_ISSOCK(m) Soket dosyası mı? (ls -l'de 's' dosya türü) Dosya türünün tespiti için ikinci yöntem st_mode içerisindeki dosya tür bitlerinin S_IFMT sembolik sabiti ile bit AND işlemi ile elde edilip aşağıdaki sembolik sabitlerle karşılaştırılmasıdır. S_IFBLK Blok aygıt dosyası S_IFCHR Karakter aygıt dosyası S_IFIFO Boru dosyası S_IFREG Sıradan disk dosyası S_IFDIR Dizin dosyası S_IFLNK Sembolik bağlantı dosyası S_IFSOCK Soket dosyası st_mode değeri S_IFMT değeri ile bir AND işlemine sokulduktan sonra bu sembolik sabitlerle karşılaştırılmalıdır. Bu sembolik sabitlerin tek biti 1 değildir. Yani karşılaştırma (mode & S_IFMT) == S_IFXXX biçiminde yapılmalıdır. Yapının st_nlink elemanı dosyanın "hard link" sayısını belirtmektedir. Hard link kavramı ileride ele alınacaktır. nlink_t türü bir tamsayı türü olmak koşuluyla herhangi bir tür olarak typedef edilebilmektedir. Yapının st_uid elemanı dosyanın kullanıcı id'sini belirtmektedir. Tabii ls -l komutu bu id'yi sayı olarak değil /etc/passwd dosyasına başvurarak isim biçiminde yazdırmaktadır. uid_t türü herhangi bir tamsayı türü olarak typedef edilebilmektedir. Yapının st_gid elemanı dosyanın grup id'sini belirtmektedir. Tabii ls -l komutu bu id'yi sayı olarak değil /etc/group dosyasına başvurarak isim biçiminde yazdırmaktadır. ugid_t türü herhangi bir tamsayı türü olarak typedef edilebilmektedir. Yapının st_rdev elemanı eğer dosya bir aygıt dosyası ise temsil ettiği atgıtın numarasını bize vermektedir. Bu eleman da dev_t türündedir. Yapının st_size elemanı dosyanın uzunluğunu bize vermektedir. off_t türü daha önceden de belittiğimiz gibi işaretli bir tamsayı türü biçiminde typedef edilmek zorundadır. Yapının st_blksize elemanı dosyanın içinde bulunduğu dosya sisteminin kullandığı blok uzunluğunu belirtmektedir. Dosyaların parçaları diskte "block" denilen ardışıl byte topluluklarında tutulmaktadır. İşte bir bloğun kaç byte olduğu bilgisi bu elemanla belirtilmektedir. Aynı zamanda programcılar dosya kopyalama gibi işlemlerde bu büyüklüğü tampon büyüklüğü (buffer size) olarak da kullanmaktadır. blksize_t işaretli bir tamsayı türüolarak typedef edilmek zorundadır. Yapının st_blocks elemanı dosyanın diskte kapladığı blok sayısını belirtmektedir. (Ancak buradaki sayı 512 byte'lık blokların sayısıdır. Yani dosya sistemindeki dosyanın parçaları olan bloklara ilişkin sayı değildir.) blkcnt_t işaretli bir tamsayı türü olarak typedef edilmek zorundadır. UNIX/Linux sistemlerinde kullanılan i-node tabanlı dosya sistemleri bir dosya için üç zaman bilgisi tutmaktadır: 1) Dosyanın son değiştirilme zamanı 2) Dosyanın son okunma zamanı 3) Dosyanın i-node bilgilerinin son değiştirilme zamanı POSIX standartları hangi POSIX fonksiyonlarının hangi zamanları dosya için güncellediğini belirtmektedir. Örneğin read fonksiyonu dosyanın son okuma zamanını, write fonksiyonu son yazma ve i-node bilgilerinin değiştirilme zamanını güncellemektedir. stat yapısının bu zamanı tutan elemanları eski POSIX standartlarında time_t türündendi ve isimleri st_atime, st_mtime ve st_ctime biçimindeydi. Bu elemanlar epoch olan 01/01/1970'ten geçen saniye sayısını tutuyordu (C Programlama Dili'nde epoch'un 01/01/1970 olması zorunlu değildir. Ancak POSIX standartlarında bu zorunludur.) Ancak daha sonra POSIX standartlarında bu zaman bilgisini nanosaniye çözünürlüğe çektiler. Dolayısıyla zamansal bilgiler time_t türü ile değil timespec bir yapıyla belirtilmeye başlandı. Yapı elemanlarının isimleri de st_atime, st_mtim ve st_ctim olarak değiştirildi. timespec yapısı geçmişe doğru uyumu koruyabilmek için aşağıdaki gibi bildirilmiştir: struct timespec { time_t tv_sec; long tv_nsec; }; Yapının tv_sec elemanı yine 01/01/1970'ten geçen saniye saniye sayısını tv_nsec elemanı ise o saniyeden sonraki nano saniye sayısını tutmaktadır. Sistemlerin çoğu POSIX standartlarında bu konuda değişiklik yapılmış olsa da eski doğru uyumu şöyle korumuştur: struct stat { ... struct timespec st_atim; /* Time of last access */ struct timespec st_mtim; /* Time of last modification */ struct timespec st_ctim; /* Time of last status change */ #define st_atime st_atim.tv_sec /* Backward compatibility */ #define st_mtime st_mtim.tv_sec #define st_ctime st_ctim.tv_sec }; Bu durumda programcı sisteminin yeni POSIX standartlarını destekleyip desteklemediğine bakmalı ve duruma göre yapının eski ya da yeni elemanlarını kullanmalıdır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 13. Ders 04/12/2022 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda dosya bilgilerini stat fonksiyonu ile alıp yazdıran bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); void disp_mode(mode_t mode); int main(int argc, char *argv[]) { struct stat finfo; struct tm *pt; if (argc == 1) { fprintf(stderr, "file(s) must be specified!\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) { if (stat(argv[i], &finfo) == -1) exit_sys("stat"); printf("i-node no: %llu\n", (unsigned long long)finfo.st_ino); printf("file mode: "); disp_mode(finfo.st_mode); printf("number of hard links: %llu\n", (unsigned long long)finfo.st_nlink); printf("user id: %llu\n", (unsigned long long)finfo.st_uid); printf("group id: %llu\n", (unsigned long long)finfo.st_gid); printf("file size: %lld\n", (long long)finfo.st_size); printf("file block size: %lld\n", (long long)finfo.st_blksize); printf("number of blocks: %lld\n", (long long)finfo.st_blocks); pt = localtime(&finfo.st_mtim.tv_sec); printf("last modification: %02d/%02d/%04d %02d:%02d:%02d\n", pt->tm_mday, pt->tm_mon + 1, pt->tm_year + 1900, pt->tm_hour, pt->tm_min, pt->tm_sec); pt = localtime(&finfo.st_atim.tv_sec); printf("last access (read): %02d/%02d/%04d %02d:%02d:%02d\n", pt->tm_mday, pt->tm_mon + 1, pt->tm_year + 1900, pt->tm_hour, pt->tm_min, pt->tm_sec); pt = localtime(&finfo.st_ctim.tv_sec); printf("last i-node changed: %02d/%02d/%04d %02d:%02d:%02d\n", pt->tm_mday, pt->tm_mon + 1, pt->tm_year + 1900, pt->tm_hour, pt->tm_min, pt->tm_sec); if (argc > 2) printf("-----------------\n"); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void disp_mode(mode_t mode) { static mode_t modes[] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; static mode_t ftypes[] = {S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK}; for (int i = 0; i < 7; ++i) if ((mode & S_IFMT) == ftypes[i]) { putchar("bcp-dls"[i]); break; } /* alternatifi if (S_ISBLK(mode)) putchar('b'); else if (S_ISCHR(mode)) putchar('c'); else if (S_ISDIR(mode)) putchar('d'); else if (S_ISFIFO(mode)) putchar('p'); else if (S_ISREG(mode)) putchar('-') else if (S_ISLNK(mode)) putchar('l'); else if (S_ISSOCK(mode)) putchar('s'); else putchar('?'); */ for (int i = 0; i < 9; ++i) putchar(mode & modes[i] ? "rwx"[i % 3] : '-'); putchar('\n'); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte get_ls isimli fonksiyon bizden stat yapısını ve dosyanın ismini alarak char türden static bir dizinin içerisine dosya bilgilerini ls -l formatında kodlamaktadır. Ancak biz henüz kullanıcı id'sini ve grup id'sini /etc/passwd ve /etc/group dosyalarına başvurarak isimlere dönüştürmedik. Buı nedenle bu örnekte dosyaların kullanıcı ve grup id'leri yazı olarak değil sayı olarak kodlanmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #define LS_BUFSIZE 4096 void exit_sys(const char *msg); char *get_ls(struct stat *finfo, const char *name); int main(int argc, char *argv[]) { struct stat finfo; struct tm *pt; if (argc == 1) { fprintf(stderr, "file(s) must be specified!\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) { if (stat(argv[i], &finfo) == -1) exit_sys("stat"); printf("%s\n", get_ls(&finfo, argv[i])); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } char *get_ls(struct stat *finfo, const char *name) { static char buf[LS_BUFSIZE]; static mode_t modes[] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; static mode_t ftypes[] = {S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK}; int index; struct tm *ptime; index = 0; for (int i = 0; i < 7; ++i) if ((finfo->st_mode & S_IFMT) == ftypes[i]) { buf[index++] = "bcp-dls"[i]; break; } for (int i = 0; i < 9; ++i) buf[index++] = finfo->st_mode & modes[i] ? "rwx"[i % 3] : '-'; ptime = localtime(&finfo->st_mtim.tv_sec); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_nlink); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_uid); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_uid); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_size); index += strftime(buf + index, LS_BUFSIZE, " %b %2e %H:%M", ptime); sprintf(buf + index, " %s", name); return buf; } /*-------------------------------------------------------------------------------------------------------------------------- fstat fonksiyonu stat fonksiyonunun yol ifadesi değil dosya betimleyicisi alan biçimidir. Prototipi şöyledir: int fstat(int fd, struct stat *buf); Genel olarak işletim sisteminin dosya betimleyicisinden hareketle i-node bilgilerine erişmesi yol ifadesinden hareketle erişmesinden daha hızlı olmaktadır. Çünkü open fonksiyonunda zaten open dosyanın i-node bilgilerine erişip onu dosya nesnesinin içerisine almaktadır. Tabii önce dosyayı açıp sonra fstat uygulamak anlamsız bir yöntemdir. Ancak zaten biz bir dosyayı başka amaçla açmışsak onun bilgilerini fstat ile daha hızlı elde edebiliriz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #define LS_BUFSIZE 4096 void exit_sys(const char *msg); char *get_ls(struct stat *finfo, const char *name); int main(int argc, char *argv[]) { int fd; struct stat finfo; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); /* burada dosyayla ilgili birtakım işlemler yapılıyor */ if (fstat(fd, &finfo) == -1) exit_sys("fstat"); printf("%s\n", get_ls(&finfo, "sample.c")); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } char *get_ls(struct stat *finfo, const char *name) { static char buf[LS_BUFSIZE]; static mode_t modes[] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; static mode_t ftypes[] = {S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK}; int index; struct tm *ptime; index = 0; for (int i = 0; i < 7; ++i) if ((finfo->st_mode & S_IFMT) == ftypes[i]) { buf[index++] = "bcp-dls"[i]; break; } for (int i = 0; i < 9; ++i) buf[index++] = finfo->st_mode & modes[i] ? "rwx"[i % 3] : '-'; ptime = localtime(&finfo->st_mtim.tv_sec); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_nlink); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_uid); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_uid); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_size); index += strftime(buf + index, LS_BUFSIZE, " %b %2e %H:%M", ptime); sprintf(buf + index, " %s", name); return buf; } /*-------------------------------------------------------------------------------------------------------------------------- Bir dosyayı işaret eden özel dosyalara "sembolik bağlantı dosyaları (symbolic link files)" denilmektedir. Sembolik bağlantı dosyaları aynı zamanda "soft link" dosyalar biçiminde de isimlendirilmektedir. Sembolik bağlantı dosyaları bir dosyayı işaret eden dosyalardır. Bunlar gerçek anlamda birer dosya değildir. Adeta bir "pointer" dosyadır. İşletim sistemleri sembolik bağlantı dosyaları için diskte yalnızca bir i-node elemanı tutmaktadır. Sembolik bağlantı dosyaları komut satırında ln -s komutuyla yaratılabilirler. Örneğin: $ ln -s x.dat y.dat Burada "x.dat" dosyanının "y.dat" isimli bir sembolik bağlantı dosyası oluşturulmuştur. ls -l komutunda sembolik bağlantı dosyaları ok işaretiyle gösterilmektedir. Örneğin: $ ls -l x.dat y.dat -rwxr-xr-x 1 kaan study 0 Kas 27 13:07 x.dat lrwxrwxrwx 1 kaan study 5 Ara 10 10:11 y.dat -> x.dat Sembolik bağlantı dosyaları "l" dosya türü ile gösterilmektedir. Bir sembolik bağlantı dosyası başka bir sembolik bağlantı dosyasını gösterebilir. Örneğin: $ ls -l x.dat y.dat z.dat -rwxr-xr-x 1 kaan study 0 Kas 27 13:07 x.dat lrwxrwxrwx 1 kaan study 5 Ara 10 10:11 y.dat -> x.dat lrwxrwxrwx 1 kaan study 5 Ara 10 10:48 z.dat -> y.dat Sembolik bağlantı dosyaları yaratıldığında erişim hakları otomatik olarak "lrwxrwxrwx" biçiminde oluşturulmaktadır. Sembolik bağlantı dosyalarının kendi erişim haklarının bir önemi yoktur. Bu dosyaların kendi erişim haklarısistem tarafından herhangi bir biçimde kullanılmamaktadır. open gibi POSIX fonksiyonlarının pek çoğu sembolik bağlantı dosyalarında bağlantıyı izlemektedir. Yani örneğin biz open fonksiyonu ile bir sembolik bağlantı dosyasını açmaya çalışsak open fonksiyonu o dosyayı değil o dosyanın gösterdiği dosyayı açmaya çalışır. Yukarıdaki örnekte biz "z.dat" dosyasını açmak istesek aslında "x.dat" dosyası açılacaktır. Bu durum ileride ele alacağımız POSIX fonksiyonlarının hemen hepsinde böyledir. Ancak lstat fonksiyonu istisnalardan biridir. Bir dosya fonksiyonuna yol ifadesi olarak sembolik bağlantı dosyası verildiğinde fonksiyon (lstat dışındaki fonksiyonlar) sembolik bağlantıyı izlemektedir. Ancak bu izleme sırasında bir döngü oluşabilir. Örneğin a sembolik bağlantı dosyası b sembolik bağlantı dosyasını, b sembolik bağlantı dosyası da c sembolik bağlantı dosyasını gösteriyor olabilir. c sembolik bağlantı dosyası da yeniden a sembolik bağlantı dosyasını gösteriyor olabilir. Böyle bir işlemde sonsuz döngü söz konusu olmaktadır. İşte dosya fonksiyonları bu durumu da dikkate alır ve böylesi bir döngüsellik varsa başarısızlıkla geri döner. Bu başarısızlık durumunda errno değeri ELOOP biçiminde set edilmektedir. Aslında POSIX sistemlerinde işletim sistemi tarafından belirlenmiş maksimum link izleme sayısı vardır. Bu sayı aşıldığında ilgili fonksiyomn başarısız olup errno değişkeni ELOOP değeri ile set edilmektedir. (POSIX standartlarında maksimum link izleme değeri içerisinde SYMLOOP_MAX sembolik sabitiyle belirtilmektedir. Ancak bu sembolik sabit define edilmiş olmak zorunda değildir. Ayrıca POSIX sistemlerinde olabilecek en düşük sembolik link izleme sayısı da _POSIX_SYMLOOP_MAX (8) değeri ile belirlenmiştir.) Yani aslında sembolik bağlantıların döngüye genellikle girmesi maksimum sayacın aşılması ile anlaşılmaktadır. Bir sembolik bağlantı dosyasının gösterdiği dosya silinirse burada tuhaf bir durum oluşur. İşte bu tür durumlarda bu sembolik bağlantı dosyası kullanıldığında (örneğin open fonksiyonuyla açılmaya çalışıldığında) sanki dosya yokmuş gibi bir hata oluşurb (ENOENT). Çünkü bağlantının işaret ettiği bir dosya bulunmamaktadır. Windows sistemlerinde sembolik bağlantı dosyalarının bir benzerleri "kısayol (shortcut)" dosyalar biçiminde karşımıza çıkmaktadır. lstat fonksiyonu ile stat fonksiyonu arasındaki tek fark eğer stat bilgisi elde edilecek dosya bir sembolik bağlantı dosyası ise stat fonksiyonun bu bağlantının gösterdiği dosyanın bilgisini alması ancak lstat fonksiyonun sembolik bağlantı dosyasının kendi bilgisini almasıdır. Diğer dosyalar için bu fonksiyon arasında bir farklılık yoktur. Örneğin: $ ls -l sample.c x -rw-rw-r-- 1 parallels parallels 1748 Dec 4 13:46 sample.c lrwxrwxrwx 1 parallels parallels 8 Dec 4 13:47 x -> sample.c Burada "x" bir sembolik bağlantı dosyasıdır ve bu dosya "sample.c" dosyasını göstermektedir. İşte biz "x" dosyasının stat bilgilerini stat fonksiyonu ile almaya çalışırsak stat bize aslında "sample.c" dosyasının bilgilerini verir. Ancak biz "x" dosyasının stat bilgilerini lstat fonksiyonu ile alırsak lstat bize "x" dosyasının kendi bilgisini verir. Aşağıdaki örnekte sembolik bağlantı dosyasının lstat ve stat fonksiyonlarıyla stat bilgileri alınmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #define LS_BUFSIZE 4096 void exit_sys(const char *msg); char *get_ls(struct stat *finfo, const char *name); int main(int argc, char *argv[]) { struct stat finfo; struct tm *pt; if (argc == 1) { fprintf(stderr, "file(s) must be specified!\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) { if (lstat(argv[i], &finfo) == -1) exit_sys("stat"); printf("%s\n", get_ls(&finfo, argv[i])); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } char *get_ls(struct stat *finfo, const char *name) { static char buf[LS_BUFSIZE]; static mode_t modes[] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; static mode_t ftypes[] = {S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK}; int index; struct tm *ptime; index = 0; for (int i = 0; i < 7; ++i) if ((finfo->st_mode & S_IFMT) == ftypes[i]) { buf[index++] = "bcp-dls"[i]; break; } for (int i = 0; i < 9; ++i) buf[index++] = finfo->st_mode & modes[i] ? "rwx"[i % 3] : '-'; ptime = localtime(&finfo->st_mtim.tv_sec); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_nlink); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_uid); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_gid); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_size); index += strftime(buf + index, LS_BUFSIZE, " %b %2e %H:%M", ptime); sprintf(buf + index, " %s", name); return buf; } /*-------------------------------------------------------------------------------------------------------------------------- Bir dosyanın stat bilgilerini görüntülemek için stat isimli kabuk komutu da bulundurulmuştur. Tabii bu komut aslında stat ve lstat POSIX fonksiyonlarını çağırarak elde ettikleri bilgileri yazdırmaktadır. Örneğin: $ stat sample.c File: sample.c Size: 329 Blocks: 8 IO Block: 4096 normal dosya Device: 805h/2053d Inode: 1207667 Links: 2 Access: (0644/-rw-r--r--) Uid: ( 1000/ kaan) Gid: ( 1001/ study) Access: 2022-12-10 10:59:52.700330245 +0300 Modify: 2022-12-10 10:59:46.620211508 +0300 Change: 2022-12-10 11:41:11.151049064 +0300 ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 14. Ders 10/12/2022 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dosyayı silmek için remove ve unlink isimli fonksiyonlar kullanılmaktadır. remove bir standart C fonksiyonudur. unlink ise bir POSIX fonksiyonudur. Bu iki fonksiyon tamamen aynı şeyi yapmaktadır. Fonksiyonların prototipleri şöyledir: #include int remove(const char *path); #include int unlink(const char *path); Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. remove ve unlink fonksiyonlarıyla bir dosyayı silebilmek için prosesin dosyanın kendisine "w" hakkının olması gerekmez. Ancak dosyanın içinde bulunduğu dizine "w" hakkının olması gerekir. Bizim eğer dosyanın içinde bulunduğu dizine "w" hakkımız varsa dosyanın sahibi olmasak bile dosyayı silebiliriz. Tabii proses id'si 0 olan prosesler herhangi bir kontrol uygulamadan bu silme işlemini yapabilirler. Bir dosya unlink ya da remove fonksiyonlarıyla silindiğinde dizin girişi silinir. Ancak dosyanın kendisi dosyanın hard link sayacı 0'a düşmüşse silinmektedir. Ynai unlink ve remove fonksiyonları dosyayı dizin girişinden silerler. Sonra dosyanın hard link sayacını 1 eksiltirler. Eğer hard link sayacı 0'a düşmüşse dosyayı fiziksel olarak silerler. HArd link sayacının ne anlama geldiği ileride ele alınacaktır. Aşağıdaki örnekte komut satırından verilen yol ifadelerine ilişkin dosyalar silinmeye çalışılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(int argc, char *argv[]) { if (argc == 1) { fprintf(stderr, "file name(s) must be specified!...\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) if (unlink(argv[i]) == -1) perror("unlink"); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi aslında "dizinler" birer dosya gibi organize edilmiştir. Dizin dosyalarının içerisinde "dizin girişleri (directory entries)" bulunmaktadır. Bir dizin girişinin formatı dosya sisteminden dosya sistemine değişebilmektedir. Ancak özet olarak bir dizin dosyasının içeriği şöyledir: Dizi Dosyası ------------- dosya_ismi i-node no dosya_ismi i-node no dosya_ismi i-node no ... dosya_ismi i-node no dosya_ismi i-node no dosya_ismi i-node no Dosyaların asıl bilgileri (yani stat fonksiyonuyla elde ettiğimiz bilgiler) Diskte "I-Node Block" denilen bir bölgede saklanmaktadır. I-Node Block i-node elemanlarından oluşur. Her i-node elemanına ilk eleman 0 olmak üzere artan sırada bir numara karşı düşürülmüştür. İşletim sistemi bir dosya ile ilgili işlem yaparken kesinlikle o dosyanın i-node elemanına erişmek ve oradaki bilgileri kullanmak zorundadır. Bir dosya unlink ya da remove fonksiyonlarıyla silindiğinde kesinlikle dizin girişi silinmektedir. Ancak dosyanın silinip silinmeyeceği hard-link sayacına bağlıdır. Farklı dizin girişleri farklı isimlerle aynı i-node numaralarını işaret ediyorsa buna "hard link" denilmektedir. Örneğin: Dizin Dosyası -------------- a.txt 12345678 b.txt 12345678 ... Burada bizim open fonksiyonuyla "a.txt" ya da "b.txt" dosyalarını açmamız arasında hiçbir farklılık yoktur. Çünkü dosyanın bütün bilgileri i-node elemanının içerisindedir. İşte biz bu dosyalardan örneğin "a.txt" dosyasını silersek aslında yalnızca dizin girişini silmiş oluruz. Çünkü işletim sistemi "a.txt" dosyasının işaret ettiği i-node elemanının başka bir giriş tarafından kullanıldığını gördüğü için i-node elemanını ve dosyanın diskteki varlığını silmez. İşte bu durum "har link sayacı" ile kontrol edilmektedir. Yukarıdaki örnekte dosyanın hard link sayacı 2'dir. Biz bu dizin girişlerinden birini sildiğimizde hard link sayacı 1'e düşer. Diğerini de sildiğimizde hard link sayacı 0'a düşer ve dosya gerçekten silinir. Bir dosyanın hard link'ini oluşturmak için ln kabuk komutu kullanılmaktadır. Örneğin: $ ln sample.c mample.c $ ls -li sample.c mample.c 1207667 -rw-r--r-- 2 kaan study 329 Ara 10 10:59 mample.c 1207667 -rw-r--r-- 2 kaan study 329 Ara 10 10:59 sample.c Dosyanın hard link sayacının 2 olduğuna dikkat ediniz. Bir dizin yaratıldığında onun içerisinde "." ve ".." biçiminde iki dizin girişi otomatik olarak yaratılmaktadır. (UNIX/Linux sistemlerinde başı "." ile başlayan dizin girişleri ls komutunda default olarak görüntülenmemektedir. Bunların görüntülenmesi için -a (all) seçeneğinin de kullanılması gerekir.) "." dizin girişi kendi dizin dosyasının i-node elemanını ".." dizin girişi ise üst dizinin i-node elemanını göstermektedir. Bu nedenle bir dizin yaratıldığında dizin dosyasına ilişkin hard-link sayacı 2 olur. O dizinin içerisinde yaratılan her dizin ".." girişini içereceğinden dolayı o dizinin hard link sayacını artıracaktır. Belli bir i-node elemanını gösteren dizin girişlerinin elde edilmesine yönelik bu sistemlerde pratik bir yol yoktur. Yapılacak şey diskteki tüm dosyaları gözden geçirip i-node numaralarından onların aynı i-node elemanını gösterip göstermediğini anlamaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi dosya bilgileri disk üzerinde i-node bloktaki i-node elemanının içerisinde tutulmaktadır. stat fonksiyonları erişim bilgilerini buradan almaktadır (ls komutu da stat fonksiyonları kullanılarak yazılmıştır). Dosyanın erişim hakları yine anımsayacağınız gibi open fonksiyonunda dosya yaratılırken belirlenmektedir. İşte bir dosyanın erişim haklarını dosyanın içine dokunmadan chmod isimli POSIX fonksiyonu ile değiştirebiliriz. Fonksiyonun prototipi şöyledir: #include int chmod(const char *path, mode_t mode); Fonksiyonun birinci parametresi dosyanın yol ifadesini, ikinci parametresi erişim haklarını belirtmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Erişim hakları 2008 stnadralarına kadar S_IXXX sembolik sabitleriyle oluşturulmak zorundaydı. Ancak 2008 ve sonrasında artık bu S_IXXX sembolik sabitlerinin değerleri belirlendiği için programcı doğrudan octal bir sayı biçiminde bu erişim haklarını verebilir. Fakat tavsiye edilen yine S_IXXX sembolik sabitlerinin kullanılmasıdır. Bir dosyanın erişim haklarını chmod fonksiyonuyla değiştirebilmek için prosesin etkin kullanıcı id'sinin dosyanın kullanıcı id'si ile aynı olması ya da prosesin etkin kullanıcı id'sinin 0 olması gerekmektedir. Dosyanın dördüncü 3 btilik S_ISUID, S_ISGID ve S_ISVTX erişim hakları da bu fonksiyonla set edilmeye çalışılabilir. Ancak bazı sistemler S_ISUID ve S_ISGID erişim haklarını değiştirmeye izin vermeyebilmektedir. chmod POSIX fonksiyonu prosesin umask değerini dikkate almamaktadır. Yani fonksiyonda belirttiğimiz erişim haklarının hepsi dosyaya yansıtılmaktadır. Aşağıdaki girilen octal digitlerle dosyaların erişim haklarını değiştiren bir örnek verilmiştir. Bu örnekte doğrudan chmod fonksiyonunda bitmask değerler sayısal olarak kullanılmıştır. Bu durumun eski sistemlerde sorunlu olabileceğini bir kez daha vuruguluyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include int check_mode(const char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int mode; if (argc < 3) { fprintf(stderr, "too few parameters!...\n"); exit(EXIT_FAILURE); } if (!check_mode(argv[1])) { fprintf(stderr, "invalid mode: %s\n", argv[1]); exit(EXIT_FAILURE); } sscanf(argv[1], "%o", &mode); for (int i = 2; i < argc; ++i) if (chmod(argv[i], mode) == -1) fprintf(stderr, "cannot change mode: %s\n", argv[1]); return 0; } int check_mode(const char *str) { if (strlen(str) > 4) return 0; for (int i = 0; str[i] != '\0'; ++i) if (str[i] < '0' || str[i] > '7') return 0; return 1; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte eski POSIX standartları da dikkate alınarak mode bilgisi S_IXXX sembolik sabitlerinin bit düzeyinde OR'lanması ile oluşturulmuştur. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include int check_mode(const char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int modeval; mode_t modes[] = {S_ISUID, S_ISGID, S_ISVTX, S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; mode_t mode; if (argc < 3) { fprintf(stderr, "too few parameters!...\n"); exit(EXIT_FAILURE); } if (!check_mode(argv[1])) { fprintf(stderr, "invalid mode: %s\n", argv[1]); exit(EXIT_FAILURE); } sscanf(argv[1], "%o", &modeval); mode = 0; for (int i = 11; i >= 0; --i) if (modeval >> i & 1) mode |= modes[11 - i]; for (int i = 2; i < argc; ++i) if (chmod(argv[i], mode) == -1) fprintf(stderr, "cannot change mode: %s\n", argv[1]); return 0; } int check_mode(const char *str) { if (strlen(str) > 4) return 0; for (int i = 0; str[i] != '\0'; ++i) if (str[i] < '0' || str[i] > '7') return 0; return 1; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- chmod POSIX fonksiyonunun yanı sıra bir de dosya betimleyicisi ile çalışan fchmod fonksiyonu vardır. Eğer dosyayı zaten açmışsak chmod yerine fchmod fonksiyonu daha hızlı bir çalışma sunmaktadır. Fonksiyonun prototipi şöyledir: #include int fchmod(int fd, mode_t mode); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dosyanın erişim haklarını değiştirmek için chmod isimli bir kabuk komutu da bulunmaktadır. Bu kabuk komutu tabii chmod POSIX fonksiyonu kullanılarak yazılmıştır. Bu kabuk komutunun kullanımının birkaç biçimi vardır. Tipik olarak komutta erişim hakları octal digitlerle belirtilmektedir. Örneğin: chmod 664 a.txt b.txt Burada 664'ün bit karşılığı şöyledir: 110 110 100. Bu erişim hakları olarak şu anlama gelmektedir: rw-rw-r--. Komutun ikinci kullanımı + ve -'li kullanımıdır. Örneğin: chmod +w a.txt Burada "a.txt" dosyasının "owner", "group" ve "other" "w" hakkı eklemektedir. Komutta "-" ilgili hakkın çıkartılacağınıbelirtmektedir. Bunların önüne u, g, o ya da a harfleri getirilebilir. Örneğin: chmod o+w a.txt Burada yalnızca "other" için "w" hakkı eklenmiştir. a hepsine anlamına gelir. Örneğin: chmod a-w a.txt Burada owner, group ve other için "w" hakları silinmiştir. Tabii birden fazlası kombine edilebilir. Örneğin: chmod 0 a.txt chmod ug+rw a.txt Komutta octal sayı belirtilirse umask etkili olmaz. Ancak octal sayı yerine ugua ve rwx belirtilirse bu durumda kabuğun umask değeri etkili olmaktadır. Komutun başka ayrıntıları da vardır. Bunun için ilgili dokimanlara başvurabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dosyanın kullanıcı id'si ve grup id'si dosya yaratılırken belirleniyordu. Ancak programcı isterse dosyanın kullanıcı id'sini ve grup id'sini chown ya da fchown isimli POSIX fonksiyonları ile değiştirebilir. Fonksiyonların prototipleri şöyledir: #include int chown(const char *path, uid_t owner, gid_t group); int fchown(int fd, uid_t owner, gid_t group); chown fonksiyonun birinci parametresi dosyanın yol ifadesini ikinci parametresi değiştirilecek kullanıcı id'sini ve üçüncü parametresi de değiştirilecek grup id'sini belirtmektedir. fchown fonksiyonu chown fonksiyonunun dosya betimleyicisi ile çalışan biçimidir. Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Bir dosyanın kullanıcı ve grup id'lerinin değiştirilmesi kötüye kullanıma açık bir durum oluşturabilmektedir. (Yani örneğin "kaan" kullancısı kendi dosyasını sanki "ali" kullanıcısının dosyayıymış gibi gösterirse burada bir kötü niyet de söz konusu olabilir.) Bu nedenle bu fonksiyonun kullanımı üzerinde bazı kısıtlar vardır. Şöyle ki: 1) Eğer prosesin etkin kullanıcı id'si dosyanın kullanıcı id'si ile aynı ise bu durumda chown fonksiyonu dosyanın grup id'sini kendi grup id'si olarak ya da ek gruplarının (supplemantary groups) birinin id'si olarak eğiştirebilmektedir. Ancak dosyanın kullanıcı id'sinin değiştirilmesi işletim sisteminin iznine bağlıdır. Modern sistemler bu izni vermemektedir. Ancak bazı eski sistemler bu izni vermektedir. Bu izin "change own restricted" ismiyle ifade edilmektedir. İlgili sistemin bu izni verip vermediği dosyası içerisindeki _POSIX_CHOWN_RESTRICTED sembolik sabitiyle derleme aşamasında sorgulanbilir. 2) Proses id'si 0 olan root prosesler (ya da uygun önceliğe sahip prosesler) her zaman dosyanın kullanıcı ve grup id'sini istedikleri gibi değiştirebilirler. (Yani biz bir dosyanın kullanıcı ve grup id'sini istediğimiz gibi değiştirmek istiyorsak programımızı "sudo" ile çalıştırmalıyız.) Fonksiyonlar ile yalnızca kullanıcı id'si ya da grup id'si değiştirilebilir. Bu durumda değiştirilmeyecek değer için -1 girilmelidir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Change on restricted durumu aşağıdaki gibi #ifdef komutuyla sorgulanabilir: #include #include int main(int argc, char *argv[]) { #ifdef _POSIX_CHOWN_RESTRICTED printf("chown restricted\n"); #else printf("chown not restricted\n"); #endif return 0; } Aşağıda chown fonksiyonun örnek bir kullanımını görüyorsunuz: ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { if (chown("test.txt", 1000, -1) == -1) exit_sys("chown"); printf("Ok\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 15. Ders 11/12/2022 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dosyanın kullanıcı ve grup id'lerini değiştirebilmek için chown isimli bir kabuk komutu da bulundurulmuştur. Komut aşağıdaki biçimlerde kullanılmaktadır: $ sudo chwon kaan:study test.txt $ sudo chown kaan test.txt $ sudo chown :study test.txt ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- truncate isimli POSIX fonksiyonu bir dosyanın boyutunu değiştirmek için kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int truncate(const char *path, off_t length); Fonksiyonun birinci parametresi dosyanın yol ifadesini almaktadır. İkinci parametresi dosyanın yeni uzunluğunu belirtir. Bu fonksiyon genellikle dosyanın sonundaki kısmı atarak onun boyutunu küçültmek amacıyla kullanılmaktadır. Burada belirtilen uzunluk dosyanın gerçek uzunluğundan küçükse dosyanın sonundaki ilgili kısım yok edilir ve dosya burada belirtilen uzunluğa getirilir. (Fonksiyonun ismi tipik olarak dosyaların küçültüleceği fikriyle "trunctate" olarak verilmiştir.) Biz truncate fonksiyonu ile dosyayı büyütmek de isteyebiliriz. Bu durumda dosya büyütülür ve büyütülen kısım 0'larla doldurulur. Bugünkü sistemlerde dosya sistemi "dosya deliklerini (file holes)" destekliyorsa; büyütme, delik (hole) oluşturularak yapılmaktadı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. Tabii truncate yapabilmek için prosesin dosyaya yazma hakkının olması olması gerekmektedir. truncate fonksiyonunun yol ifadesini alarak değil, dosya betimleyicisini alarak aynı işlemi yapan ftruncate isminde bir benzeri de vardır. Fonksiyonun prototipi şöyledir: #include int ftruncate(int fd, off_t length); Fonksiyonun birinci parametresi dosya betimleyicisini almaktadır. İkinci parametresi dosyanın yeni uzunluğunu belirtir.. 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. Tabii ftruncate yapabilmek için dosyanın yazma modunda açılmış olması gerekmektedir. Fonksiyonun işlev bakımından truncate fonksiyonundan hiçbir farkı yoktur. Aşağıdaki örnekte daha önce var olan "test.dat" dosyası 1000 byte uzunluğa çekilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("test.dat", O_RDWR)) == -1) exit_sys("open"); if (ftruncate(fd, 1000) == -1) exit_sys("open"); printf("success...\n"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- truncate işlemini yapan bir kabuk komutu da bulunmaktadır. Komut -s seçeneği ile dosyanın yeni uzunluğunu almaktadır. Örneğin: $ truncate -s 100 test.txt Dosya uzunluklarında uzunluğun sonuna birim belirten karakterler de eklenebilmektedir. Örneğin: $ truncate -s 100K test.txt Burada dosya 100K uzunluğuna çekilmektedir. Komutun diğer ayrıntıları için man sayfalarına başvurabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dizin (directory) yaratmak için mkdir isimli POSIX fonksiyonu kullanılmaktadır. Dizin yaratma işlemi open fonksiyonuyla yapılamamaktadır. mkdir fonksiyonunun prototipi şöyledir: #include int mkdir(const char *path, mode_t mode); Fonksiyonun birinci parametresi yaratılacak dizinin yol ifadesini, ikinci parametresi 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. Dizin yaratırken erişim haklarında 'x' hakkını bulundurmayı unutmayınız. Anımsanacağı gibi dizinlerde 'x' hakkı "içinden geçilebilirlik" anlamına geliyordu. mkdir fonksiyonu tıpkı open fonksiyonu gibi prosesin umask değerinden etkilenmektedir. O halde istediğiniz erişim haklarının hepsinin dizine yansıtılmasını istiyorsanız işin başında umask(0) çağrısıyla prosesinizin umask değerini sıfırlayabilirsiniz. Bir dizin yaratıldığında içerisinde "." ve ".." isminde iki dizin girişi bulunmaktadır. Daha önceden de belirtildiği gibi "." dizin girişi bulunulan dizinin i-node elemanını, ".." dizin girişi ise üst dizinin i-node elemanını işaret eder. Bu nedenle bir dizin yaratıldığında kendi dizininin ve üst dizinin har link sayaçları bir artırılmaktadır. Aşağıda komut satırından verilen isimle bir dizin yaratn örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (mkdir(argv[1], S_IRWXU|S_IRWXG|S_IRWXO) == -1) exit_sys("mkdir"); printf("success...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Komut satırında dizin yaratmak için "mkdir" isminde bir kabuk komutu da bulunmaktadır. Tabii bu komut mkdir POSIX fonksiyonu kullanılarak yazılmıştır. Komut default durumda umask değerinden etkilenir. Ancak -m ya da --mode seçeneği ile biz erişim haklarını octal basamaklar biçiminde belirtebilmekteyiz. Örneğin: mkdir xxx mkdir -m 777 yyy Dizinler için de hard link çıkartılabilmektedir. Ancak bu durum dizin ağacını dolaşan kodların sonsuz döngüye girmesine yol açabilmektedir. Bu nedenle dizinler için hard link çıkartmak yerine soft link (sembolik bağlantı) çıkartmak tercih edilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dizini silmek için unlink ya da remove fonksiyonları kullanılamaz. Dizin silmek için rmdir isimli özel bir POSIX fonksiyonu bulundurulmuştur. Fonksiyonun prototipi şöyledir: #include int rmdir(const char *path); Fonksiyon parametre olarak silinecek dizinin yol ifadesini alır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. rmdir fonksiyonu ile içinde dosya olan dizinler silinememektedir. Bu durum güvenlik amacıyla düşünülmüştür. İçi boş dizin demek içinde yalnızca "." ve ".." girişlerinin bulunduğu dizin demektir. Zaten UNIX/Linux, macOS ve Windows sistemlerinde bu iki özel dizin girişi silinememektedir. rmdir fonksiyonuna bir dizini işaret eden sembolik bağlantı dosyası verilirse fonksiyon bağlantıyı izlemez. Başarısız olur ve errno değeri ENOTDIR biçiminde set edilir. rmdir fonksiyonun başarılı olabilmesi için prosesin dizine yazma hakkına sahip olması gerekmez ancak dizinin içinde bulunduğu dizine yazma hakkına sahip olması gerekir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Komut satırından dizin silmek için "rmdir" isimli bir kabuk komutu da bulunmaktadır. Tabii bu komut aslıda "rmdir" POSIX fonksiyonu kullanılarak yazılmıştır. rmdir komutuyla dizin silmek için yine dizinin boş olması gerekir. İçi dolu dizinleri tek hamlede silmek için "rm" komutu "-r" seçeneği ile kullanılabilir. Örneğin: rm -r xxx ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi genel olarak pek çok UNIX/Linux sisteminde kullanıcılar hakkında bilgiler /etc/passwd ve /etc/group dosyalarında tutuluyordu. Bu dosyalardaki satırlar ':' ile ayrılmış olan alanlardan oluşmaktaydı. Bu dosyalar ve bunların formatları POSIX standartlarında belirtilmemiştir. Onun yerine POSIX standartlarında bu dosyalardan okuma yapan özel fonksiyonlar bulundurulmuştur. (Yani aslında bir POSIX sisteminde /etc/passwd ve /etc/group dosyaları bu isimlerde ve Linux'tak içerikte bulunmak zorunda değildir. Ancak bu bilgileri alan aşağıda açıklayacağımız POSIX fonksiyonları bulunmak zorundadır.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- /etc/passwd dosyası üzerinde parse işlemi yapan fonksiyonların prototipleri dosyası içerisinde bulundurulmuştur. getpwnam POSIX fonksiyonu bir kullanıcının ismini alarak o kullanıcı hakkında /etc/passwd dosyasında belirtilen bilgileri vermektedir. Fonksiyonun prototipi şöyledir: #include struct passwd *getpwnam(const char *name); Fonksiyon parametre olarak kullanıcı ismini almaktadır. Başarı durumunda o kullanıcıya ilişkin bilgileri barındıran statik düzeyde tahsis edilmiş olan struct passwd isimli bir yapı nesnesinin adresiyle geri dönmektedir. struct passwd yapısı şöyle bildirilmiştir: 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 */ }; Aslında bu yapının elemanları /etc/passwd dosyasındaki satır bilgilerinden oluşmaktadır. Aşağıda /etc/passwd dosyasından birkaç satır verilmiştir: ... kaan:x:1000:1001:Kaan Aslan,,,:/home/kaan:/bin/bash student:$6$EW3bJuIgtpIfgbdm$Sy4Z4XNdxgBrNlzc7cEnEJn2gp36XCvaIUqaH9p8ZZrtfF3qQZ7KTK7qpM4T54/p5Lck24ZknXC1EuXm2hnBm1:1001:1001:Student,,,:/home/student:/bin/ulak-shell ali:x:1001:1001::/home/ali:/bin/myshell veli:x:1002:1002::/home/veli:/bin/bash ... Yapının pw_nam elemanı kullanıcı ismini, pw_passwd elemanı parola bilgisini, pw_uid ve pw_gid elemanları login olunduğunda çalıştırılacak programa ilişkin prosesin gerçek ve etkin kullanıcı ve group id değerlerini pw_gecos yorum bilgisini (kullanıya ilişkin ek birtakım bilgileri, pw_dir login olunduğunda çalıştırılacak programa ilişkin prosesin çalışma dizinini ve pw_shell elemanı da login olunduğunda çalıştırılacak programı belirtmektedir.) getpwnam fonksiyonu iki nedenden dolayı başarısız olabilir. Birincisi belirtilen isme ilişkin bir kullanıcının /etc/passwd dosyası içerisinde bulunamamasıdır. İkincisi ise daha patolojik durumlardır. Yani bir IO hatası, /etc/passwd dosyasının silinmiş olması gibi. Programcının Eğer fonksiyon isme ilişkin bir kayıt bulamadıysa errno değerini değiştirmemektedir. Ancak diğer hatalı durumlarda errno değerini uygun biçimde set etmektedir. Dolayısıyla programcı bu tür durumlarda fonksiyonu çağırmadan önce errno değerini 0'a çeker. Sonra fonksiyon başarısız olduğunda errno değerine bakar. Eğer bu değer hala 0 ise fonksiyonun ilgili kullanıcı ismini bulamadığından dolayı başarısız olduğu anlaşılır. Aşağıdaki örnekte komut satırından ismi alınan kullanıcının /etc/passwd dosyasındaki bilgileri ekrana (stdout dosyasına) yazdırılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { struct passwd *pass; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } errno = 0; if ((pass = getpwnam(argv[1])) == NULL) { if (errno == 0) { fprintf(stderr, "user name cannot found!...\n"); exit(EXIT_FAILURE); } exit_sys("getpwnam"); } printf("User Name: %s\n", pass->pw_name); printf("Password: %s\n", pass->pw_passwd); printf("User id: %llu\n", (unsigned long long)pass->pw_uid); printf("Group id: %llu\n", (unsigned long long)pass->pw_gid); printf("Gecos: %s\n", pass->pw_gecos); printf("Current Working Directory: %s\n", pass->pw_dir); printf("Login Program: %s\n", pass->pw_shell); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- getpwuid fonksiyonu da getpwnam fonksiyonu gibidir. Yalnızca kullanıcı ismi ile değil kullanıcı id'si ile kullanıcı bilgilerini elde etmektedir. Fonksiyonun prototipi şöyledir: #include struct passwd *getpwuid(uid_t uid); Fonksiyon yine başarı durumunda statik düzeyde tahsis edilmiş olan struct passwd türünden yapı nesnesinin adresiyle, başarısızlık durumunda NULL adresle geri dönmektedir. Başarısızlığın nedeni kullanı id'sine ilişkin kullanıcının bulunamaması nedeni ile ise bu durumda fonksiyon errno değerini değiştirmemektedir. Aşağıdaki örnekte komut satırından verilen kullanıcı id'sine ilişkin kullanıcı bilgileri ekrana (stdout dosyasına) yazdırılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { struct passwd *pass; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } errno = 0; if ((pass = getpwuid(atoi(argv[1]))) == NULL) { if (errno == 0) { fprintf(stderr, "user name cannot found!...\n"); exit(EXIT_FAILURE); } exit_sys("getpwnam"); } printf("User Name: %s\n", pass->pw_name); printf("Password: %s\n", pass->pw_passwd); printf("User id: %llu\n", (unsigned long long)pass->pw_uid); printf("Group id: %llu\n", (unsigned long long)pass->pw_gid); printf("Gecos: %s\n", pass->pw_gecos); printf("Current Working Directory: %s\n", pass->pw_dir); printf("Login Program: %s\n", pass->pw_shell); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bazen programcı /etc/passwd dosyasındaki tüm kayıtları elde etmek isteyebilir. Bunun için getpwent, endpwent ve setpwend POSIX fonksiyonları bulundurulmuştur. Fonksiyonların prototipileri şöyledir: #include struct passwd *getpwent(void); void setpwent(void); void endpwent(void); getpwent fonksiyonu her çağrıldığında sıraki bir kullanıcının bilgisini verir. Fonksiyon /etc/passwd dosyasının sonuna gelindiğinde (yani artık bilgisi verilecek kullanıcı kalmadığında) NULL adrese geri döner. Tabii getpwent IO hatası nedeniyle de başarısız olabilir. Bu durumda errno değerini değiştirmez. Programcı bu sayede başarısızlığın nedenini anlayabilir. İşlem bitince endpwent fonksiyonu son kez çağrılmalıdır. (Bu fonksiyon arka planda muhtemelen /etc/passwd dosyasını kapatmaktadır.) Eğer dolaşım yeniden yapılacaksa setpwent fonksiyonu çağrılır. İlk dolaşımda setpwent fonksiyonun çağrılması gerekmemektedir. Aşağıdaki programda tüm kullanıcı bilgileri bir döngü içerisinde elde edilip ekrana (stdout dosyasına) yazdırılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { struct passwd *pass; while ((errno = 0, pass = getpwent()) != NULL) { printf("User Name: %s\n", pass->pw_name); printf("Password: %s\n", pass->pw_passwd); printf("User id: %llu\n", (unsigned long long)pass->pw_uid); printf("Group id: %llu\n", (unsigned long long)pass->pw_gid); printf("Gecos: %s\n", pass->pw_gecos); printf("Current Working Directory: %s\n", pass->pw_dir); printf("Login Program: %s\n", pass->pw_shell); printf("-----------------------------------------------------------\n"); } if (errno != 0) exit_sys("getpwent"); endpwent(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bilindiği gibi pek çok UNIX türevi sistemde grup bilgileri /etc/group isimli bir dosyada tutulmaktadır. (POSIX standartları grup bilgilerinin böyle bir dosyada tutulacağına yönelik bir bilgi içermemektedir.) İşte grup bilgilerinin bu dosyadan alınması için de benzer bir mekanizma oluşturulmuştur. Aşağıda grup /etc/group dosyasından birkaç satır görüyprsunuz: ... nm-openvpn:x:133: kaan:x:1000: sambashare:x:134:kaan study:x:1001 test:x:1002 ... Grup bilgilerini elde etmek için kullanılan POSIX fonksiyonları da şöyledir: #include struct group *getgrnam(const char *name); struct group *getgrgid(gid_t gid); struct group *getgrent(void); void setgrent(void); void endgrent(void); Bu fonksiyonlardaki struct group yapısı dosyası içerisinde şöyle bildirilmiştir: 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 */ }; Yapının gr_name elemanı grubun ismini belirtmektedir. gr_passwd elemanı grubun parola bilgisini belirtir. Gruplarda da parola kavramı vardır. Ancak seyrek kullanılmaktadır. gr_gid elemanı grubun numarısını belirtir. Anımsanacağı gibi bir kullanıcı birdenfazla gruba üye olabilmektedir. Kullanıcının asıl grubu /etc/passwd dosyasında belirtilen grup id'ye ilişkin gruptur. Örneğin /etc/group dosyasında aşağıdaki gibi bir satır bulunuyor olsun: study:x:1001:ali,veli,selami Burada grup bilgilerinin sonundaki ali, vel, selami bu study grubuna ek grup olarak dahil edilen kullanıcıları belirtmektedir. Örneğin kaan kullanıcısının asıl grubu project olabilir. Ancak kaan kullanıcısı aynı zamanda "ek grup (supplementary group)" olarak study grubuna da dahil olabilir. Yani sistemin bir kullanıcının ek gruplarını elde edebilmesi için /etc/group dosyasını baştan sona gözden geçirip kullanıcının hangi satırların ':' ayrılmış son bölümünde geçtiğini belirlemesi gerekmektedir. İşte group yapısının gr_mem elemanı bir göstericiyi gösteren göstericidir ve bu gruba ait olan kullanıcıları belirtmektedir. Tabii bu gr_mem ile belirtilmiş olan gösterici dizisinin son elemanı NULL adres içermektedir. getgrnam fonksiyonu grubun isminden hareketle grup bilgilerini, getgrgid fonksiyonu ise grup id'sinden hareketle grup bilgilerini vermektedir. Tıpkı kullanıcı bilgilerinde olduğu gibi grup bilgilerinin de tek tek elde edilmesi benzer biçimde get_grent, endgrent ve setgrent fonksiyonlarıyla yapılmaktadır. Aşağıdaki örnekte tüm gruplara ilişkin grup bilgileri ekrana (stdout dosyasına) yazdırılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { struct group *grp; while ((errno = 0, grp = getgrent()) != NULL) { printf("Group name: %s\n", grp->gr_name); printf("Password: %s\n", grp->gr_passwd); printf("Group id: %llu\n", (unsigned long long)grp->gr_gid); printf("Supplemenray userf of this group: "); for (int i = 0; grp->gr_mem[i] != NULL; ++i) { if (i != 0) printf(", "); printf("%s", grp->gr_mem[i]); } printf("\n-----------------------------------------------------\n"); } if (errno != 0) exit_sys("getgrent"); endgrent(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 16. Ders 17/12/2022 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Daha önce ls -l komutu formatında dosya bilgilerini yazdıran bir örnek yapmıştık. Ancak o örnekte kullanıcı ve grup isimleri isim olarak değil kullanıcı ve grup id'leri olarak ekrana (stdout dosyasına) yazdırılmıştı. Şimdi artık getpwuid ve getgrgid fonksiyonları ile bu sayısal id değerlerinden kullanıcı ve grup isimlerini elde edebiliriz. Aşağıda ls -l komutunun düzeltilmiş hali verilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #define LS_BUFSIZE 4096 void exit_sys(const char *msg); char *get_ls(struct stat *finfo, const char *name); int main(int argc, char *argv[]) { struct stat finfo; if (argc == 1) { fprintf(stderr, "file(s) must be specified!\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) { if (lstat(argv[i], &finfo) == -1) exit_sys("stat"); printf("%s\n", get_ls(&finfo, argv[i])); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } char *get_ls(struct stat *finfo, const char *name) { static char buf[LS_BUFSIZE]; static mode_t modes[] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; static mode_t ftypes[] = {S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK}; int index; struct tm *ptime; struct passwd *pw; struct group *gr; pw = getpwuid(finfo->st_uid); gr = getgrgid(finfo->st_gid); index = 0; for (int i = 0; i < 7; ++i) if ((finfo->st_mode & S_IFMT) == ftypes[i]) { buf[index++] = "bcp-dls"[i]; break; } for (int i = 0; i < 9; ++i) buf[index++] = finfo->st_mode & modes[i] ? "rwx"[i % 3] : '-'; ptime = localtime(&finfo->st_mtim.tv_sec); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_nlink); if (pw == NULL) index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_uid); else index += sprintf(buf + index, " %s", pw->pw_name); if (gr == NULL) index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_gid); else index += sprintf(buf + index, " %s", gr->gr_name); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_size); index += strftime(buf + index, LS_BUFSIZE, " %b %2e %H:%M", ptime); sprintf(buf + index, " %s", name); return buf; } /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi dizinler (directories) de aslında tamamen dosyalar gibi organize ediliyordu. Dizinlerin içerisinde aşağıdaki gibi dizin girişleri bulunyordu: isim i-node_no isim i-node_no isim i-node_no ... Dizin dosyalarının gerçek formatları biraz daha detay içerebilmektedir. Kursumuzun sonlarında doğru Ext-2 dosya sisteminin disk organizasyonu üzerinde duracağız. Bir dizini erişim hakları yeterliyse open fonksiyonuyla açabiliriz. Ancak POSIX standartlarında dizin dosyalarından okuma, yazma ve konumlandırma işlemlerinin yapılıp yapılamayacağı işletim sistemini yazanların isteğine bırakılmıştır. Linux, BSD, macOS gibi sistemler dizin dosyalarından read ve write fonksiyonları ile okuma ve yazma yapmaya izin vermemektedir. Ancak bu sistemler lseek fonksiyonuyla dizin dosyalarının dosya göstericilerinin konumlandırılmasına izin vermektedir. Pekiyi mademki işletim sistemleri dizin dosyalarından okuma yazma yapmaya izin vermeyebiliyorlar, bu durumda open fonksiyonuyla dizin dosyalarını hangi modda açabiliriz? İşte bunun POSIX standartlarında O_SEARCH isimli bir mod bulunmaktadır. Bu mod aslında ileride ele alacağımız at'li POSIX fonksiyonları için düşünülmüştür. Eğer O_SEARCH modunda bir dizin açılırsa bu dizinden okuma yazma yapılamaz ancak bu at'li fonksiyonlar kullanılabilir. Ancak O_SEARCH modu Linux tarafıdan desteklenmemektedir. Bu durumda mecburen Linux'ta bir dizini açacaksak işletim sistemi read fonksiyonu ile okuma yapılmasına izin vermiyor olsa da biz açış modu olarak O_RDONLY kullanırız. Pekiyi bir dizini O_SEARCH modunda açmak ile O_RDONLY modunda açmak arasında ne fark vardır? O_SEARCH modu POSIX standartlarına bir dizin üzerinde "read", "write" yapmamak ancak başka işlemlerde kullanılmak amacıyla kullanılmak için eklenmiştir. Dolayısıyla bir işletim sistemi örneğin dizin dosyalarından read fonksiyonu ile okuma yapmaya izin veriyorsa bu durumda biz o dizini O_SEARCH modunda açarsak okuma yapamayız. Ancak O_RDONLY modunda açarsak okuma yapabiliriz. Linux ve macOS O_SEARCH modunu desteklememektedir. Ancak BSD türevi sistemler bu modu desteklemektedir. Aşağıdaki Linux sistemlerinde bir dizin'inin open fonksiyonuyla açılmasına örnek verdik. Linux açış modu olarak O_SREACH modunu desteklemediği için O_RDONLY modunu kullandık. İşletim sistemleri genel olarak dizinlerin write modda açılmasına izin vermemektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open(".", O_RDONLY)) == -1) exit_sys("open"); printf("Ok\n"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi mademki işletim sistemlerinin çoğu bir dizin üzerinde read ve write fonksiyonları ile işlem yapmaya izin vermiyorsa bu durumda bir dizini open fonksiyonu ile açmanın ne anlamı vardır? İşte anımsanacağı gibi yol ifadesi alan POSIX dosya fonksiyonlarının başı f ile başlayan dosya betimleyicisi alan biçimleri de vardı. Örneğin stat ve lstat fonksiyonları yol ifadesi alırken fstat fonksiyonu dosya betimleyicisi alıyordu. Benzer biçimde chmod için fchmod, chown fchown fonksiyonları bulunmaktaydı. İşte bu f'li fonksiyonların bir de at'li versiyonları vardır. Örneğin fstatat, fchmodat, fchownat gibi. Ayrıca başı f ile başlamayan çeşitli dosya fonksiyonlarının da at'li versiyonarı bulunmaktadır. Örneğin open fonksiyonun da bir at'li versiyonu vardır. Aslında at'li versiyonlar seyrek kullanılan fonksiyonlardır. Ancak biz kursumuzda bunlar hakkında açıklama yapmayı da uygun görüyoruz. Pekiyi bu at'li fonksiyonlar ne yapmaktadır. Aşağıda openat fonksiyonunun prototipini görüyorsunuz: #include int openat(int fd, const char *path, int oflag, ...); Fonksiyonun prototipini open fonksiyonu ile karşılaştırınız: int open(const char *path, int oflag, ...); Fonksiyonların at'li vesiyonları genel olarak bir dosya betimleyicisi de almaktadır. Bu dosya betimleyicisinin bir dizin'e ilişkin olması gerekir. Eğer bu dosya betimleyicisi bir dizin'e ilişkin değilse fonksiyon başarısız olur. at'li versiyonlara bir dizine ilişkin dosya betimleyicisi verdikten sonra ayrıca bu fonksiyonlar bir de yol ifadesi de alırlar. Buradaki yol ifadesi eğer mutlak (absolute) ise bu at'li versiyonların at'siz versiyonlardan (flag parametreleri dışında) hiçbir farkı kalmaz. Dolayısıyla bu durumda geçerli olsa da at'li versiyonları kullanmanın anlamı kalmamaktadır. (Bazı at'li versiyonlar flag parametresine de sahiptir. Bu parametrenin işlevinden faydalanmak için de at'li fonksiyonlar kullanılabilmektedir.) Yani fonksiyon bu dizin betimleyicisinden faydalanmamaktadır. Ancak yol ifadesi göreli (relative) ise bu durumda dosya prosesin çalışma dizininden itibaren değil dizin betimleyicisinin belirttiği dizinden itibaren orijin belirtmektedir. Yani biz at'li versiyonlarla göreli yol ifadelerinin orijinlerini prosesin çalışma dizinin dışında başka bir dizine kaydırabilmekteyiz. Tabii fonksiyonların at'li versiyonları kullanılacaksa bu durumda dizin dosyalarının O_SEARCH modunda açılması daha uygundur. Çünkü bu at'li versiyonlar için dizin dosyalarının okuma modunda açılması gerekmemektedir. Zaten POSIX'te O_SEARCH modu bu at'li fonksiyonlar için bulundurulmuştur. Linux ve macOS sistemleri O_SEARCH modunu desteklemediğine göre bu sistemlerde at'li fonksiyonları kullanırken dizin'leri O_RDONLY modda açmamız gerekir. POSIX standartlarına göre at'li fonksiyonlarda eğer dizin O_SEARCH modunda açılmışsa belirtilen dizinin "x" hakkına sahilik kontrolü yapılmaz. Eğer dizin O_SERACH yerine diğer modlarla (örneğin O_RDONLY) açılmışsa bu durumda belirtilen dizinde "x" hakkı kontrolü yapılmaktadır. Ayrıca fonksiyonların at'li versiyonlarında dizine ilişkin dosya betimleyicisine özel olarak AT_FDCWD değeri geçirilirse bu durumda sanki prosesin çalışma dizinine ilişkin dizin betimleyicisi geçirilmiş gibi bir etki oluşmaktadır. Tabii bu durumda fonksiyonun at'li versiyonu ile at'siz versiyonu arasında bir fark kalmaz. Ancak fonksiyonarın at'li versiyonlarının ekstra parametreleri de olabilmektedir (genellikle bu ekstra parametre flag parametresi biçimindedir). İşte programcı bu ekstra parametrelerden faydalanabilmek için dosya betimleyici parametresini AT_FDCWD biçiminde geçebilmektedir. Ayrıca fonksiyonların at'li versiyonlarında biz yol ifadesi olarak mutlak ifadesi geçtiğimizde fonksiyonun dizin betimleyici parametresi zaten hiç kontrol edilmemektedir (yani bu parametre geçersiz bir betimleyici belirtse bile eğer yol ifadesi mutlak ise fonksiyon için bir sorun oluşturmamaktadır.) Diğer at'li bazı fonksiyonların da prototipleri şöyledir: int fchmodat(int fd, const char *path, mode_t mode, int flag); int fchownat(int fd, const char *path, uid_t owner, gid_t group, int flag); int fstatat(int fd, const char *restrict path, struct stat *restrict buf, int flag); Aşağıda openat fonksiyonunun kullanımına bir örnek verilmiştir. Burada çalışma dizininde "test.txt" dosyası bulunduğu halde fonksiyon başarısız olacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fddir, fd; if ((fddir = open("/usr/include", O_RDONLY)) == -1) exit_sys("open"); if ((fd = openat(fddir, "test.txt", O_RDONLY)) == -1) exit_sys("openat"); printf("Ok\n"); close(fd); close(fddir); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bir dizin dosyası içerisindeki "dizin girişlerini (directory entry)" elde etmek için bir grup POSIX fonksiyonu bulundurulmuştur. (Dizin dosyalarının open ya da openat ile fonksiyonu ile açılabildiğine ancak pek çok sistemde read fonksiyonu ile okunamadığına dikkat ediniz. Ayrıca dizin dosyalarının iç formatı dosya sisteminden dosya sistemine değişebilmektedir. Bu nedenle POSIX standartlarında bu işi yapan ayrı fonksiyonlar bulundurulmuştur.) Linux sistemlerinde dizin girişlerinin okunması için getdents isimli bir sistem fonksiyonu bulundurulmuştur. Dolayısıyla aşağıda açıklayacağımız POSIX fonksiyonları arka planda Linux sistemlerinde getdents sistem fonksiyonunu çağırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dizin girişlerini elde etmek için önce dizin'in opendir fonksiyonuyla açılması gerekmektedir. Bunun için dizin'e okuma hakkının bulunuyor olması gerekmektedir. opendir fonksiyonunun prototipi şöyledir: #include DIR *opendir(const char *dirname); Fonksiyon parametre olarak açılacak dizin'in yol ifadesini almaktadır. Fonksiyonun geri dönüş değeri DIR isimli bir yapı türünden (DIR bir typedef ismidir) bir adrestir. Bu DIR adresi bir handle gibi kullanılmaktadır. Fonksiyon başarısızlık durumunda NULL adrese geri döner ve errno uygun biçimde değer alır. opendir fonksiyonun fdopendir isimli bir versiyonu da vardır. Bu versiyon eğer zateb dizin O_SEARCH ya da O_RDONLY modda açılmışsa o dizine ilişkin betimleyici yoluyla DIR adresini vermektedir: #include DIR *fdopendir(int fd); Dizin opendir ya da fdopendir fonksiyonuyla açılıp, DIR handle'ı elde edildikten sonra, artık dizin girişleri readdir POSIX fonksiyonuyla tek tek bir döngü içerisinde okunabilir. readdir fonksiyonu her çağrıldığında bir sonraki dizin girişi elde edilir. Fonksiyonun prototipi şöyledir: #include struct dirent *readdir(DIR *dirp); Fonksiyon parametre olarak DIR yapısının adresini alır sıradaki dizin girişini elde eder. Bu dizin girişinin bilgilerini statik ömürlü struct dirent isimli bir yapı nesnesinin içerisine yerleştirir. Bize de onun adresini verir. Eğer readdir dizin listesinin sonuna gelirse NULL adrese geri dönmektedir. Ancak fonksiyon IO hatalarından dolayı da başarısız olabilir. Bu durumda başarısızlığın dizin sonuna gelmekten dolayı mı yoksa IO hatalarından dolayı mı olduğunu anlamak gerekebilir. İşte readdir fonksiyonu eğer dizin sonuna gelindiğinden dolayı NULL adrese geri dönmüş ise bu durumda errno değişkeninin değerini değiştirmemektedir. O halde programcı fonksiyonu çağırmadan önce rrno değişkenine 0 atamalı sonra fonksiyonu çağırmalıdır. Eğer fonksiyon NULL adrese geri dönmüşse errno değişkenine bakmalı eğer errno hala 0 ise fonksiyonun dizin sonuna gelindiğinden dolayı başarısız olduğu sonucunu çıkarmalıdır. O halde fonksiyon tipik olarak şöyle kullanılmalıdır: struct dirent *de; ... while (errno = 0, (de = readdir(dir)) != NULL) { /* ... */ } if (errno != 0) exit_sys("readdir); dirent yapısı POSIX standartlarına göre en az iki elemana sahip olmak zorundadır. Bu elemanlar d_ino ve d_name elemanlarıdır. d_ino elemanı ino_t türündendir. d_name elemanı ise char türden bir dizidir. Ancak işletim sistemleri genellikle bu dirent yapısında daha fazla eleman bulundurmaktadır. Örneğin 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 */ }; Görüldüğü gibi Linux'ta yapının içerisinde d_off, d_reclen ve d_type elemanları da bulunmaktadır. d_off ve d_reclen elemanları önemli değildir. Ancak d_type elemanı dosyanın ne dosyası olduğunu belirtmektedir. Bu eleman sayesinde programcı dosyanın türünü anlamak için stat fonksiyonlarını çağırmak zorunda kalmaz. Gerçekten de i-node tabanlı dosya sistemleri dizin girişlerinde dosyanın türünü de zaten tutmaktadır. Ancak POSIX standartlarında bu elemanlar zorunlu olarak belirtilmediğinden taşınabilir programlarda yalnızca yapının d_ino ve d_name elemanları kullanılmalıdır. dirent yapısının d_ino elemanı bize dosyanın i-node numarasını verir. d_name elemanı ise dizin girişinin ismini vermektedir. Linux sistemlerinde d_type bit düzeyinde kodlanmamıştır. Aşağıdaki değerlerden birine eşit olmak zorundadır: DT_BLK block device DT_CHR character device DT_DIR directory DT_FIFO named pipe (FIFO) DT_LNK symbolic link DT_REG regular file. DT_SOCK UNIX domain socket. DT_UNKNOWN Bilinmeyen bir tür readdir ile dizin girişleri dosya sistemindeki kayıtlara göre verilmektedir. Halbuki ls komutu default durumda önce dizin girişlerini isme göre sıraya dizmekte sonra onları göstermektedir. (Linux'ta -f'den sonra -l'yi kullanınız, ters sırada çalışmıyor.) Doğal sıranın ne anlam ifade ettiği dosya sistemlerinin anlatıldığı bölümde ele alımacaktır. Dizin girişleri elde edildikten sonra dizin closedir POSIX fonksiyonuyla kapatılmaldır: #include int closedir(DIR *dirp); Fonksiyon başarı durumunda 0, başarısızlık durumunda -1 değerine geri dönmektedir. closedir fonksiyonu kendi içerisinde kullandığı betimleyicileri close etmektedir. Örneğin biz DIR nesnesini (directory stream) fdopendir ile dizin betimleyicisini vererek yaratmış olalım. closedir bu betimleyiciyi kendisi close etmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { DIR *dir; struct dirent *de; 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, (de = readdir(dir)) != NULL) printf("%s\n", de->d_name); if (errno != 0) exit_sys("readdir"); closedir(dir); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte bir dizindeki dosyaların hepsini ls -l stili ile yazdırıyoruz. Bu örnekte bazı noktalara dikkat ediniz: - Biz argv[1] ile görüntülenecek dizini alıyoruz. Ancak bu dizindeki dosyaların stat bilgileri elde edilirken yok ifadesinin dosya isminin başına eklenmesi gerekmektedir: while (errno = 0, (de = readdir(dir)) != NULL) { sprintf(path, "%s/%s", argv[1], de->d_name); if (lstat(path, &finfo) == -1) exit_sys("stat"); printf("%s\n", get_ls(&finfo, de->d_name)); } Aslında bu tür durumlarda fonksiyonların at'li versiyonlarını kullanmak daha uygun olabilmektedir. - Biz burada bir hizalama yapmadık. Halbuki orijinal ls -l komutu yazısal sütnları karakter sayısına göre hizalayıp sola dayalı olarak, sayısal sütunları ise hizalayıp sağa dayalı olarak yazdırmaktadır. Tabii bunun için dütnun en geniş elemanının bulunması da gerekmektedir. Bu işlem "çalışma sorusu" olarak sorulacaktır. - Biz bu örnekte dizin girişlerini doğal sıraya göre görüntüledik. Halbuki ls -l komutu önce onları isme göre sıraya dizip sonra görüntülemektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #include #define LS_BUFSIZE 4096 void exit_sys(const char *msg); char *get_ls(struct stat *finfo, const char *name); int main(int argc, char *argv[]) { struct stat finfo; DIR *dir; struct dirent *de; char path[4096]; 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, (de = readdir(dir)) != NULL) { sprintf(path, "%s/%s", argv[1], de->d_name); if (lstat(path, &finfo) == -1) exit_sys("stat"); printf("%s\n", get_ls(&finfo, de->d_name)); } if (errno != 0) exit_sys("readdir"); closedir(dir); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } char *get_ls(struct stat *finfo, const char *name) { static char buf[LS_BUFSIZE]; static mode_t modes[] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; static mode_t ftypes[] = {S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK}; int index; struct tm *ptime; struct passwd *pw; struct group *gr; pw = getpwuid(finfo->st_uid); gr = getgrgid(finfo->st_gid); index = 0; for (int i = 0; i < 7; ++i) if ((finfo->st_mode & S_IFMT) == ftypes[i]) { buf[index++] = "bcp-dls"[i]; break; } for (int i = 0; i < 9; ++i) buf[index++] = finfo->st_mode & modes[i] ? "rwx"[i % 3] : '-'; ptime = localtime(&finfo->st_mtim.tv_sec); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_nlink); if (pw == NULL) index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_uid); else index += sprintf(buf + index, " %s", pw->pw_name); if (gr == NULL) index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_gid); else index += sprintf(buf + index, " %s", gr->gr_name); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_size); index += strftime(buf + index, LS_BUFSIZE, " %b %2e %H:%M", ptime); sprintf(buf + index, " %s", name); return buf; } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz üzere aslında opendir, readdir, closedir gibi POSIX fonksiyonları arka planda işletim sisteminin sistem fonksiyonlarını çağırmaktadır. Örneğin Linux'ta aslında işletim sistemi düzeyinde işlemler önce sys_open sistem fonksiyonu ile dizin'in açılması sonra sys_getdents sistem fonksiyonu ile dizin girişlerinin okunması ve nihayet sys_close fonksiyonu ile dizin'in kapatılması yoluyla yapılmaktadır. Ancak POSIX standartlarında bu işlemler taşınabilir biçimde opendir, readdir ve closedir fonksiyonlarına devredilmiştir. Şüphesiz bu fonksiyonlar asında dizini açıp onun betimleyicisini DIR yapısının içerisinde saklamaktadır. İşte elimizde DIR yapısı varsa biz de açık dizin'in betimleyicisini elde etmek isttyorsak bunun için dirfd isimli POSIX fonksiyonundan faydalanabiliriz: #include int dirfd(DIR *dirp); Fonksiyon parametre olarak DIR yapısının adresini alır, geri dönüş değeri olarak dizine ilişkin betimleyiciyi verir. Fonksiyon başarısızlık durumunda -1 değerine geri dönmektedir. Yukarıdaki örneği fstatat fonksiyonunu kullanarak basitleştirebiliriz. fstatat fonksiyonunun prototipi şöyledir: #include int fstatat(int fd, const char *restrict path, struct stat *restrict buf, int flag); Fonksiyonun fd parametresinin yanı sıra aynı zamanda bir flag parametresinin olduğuna dikkat ediniz. Bu parametre stat semantiğinin mi yoksa lstat semantiğinin mi uygulanacağını belirtmektedir. Eğer bu parametreye 0 geçilirse bu durumda stat semantiği uygulanır. Eğer bu parametreye AT_SYMLINK_NOFOLLOW değeri geçilirse bu durumda lstat semantiği uygulanmaktadır. AT_SYMLINK_NOFOLLOW sembolik sabiti içerisinde değil içerisinde bildirilmiştir. İşte biz yukarıdaki örnekte önce dizin'in betimleyicisini dirfd fonksiyonu ile alıp bunu fstatat fonksiyonunda kullanırsak yol ifadesini düzenlememize gerek kalmaz. Aşağıdaki örnekte bu çözüm verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #include #include #define LS_BUFSIZE 4096 void exit_sys(const char *msg); char *get_ls(struct stat *finfo, const char *name); int main(int argc, char *argv[]) { struct stat finfo; DIR *dir; struct dirent *de; int fd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((dir = opendir(argv[1])) == NULL) exit_sys("opendir"); if ((fd = dirfd(dir)) == -1) exit_sys("dirfd"); while (errno = 0, (de = readdir(dir)) != NULL) { if (fstatat(fd, de->d_name, &finfo, AT_SYMLINK_NOFOLLOW) == -1) exit_sys("stat"); printf("%s\n", get_ls(&finfo, de->d_name)); } if (errno != 0) exit_sys("readdir"); closedir(dir); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } char *get_ls(struct stat *finfo, const char *name) { static char buf[LS_BUFSIZE]; static mode_t modes[] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; static mode_t ftypes[] = {S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK}; int index; struct tm *ptime; struct passwd *pw; struct group *gr; pw = getpwuid(finfo->st_uid); gr = getgrgid(finfo->st_gid); index = 0; for (int i = 0; i < 7; ++i) if ((finfo->st_mode & S_IFMT) == ftypes[i]) { buf[index++] = "bcp-dls"[i]; break; } for (int i = 0; i < 9; ++i) buf[index++] = finfo->st_mode & modes[i] ? "rwx"[i % 3] : '-'; ptime = localtime(&finfo->st_mtim.tv_sec); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_nlink); if (pw == NULL) index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_uid); else index += sprintf(buf + index, " %s", pw->pw_name); if (gr == NULL) index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_gid); else index += sprintf(buf + index, " %s", gr->gr_name); index += sprintf(buf + index, " %llu", (unsigned long long)finfo->st_size); index += strftime(buf + index, LS_BUFSIZE, " %b %2e %H:%M", ptime); sprintf(buf + index, " %s", name); return buf; } /*-------------------------------------------------------------------------------------------------------------------------- 17. Ders 18/12/2022 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- rewinddir isimli POSIX fonksiyonu dolaşımı yeniden başlatmak amacıyla kullanılır. Yani bu işlem adeta dosya göstericisinin dizin dosyasının başına çekilmesi işlemi gibidir. Aşağıdaki örnekte dizin girişleri rewindir fonksiyonu ile iki kez elde edilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { DIR *dir; struct dirent *de; 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, (de = readdir(dir)) != NULL) printf("%s\n", de->d_name); if (errno != 0) exit_sys("readdir"); printf("---------------------------------------\n"); rewinddir(dir); while (errno = 0, (de = readdir(dir)) != NULL) printf("%s\n", de->d_name); if (errno != 0) exit_sys("readdir"); closedir(dir); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Dizin girişlerini dolaşırken belli bir noktada dizin dosyasının dosya göstericisinin konumunu telldir POSIX fonksiyonu ile alabiliriz ve o offset'e seekdir POSIX fonksiyonu ile yeniden konumlandırma yapabiliriz. Fonksiyonların prototipleri şöyledir: #include long telldir(DIR *dirp); void seekdir(DIR *dirp, long loc); Tabii biz belli bir konumu okuduktan sonra kaydedersek bu durumda okumadan dolayı dizin dosyasının dosya göstericisi ilerletilmiş olacaktır. Aşağıdaki örnekte dizin içerisinde "sample.c" dosyası bulunup onun konumu telldir fonksiyonu ile saklanmıştır. Sonra seekdir fonksiyonu ile konuma konumlandırma yapılmıştır. Tabii burada kaydedilen konum "sample.c" dosyasından sonraki dosyanın konumdur. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { DIR *dir; struct dirent *de; long loc; 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, (de = readdir(dir)) != NULL) { printf("%s\n", de->d_name); if (!strcmp(de->d_name, "sample.c")) loc = telldir(dir); } if (errno != 0) exit_sys("readdir"); printf("----------------------------------------------\n"); seekdir(dir, loc); while (errno = 0, (de = readdir(dir)) != NULL) printf("%s\n", de->d_name); if (errno != 0) exit_sys("readdir"); closedir(dir); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Dizin ağacının dolaşılması özyinelemeli bir algoritmayla yapılmaldır. Bu işlem çeşitli biçimlerde gerçekleştirilebilir. En basit gerçekletirimi dolaşılacak ağacın kök yol ifadesini alan özyinelemeli bir fonksiyon yazmaktır. Bu fonksiyon dizin girişlerini tek tek elde eder. Eğer söz konusu dizin girişi bir dizine ilişkinse o dizinin yol ifadesiyle kendini çağırır. Bu algoritmada dikkat edilmesi gereken birkaç nokta vardır: 1) Dizin girişleri dolaşılırken "." ve ".." dizinleri continue ile geçilmelidir. Aksi takdirde sonsuz döngü oluşabilir. 2) stat fonksiyonu yerine lstat fonksiyonu kullanılmalıdır. Çünkü dizin ağacı dolaşılırken sembolik bağlantı bir dizine ilişkinse sembolik bağlantının hedefine gidilmesi özyinelemeyi bozup sonsuz döngülere yol açabilir. 3) readdir fonksiyonu dizin girişini okuduğunda bize yalnız girişin ismini vermektedir. Dolayısıyla lstat fonksiyonu uygulanırken prosesin çalışma dizinin uygun olması gerekir. Bunu sağlayabilmek için her dizine geçişte chdir fonksiyonu ile prosesin çalışma dizinini değiştebiliriz. Ya da alternatif olarak mutlak bir yol ifadesi sürekli güncellenebilir. Aslında burada seçeneklerden biri de fonksiyonların at'li biçimlerini kullanmak olabilir. 4) Her özyineleme bittiğinde opendir ile açılan dizin closedir ile kapatılmalıdır. 5) Genellikle böylesi fonksiyonlar bir fatal error ile programı sonlandırmamalıdır. chdir fonksiyonu ile prosesin çalışma dizini değiştirilemeyebilir. Ya da örneğin opendir ile biz bir dizini açamayabiliriz. Bu tür durumlarda hata stderr dosyasına rapor edilip işlemin devem ettirilmesi uygun olabilir. 6) Özyinelemeli dolaşım bittikten sonra prosesin çalışma dizini orijinal halde bırakılmalıdır. Aşağıda tipik bir özyinelemeli "depth-first" dolaşım örneği verilmiştir. Ancak burada prosesin çalışma dizini özyineleme bittikten sonra orijinal dizin ile yeniden set edilmemiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include void walkdir(const char *path); void exit_sys(const char *msg); int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } walkdir(argv[1]); return 0; } void walkdir(const char *path) { DIR *dir; struct dirent *de; struct stat finfo; if ((dir = opendir(path)) == NULL) { fprintf(stderr, "cannot read directory: %s\n", path); return; } if (chdir(path) == -1) { fprintf(stderr, "directory cannot change: %s\n", path); goto EXIT; } while (errno = 0, (de = readdir(dir)) != NULL) { printf("%s\n", de->d_name); if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; if (lstat(de->d_name, &finfo) == -1) { fprintf(stderr, "cannot get stat info: %s\n", de->d_name); continue; } if (S_ISDIR(finfo.st_mode)) { walkdir(de->d_name); if (chdir("..") == -1) { fprintf(stderr, "directory cannot change: %s\n", path); goto EXIT; } } } if (errno != 0) fprintf(stderr, "cannot read directory info: %s\n", path); EXIT: closedir(dir); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Özyineleme çağırmada hangi kademede bulunulduğunu belirten bir bilginin de özyinelemeli fonksiyona parametre yoluyla aktarılması faydaları olabilmektedir. Örneğin bu sayede biz ağacı kademeli bir biçimde görüntüleyebiliriz. Aşağıdaki örnekte walkdir fonksiyonuna bir kademe bilgisi de eklenmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include void walkdir(const char *path, int level); void exit_sys(const char *msg); int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } walkdir(argv[1], 0); return 0; } void walkdir(const char *path, int level) { DIR *dir; struct dirent *de; struct stat finfo; if ((dir = opendir(path)) == NULL) { fprintf(stderr, "cannot read directory: %s\n", path); return; } if (chdir(path) == -1) { fprintf(stderr, "directory cannot change: %s\n", path); goto EXIT; } while (errno = 0, (de = readdir(dir)) != NULL) { printf("%*s%s\n", level * 4, "", de->d_name); if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; if (lstat(de->d_name, &finfo) == -1) { fprintf(stderr, "cannot get stat info: %s\n", de->d_name); continue; } if (S_ISDIR(finfo.st_mode)) { walkdir(de->d_name, level + 1); if (chdir("..") == -1) { fprintf(stderr, "directory cannot change: %s\n", path); goto EXIT; } } } if (errno != 0) fprintf(stderr, "cannot read directory info: %s\n", path); EXIT: closedir(dir); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aslında yukarıdaki walkdir fonksiyonu bir sarma fonksiyonla daha iyi hale getirilebilir. Bu sayede level parametresi kullanıcıdan gizlenebilir ve prosesin çalışma dizini alınıp geri set edilebilir. Aşağıdaki örnekte walkdir fonksiyonu asıl özyineleme işlemini yapan walkdir_recur fonksiyonunu çağırmaktadır. Fonksiyonda kademeli yazım için printf fonksiyonu şöyle çağrılmıştır: printf("%*s%s\n", level * 4, "", de->d_name); Burada * format karakteri level * 4 ile eşleştirilmiştir. İlk %s format karakteriyle de "" biçiminde boş string eşleşecektir. O halde biz yalnızca satırın başında level * 4 kadar boşluk oluşturmuş oluyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include void walkdir(const char *path); void walkdir_recur(const char *path, int level); void exit_sys(const char *msg); int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } walkdir(argv[1]); return 0; } void walkdir(const char *path) { char cwd[PATH_MAX]; if (getcwd(cwd, PATH_MAX) == NULL) { perror("getcwd"); return; } walkdir_recur(path, 0); if (chdir(cwd) == -1) { perror("chdir"); return; } } void walkdir_recur(const char *path, int level) { DIR *dir; struct dirent *de; struct stat finfo; if ((dir = opendir(path)) == NULL) { fprintf(stderr, "cannot read directory: %s\n", path); return; } if (chdir(path) == -1) { fprintf(stderr, "directory cannot change: %s\n", path); goto EXIT; } while (errno = 0, (de = readdir(dir)) != NULL) { printf("%*s%s\n", level * 4, "", de->d_name); if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; if (lstat(de->d_name, &finfo) == -1) { fprintf(stderr, "cannot get stat info: %s\n", de->d_name); continue; } if (S_ISDIR(finfo.st_mode)) { walkdir_recur(de->d_name, level + 1); if (chdir("..") == -1) { fprintf(stderr, "directory cannot change: %s\n", path); goto EXIT; } } } if (errno != 0) fprintf(stderr, "cannot read directory info: %s\n", path); EXIT: closedir(dir); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } *-------------------------------------------------------------------------------------------------------------------------- Dizin ağacını dolaşırken her defasında prosesin çalışma dizinini değiştirmek yerine fonksiyonların at'li biçimlerinden de faydalanabiliriz. Aşağıdaki örnekte özyinelemeli fonksiyona üst dizinin betimleyicisi (fdp) ve dosyanın ismi geçirilmiştir. at'li fonksiyonların eğer yol ifadesi mutlak ise at'siz fonksiyonlar gibi davrandığını anımsayınız. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #include void walkdir(const char *path); void walkdir_recur(int fddir, const char *fname, int level); void exit_sys(const char *msg); int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } walkdir(argv[1]); return 0; } void walkdir(const char *path) { int fddir; if ((fddir = open(path, O_RDONLY)) == -1) exit_sys(path); walkdir_recur(fddir, path, 0); close(fddir); } void walkdir_recur(int fdp, const char *fname, int level) { DIR *dir; int fdc; struct dirent *de; struct stat finfo; if ((fdc = openat(fdp, fname, O_RDONLY)) == -1) { fprintf(stderr, "cannot open file: %s\n", fname); return; } if ((dir = fdopendir(fdc)) == NULL) { fprintf(stderr, "cannot read directory: %s\n", fname); close(fdp); return; } while (errno = 0, (de = readdir(dir)) != NULL) { printf("%*s%s\n", level * 4, "", de->d_name); if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; if (fstatat(fdc, de->d_name, &finfo, AT_SYMLINK_NOFOLLOW) == -1) { fprintf(stderr, "cannot get stat info: %s\n", de->d_name); continue; } if (S_ISDIR(finfo.st_mode)) walkdir_recur(fdc, de->d_name, level + 1); } if (errno != 0) fprintf(stderr, "cannot read directory info: %s\n", fname); EXIT: closedir(dir); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Dizin ağacını dolaşırken genelleştirme sağlamak için fonksiyon göstericilerinden faydalanabiliriz. Yani fonksiyonumuz dizin ağacını dolaşırken dosya isimlerini ekrana yazdırmak yerine parametresiyle aldığı bir callback fonksiyonu çağırabilir. Aşağıda dizin girişi bulundukça çağrılan bir callback mekanizması örneği verilmiştir. Buradaki fonksiyonun prototipi şöyledir: int walkdir(const char *path, int (*proc)(const char *, const struct stat *, int)); Fonksiyonun birinci parametresi dolaşılacak dizinin yol ifadesini belirtir. İkinci parametre callback fonksiyonun adresini almaktadır. callback fonksiyonun birinci parametresi bulunan dizin girişinin ismini (tüm yol ifadesi değil), ikinci parametresi bu dosyanın stat bilgilerini belirtmektedir. Üçüncü parametre ise özyineleme için kademe bilgisini belirtir. Callback fonksiyon 0 ile geri dönerse özyineleme devam ettirlir. Ancak sıfır dışı bir değerle geri dönerse özyineleme sonlandırılır ve walkdir fonksiyonu da bu değerler geri döner. Bu durumda walkdir fonksiyonun geri dönüş değeri üç biçimde olabilir: -1: POSIX fonksiyonlarından birinin hatayla geri dönmesi > 0: Erken sonlanmayı belirtir. 0: Normal sonlanmayı belirtir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include int walkdir(const char *path, int (*proc)(const char *, const struct stat *, int)); int walkdir_recur(const char *path, int level, int (*proc)(const char *, const struct stat *, int)); int disp(const char *fname, const struct stat *finfo, int level) { printf("%*s%s\n", level * 4, "", fname); if (!strcmp(fname, "d.dat")) return 1; return 0; } int main(int argc, char *argv[]) { int result; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((result = walkdir(argv[1], disp)) == -1) { fprintf(stderr, "function terminates problematically!\n"); exit(EXIT_FAILURE); } if (result != 0) printf("function terminates prematurely with %d code\n", result); else printf("function terminates normally!...\n"); return 0; } int walkdir(const char *path, int (*proc)(const char *, const struct stat *, int)) { char cwd[PATH_MAX]; int result; if (getcwd(cwd, PATH_MAX) == NULL) { perror("getcwd"); return - 1; } result = walkdir_recur(path, 0, proc); if (chdir(cwd) == -1) { perror("chdir"); return -1; } return result; } int walkdir_recur(const char *path, int level, int (*proc)(const char *, const struct stat *, int)) { DIR *dir; struct dirent *de; struct stat finfo; int result = 0; if ((dir = opendir(path)) == NULL) { fprintf(stderr, "cannot read directory: %s\n", path); return -1; } if (chdir(path) == -1) { fprintf(stderr, "directory cannot change: %s\n", path); result = -1; 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) { fprintf(stderr, "cannot get stat info: %s\n", de->d_name); continue; } if ((result = proc(de->d_name, &finfo, level)) != 0) { result = -1; goto EXIT; } if (S_ISDIR(finfo.st_mode)) { result = walkdir_recur(de->d_name, level + 1, proc); if (chdir("..") == -1) { fprintf(stderr, "directory cannot change: %s\n", path); result = -1; goto EXIT; } if (result != 0) goto EXIT; } } if (errno != 0) fprintf(stderr, "cannot read directory info: %s\n", path); EXIT: closedir(dir); return result; } /*-------------------------------------------------------------------------------------------------------------------------- scandir bir dizindeki belli koşulları sağlayan girişleri veren biraz karmaşık parametreye sahip bir POSIX fonksiyonudur. Fonksiyonun parametrik yapısı şöyledir: #include int alphasort(const struct dirent **d1, const struct dirent **d2); int scandir(const char *dir, struct dirent ***namelist, int (*sel)(const struct dirent *), int (*compar)(const struct dirent **, const struct dirent **)); scandir fonksiyonunun birinci parametresi dizin'in yol ifadesini almaktadır. İkinci parametreye struct dirent türünden göstericiyi gösteren bir göstericinin adresi geçirilmelidir. Üçüncü parametre filte işleminde kullanılacak fonksiyonu belirtmektedir. Her dizin girişi bulundukça bu fonksiyon çağrılır. Eğer bu fonksiyon sıfır dışı bir değerle geri dönerse dizin girişi biriktirilir. Bu parametre NULL adres geçilebilir. Bu durumda dizindeki tüm girişler elde edilir. Son parametre filtrelenen girişlere ilişkin gösterici dizisini sort etmek için kullanılacak karşılaştırma fonksiyonunu belirtmektedir. Bu karşılaştırma fonksiyonunun prototipi şöyle olmalıdır: int cmp(const struct **direnet1, const struct **dirent2); Fonksiyon tıpkı qsort fonksiyonunda olduğu gibi birinci parametresiyle belirtilmiş olan dizin girişi ikinci parametresiyle belirtilmiş olan dizin girişinden büyükse pozitif herhangi bir değere, küçükse negatif herhangi bir değere ve eşitse sıfır değerine geri dönmelidir. Alfabetik sıralamayı sağlamak amacıyla zaten hazır bir alphasort isimli fonksiyon bulundurulmuştur. scandir fonksiyonu başarı durumunda gösterici dizisine yerleştirilen eleman sayısı ile başarısızlık durumunda -1 ile geri döner ve errno uygun biçimde değer alır. scandir fonksiyonu tüm tahsisatları malloc fonksiyonunu kullanarak yapmaktadır. Dolayısıyla programcının tahsis edilen bu alanları kendisinin free hale getirmesi gerekmektedir. scandir kendi içerisinde her biriktirilecek dizin girişi için malloc fonksiyonu ile bir truct dirent yapısı tahsis eder, bunların adreslerini de yine tahsis ettiği bir gösterici dizisine yerleştirir. Bu gösterici dizisinin adresini de bizim adresini geçtiğimiz göstericiyi gösteren göstericinin içerisine yerleştirmektedir. Aşağıdaki örnekte komut argümanı olarak girilen bir dizinde başı 'a' ya da 'A' harfi ile başlayan girişler elde edilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int myfilter(const struct dirent *de) { return de->d_name[0] == 'a' || de->d_name[0] == 'A'; } int main(int argc, char *argv[]) { int result; struct dirent **dents; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((result = scandir(argv[1], &dents, myfilter, alphasort)) == -1) exit_sys("scandir"); for (int i = 0; i < result; ++i) printf("%s\n", dents[i]->d_name); for (int i = 0; i < result; ++i) free(dents[i]); free(dents); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- scandir fonksiyonun tasarımında bize göre kusurlar vardır. Fonksiyonun dirent yapılarını biriktirmesi karşılaştırma fonksiyonu yazacak kişiler için yük oluşturmaktadır. Buradaki daha doğru tasarım yeni bir yapı bildirip yapının içerisinde hem dirent bilgilerinin hem de stat bilgilerinin bulunması olabilir. Aşağıda bir karşılaştırma fonksiyonu yazımı örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void exit_sys(const char *msg); int myfilter(const struct dirent *de) { return de->d_name[0] == 'a' || de->d_name[0] == 'A'; } int cmp_size(const struct dirent **de1, const struct dirent **de2) { struct stat finfo1, finfo2; if (stat((**de1).d_name, &finfo1) == -1) exit_sys("stat"); if (stat((**de2).d_name, &finfo2) == -1) exit_sys("stat"); if (finfo1.st_size > finfo2.st_size) return 1; if (finfo1.st_size < finfo2.st_size) return -1; return 0; } int main(int argc, char *argv[]) { int result; struct dirent **dents; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (chdir(argv[1]) == -1) exit_sys("chdir"); if ((result = scandir(argv[1], &dents, myfilter, cmp_size)) == -1) exit_sys("scandir"); for (int i = 0; i < result; ++i) printf("%s\n", dents[i]->d_name); for (int i = 0; i < result; ++i) free(dents[i]); free(dents); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 18. Ders 24/12/2022 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dizin ağacını özyinelemeli biçimde dolaşan ftw (file traverse walk) ve nftw (new file traverse walk) isimli POSIX fonksiyonları bulunmaktadır. Aslında eskiden yalnızca ftw fonksiyonu vardı. Ancak bu fonksiyona bazı eklemeler yapılıp nftw fonksiyonu oluşturuldu ve ftw fonksiyonu "deprecated" yapıldı. Yani bugün hem ftw hem de nftw fonksiyonları bulunuyor olsa da nftw fonksiyonun kullanılması önerilmektedir. Zaten nftw fonksiyonu işlevsel olarak ftw fonksiyonu kapsamaktadır. nftw fonksiyonunun prototipi şöyledir: #include int nftw(const char *path, int (*fn)(const char *, const struct stat *, int, struct FTW *), int fd_limit, int flags); Linux altında bu fonksiyonu libc kütüphanesi ile kullanırken "feature test macro" oluşturulmalıdır. Burada başlık dosyalarının yukarısında aşağıdaki gibi bir sembolik sabit bulundurmak gerekir: #define _XOPEN_SOURCE 500 Feature test macro kavramından daha sonra bahsedilecektir. Budaraki sayının 500'e eşit ya da daha büyük olması gerekmektedir. Fonksiyonun birinci parametresi özyinelemeli dolaşılacak dizin'in yol ifadesini almaktadır. İkinci parametre her dizin girişi bulundukça çağrılacak "callback" fonksiyonun adresini almaktadır. Buradaki fonksiyonun aşağıdaki parametrik yapıya sahip olması gerekir: int callback(const char *path, const struct stat *finfo, int flag, struct FTW *ftw); nftw fonksiyonun üçüncü parametresi kullanılacak maksimum dosya betimleyici sayısını belirtmektedir. Fonksiyon her derine indikçe o dizini opendir fonksiyonu ile açtığı için (bizde öyle yapmıştık) dosya betimleyici tablosunda bir betimleyici harcamaktadır. Linux'ta default durumda prosesin dosya betimleyici tablosunda 1024 tane betimleyici için yer vardır. Dolayısıyla derine inildikçe bu tabloda betimleyici yer kaplayacağından derin ağaçlarda betimleyici sıkıntısı çekilebilir. İşte fonksiyonun dördüncü parametresi (fd_limit) fonksiyonun en fazla kaç betimleyiciyi açık olarak tutacığını belirtmektedir. Programcı bu parametreye ortalama bir değer girebilir. Fonksiyon kendi içerisinde burada belirtilen derinlik aşıldığında özyineleme yaparken üst dizin'in betimleyicisini kapatıp geri dönüşte yeniden açmaktadır. Ayrıca fonksiyonun dokümantasyonunda fonksiyonun her kademe için en fazla bir tane betimleyici kullanacağı belirtilmiştir. Fonksiyonun son parametresi özyinelemeli dolaşım sırasında bazı belirlemeler için kullanılmaktadır. Bu parametre çeşitli sembolik sabitlerin bit düzeyinde OR'lanması ile oluşturulmaktadır. Bu sembolik sabitler şunlardır: FTW_CHDIR: Eğer bu bayrak belirtilirse fonksiyon her dizine geçtiğinde prosesin çalışma dizinini de o dizin olarak değiştirmektedir. FTW_DEPTH: Normalde dolaşım "pre-order" biçimde yapılmaktadır. Bu bayrak girilirse "post-order" dolaşım yapılır. Bayrağın ismi yanlış verilmiştir. "pre-order" dolaşım demek bir dizin ile karşılaşıldığında önce dizin girişinin ele alınması sonra özyineleme yapılması demektir. "post-order" dolaşım ise önce özyineleme yapılıp sonra dizin girişinin ele alınması demektir. Defaul durum "pre-order" dolaşım biçimindedir. FTW_MOUNT: Bu bayrak belirtilirse özyineleme yapılırken bir "mount point" ile karşılaşılırsa o dosya sistemine girilmez. Default durumda özyineleme sırasında bir "mount point" ile kaşılaşılırsa özyineleme o dosya sisteminin içine girilerek devam ettirilmektedir. FTW_PHYS: Default durumda nftw fonksiyonu bir sembolik link dosyası ile karşılaştığında linki izler ve link'in hedefine yönelik hareket eder. Daha önce bir böyle bir durumun sonsuz döngüye yol açabileceğinden bahsetmiştik. Bu nedenle biz özyinelemede stat fonksiyonu yerine lstat fonksiyonunu kullanmıştık. İşte bu bayrak belirtilirse artık nftw fonksiyonu sembolik link dosyası ile karşılaştığında link'i izlemez, sembolik link dosyasının kendisi hakkında bilgi verir. Programcı bu dördüncü parametreye hiçbir bayrak geçmek istemezse 0 girebilir. nftw fonksiyonun geri dönüş değeri fonksiyon başarısızsa -1, başarılıysa 0'dır. Ancak aslında fonksiyon başarılı durumda callback fonksiyonun geri dönüş değeri ile geri döner. Şöyle ki: Biz callback fonksiyonu 0 ile geri döndürürsek özyinelemeye devam etmek istediğimizi belirtmiş oluruz. Bu durumda bir IO hatası da olmazsa nftw fonksiyonu 0 ile geri döner. Eğer biz bu fonksiyondan sıfır dışı bir değerle geri dönersek. nftw fonksiyonu özyinelemeyi bırakıp hemen geri çıkar ve bizim callback fonksiyondan döndürdüğümüz sıfır dışı değerle geri döner. Şimdi de callback fonksiyonun parametrelerine gelelim: int callback(const char *path, const struct stat *finfo, int flag, struct FTW *ftw); Fonksiyonun birinci parametresine bulunun dizin girişinin yol ifadesi yerleştirilir. Bu yol ifadesinin baş kısmı tamamen bizim nftw fonksiyonuna verdiğimiz dizin ifadesinden oluşmaktadır. (Yani biz nftw fonksiyonuna mutlak bir yol iafdesi verirsek buraya mutlak bir yol ifadesi geçirilir, biz nftw fonksiyonuna göreli bir yol ifadesi verirsek burada göreli bir yol ifadesi geçirilir.) Fonksiyonun ikinci parametresi bulunan dizin girişine ilişkin struct stat yapısının adresini belirtmektedir. Fonksiyonun üçüncü parametresi ise bulunan dizin girişinin türünü belirtmektedir. Bu tür şunlardan birine tam eşit olmak zorundadır: FTW_D: Bulunan giriş bir dizin girişidir. FTW_DNR: Bulunan giriş bir dizin girişidir. Ancak bu dizin'in içi okunamamaktadır. Dolayısıyla bu dizin özyinelemede dolaşılamayacaktır. FTW_DP: Post-order dolaşımda bir dizinle karşılaşıldığında bayrak FTW_D yerine FTW_DP olarak set edilmektedir. FTW_F: Bulunan dizin girişi sıradan bir dosyadır (regular file). FTW_NS: Bulunan dizin girişi için stat ya da lstat fonksiyonu başarısız olmuştur. Dolayısıyla fonksiyona geçirilen stat yapısı da anlamlı değildir. FTW_SL: Bulunan giriş bir sembolik bağlantı dosyasına ilişkindir. Sembolik bağlantı dosyasının hedefi mevcuttur. FTW_SLN: Bulunan giriş bir sembolik bağlantı dosyasına ilişkindir. Sembolik bağlantı dosyasının hedefi mevcut değildir ("danging link" durumu). callback fonksiyonun son parametresi FTW isimli bir yapı türündendir Bu yapı şöyle bildirilmiştir: struct FTW { int base; int level; }; Yapının level elemanı ağaçtaki derinlik düzeyini belirtmektedir. Bu değer 0'dan başlayarak derine indikçe artırılmaktadır. base elemanı ise dizin girişinin birinci parametrede belirtilen yol ifadesinin kaçıncı indeksinden başladığını belirtmektedir. Örneğin biz "/home/kaan/Study" dizinini dolaşmak istemiş olalım. Fonksiyon da dizin girişi olarak "sample.c" bulmuş olsun. Fonksiyon bize bu girişi "/home/kaan/Study/sample.c" biçiminde verecektir. İşte buradaki base 17 olarak verilecektir. Aşağıda nftw fonksiyonunun kullanımına bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #define _XOPEN_SOURCE 500 #include #include #include int callback(const char *path, const struct stat *finfo, int flag, struct FTW *ftw); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int result; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((result = nftw(argv[1], callback, 100, FTW_PHYS)) == -1) exit_sys("nftw"); printf("result = %d\n", result); return 0; } int callback(const char *path, const struct stat *finfo, int flag, struct FTW *ftw) { switch (flag) { case FTW_DNR: printf("%*s%s (cannot read directory)\n", ftw->level * 4, "", path + ftw->base); break; case FTW_NS: printf("%*s%s (cannot get statinfo)\n", ftw->level * 4, "", path + ftw->base); break; default: printf("%*s%s\n", ftw->level * 4, "", path + ftw->base); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bir dosyanın hard link'ini programlama yoluyla oluşturabilmek için link isimli POSIX fonksiyonu kullanılmaktadır. Linux sistemlerinde link fonksiyonu doğrudan işletim sisteminin sys_link isimli sistem fonksiyonunu çağırmaktadır. link fonksiyonunun prototipi şöyledir: #include int link(const char *oldpath, const char *newpath); Fonksiyonun birinci parametresi hard link'i çıkartılacak dosyanın yol ifadesini, ikinci parametresi yeni dizin girişinin ismini belirtmektedir. Tabii prosesin ilgili dizine yazma hakkının olması gerekir. POSIX standartlarına göre bir sembolik bağlantı dosyasının har link'i çıkartılırken bu sembolik bağlantının kendisinin mi yoksa onun hedefinin mi link'inin çıkartılacağı işletim sistemini yazanların isteğine bırakılmıştır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner ve errno uygun biçimde değer alır. Bir dizin'in hard link'inin çıkartılması özyinelemeli fonksiyonları sonsuz döngüye sokabilmektedir. Bu nedenle dizinler üzerinde hard link çıkartma şüpheli bir durumdur. POSIX standartları bir dizin'in hard link'ini çıkartılabilmesi için prosesin bunu yapabilecek önceliğe sahip olması gerektiğini (yani etkin kullanıcı id'sinin 0 olması gerektiğini) ve işletim sisteminin de dizinlerin hard link'lerinin çıkartılabilmesine izin vermesi gerektiğini belirtmektedir. Eskiden Linux sistemleri root prosesler için dizinlerin hard link'lerinin çıkartılmasına izin veriyordu. Ancak sonraları bunu da kaldırdı. Yani Linux istemlerinde dizinlerin hard link'leri artık çıkartılamamaktadır. Daha önceden de belirtildiği gibi bir dosyanın hard link'i komut satırından ln komutuyla oluşturulabilmektedir. Tabii aslında bu program link fonksiyonu çağrılarak yazılmıştır. Örneğin ln a b Aşağıdaki örnekte komut satırından verilen bir dosyanın hard link'i çıkartılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (link(argv[1], argv[2]) == -1) exit_sys("link"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- link fonksiyonunun linkat ismiyle "at"li bir versiyonu da vardır. linkat fonksiyonunun prototipi şöyledir: #include int linkat(int fd1, const char *path1, int fd2, const char *path2, int flag); Fonksiyonun birinci parametresi ikinci parametresiyle belirtilen yok ifadesi göreli ise aramanın yapılacağı dizinin betimleyicisini alır. Üçüncü parametres ise dörddüncü parametrede belirtilen yol ifadesi göreli ise aramanın yapılacağı dizin'in betimleyicisini almaktadır. Son parametre sembolik bağlantının izlenip inzlenmeyeceğini belirtir. Eğer bu parametre AT_SYMLINK_FOLLOW biçiminde girilirse semboli bağlantı izlenir. Eğer bu parametre 0 girilirse sembolik bağlantı izlenmez. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dosyanın sembolik bağlantı dosyası symlink isimli POSIX fonksiyonuyla oluşturulmaktadır. Bu fonksiyon Linux sistemlerinde doğrudan sys_symlink isimli sistem fonksiyonunu çağırmaktadır. Fonksiyonun prototipi şöyledir: #include int symlink(const char *path1, const char *path2); Fonksiyonun birinci parametresi sembolik bağlantısı çıkartılacak dosyanın ypl ifadesini, ikinci parametresi ise sembolik bağlantı dosyasının yol ifadesini alır. Fonksiyon başarı durumunda sıfır değerine, başarısızlık durumunda -1 değerine geri döner. Prototipten de gördüğünüz gibi sembolik bağlantı dosyasının kendisine ilişkin erişim hakları bizden istenmemektedir. Çünkü sembolik bağlantı dosyasının kendi erişim haklarının bir önemi yoktur. Bu fonksiyon bu erişim hakları için "rwxrwxrwx" haklarını vermektedir. Sembolik bağlantı dosyasını izleyen fonksiyonlar hedef dosyanın erişim haklarını dikkate alırlar. Sembolik bağlantı dosyasının kendi erişim haklarının bir önemi yoktur. symlink fonksiyonu ile "dangling" link oluşturulabilmektedir. Yani başka bir deyişle fonksiyonun birinci parametresinde belirtilen dosyanın bulunuyor olması gerekmez. Bir POSIX fonksiyonun sembolik bağlantı dosyasını izleyip izlemediğine dikkat ediniz. Şüphe duyarsanız dokümanlardan bunu doğrulayınız. Örneğin open fonksiyonu sembolik bağlantıyı izlemektedir. Ancak remove ve unlik fonksiyonları sembolik bağlantı dosyalarını izlememektedir. (Yani remove ve unlink ile sembolik bağlantı dosyası silinmeye çalışılırsa bu fonksiyonlar sembolik bağlantı dosyasının kendisini silmektedir.) Genel olarak POSIX fonksiyonlarının büyük bölümü sembolik bağlantı dosyasını izlemektedir. Bir POSIX fonksiyonu "pathname resolution" işlemini yaparken belli sayıdadan fazla sembolik link üzerinden geçilmişse döngüsel bir durumun oluştuğunu düşünerek ELOOP özel değeri ile geri dönmektedir. Örneğin: a -> b b -> a Bu durumda biz "a" dosyasını open fonksiyonuyla açmak istersek fonksiyon başarısız olur ve errno ELOOP değerini alır. Dizin'lerin hard link'lerinin çıkartılması sorunlu bir durum oluşturduğundan yukarı söz etmiştik. Halbuki aynı durum sembolik bağlantılar için geçerli değildir. Yani sıradan bir proses bir dizin'in sembolik bağlantısını oluşturabilmektedir. Daha önceden de belirtildiği gibi bir dosyanın sembolik bağlantısı ln -s kabuk komutuyla oluşturulabilmektedir. Örneğin: ln -s a b Burada b sembolik bağlantı dosyası a dosyasını göstermektedir. Aşağıda komut satırından hareketle bir sembolik bağlantı oluşturma örneği verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (symlink(argv[1], argv[2]) == -1) exit_sys("symlink"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- symlink fonksiyonun symlinkat isminde bir de "at"li versiyonu vardır: #include int symlinkat(const char *path1, int fd, const char *path2); Fonksiyonun ikinci parametresi üçüncü parametresindeki yol ifadesi göreli ise aramanın yapılacağı dizinin betimleyicisini almaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz lstat fonksiyonuyla bir dosyanın bilgilerini elde ettiğimizde o dosyanın bir sembolik bağlantı dosyası olup olmadığını anlayabiliyorduk. Ancak o sembolik bağlantı dosyasının hangi dosyaya referans ettiğini lstat fonksiyonu bize vermemektedir. İşte readlink isimli POSIX fonksiyonu bu işi yapmaktadır. Fonksiyonun prototipi şöyledir: #include ssize_t readlink(const char *path, char *buf, size_t bufsize); Fonksiyonun birinci parametresi sembolik bağlantı dosyasının yol ifadesini belirtir. İkinci ve üçüncü parametreler sembolik bağlantı dosyasının referans ettiği dosyanın yol ifadesinin yerleştirileceği yerin adresini ve uzunluğu almaktadır. Bu alan küçük ise fonksiyon başarısız olmaz ancak yol ifadesinin son kısmı budanır. Fonksiyon verdiğimiz adrese yerleştirdiği karakter sayısına geri dönmektedir. Fonksiyon (diğer fonksiyonların aksine) null karakteri dizinin sonuna yerleştirmez. Bu durumda programcı referans edilen yol ifadesine erişirken dikkat etmelidir. Fonksiyon başarı durumunda yerleştirilen karakter sayısına, başarısızlık durumunda -1 değerine geri dönmektedir. Aşağıda readlink fonksiyonun kullanımına bir örnek verilmiştir. readlink fonksiyonunun null karakteri diziye yerleştirmediğine dikkat ediniz. Sonunda null karakter olmayan result uzunlukta bir yazının printf ile bastırılması şöyle yapılabilir: printf("%.*s\n, result, buf); printf "%.10s" gibi bir format karakterlerinde yazıyı null karakter görene kadar değil n karakter yazdırmaktadır. (Örneğimizde 10). Tabii biz burada istersek null karakteri dizinin sonuna yerleştirip onu normak olarak yazdırabiliriz. Ancak bu durumda da dizi uzunluğunun yeterli olduğuna dikkat etmemiz gerekir. Aşağıdaki örnekte biz yoli fadesinin yerleştirileceği diziyi 4096 + 1 eleman uzunluğunda açtık. Linux sistemlerinde x86 ve x64 mimarilerinde (sayfa uzunluğunun 4K olduğu mimarilerde) yol ifadeleri en fazla 4096 karakter olabilmektedir. Ancak diğer mimarilerde ve POSIX genelinde böyle bir zorunluluk yoktur. O sistemdeki maksimum yol ifadesi uzunluğu dosyası içerisindeki PATH_MAX sembolik sabitiyle belirtilmektedir. Ancak maalesef bu sembolik sabitin define edilmiş olması da zorunlu değildir. Bu konunun biraz ayrıntıları olduğu için konu bir başlık altında ileride ele alınacaktır. Aşağıdaki örnekte yol ifadesi tam olarak 4096 karakter de olabilir. Ya da daha uzun olup budanmış da olabilir. Bu tür durumlarda tavsiye edilen diziyi büyütüp fonksiyonu başarılı olan kadar tekrar tekrar çağırmaktır. Ancak bir yol ifadesinin 4096 karakterden büyük olması çok çok uç bir noktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { char buf[4096 + 1]; ssize_t result; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((result = readlink(argv[1], buf, 4096)) == -1) exit_sys("readlink"); printf("result = %lld\n", (long long)result); if (result < 4096) { buf[result] = '\0'; /* alternatifi -> printf("%.*s\n", (int)result, buf); */ puts(buf); } else fprintf(stderr, "path maybe truncated!...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 19. Ders 24/12/2022 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- access isimli POSIX fonksiyonu bir dosyaya okuma, yazma, çalıştırma gibi erişimlerin mümkün olup olmadığı bilgisini bize vermektedir. Fonksiyonun prototipi şöyledir: #include int access(const char *path, int amode) Fonksiyonun birinci parametresi erişim testinin yapılacağı dosyanın yol ifadesini belirtmektedir. İkinci parametresi test edilecek erişimi belirtir. Bu parametre aşağıdaki sembolik sabitlerin bir düzeyinde OR'lanmasıyla oluşturulabilir: R_OK: Okuma yapılabilir mi? W_OK: Yazma yapılabilir mi? X_OK: Çalıştırılabilir mi? F_OK: Dosya var mı? access fonksiyonuyla ilgili iki önemli nokta vardır. Birincisi access fonksiyonu test işleminde prosesin etkin kullanıcı id'sini ve grup id'sini değil gerçek kullanıcı id'sini ve grup id'sini işleme sokar. Her ne kadar prosesin gerçek kullanıcı ve grup id'leri çoğu kez etkin kullanıcı id'leri ve grup id'leri ile aynı olsa da bazen farklılaşabilmektedir. İkinci durum ise, access ile bir test yapıldıktan sonra bu teste dayalı olarak dosya üzerinde işlem yapılmak istendiğinde bu işlemin başarılı olması garanti değildir. Çünkü o arada sistemdeki başka bir proses dosyanın erişim hakları üzerinde değişiklik yapmış olabilir. access fonksiyonu test olumluysa 0 değerine olumsuzsa -1 değerine geri dönmektedir. Tabii access fonksiyonun başarısızlığının başka nedenleri de olabilir. Ancak programcı genellikle öyle ya da böyle istediği işlemi yapıp yapamayacağı ile ilgilenmektedir. Ancak yine de fonksiyon başarısız olduğunda errno değeri incelenebilir ve başarısızlığın EACCESS nedeniyle olduğu doğrulanabilir. Biz aşağıdaki örnekte bu yola gitmiyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (access(argv[1], F_OK) == 0) printf("file exists...\n"); else { printf("file doesn't exist!...\n"); exit(EXIT_SUCCESS); } if (access(argv[1], R_OK) == 0) printf("read access ok...\n"); else printf("can't read...\n"); if (access(argv[1], W_OK) == 0) printf("write access ok...\n"); else printf("can't write...\n"); if (access(argv[1], X_OK) == 0) printf("execute access ok\n..."); else printf("can't execute...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- access fonksiyonunun GNU libc kütüphanesinde prosesin etkin kullanıcı is'sini ve etkin grup id'sini kullanarak test eden euidaccess ve eaccess (ikisi aynı şeyi yapmaktadır) versiyonları da bulunmaktadır. Ancak bu iki fonksiyon POSIX standartlarında yoktur. Dolayısıyla taşınabilir programlar için bu konuya dikkat edilmesi gerekir. #define _GNU_SOURCE /* See feature_test_macros(7) */ #include int euidaccess(const char *pathname, int mode); int eaccess(const char *pathname, int mode); Bu fonksiyonların semantiği etkin kullanıcı id'sini ve grup id'sini kullanmalarının dışında bir farklık içermemektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- access fonksiyonunun faccassat isminde "at"li bir versiyonu da vardır. Bu versiyonda aynı zamanda istenirse gerçek kullanıcı ve grup id'leri yerine etkin kullanıcı ve grup id'leri de işleme sokulabilmektedir. Fonksiyonun parametrik yapısı şöyledir: #include int faccessat(int fd, const char *path, int amode, int flag); Fonksiyonun birinci parametresi ikinci parametresiyle belirtilen yol ifadesinin göreli olması durumunda aramanın yapılacağı dizini belirtmektedir. Son parametre 0 geçilebilir ya da AT_EACCESS geçilebilir. Bu AT_EACCESS değeri test işleminin etkin kullanıcı ve grup id'lerine bakılarak yapılacağı anlamına gelmektedir. (Tabii ikinci parametre ile belirtilen yol ifadesi mutlak olduğunda birinci parametrede belirtilen dizine ilişkin betimleyici yine dikkate alınmaz. Ancak üçüncü parametreyle belirtilen bayrak dikkate alınır) ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (faccessat(AT_FDCWD, argv[1], F_OK, AT_EACCESS) == 0) printf("file exists...\n"); else { printf("file doesn't exist!...\n"); exit(EXIT_SUCCESS); } if (faccessat(AT_FDCWD, argv[1], R_OK, AT_EACCESS) == 0) printf("read access ok...\n"); else printf("can't read...\n"); if (faccessat(AT_FDCWD, argv[1], W_OK, AT_EACCESS) == 0) printf("write access ok...\n"); else printf("can't write...\n"); if (faccessat(AT_FDCWD, argv[1], X_OK, AT_EACCESS) == 0) printf("execute access ok\n..."); else printf("can't execute...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi "dosya betimleyici tablosu (file descriptor table)" proses kontrol blok yoluyla erişilebilen dosya nesnelerinin adreslerinin tutulduğu bir gösterici dizisi biçimindeydi. İşletim sisteminin çekirdeği ne zaman bir dosya açılsa o dosya için bir dosya nesnesi (Linux'ta "file" yapısı) yaratıp dosya betimleyici tablosunda bir slotun o nesneyi göstermesini sağlıyordu. Zaten "dosya betimleyicisi (file descriptor)" de dosya betimleyici tablosunda bir indeks belirtiyordu. Linux çekirdeklerinde buradaki veri yapıları zamanla biraz değiştirilmiştir. Güncel çekirdekte proses kontrol bloktan dosya nesnesine erişim birkaç yapıdan geçilerek yapılmaktadır: task_struct (files) ---> files_struct (fdt) ---> fdtable (fd) ---> file * türünden bir dizi ---> file Genellikle bir proses çalışmaya başladığında ilk üç betimleyici doludur. Bu betimleyicilere sırasıyla "stdin", "stdout" ve "stderr" betimleyicileri denilmektedir. Bu ilk üç betimleyici için dosyasında üç sembolik de bulundurulmuştur: #define STDIN_FILENO 0 #define STDOUT_FILENO 1 #define STDERR_FILENO 2 Aygıt "sürücüler (device drivers)" dosya gibi açılarak kullanılmaktadır. (Yani bir aygıt sürücü de kullanılmadan önce "open" fonksiyonuyla açılır, sonra "read" fonksiyonuyla ondan okuma yapılıp "write" fonksiyonu ile ona yazma yapabilir.) Dolayısıyla bir dosya nesnesi bir disk dosyasına ilişkin olabileceği gibi bir aygıt sürücüs dosyasına da ilişkin olabilir. Örneğin biz bir betimleyiciden read fonksiyonu ile okuma yapmak istediğimizde sistem eğer bu betimleyicinin gösterdiği dosya nesnesi bir disk dosyasına ilişkinse bizim dosyadan okuma yapmamızı sağlar. Ancak bir aygıt sürücüye ilişkinse bu durumda sistem o aygıt sürücünün "read" fonksiyonunu çağırır. Yani aygıt sürücülerin içerisinde "read" yapıldığında "write" yapıldığında çağrılacak fonksiyonlar vardır. İşte örneğin biz 0 numaralı betimleyiciden okuma yapmak istediğimizde aslında "terminal aygıt sürücüsünün" "read" fonksiyonu çağrılmaktadır. 0 numaralı betimleyici O_RDONLY modunda açılmıştır. Terminal aygıt sürücüsünün "read" fonksiyonu da bize klavyeden okunanları verir. Program çalışmaya başladığında 1 ve 2 numaralı betimleyicilerin her ikisi de aynı dosya nesnesini göstermektedir. Bu dosya da O_WRONLY modunda açılmıştır. Bu dosya nesneleri de yine "terminal aygıt sürücüsüne" ilişkindir. Dolayısıyla biz write işlemi yaptığımızda aslında terminal aygıt sürücüsünün "write" fonksiyonunu çağırmış oluruz. O da bilgileri imlecin bulunduğu noktadan itibaren ekrana yazar. Burada stdout ve stderr betimleyicilerinin aynı dosya nesnesini gösterdiğine dikkat ediniz. Dolayısıyla bu betimleyiciler kullanıldığında yazdırılmak istenen şeyler ekrana çıkacaktır. (O halde stdout ile stderr arasında ne farklılık vardır? İzleyen bölümlerde bu durum açıklanacaktır) open fonksiyonunun ilk boş betimleyici veridiği garanti edilmiştir. Yani örneğin programımız başladığında 0, 1 ve 2 numaralı betimleyiciler dolu olduğuna göre open fonksiyonu bize 3 numaralı betimleyiciyi vercektir. Tabii dosyaları kapattığımızda o betimleyicilere ilişkin slot'lar serbest bırakılır. Bu durumda open ilk boş betimleyiciyi bize verir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dosya betimleyici tablosunda iki dosya betimleyicisi aynı dosya nesnesini gösteriyorsa bu duruma "dosya betimleyicilerinin çiftlenmiş (duplicate) olması" denilmektedir. Bu durumda bizim bu betimleyicilerden hangisini kullandığımız bir önemi kalmamaktadır. Pekiyi böyle bir durumda bir betimleyiciyi close fonksiyonuyla kapattığımızda ne olacaktır? İşte dosya nesnelerinin içerisinde bir sayaç bulunmaktadır. close fonksiyonu bu sayacın değerini bir eksiltir. Dosya nesnesinin silinmesi sayaç 0'a düştüğünde yapılmaktadır. O halde close her durumda betimleyici slotunu boşaltır. Ancak dosya nesnesinin referans sayıcını bir eksilttikten sonra eğer referans sayacı 0'a düşmüşse dosya nesnesini siler. Aşağıda Linux'un güncel çekirdeğindeki dosya nesnesi verilmiştir. Buradaki f_count elemanı bu sayacı belirtmektedir: struct file { union { struct llist_node f_llist; struct rcu_head f_rcuhead; unsigned int f_iocb_flags; }; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; /* * Protects f_ep, f_flags. * Must not be taken from IRQ context. */ spinlock_t f_lock; atomic_long_t f_count; unsigned int f_flags; fmode_t f_mode; struct mutex f_pos_lock; loff_t f_pos; struct fown_struct f_owner; const struct cred *f_cred; struct file_ra_state f_ra; u64 f_version; #ifdef CONFIG_SECURITY void *f_security; #endif /* needed for tty driver, and maybe others */ void *private_data; #ifdef CONFIG_EPOLL /* Used by fs/eventpoll.c to link all the hooks to this file */ struct hlist_head *f_ep; #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; errseq_t f_wb_err; errseq_t f_sb_err; /* for syncfs */ }; ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir betimleyicinin gösterdiği dosya nesnesini gösteren yeni bir betimleyici oluşturulabilir. Bunun için dup ve dup2 isimli POSIX fonksiyonları kullanılmaktadır. dup fonksiyonunun prototipi şöyledir: #include int dup(int fildes); Fonksiyon parametre olarak açık bir dosyanın betimleyicisini alır, o betimleyicinin gösterdiği dosya nesnesini gösteren yeni bir betimleyici oluşturup o betimleyiciye geri döner. Fonksiyon başarısızlık durumunda -1'e geri dönmektedir. dup fonksiyonunun en düşük boş betimleyici slotunu tahsis etmesi garanti edilmiştir. Açık dosyanın tüm bilgileri dosya nesnesinin içerisinde olduğuna göre iki betimleyici aynı dosya nesnesini gösteriyorsa örneğin aynı dosya göstericisine sahip gibi davranırlar. Aşağıdaki programda dup fonksiyonuna bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; int fd2; char buf[10 + 1]; ssize_t result; if ((fd = open("sample.c", O_RDONLY)) == -1) exit_sys("open"); if ((fd2 = dup(fd)) == -1) exit_sys("dup"); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); buf[result] = '\0'; puts(buf); if ((result = read(fd2, buf, 10)) == -1) exit_sys("read"); buf[result] = '\0'; puts(buf); close(fd2); close(fd); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- dup2 isimli POSIX fonksiyonu dup fonksiyonunun biraz daha ayrıntılı biçimidir. Fonksiyonun prototipi şöyledir: #include int dup2(int fildes, int fildes2); Bu fonksiyon yine birinci parametresiyle belirtilen betimleyici çiftlemek için kullanılmaktadır. Ancak bu fonksiyon ilk boş betimleyici ile değil ikinci parametresiyle belirtilen betimleyici ile geri dönmek ister. Yani biz istersek bu fonksiyon sayesinde istediğimiz bir betimleyicinin birinci parametresiyle belirtilen betimleyici ile aynı dosya nesnesini göstermesini sağlayabiliriz. Eğer ikinci parametresiyle belirtilen betimleyici zaten açık bir dosyaya ilişkinse bu durumda dosya önce kapatılır, sonra o betimleyicinin birinci parametresiyle belirtilen betimleyicinin gösterdiği dosya nesnesini göstermesi sağlanır. Fonksiyon başarı durumunda ikinci parametresiyle belirtilen betimleyicinin aynısına, başarısızlık durumunda -1 değerine geri dönmektedir. Tabii fonksiyon birinci ve ikinci parametresinin aynı betimleyiciye ilişkin olduğunu da kontrol etmektedir. Bu durumda bir şey yapmadan başarılı olur. Fonksiyonun iki argümanı aynı betimleyiciyi belirtiyorsa dup2 hiçbir şey yapmaz, argümanlarla belirtilen betimleyiciye geri döner. Aşağıda dup2 fonksiyonunun kullanımına bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; int fd2; char buf[10 + 1]; ssize_t result; if ((fd = open("sample.c", O_RDONLY)) == -1) exit_sys("open"); if ((fd2 = dup2(fd, 25)) == -1) exit_sys("dup"); printf("fd = %d, fd2 = %d\n", fd, fd2); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); buf[result] = '\0'; puts(buf); if ((result = read(fd2, buf, 10)) == -1) exit_sys("read"); buf[result] = '\0'; puts(buf); close(fd2); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 20. Ders 07/01/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dosyalar konusundaki önemli bir alt konu da "IO Yönlendirmesi (IO Redirection)" denilen konudur. IO yönlendirmesi teknik olarak bir dosya betimleyicisinin gösterdiği dosya nesnesinin değiştirilmesi işlemidir. Bu sayede bir kişi belli bir dosya üzerinde işlem yaptığını sanırken aslında başka bir dosya üzerinde işlem yapar hale gelmektedir. IO yönlendirmesi en çok "stdin", "stdout" ve "stderr", dosyaları üzerinde uygulanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi bir proses yaratıldığında genellikle işin başında 0, 1 ve 2 numaralı betimleyiciler zaten dolu durumdadır. UNIX/Linux dünyasında 0 numaralı betimleyiciye "stdin" betimleyicisi, "1 numaralı betimleyiciye "stdout" betimleyicisi ve 2 numaralı betimleyiciye ise "stderr" betimleyicisi denilmektedir. Bir dosya betimleyicisinin gösterdiği dosya nesnesi bir disk dosyasına ilişkin olabileceği gibi bir "aygıt sürücü (device driver)" dosyasına ilişkin de olabilmektedir. Gerçekten de 0, 1 ve 2 numaralı betimnleyicilerin gösterdiği dosya nesneleri "terminal aygıt sürücüsüne" ilişkindir. Bir aygıt sürücü kernel mode'da çalışan bir kod topluluğudur. Bir dosya betimleyicisi bir aygıt sürücüne ilişkinse bu betimleyici ile read fonksiyonu çağrıldığında aygıt sürücüsünü yazanların "read" olarak tanımladıkları fonksiyon çağrılmaktadır. Benzer biçimde bir aygıt sürücüne ilişkinse bu betimleyici ile write fonksiyonu çağrıldığında aygıt sürücüsünü yazanların "write" diye tanımladıkları fonksiyon çağrılmaktadır. 0 numaralı stdin betimleyicisi "read-only" modda açılmış durumdadır. Benzer biçimde 1 ve 2 numaralı betimleyiciler de "write-only" modda açılmış durumdadır. 0 numaralı betimleyiciden okuma yapılmak istendiğinde aygıt sürücüsünün okuma fonksiyonu çalıştırılır ve bu fonksiyon da aslında klavyeden okunanları bize verir. 1 ve 2 numaralı betimleyiciler dup yapılmış durumdadır. Yani bu betimleyiciler aynı dosya nesnesini göstermektedir. Bu betimleyicilerle yazma işlemi yapılırsa aygıt sürücülerin yazma fonksiyonları devreye girer ve bu fonksiyonlar da yazdırılacak şeyleri ekrana çıkartırlar. Görüldüğü gibi aygıt sürücüler sanki bir dosyaymış gibi ele alınmaktadır. Bunun önemli faydaları vardır. Yani programcı bu sayede "sanki klavye ve ekran birer dosyaymış gibi" dosya fonksiyonlarını kullanarak onlarla işlem yapabilmektedir. Aşağıdaki örnekte 0 numaralı stdin betimleyicisinden read fonksiyonuyla okuma yapılmış ve okunanlar 1 numaralı stdout betimleyicisine yazılmıştır. Biz read fonksiyonuyla stdin dosyasından okuma yapmak istediğimizde read fonksiyonu ENTER tuşuna basılına kadarki klavyeden girilenleri bize vermektedir. dosyasında okunabilirliği artırmak için şu sembolik sabitler bildirilmiştir: #define STDIN_FILENO 0 #define STDOUT_FILENO 1 #define STDERR_FILENO 2 Bir program çalışmaya başladığında 0, 1 ve 2 numaralı betimleyiciler zaten hazır durumdadır. Bu betimleyicileri programcı oluşturmamıştır. O halde bu betişmleyicilerin kapatılmasını da programcı yapmamalıdır. Buradaki mekanizma ileride ele alınacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { char buf[4096]; ssize_t result; if ((result = read(0, buf, 4096)) == -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); } /*-------------------------------------------------------------------------------------------------------------------------- C'nin dosyası içerisinde prototipleri bulunan stdin ve stdout dosyaları üzerinde işlem yapan "scanf", "puts", "printf" gibi fonksiyonları eninde sonunda read ve write fonksiyonlarını 0 ve 1 numaralı betimleyicilerle çağırarak işlemlerini yapmaktadır. Zaten bu sistemlerde ekrana bir şey yazdırmak için klavyeden bir şey okumak için başka bir yol yoktur. Örneğin biz printf fonksiyonu ile ekrana bir şeyler yazdırmak istediğimiz zaman aslında printf önce yazdırılacak yazıyı bir dizide oluşturur sonra write fonksiyonunu 1 numaralı betimleyici ile çağırır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte biz close(1) ile 1 numaralı betimleyicinin gösterdiği terminal aygıt sürücüsüne ilişkin dosyayı kapattık. Sonra da open fonksiyonu ile yeni bir dosyayı açtık. open fonksiyonu en düşük boş betimleyiciyi vereceğine göre artık 1 numaralı betimleyici terminal aygıt sürücüne ilişkin dosya nesnesini değil bizim açtığımız dosya nesnesini gösteriyor durumda olacaktır. Daha bu örnekte biz printf fonksiyonu ile ekrana bir şeyler yazdık. printf eninde sonunda write(1, ...) çağrısıyla ekrana bir şeyler yazdırmak isteyeceğine göre artık printf ekrana değil bizim açtığımız dosyaya yazma yapacaktır. IO yönlendirmesinin temel mekanizması bu biçimdedir. ---------------------------------------------------------------------------------------------------------------------------*/ #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("Number: %d\n", i); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- IO yönlendirmesinin yukarıdaki gibi yapılmasının iki önemli problemi vardır: 1) Bu yönlendirme aynı biçimde yüksek numaralı betimleyiciler için yapılmak istenirse o betimleyicilerden önce boş boş betimleyicilerin bulunuyor olma olasılığı yükselir. Dolayısıyla open istediğimiz betimleyiciyi değil başka bir betimleyiciyi tahsis edebilir. 2) Çok thread'li programlarda close işleminden sonra henüz open yapılmadan önce başka bir thread dosya açarsa bu betimleyiciyi o thread kapabilir. Çünkü close ile open işlemleri atomik değildir. IO yönlendirmesi daha sağlıklı bir biçimde dup2 fonksiyonuyla yapılabilir. Anımsanacağı gibi dup2(fd1, fd2) işleminde fd2 betimleyicisi fd1 betimleyicisi ile aynı dosya nesnesini gösterir hale getirilmektedir. fd2 zaten açık bir dosyaya ilişkinse önce o betimleyici üzerinde atomik bir biçimde close işlemi uygulanmaktadır. dup2 fonksiyonun en düşük betimleyiciyi değil ikinci parametresiyle blirtilen betimleyiciyi yönlendirdiğine dikkat ediniz. O halde örneğin 1 numaralı betimleyici şöyle yönlendirilebilir: 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); Burada dup2 ile birlikte hem 1 numaralı betimleyicinin hem de fd numaralı betimleyicinin yeni açılan dosyaya ilişkin dosya nesnesini gösteridiğine dikkat ediniz. fd betimleyicisini kapatmak doğru tekniktir. 1 numaralı betimleyici zaten ileride de ele alınacağı gibi proses bittiğinde kapatılmaktadır. Burada gerçekleşmesi beklenmeyen bir küçük nokta üzerinde de durmak istiyoruz. Bizim open fonksiyonuyla yönlendirilecek dosyayı açtığımız durumda ya stdout dosyası zaten kapatılmışsa ne olacaktır? İşte bu durumda close işlemi bizim için sorun oluşturur. Şöyle ki bu durumda open fonksiyonu en düşük betimleyici olan 1 numaralı betimleyiciyi tahsis edecektir. dup2(fd, 1) çağrısında her iki betimleyici de aynı olduğu için dup2 bir şey yapmayacaktır. Ancak bundan sonda fd betimleyicisinin kapatılması aslında 1 numaralı betimleyicinin kapatılması anlamına gelecektir. Yani sakıncalı bir durum oluşaçaktır. Bu sakıncalı durum aşağıdaki gibi bir kontrolle elimine edilebilir: 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 (fd != 1) { if (dup2(fd, 1) == -1) exit_sys("dup2"); close(fd); } Tabii programcının genellile böyle bir kontrol yapmasına gerek yoktur. Çünkü içinde bulunduğu durumda 1 numaralı betimleyicinin stdout dosyasını göstermesi normal bir durumdur. ---------------------------------------------------------------------------------------------------------------------------*/ #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 (fd != 1) { /* kontrol özel bir durum yoksa gerekmemektedir */ if (dup2(fd, 1) == -1) exit_sys("dup2"); close(fd); } for (int i = 0; i < 10; ++i) printf("Number: %d\n", i); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki örnekte biz 1 numaralı betimleyicinin bizim dosyamıza ilişkin dosya nesnesini göstermesini sağladık. Pekiyi bundan geri dönebilir miyiz? Yani 1 numaralı betimleyicinin yeniden terminale ilişkin aygıt sürücüsünü göstermesini sağlayabilir miyiz? Anımsanacağı gibi 1 ve 2 numaralı betimleyicilerin her ikisi de terminal aygıt sürücüsüne ilişkin dosya nesnesini belirtiyordu. İşte biz bu sayede geri dönüşü aşağıdaki gibi yapabiliriz: 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); ... if (dup2(2, 1) == -1) exit_sys("dup2"); Aşağıdaki örnekte bu işlem uygulanmıştır. Ancak burada bir fflush çağırması da yapılmıştır. Bunun nedeni izleyen konularda anlaşılabilecektir. ---------------------------------------------------------------------------------------------------------------------------*/ #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("Number: %d\n", i); fflush(stdout); if (dup2(2, 1) == -1) exit_sys("dup2"); for (int i = 0; i < 10; ++i) printf("Number: %d\n", i); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi yukarıdaki örnekte 2 numaralı betimleyici bir biçimde yönlendirilmişse ya da close edilmişse geri dönüş nasıl sağlanabilir? Burada artık işleme başlamadan önce dup işlemi ile 1 numaralı betimleyicinin gösterdiği dosya nesnesini gösteren başka bir yedek betimleyicinin oluşturulması gerekir. Aşağıda bu duruma örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd, fd_stdout; close(2); if ((fd = open("test.txt", O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("open"); if ((fd_stdout = dup(1)) == -1) exit_sys("dup2"); if (dup2(fd, 1) == -1) exit_sys("dup2"); close(fd); for (int i = 0; i < 10; ++i) printf("Number: %d\n", i); fflush(stdout); if (dup2(fd_stdout, 1) == -1) exit_sys("dup2"); for (int i = 0; i < 10; ++i) printf("Number: %d\n", i); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de 0 numaralı betimleyiciyi yönlendirelim. Eğer biz 0 numaralı betimleyiciyi bir dosyaya yönlendirirsek bu durumda klavyeden (stdin dosyasından) okuma yaptığını sanan standart C fonksiyonları aslında bu dosyadan okuma yapacaktır. Yani adeta sanki bu dosyanın içindekiler klavyeden girilmiş gibi bit etki oluşturacaktır. Aşağıdaki örnekte "test.txt" dosyasının içeriği şöyledir: 10 20 30 40 50 60 70 ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; int val; if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); if (fd != 0) { /* özel bir durum yoksa bu kontrole gerek yok */ if (dup2(fd, 0) == -1) exit_sys("dup2"); close(fd); } while (scanf("%d", &val) == 1) printf("%d\n", val); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- IO yönlendirmesi kabuk üzerinden de yapılabilmektedir. Kabukta ">" sembolü 1 numaralı betimleyicinin yönlendirileceği anlamına gelmektedir. Örneğin: $ ./sample > test.txt Bu durumda kabuk önce ">" sembolünün sağındaki dosyayı O_WRONLY|O_TRUNC modunda açar. Sonra ./sample programını çalıştırarak bu prosesin 1 numaralı betimleyicisini dup2 fonksiyonu ile bu dosyaya yönlendirir. Böylece ./sample, sample programının ekrana yazdığını zannettiği şeyler bu dosyaya yazılmış olacaktır. ls gibi, cat gibi kabuk komutlarının da aslında birer program olduğuna bunların da 1 numaralı betimleyiciyi kullanarak yazdırma yaptığına dikkat ediniz. Örneğimn biz kabuk üzerinde şu komutu uygulayabiliriz: $ ls -l > test.txt Eğer kabukta ">" yerine ">>" sembolü kullanılırsa bu durumda ">>" sembolünün sağındaki dosya O_CREAT|O_WRONLY|O_APPEND modunda açılmaktadır. Yani dosya varsa bu durumda olan dosyanın sonuna ekleme yapılacaktır. Örneğin: $ ls -l >> test.txt ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kabuk üzerinde "<" sembolü de 0 numaralı betimleyiciyi yönlendirmektedir. Örneğin: $ ./sample < test.txt Burada kabuk "test.txt" dosyasını O_RDONLY modda açar. Sonra ./sample programını çalıştırır. Prosesin 0 numaralı betimleyicisini "test.txt" dosyasına dup2 fonksiyonuyla yönlendirir. Böylece program sanki klavyeden okuduğunu sanırken aslında dosyadan okuma yapacaktır. Aşağıdaki örnekte programın "sample.c" olduğunu kabul edelim. "test.txt" dosyasının içeriği de şöyle olsun: 10 20 30 40 50 60 70 Programı kabuktan aşağıdaki gibi çalıştırıp sonucu inceleyiniz: $ ./sample < test.txt ---------------------------------------------------------------------------------------------------------------------------*/ #include int main(void) { int val; while (scanf("%d", &val) == 1) printf("%d\n", val); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Aslında kabukta genel olarak yönlendirme için "n>" ve "n<" sembolleri de kullanılabilmektedir. Buradaki n betimleyicinin numarasını belirtir. Bu sayede biz herhangi bir betimleyiciyi okuma ve yazma amaçlı bir dosyaya yönlendirebiliriz. Örneğin: ./sample 2> test.txt Burada "test.txt" dosyası açılıp ./sample programının "stderr" olarak isimlendirilen 2 numaralı betimleyicisi bu dosyaya yönlendirilecektir. Kabuk programları ">", "<", "n>" "n<" gibi yönlendirmeleri nasıl yapmaktadır? Bu konu ileride ele alınacaktır. Kabuk önce bir kez fork işlemi yapar. Sonra yönlendirme işlemini gerçekleştirir. Sonra da exec işlemi yapmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Tabii hem stdout dosyasını hem de stdin dosyasını kabuk üzerinden birlikte de yönlendirebiliriz. Örneğin: ./sample > out.txt < in.txt Burada 1 numaralı betimleyici "out.txt" dosyasına, 0 numaralı betimleyici "in.txt" dosyasına yönlendirilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aygıt sürücüler yine open fonksiyonuyla açılmaktadır. open fonksiyonunda aygıt sürücüyü temsil eden bir dizin girişi belirtilie. Örneğin: fd = open("/dev/null", O_WRONLY); Ancak bu dizin girişi gerçek bir dosya değildir. Bu giriş için yalnızca bir i-node elemanı bulundurulmaktadır. İşletim sistemi böyle bir dosya açılmaya çalışıldığında aslında "bir aygıt sürücü ile işlem yapılmak istendiğini" anlamaktadır. Yani aygıt sürücü bir dosya gibi açılıyor olsa da aslında onun bir dosyayla ilgisi yoktur. Aygıt dosyaları (örneğimizdeki "dev/null" dosyası) dummy bir dosyadır. Aslında kernel içerisindeki aygıt sürücüyü temsil etmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi stderr dosyası ne anlama gelmektedir? Anımsanacağı gibi stderr 2 numaralı betimleyici ile temsil edilmektedir. 1 ve 2 numaralı betimleyiciler dup yapılmış durumdadır. Yani her iki betimleyici ile de write yapıldığında yazılanlar ekrana çıkacaktır. O halde stderr dosyasının ne anlamı vardır? C'de stdin, stdout ve stderr isimli değişkenler betimleyici belirtmezler. Bu değişkenler FILE * türündendir. Tabii stdin UNIX/Linuz sistemlerinde 0 numaralı betimleyici ile stdout 1 numaralı betimleyici ile stderr de 2 numaralı betimleyici ile ilişkilidir. Biz C'de stderr dosyasına fprintf fonksiyonu ile aşağıdaki gibi bir şeyler yazabiliriz: fprintf(stderr, "stderr\n"); Tabii aslında bilindiği gibi printf ile fprintf arasında, scanf ile fscanf arasındaki tek farklılık printf ve scanf fonksiyonlarının default olarak stdout ve stdin dosya bilgi göstericilerini kullanmasıdır. Yani örneğin printf(...) çağrısı tamamen fprintf(stdout, ...) çağrısı eşdeğerdir. Benzer biçimde scanf(...) çağrısı ile de fscanf(stdin, ...) eşdeğerdir. Programcı hata mesajlarını her zaman "stderr" dosyasına yazdırmalıdır. Bu iyi bir tekniktir. Örneğin: if ((f = fopen("test.txt", "r")) == NULL) { fprintf(stderr, "cannot open file!...\n"); exit(EXIT_FAILURE); } Böylece ileride gerekirse programın normal çıktılarıyla hata mesajları IO yönlendirmesiyle birbirinden ayrılabilir. Tabii biz IO yönlendirmesi yapmadıktan sonra programın normal mesajlarıyla hata mesajlarının her ikisi de ekrana çıkacaktır. Aşağıdaki gibi bir program olsun: /* sample.c */ #include int main(void) { fprintf(stderr, "stderr\n"); fprintf(stdout, "stdout\n"); return 0; } Biz bu programı çeşitli biçimlerde çalıştıralım: $ ./sample stderr stdout $ ./sample > test.txt stderr $ ./sample 2> test.txt stdout Görüldüğü gibi biz programın hata mesajları ile normal mesajları artık ayırabilmekteyiz. Eğer her mesayı printf ile stdout dosyasına yazdırsaydık bunun imkanı olmayacaktı. Örneğin biz find programı ile "sample.c" dosyasını dizin ağacında aramak isteyelim: find / -name "sample.c" Burada erişilemeyen dizinler için find programı bir sürü hata mesajını stderr dosyasına yazdırcaktır. Dolayısıyla kafamız karışacaktır. Şimdi programı şöyle çalıştıralım: find / -name "sample.c" 2> test.txt Artık hata mnesajları ekranda görünmeyecektir. Bu tür durumlar için /dev/null isimli bir aygıt sürücü bulundurulmuştur. Bu aygıt sürücü açılırsa ve ona yazma yapılırsa yazılanlar atılmaktadır. O halde programın yazdığı hata mesajları gereksiz yer kaplamasın diye biz yönlendirmeyi /dev/null aygıt sürücüsüne yapabiliriz. Örneğin: find / -name "sample.c" 2> /dev/null /dev/null aygıt sürücüsünden okuma yapılmaya çalışılırsa sanki dosya sonuna gelinmiş (yani EOF durumuna gelinmiş) gibi bir durum oluşur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 21. Ders 08/01/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- "/dev/zero" aygıt sürücüsü "/dev/null" aygıt sürücüsüne çok benzemektedir. "/dev/zero" aygıt sürücüsüne yazılanlar da atılır. Ancak bu aygıt sürücüden okuma yapıldığında hep sıfır okunmaktadır. Aşağıdaki örnekte bu aygıt sürücü açılıp okuma yapılmıştır. Burada standart C fonksiyonlarını kullanmanın bizim için bir dezavantajı yoktur. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include int main(void) { FILE *f; int ch; if ((f = fopen("/dev/zero", "rb")) == NULL) { fprintf(stderr, "cannot open file!...\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 10; ++i) { if ((ch = fgetc(f)) == EOF) { fprintf(stderr, "cannot read from file!...\n"); exit(EXIT_FAILURE); } printf("%d ", ch); fflush(stdout); } printf("\n"); fclose(f); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- "/dev/random" ve "/dev/urandom" aygıt sürücüleri her okunduğunda rastgele byte'lar elde edilmektedir. Bu iki aygıt sürücü arasında bazı küçük farklılıklar vardır. Ancak burada onun üzerinde durmayacağız. Ayrıca bu aygıt sürücülerden okumayı pratik hale getirmek için Linux'a 3.17 çekirdeği ile birlikte "sys_getrandom" isimli bir sistem fonksiyonu da eklenmiştir. Aşağıdaki örnekte bu aygıt sürücüden rastgele byte'lar okunup hex sistemde ekrana yazdırılmıştır. Program beklemelere yol açarsa şaşırmayınız. Çünkü konunun bazı ayrıntıları vardır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include int main(void) { FILE *f; int ch; if ((f = fopen("/dev/random", "rb")) == NULL) { fprintf(stderr, "cannot open file!...\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 256; ++i) { if ((ch = fgetc(f)) == EOF) { fprintf(stderr, "cannot read from file!...\n"); exit(EXIT_FAILURE); } printf("%02X%c", ch, i % 16 == 15 ? '\n' : ' '); fflush(stdout); } printf("\n"); fclose(f); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Komut satırındaki diğer önemli bir işlem de "boru (pipe)" işlemidir. Boru işlemi "|" ile temsil edilmektedir. Kabuk üzerinden aşağıdaki gibi bir komut uygulamış olalım: a | b Burada kabuk bu yazıyı "|" karakterinden parse eder. "|" karakterinin solundaki ve sağındakileri birer program olarak ele alır. Her iki programı da çalışırır. Yani burada "a" programı da "b" programı da çalıştırılacaktır. "a" programının "stdout" dosyasına yazdıklarını "b" programı "stdin" dosyasından okuyacaktır. Başka bir deyişle "a" programının 1 numaralı betimleyiciyle yaptığı write işlemlerini "b" programı 0 numaralı betimleyici ile read fonksiyonunu kullanarak okuyabilecektir. Kabuk boru işlemlerini "prosesler arası haberleşme yöntemlerinden biri olan boru haberleşmesi ile" gerçekleştirmektedir. Zaten ilerleyen bölümlerde bu konu ele alınacaktır. Tabii boru işlemi yapılırken programların komut satırı argümanları da verilebilir. Örneğin: a b c | d e f Burada aslında çalıştırılacak programlar "a" ve "d" programlarıdır. Diğerleri bunların komut satırı argümanlarıdır. Aşağıdaki örnekte "a" programı ekrana (stdout dosyasına) 0'dan 10'a kadar sayıları yazdırmaktadır. "b" programı ise döngü içerisinde klavyeden (stdin dosyasından) değer okuyup ekrana yazdırmaktadır. Bu iki programı aşağıdaki gibi çalıştıralım: ./a | ./b Burada artık a'nın ekrana yazdıklarını sanki b klavyeden okuyormuş gibi bir etki oluşacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /* a.c */ #include int main(void) { for (int i = 0; i < 10; ++i) printf("%d\n", i); return 0; } /* b.c */ #include int main(void) { int val; while (scanf("%d", &val) == 1) printf("%d\n", val); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerindeki dosya yol ifadesi alan POSIX kabuk komutları eğer doys yol ifadesi verilmezse genellikle "stdin" dosyasından okuma yapacak biçimde yazılmışlardır. Örneğin "cat" komutu bir dosyanın içeriğini stdout dosyasına yazdırır: cat test.txt Ancak bu "cat" komutu argümansız kullanılırsa okumayı "stdin" dosyasından yapar. Örneğin "wc" isimli kabuk komutu normal olarak bir dosyayı argüman olarak alır ve o dosyadaki satır sayısını, sözcük sayısını ve byte sayısını stdout dosyasına yazdırır. Ancak bu program komut satırı argümanı verilmeden kullanılırsa klavyeden (stdin dosyasından) okuma yapacaktır. Bu biçimdeki tasarımın nedeni bu komutların "boru" eşliğinde kullanımını sağlamaktır. Örneğin: ps -e | wc Burada "ps -e" komutu satır satır sistemdeki prosesleri "stdout" dosyasına yazmaktadır. "wc" komutuna argüman verilmediğinde göre bu komut "stdin" dosyasından okuma yapacaktır. O halde bu durumda aslında "ps -e" komutunun ekrana yazdıklarını "wc" komutu işleme sokacaktır. Örneğin bir çıktıyı sayfa sayfa görüntülemek için "more" isimli bir komut bulunmaktadır. "more" programı normalde bir dosyayı argüman olarak alır. Ancak eğer dosya verilmezse bu durumda "more" stdin dosyasından okunanları sayfa sayfa görüntüler. Biz de bu sayede aşağıdaki gibi faydalı işlemler yapabiliriz: ps -e | more Burada "ps -e" komutunun ekrana yazdırdıkları sayfa sayfa görüntülenecektir. Pekiyi "|" karakterinin sağındaki program tdin dosyasından okuma yapmıyorsa ne olur? Örneğin: ps -e | wc sample.c Burada "wc" komutu artık stdin dosyasından okuma yapmayacaktır. Bu durumda yine "ps -e" komutunun çıktısı boruya yönlendirilir. Ancak "sample.c" stdin dosyasından okuma yapmadığı için "ps e" komutunun ekrana yazdıklarını işleme sokmayacaktır. Boru işlemleri yinelemeli olarak yapılabilir. Örneğin: a | b | c Burada "a" programının stdout dosyasına yazdıklarını "b" programı stdin dosyasından okuyacaktır. "b" programının da stdout dosyasına yazdıklarını "c" programı stdin dosyasından okuyacaktır. Eğer boru mekanizması olmasaydı yukarıdaki işlemler yine yapılabilirdi. Ancak bu durumda geçici dosyaların oluşturulması gerekirdi. Örneğin: ps -e > temp.txt wc temp.txt rm temp.txt ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Çok prosesli (multiprocessing) işletim sistemlerinin çalıştığı donanımlarda kullanılan mikroişlemcilerin "koruma mekanizması (protection mechanism)" denilen bir özelliği vardır. Çok prosesleri sistemlerde bütün programlar o anda RAM'de bir biçimde bulunmaktadır. Tabii işletim sisteminin kendisi de RAM'de bulunur. Bir programın göstericiler yoluyla kendi bellek alanının dışına çıkarak başka bir prosesin bellek alanına erişmesi mutlaka engellenmesi gereken bir durumdur. Çünkü eğer bu durum engellenmezse bir program başka bir programın bellek alanını bozabilir. Bu bozulma da o programın hatalı çalışmasına ya da çökmesine yol açabilir. Program başka bir programın bellek alanını bozmasa bile oradaki programlar üzerinde casusluk faaliyetleri yürütebilir. Buna ek olarak bazı makine komutları tamamen sistemin çökmesine yol açabilmektedir. Bir programın bu makine komutlarını kullanmasının tüm sistemi çökertebileceği için önüne geçilmesi gerekir. İşte işlemcilerin koruma mekanizması bu tür ihlallerin birinci elden işlemci tarafından tespit edilip engellenmesini sağlamaktadır. İşlemcilerin koruma mekanizmasının iki yönü vardır: 1) Bellek Koruması 2) Komut Koruması Bellek koruması bir prosesin kendi bellek alanının dışına erişimlerinin tespit edilmesine yönelik mekanizmadır. Komut koruması ise sistemi çökertme potansiyeline sahip makine komutlarının kullanımının engellenmesine yönelik mekanizmadır. Tabii her türlü mikroişlemci böyle bir mekanizmaya sahip değildir. Ancak güçlü işlemcilerde bu mekanizma bulunmaktadır. Örneğin Intel'in 80386 ve sonrası işlemcileri ARM'nin Cortex A serisi işlemcileri, Alpha işlemcileri, PowerPC işlemcileri, Itanium işlemcileri bu mekanizmalarsa sahiptir. Mikrodenetleyiciler genel olarak küçük işlemciler oldukları için bu mekanizmaya sahip değillerdir. Windows gibi Linux gibi macOS gibi işletim sistemleri bu mekanizmaya sahip olmayan işlemcilerin bulundurğu sistemlerde kullanılamazlar. Bir prosesin bellek korumasını ve komut korumasını ihlal etmesi birinci elde mikroişlemci tarafından tespit edilmektedir. Mikroişlemci ihlali tespit eder ve işletim sistemine bildirir. İşletim sistemi de hemen her zaman programı sonlandırır. Öte yandan kernel içerisindeki kodların ve aygıt sürücülerin kodlarının bu koruma engeline takılmaması gerekmektedir. Kernel belleğin her yerine erişebilmelidir. Çünkü programları bile belleğe yükleyen kernel'dır. Aynı zamanda kernel sistemi çökertme potansiyelinde olan pek çok makine komutunu uygun bir biçimde kullanmaktadır. Benzer biçimde aygıt sürücüler de mecburen bu tür makine komutlarını kullanmak zorundadırlar. İşte kernel kodlarının ve aygıt sürücü kodlarının bir biçimde bu koruma mekanizmasından muaf olması gerekmektedir. İşlemcileri tasarlayanlar genellikle prosesler için iki çalışma modu tanımlamışlardır: "Kernel Mode" ve "User Mode". Eğer bir kod "kernel mode'da" çalışıyorsa işlemci koruma mekanizmasını o kod için işletmez. Böylece o kod her şeyi yapabilir. Ancak eğer bir kod "user mode'da" çalışıyorsa işlemci o kod için koruma mekanizmasını işletmektedir. Normal programların hepsi user mode'da çalışmaktadır. Ancak kernel kodları ve aygıt sürücüler (kernel modülleri) kernel mode'da çalışırlar. Bir programın "sudo" ile çalıştırılmasının (yani programın proses id'sinin 0 olmasının) bu konuyla hiçbir ilgisi yoktur. Proses id'nin 0 olması yalnızca dosya erişimleri için avantaj sağlayabilmektedir. Yoksa biz bir programı "sudo" ile çalıştırsak bile o program yine "user mode'da" çalıştırılmaktadır. Pekiyi biz kendi programımızı kernel mode'da çalıştıramaz mıyız? Bu sorunun yanıtı genel olarak "hayır" biçimindedir. Bunun tek yolu "aygıt sürücü" ya da "kernel modül" denilen biçimde kod yazmaktır. Zaten aygıt sürücülerin en önemli özelliği onların kernel mode'da çalışmasıdır. Tabii aygıt sürücüler ancak sistem yöneticisitarafından bir parola eşiliğinde (yani sudo ile) yüklenebilmektedir. Sistem fonksiyonları kernel'ın içerisindeki fonksiyonlardır. Dolayısıyla bu fonksiyonlar özel makine komutalrını kullanırlar ve bellekte her yere erişebilirler. Aksi takdirde bu fonksiyonların yazılabilmesi mümkün değildir. Pekiyi bizim programlarımız user mode'da çalıştığına göre biz bir sistem fonksiyonunu çağırdığımızda ne olacaktır? İşte user mod bir proses bir sistem fonksiyonunu çağırdığında prosesin modu otomatik olarak kernel mode'a geçirilmektedir. Böylece sistem fonksiyonu yine kernel mode'da çalışmış olmaktadır. Sistem fonksiyonunun çalışması bittiğinde proses yine otomatik olarak user mode'a dönmektedir. Örneğin Intel işlemcilerinde bu geçişi sağlayan mekanizmaya "kapı (gate)" denilmektedir. Tabii kapı yerleştirmek kernel mode'da yapılabilecek ir işlemdir. Dolayısıyla user mod proses yalnızca zaten belirlenmiş olan kodları çalıştırmak üzere kernel mode'a geçebilmektedir. Sistem fonksiyonlarını çağırmanın zamansal bir maliyeti vardır. Çünkü prosesin user mode'dan kernel mode'a geçmesi ve birtakım gerekli kontrollerin kernel mode'da yapılması zaman kaybına yol açmaktadır. Örneğin: read(fd, (char *)0x123456, 10) Linux'ta read POSIX fonksiyonu doğrudan sys_read sistem fonksiyonunu çağıracaktır. Eğer bu sistem fonksiyonu ikinci parametreyle verilen adresi hiç kontrol etmezse koruma mekanizmasından da muaf olduğu için tuzağa düşecektir. İşte bu tür sistem fonksiyonları kesinlikle adreslerin o prosesin alanı içerisinde olup olmadığını test ederler. Bunun gibi pek çok yapılması egreken irili ufaklı kontroller vardır. O halde aslında bir proses yaşamının önemli bir kısmını user mode'da geçrirken bir kısmını da kernel mode'da geçirebilmektedir. Örneğin "time" isimli kabuk komutuyla biz prosesin ne kadar zamanı kernel mode'da ne kadar zamanı user mode'da geçirdiğini görebiliriz: $ time ./sample real 0m0,189s user 0m0,185s sys 0m0,005s Burada "sys" kernel modu, "user" user modu ve "real" da toplam zamanı vermektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- C'nin prototipleri içerisinde olan ve başı "f" ile başlayan dosya fonksiyonları aslında birer "sarma fonksiyon (wapper function)" gibidir. Biz bu fonksiyonları kullandığımızda arka planda bu fonksiyonlar UNIX/Linux ve macOS sistemlerinde POSIX fonksiyonlarını, Windows sistemlerinde ise Windows API fonksiyonlarını kullanmaktadır. Tabi bu fonksiyonlar da ilgili sistemdeki sistem fonksiyonlarınıçağırmaktadır. Örneğin biz Linux sistemlerinde fopen fonksiyonunu kullanmış olalım: fopen (user mode) ---> open (user mode) ---> sys_open (kernel mode) fopen fonksiyonu bize FILE * türünden bir dosya bilgi göstericisi vermektedir. Aslında FILE typedef edilmiş bir yapıdır: typedef struct { ... ... ... } FILE; Pekiyi bu yapının içerisinde hangi bilgiler vardır? Bir kere fopen dosyayı gerçekte UNIX/Linux sistemlerinde open POSIX fonksiyonunu kullanarak açtığına göre bir biçimde onun içerisinde open fonksiyonundan elde edilen dosya betimleyicisi bulunacaktır: typedef struct { ... int fd; ... } FILE; Standart dosya fonksiyonlarının en önemli özellikleri bir "cache sistemi" oluşturmalarıdır. Burada "cache" terimi daha uygun olmasına karşın daha çok "tampon (buffer)" terimi kullanılmaktadır. Bu nedenle C'nin dosya fonksiyonlarına "tamponlu (buffered) IO fonksiyonları" denilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda iki program verilmiştir. Bu iki program da bir dosyanın bütün karakterlerini ekrana yazdırmaktadır. "a.c" programı bu işlemi her defasında read fonksiyonu çağırarak yaparken "b.c" programı bir defasında 512 byte okuma yaparak okunanları bir tampona yerleştirip oradan alıp yazdırmaktadır. Dolayısıyla "b.c" programının daha hızlı çalışması beklenir. Çünkü bu program sistem fonksiyonlarını daha az çağırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* a.c */ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; char ch; 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, &ch, 1)) > 0) putchar(ch); if (result == -1) exit_sys("read"); putchar('\n'); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* b.c */ #include #include #include #include void exit_sys(const char *msg); #define BUFSIZE 512 int main(int argc, char *argv[]) { int fd; char buf[BUFSIZE]; 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, BUFSIZE)) > 0) { for (int i = 0; i < result; ++i) putchar(buf[i]); } if (result == -1) exit_sys("read"); putchar('\n'); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- İşte standart C fonksiyonları da yukarıdaki örnekte olduğu gibi sistem fonksiyonlarını daha az çağırmak için bir tampon kullanmaktadır. Biz örneğin fgetc fonksiyonu ile bir byte bile okumak istesek fgetc bir tamponluk bilgiyi okur ve bize onun içerisinde bir byte'ı verir. Biz daha sonra yeniden fgetc fonksiyonunu çağırdığımızda fgetc zaten tamponda daha önce okunmuş olan bilgi yığını olduğu için read fonksiyonu ile okuma yapmaz bize doğrudan tampondan verir. Tabii tampondaki her byte okunduktan sonra (yani tamponun sonuna gelindiğinde) fgetc yeniden read fonksiyonunu çağıracak ve tamponu yeniden dolduracaktır. fopen fonksiyonun geri döndürdüğü FILE türünden yapının içerisinde aslında bu tamponu yönetmek için gerekli olan bilgiler de bulunmaktadır. Standart C fonksiyonlarının kullandıkları default tampon büyüklüğü içerisinde BUFSIZ sembolik sabitiyle ifade edilmiştir. (Tabii bu BUFSIZ değerini değiştirmenin bir anlamı yoktur. Kod çoktan derlenmiştir. Bu sembolik sabit sadece dış dünyaya default durum hakkında bilgi vermek için bulundurulmuştur.) ---------------------------------------------------------------------------------------------------------------------------*/ #include int main(void) { printf("%d\n", BUFSIZ); /* 8192 */ return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Standart C fonksiyonlarının oluşturdukları bu tampon read/write bir tampondur. Yani yalnızca okuma sırsında da değil yazma sırasında da kullanılmaktadır. Örneğin biz fputc fonksiyonu le bir byte'ı dosyaya yazamak istesek bu bir byte aslında bu tampona yazılır. Bu tampon dolduğunda (bu konusunun ayrıntısı ele alınacaktır) ya da fflush fonksiyonu çağrıldığında ya da en kötü olasılıkla fclose işlemi sırasında write fonksiyonu çağrılarak diske yazdırılır. Biz tampondaki bilginin aktarılmasını garanti etmek için fflush fonksiyonu kullanabiliriz. Tampondaki bilginin write gibi bir fonksiyonla diske yazılması işlemine dosya terminolojisinde "flush işlemi" denilmektedir. fflush fonksiyonunun kullanılabilmes için dosyanın "yazma modunda açılmış olması (yani "w", "r+" gibi modlarda)" gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 22. Ders 14/01/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi C'nin dosya açmakta kullanılan fopen fonksiyonu bize FILE türünden bir yapı nesnesinin adresini vermektedir. Bu FILE nesnesine "stream" de denilmektedir. Biz Derneğimizde buna genel olarak "dosya bilgi göstericisi" diyoruz. İşte bu FILE yapısının içerisinde söz konusu bu tamponu yönetmek için de bilgiler bulunmaktadır. Örneğin FILE yapısının içerisinde tipik olarak şu bilgiler bulunur: - İşletim sistemi düzeyinde okuma/yazma işlemleri için gereken dosya betimleyicisi - Tamponun başlangıç adresini tutan bir gösterici - Tampondaki aktif noktayı tutan bir gösterici - Tamponun uzunluğunu tutan bir eleman ya da tamponun sonunu tutan bir gösterici - Diğer bilgiler Pekiyi fopen tarafından bu FILE yapısı nasıl tahsis edilmektedir? Standart C kütüphanelerini yazanlar birkaç teknik kullanabilmektedir. Birincisi doğrudan tahsisatın malloc fonksiyonu ile yapılmasıdır. Tabii bu durumda free işlemi fclose fonksiyonu tarafından yapılacaktır. İkincisi bu FILE yapısı zaten işin başında static düzeyde tahsis edilmiş bir FILE dizisinin içerisinde alınabilir. Örneğin: static FILE g_files[FILE_MAX]; ... C standartlarında FILE yapısının içeriği hakkında bir bilgi verilmemiştir. Bu durumda bu FILE yapısının içeriği kütüphaneyi yazanlar tarafından istenildiği gibi alınabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- fileno isimli POSIX fonksiyonu FILE yapısının içerisindeki dosya betimleyicini bize vermektedir. Yani biz bir dosyayı fopen fonksiyonuyla açıp o dosyanın dosya betimleyicisini elde edebiliriz. fileno fonksiyonunun prototipi şöyledir: #include int fileno(FILE *stream); Fonksiyonun geri dönüş değeri dosya betimleyicisidir. Pekiyi bu fonksiyon başarısız olabilir mi ya da başarısızlığı tespit edebilir mi? POSIX standartlarına göre fonksiyon başarısız olabilir. Bu durumda -1 değerine geri döner. Ancak fonksiyonun başarısızlığı tespit etmesi yeterli bir biçimde yapılamayabilir. Fonksiyon FILE yapısının içerisindeki elemana başlangıçta geçersiz bir değer atayıp bu değere bakmaktadır. Tabii fileno fonksiyonuyla FILE yapısı içerisindeki dosya betimleyicisini alıp onunla işlem yapınca onun gösterdiği dosya göstericisi değiştirilmiş olur. Pek çok gerçekleştirim bu durumda soruna yol açmamaktadır. Ancak bu konuda dikkat etmek gerekir. fileno fonksiyonu kısıtlı biçimde bazı zorunlu durumlarda kullanılmalıdır. Aşağıdaki örnekte dosya önce fopen fonksiyonuyla açılıp fileno fonksiyonuyla dosya betimleyicisi elde edilmiş ve sonra o betimleyici ile okuma yapılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { FILE *f; int fd; char buf[10 + 1]; ssize_t result; if ((f = fopen("test.txt", "r")) == NULL) { fprintf(stderr, "cannot open file!...\n"); exit(EXIT_FAILURE); } if ((fd = fileno(f)) == -1) exit_sys("fileno"); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); buf[result] = '\0'; puts(buf); fclose(f); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- fileno POSIX fonksiyonunun mantısksal olarak tersini yapan fdopen isimli bir POSIX fonksiyonu da vardır. (fdopen bir standart C fonksiyonu değildir). fdopen fonksiyonu open POSIX fonksiyonuyla açıp betimleyicisini elde ettiğimiz dosyaya ilişkin dosya bilgi göstericisini (FILE *) bize verir. Yani fdopen sanki o dosyayı fopen ile açmışız gibi bir durum oluşturmaktadır. fdopen fonksiyonunun prototipi şöyledir: #include FILE *fdopen(int fd, const char *mode); Fonksiyonun birinci parametresi open fonksiyonu ile elde edilen dosya betimleyicidir. İkinci parametre dosyanın fopen fonksiyonundaki açış modudur. Tabii buradaki açış modunun open fonksiyonuyla dosya açılırkenki mod ile uyuşması gerekir. Fonksiyon başarı durumunda dosya bilgi göstericisine, başarısızlık durumunda NULL adrese geri döner. errno değeri uygun biçimde set edilir. Aşağıdaki örnekte önce open POSIX fonksiyonu ile dosya açılmış sonra dosya betimleyicisi kullanılarak fdopen fonksiyonu ile dosya bilgi göstericisi elde edilmiştir. İşlemlere standart C fonksiyonlarıyla devam edilmiştir. fclose işlemi zaten bu betimleyiciyi kapacağı için ayrıca close fonksiyonu çağrılmamıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { int fd; FILE *f; int ch; if ((fd = open("test.txt", 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); } /*-------------------------------------------------------------------------------------------------------------------------- Standart C'nin fonksiyonları tamponlamayı üç moda (ya da stratejiye) göre farklı biçimlerde yapmaktadır. Üç tamponlama modu şöyledir: Tam Tamponlamalı Modu (Full Buffering): Burada okuma sırasında tampon tamamen doldurulur. Tamponun sonuna gelindiğinde tampon yeniden doldurulur. Yazma sırasında da tampona yazılır. Tamponun sonuna gelindiğinde tampona yazılmış olanlar flush edilir. (Tabii her zaman fflush ve fclose zaten flush işlemini yapmaktadır.) Satır Tamponlamalı Mod (Line Buffering): Bu modda tampon tamamen doldurulmaz. Yalnızca tek satırlık bilgi ('\n' karakterş dahil olmak üzere) tampona çekilmektedir. Okuma sırasında bu tampondan byte'lar verilir. Yazma sırasına yine tampona yazılır. flush işlemi '\n' karakteri tampona yazılınca (ya da fflush ve fclose fonksiyonları çağrılınca) yapılmaktadır. Satır tamponlamalı mod tipik olarak text dosyalar için kullanılmaktadır. Binary dosyalar için bu mod kullanılabilse de anlamsızdır. Sıfır Tamponlamalı Mod (No Buffering): Burada tampon hiç kullanılmaz. Doğrudan ilgili aşağı seviyeli fonksiyonlarla (yani read ve write POSIX fonksiyonlarıyla) aktarım yapılır. Satır tamponlaması kişilere biraz tuhaf gelebilmektedir. Çünkü satır tamponlaması yapabilmek için standart C kütüphanesinin okuma sırasında '\n' karakterini görmesi gerekir ki bazı durumlarda bunun etkin bir biçimde yapılabilme olanağı yoktur. Ancak bazı durumlarda zaten aygıt sürücüler bize satırsal bilgi vermektedir. Standart kütüphaneleri disk dosyaları için satır tamponlaması yaparken aslında çoğu kez '\n' karakterine kadar değil tüm tampon kadar okuma yapmaktadır. Ancak '\n' karakteri tampona yazıldığında flush işlemi yapmaktadırlar. C standartları bu üç tamponlama biçimini belirtmiş olsa da detaylar konusunda bir açıklama yapmamıştır. Dolayısıyla kütüphaneyi gerçekleştirenler satır tamponlaması ile okuma yapılırken '\n' karakterine kadar değil tüm tamponu da doldurabilmektedir. C standartlarında tamponlama stratejisi için "niyet" belirtilmiştir. Ancak yukarıda da belirttiğimiz gibi detay belirtilmemiştir. Tamponlama modu ile ilgili iki önemli soru gündeme gelmektedir? 1) Dosyanın default tamponlama modu nedir? 2) Dosyanın tamponlama modu nasıl değiştirilmektedir? fopen fonksiyonu ile dosya açıldığında dosyanın default tamponlama modu hakında C standartlarında bir şey söylenmemiştir. Bu durum "bunun herhangi bir biçimde olabileceği" anlamına gelmektedir.Fakat mevcut standart C kütüphaneleri genel olarak default durumda "tam tamponlamalı (full buffered)" modu esas almaktadır. Ancak standartlarda "stdin", "stdout" ve "strderr" dosyaları için bazı şeyler söylenmiştir. Bir dosyanın tamponlama modu dosya fopen fonksiyonuyla açıldıktan sonra ancak henüz hiçbir işlem yapmadan "setbuf" ve "setvbuf" standart C fonksiyonlarıyla değiştirilebilmektedir. Dosya üzerinde herhangi bir işlem yaptıktan sonra bu fonksiyonların çağrılması "tanımsız davranışa (undefined behavior)" yol açmaktadır. "setvbuf" fonksiyonu işlevsel olarak "setbuf" fonksiyonunu zaten kapsamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- setbuf fonksiyonu temel olarak kullanılan tamponun yerini değiştirmek için tasarlanmıştır. Fonksiyonun prototipi şöyledir: #include void setbuf(FILE *stream, char *buf); Fonksiyonun birinci parametresi dosya bilgi göstericisini, ikinci parametresi yeni tamponun yerini belirtmektedir. Bu tamponun BUFSIZ uzunluğunda olması gerekir. Eğer ikinci parametre NULL adres olarak girilirse bu durumda dosya "sıfır tamponlamalı moda" sokulmaktadır. Fonksiyon başarıyı kontrol edememektedir. Aşağıdaki örnekte setbuf fonksiyonu ile dosya için kullanılacak tamponun yeri değiştirilmiştir. fgetc işlemi sonrasında bu tamponun doldurulduğuna dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include int main(void) { FILE *f; char mybuf[BUFSIZ]; int ch; if ((f = fopen("test.txt", "r")) == NULL) { fprintf(stderr, "cannot open file!...\n"); exit(EXIT_FAILURE); } setbuf(f, mybuf); ch = fgetc(f); putchar(ch); for (int i = 0; i < 512; ++i) putchar(mybuf[i]); putchar('\n'); fclose(f); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- setvbuf fonksiyonu ile hem tamponun yeri, hem büyüklüğü hem de tamponlama modu değiştirilebilmektedir. Fonksiyonun prototipi şöyledir: #include int setvbuf(FILE *stream, char *buf, int mode, size_t size); Fonksiyonun birinci parametresi dosya bilgi göstericisini (stream) belirtir. Üçüncü parametre değiştirilecek tamponlama modunu belirtmektedir. Bu parametre şu değerlerden birini alabilmektedir: _IONBF (unbuffered) _IOLBF (line buffered) _IOFBF (fully buffered) İkinci parametre tamponu değiştirmek için kullanılmaktadır. Bu parametre NULL adres geçilirse tamponun yeri değiştirilmez. Son parametre ise tamponun yeni uzunluğunu belirtmektedir. Programcı ikinci parametreye NULL adres geçip son parametre yoluyla tamponun büyüklüğünü de değiştirebilir. Bu durumda tamponu setvbuf kendisi tahsis edecektir. Eğer tamponlama modu ikinci parametreye _IONBF geçilerek sıfır tamponlamalı mod olarak ayarlanırsa artık ikinci ve dördündü parametrenin bir önemi kalmamaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda sıfır dışı bir değere geri dönmektedir. POSIX sistemlerinde errno değeri yine uygun biçimde set edilmektedir. glibc kütüphanesinde stebuffer ve setlinebuf isimli iki fonksiyon da bulunmaktadır. Ancak bu fonksiyonların taşınabilirliği yoktur. Aşağıdaki örnekte bir dosya fopen fonksiyonuyla açılmış ve "satır tamponlamalı moda" geçirilmiştir. Yukarıda da belirtildiği gibi C standartları tamponlama modları için mutlak uyulması gereken kuralları açıkça belirtmemiştir. Örneğin glibc kütüphanesi normal dosyalarda satır tamponlaması sırasında satır sonuna kadar değil tamponun tamamını doldurmaktadır. Ancak '\n' karakteri dosyaya yazıldığında flush işlemi yapmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include int main(void) { FILE *f; char mybuf[512]; int ch; if ((f = fopen("test.txt", "r")) == NULL) { fprintf(stderr, "cannot open file!...\n"); exit(EXIT_FAILURE); } if (setvbuf(f, mybuf, _IOLBF, 512) != 0) { fprintf(stderr, "cannot set buffer!...\n"); exit(EXIT_FAILURE); } ch = fgetc(f); putchar(ch); for (int i = 0; i < 512; ++i) putchar(mybuf[i]); putchar('\n'); fclose(f); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Çeşitli standart C kütüphanelerinin özellikle stdio fonksiyonlarının gerçekleştirimini üşenmeden inceleyebilirsiniz. Alternatifler şunlar olabilir: - uclibc (Mikro C kütüphanesi): https://elixir.bootlin.com/uclibc-ng/latest/source - musl libc kütüphanesi: http://www.musl-libc.org/ - diet libc kütüphanesi: http://www.fefe.de/dietlibc/ - Plauger'in "The C Standard Library" kitabında gerçekleştimini yaptığı kütüphane: https://github.com/topics/c-standard-library ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- C'nin dosyası içerisinde FILE * türünden yani "stream" belirten üç değişken ismi bulnmaktadır: stdin, stdout ve stderr. Bu değişkenler fopen fonksiyonun geri döndürdüğü FILE nesnesi türünden adres belirtmektedir. Dolayısıyla C'nin standart dosya fonksiyonlarında bunları kullanabiliriz. Örneğin aslında: printf(...); çağrısı ile aşağıdaki fprintf çağrısının bir farkı yoktur: fprintf(stdout, ...); stdin, stdout ve stderr dosya bilgi göstericileri (streams) programcı tarafından açılmamıştır ve programcı tarafından kapatılmamalıdır. Programcı bunları doğrudan kullanabilir. Şüphesiz UNIX/Linux sistemlerinde stdin dosya bilgi göstericisinin gösterdiği FILE nesnesinin içerisinde 0 numaları betimleyici, stdout FILE nesnesinin içerisinde 1 numaralı betimleyici ve stderr FILE nesnesinin içerisinde 2 numaralı betimleyici vardır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- C standartları herhangi bir dosyanın default tamponlaması hakkında bir şey söylememiş olsa da "stdin", "stdout" ve "stderr" dosyalarının default tamponlaması hakkında şunları söylemiştir: - stdin ve stdout dosyaları default durumda "eğer interaktif olmayan bir aygıta yönlendirilmişse işin başında tam tamponlamalı" moddadırlar. Ancak bu dosyalar "interaktif olan bir aygıta yönlendirilmişse işin başında tam tamponalamlı olamazlar, satır tamponlamalı ya da sıfır tamponlamalı" olabilirler. Klavye ve ekran yani terminal "interaktif aygıt" kabul edilmektedir. Ancak disk dosyaları interaktif aygıt kabul edilmemektedir. - stderr dosyası ister interaktif olamayan aygıta yönlendirilmiş olsun isterse interaktif aygıta yönlendirilmiş olsun işin başında tam tamponlamalı olamaz. Ancak satır tamponlamalı ya da sıfır tamponlamalı olabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 23. Ders 15/01/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Örneğin Windows sistemlerindeki C derleyicilerinde default durumda dosyay yönlendirme yapılmamışsa stdout sıfır tamponlamalı stdin satır tamponlamalıdır. Ancak UNIX/Linux sistemlerinde stdout ve stdin dosyaları satır tamponlamalıdır. Aşağıdaki örnekte bu durum anlaşılabilir. #include int main(void) { printf("ankara"); /* Windows sistemlerinde yazı gözükecek, UNIX/Linux'ta gözükmeyecek */ for (;;) ; return 0; } Tabii program sonlanırken stdin, stdout ve stderr dosyaları zaten derleyiciler tarafından kapatılacağı için her durumda bu flush işlemi yapılacaktır. Örneğin aşapıdaki programda programın çalışması bitince her sistemde yazı görünecektir: #include int main(void) { printf("ankara"); return 0; } stdout dosyası default terminale yönlendirilmişken satır tamponlamalı ya da sıfır tamponlamalı modda olabiliyorsa bir yazının ekrana çıkmasını nasıl garanti edebiliriz? Mademki stdout terminale yönlendirildiğinde en kötü olasılıkla satır tamponlamalı olabilir. O zaman yazının sonuna '\n' karakteri koyarız. Örneğin: #include int main(void) { printf("ankara\n"); /* Hem Windows'ta hem de Linux sistemlerinde yazı görülecek */ for (;;) ; return 0; } Ancak burada imleç aynı zamanda aşağı satıra geçirilmektedir. Pekiyi imleç aşağı satıra geçirilmeden yazının ekrana çıkması nasıl garanti edilebilir? Bunun iki yolu vardır. Birincisi stdout dosyasını fflush(stdout) çağrısıyla flush etmektir: #include int main(void) { printf("ankara"); fflush(stdout); for (;;) ; return 0; } İkincisi stdout dosyasını her ihtimale karşı açıkça sıfır tamponlamalı moda çekmektir: #include int main(void) { setvbuf(stdout, NULL, _IONBF, 0); /* setbuf(stdout, NULL */ printf("ankara"); for (;;) ; return 0; } C derleyicilerinin hemen hepsinde stdin dosyasından okuma yapıldığında okuma yapan fonksiyonlar öne stdout dosyasını flush etmektedir. Standartlarda bu durum garanti edilmemiştir. Ancak derleyicilerin hemen hepsi böyle yapmaktadır. Örneğin: #include int main(void) { printf("ankara"); getchar(); /* Hem Windows hem de UNIX/Linux sistemlerindeki derleyicilerde stdout flush edilecek */ return 0; } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- stdin dosyası hem Windows hem de UNIX/Linux sistemlerinde dosyaya yönlendirilmemişse satır tamponlamalı moddadır. Dolayısıyla biz klavyeden bir karakter bile okumak istesek UNIX/Linux sistemlerinde read fonksiyonu 0 numaralı betimleyici ile çağrılarak bir satırlık bilgi okunup tampona yerleştirilmektedir. Artık tamponda bilgi olduğu sürece okuma fonksiyonları tampondakileri okuyacaktır. Örneğin üst üste ik getchar çağrısı ile iki karakteri stdin dosyasından okumak isteyelim: ch1 = getchar(); ch2 = getchar(); Birinci getchar fonksiyonu bizden bir satır alarak onu stdin dosyasının tampona yerleştirir. Tabii tamponun sonunda '\n' karakteri de bulunacaktır. İkinci getchar tampon boş olmadığı sürece artık klavyeden giriş istemeyip tampondan girişi karşılayacaktır. Yukarıdaki örnekte biz ilk getchar fonksiyonunda klavyeden "a" karakterine basıp ENTER tuşuna basalım. Bu durumda tamponda şu karakter olacaktır: a\n İlk getchar bu 'a' karakterini ikinci getchar ise '\n' karakterini alacaktır. Biz getchar fonksiyonunu üçüncü kez çağırdığımızda artık yeni bir satır istenecektir. Yani stdin dosyasından okuma yapan fonksiyonlar tampon boşsa read fonksiyonunu çağırarak bizden bir satırlık bilgi istemektedir. Aşağıdaki programla test işlemini yapabilirsiniz: #include int main(void) { int ch; ch = getchar(); printf("%c (%d)\n", ch, ch); ch = getchar(); printf("%c (%d)\n", ch, ch); return 0; } Tabii scanf, getchar, gets gibi fonksiyonların hepsi ortak tampondan çalışmaktadır. Yani bu fonksiyonların hepsi stdin dosyasından okuma yapar. stdin dosyasının da bir tane tamponu vardır. Pekiyi biz gerçekten ikinci getchar fonksiyonu ile yeni bir klavye girişi yapmak istiyorsak bunu nasıl sağlayabiliriz? stdin dosyasının flush edilmesi geçersiz bir işlemdir. Zira C'de "read-only" dosyalar flush edilemezler. Bunun için özel bir fonksiyon da bulundurulmamıştır. O zaman tek yapılacak şey '\n' karakterini görene kadar stdin dosyasından karakter karakter okuma yapmaktır. Bu işlem şöyle bir döngü ile yapılabilir: while(getchar() != '\n') ; Tabii sonraki anlatımlarda görüleceği üzere EOF durumunun da kontrol edilmesi daha uygun olur. Bu nedenle aşağıdaki gibi bir fonksiyon bu iş için kullanılabilir: void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } Maalesef bu işlemin daha pratik bir yolu yoktur. Örneğin: #include void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } int main(void) { int ch; ch = getchar(); printf("%c (%d)\n", ch, ch); clear_stdin(); ch = getchar(); printf("%c (%d)\n", ch, ch); return 0; } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- stdin dosyası default durumda pek çok sistemde terminal aygıt sürücüsüne (yani klavyeye) yönlendirilmiş durumdadır. Biz stdin dosyasında okuma yaptığımızda EOF ile karşılaşabiliriz. Çünkü stdin bir dosyaya yönlendirdildiğinde dosyanın sonuna gelinmiş de olabilir. Pekiyi stdin defaut durumda klavyeden okuma yaparken dosya sonu kavramı ne olacaktır? İşte terminal aygıt sürücüsü bazı özel tuş kombinasyonlarında yalancı bir EOF etkisi oluşturmaktadır. Windows sistemlerinde Ctrl+z tuşu UNIX/Linux sistemlerinde Ctrl+d tuşu bu amaçla kullanılmaktadır. Örneğin: ch = getchar(); Burada Windows sistemlerinde Ctrl+z tuşuna UNIX/Linux sistemlerinde Ctrl+d tuşuna basıldığında "dosya sonuna gelme etkisi" yaratılacak ve getchar fonksiyonu EOF değerine (-1) geri dönecektir. Tabii bu tuş kombinasyonlarına basıldığında gerçekte dosya sonuna gelme gibi bir durum oluşmamaktadır. Bu yalancı bir etkidir. Yani daha sonra stdin dosyasından yine okuma yapılabilir. Bu nedenle stdin tamponunu boşaltırken kullanıcının EOF etkisi yaratmak isteyebileceğine de dikkat edilmelidir: void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- C'de stdin dosyasından okuma yapan standart fonksiyonlar şunlardır: getchar scanf gets (C11'de kaldırılıdı) gets_s (C11 ile birlikte eklendi ancak "isteğe bağlı (optional), VS ve glibc kütüphanelerinde yok") Bunların hepsi aynı tampondan çalışmaktadır. Şimdi bu fonksiyonlar üzerinde duralım. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- getchar fonksiyonu stdin dosyasından bir karakter okur. Tabii önce tampona bakar. Tamponda en az bir karakter varsa onu verir. Tampon tamamen boşsa klavyeden bir satır okuyarak tamponu doldurur. Ondan sonra karakteri verir. Aslında gets ve scanf gibi fonksiyonların hepsi getchar kullanılarak yazılmıştır. Yani temel fonksiyon getchar fonksiyonlarıdır. getchar fonksiyonu dosya sonuna gelindiğinde ya da IO hatası olduğunda EOF (-1) değerine geri dönmektedir. getchar fonksiyonunun prototipi şöyledir: #include int getchar(void); Eğer fonksiyonun geri dönüş değeri char olsaydı bu durumda 0xFF gibi bir okumayla EOF değeri birbirinden ayırt edilemezdi. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- gets fonksiyonu C99'da "derecated" yapılmış ve C11'de C'den kaldırılmıştır. Ancak hala derleyiciler bunu muhafaza etmektedir. gets fonksiyonu stdin dosyasından karakter karakter okuma yapar ve okuduğu karakterleri verilen bir diziyeyerleştirir. gets fonksiyonu '\n' karakterini de okur ancak onun yerine diziye '\0' karakterini yerleştirir. Yani gets fonksiyonu aslında stdin tamponunu da tamamen boşaltmaktadır. Tabii gets fonksiyonu çağrıldığında stdin tamponunda zaten karakterler varsa gets klavyeden bir giriş beklemeden onları okuyup geri dönecektir. gets fonksiyonunun prototipi şöyledir: char *gets(char *s); gets fonksiyonu parametresiyle girilen adresin aynısıyla geri döner. Ancak henüz hiçbir karakter okunmadan EOF ile karşılaşılırsa gets NULL adresle geri dönmektedir. gets fonksiyonu aşağıdaki gibi yazılabilir. ---------------------------------------------------------------------------------------------------------------------------*/ #include char *mygets(char *s) { int ch; size_t i; for (i = 0; (ch = getchar()) != '\n' && ch != EOF; ++i) s[i] = ch; if (i == 0 && ch == EOF) return NULL; s[i] = '\0'; return s; } int main(void) { char buf[64]; mygets(buf); puts(buf); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- gets fonksiyonun problemi parametre olarak verdiğimiz dizinin her zaman taşırılabilme olasılığıdır. Fonksiyonun dizi uzunluğunu da parametre olarak alması gerekirdi. İşte C11 ile birlikte isteğe bağlı biçimde standartlara eklenmiş olan gets_s bununu yapmaktadır. gets_s fonksiyonunun prototipi şöyledir: char *gets_s(char *s, rsize_t n); Buradaki rsize_t türü de yine "isteğe bağlı typedef edilmesi gereken" bir türdür. Aşağıdak gets_s fonksiyonun muhtemel bir gerçekleştirimi verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include char *mygets_s(char *s, size_t n) { int ch; size_t i; for (i = 0; i < n - 1; ++i) { if ((ch = getchar()) == '\n' || ch == EOF) break; s[i] = ch; } s[i] = '\0'; if (i == 0 && ch == EOF) return NULL; return s; } int main(void) { char buf[3]; mygets_s(buf, 3); printf("%s\n", buf); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Bazı programcılar gets_s fonksiyonu derleyicilerde bulunmadığı için onun işlevselliğini fgets fonksiyonu ile karşılamaya çalışmaktadır. fgets fonksiyonunun prototipi şöyledir: char *fgets(char *s, size_t n, FILE *f); Ancak fgets fonksiyonu ile eğer belirtilen uzunluktan daha kısa bir satır girilmişse '\n' karakterini de diziye yerleştirmektedir. Bu durumda programcının bu '\n' karakterini kendisinin aşağıdaki gibi silmesi gerekmektedir: char buf[64]; char *str; fgets(buf, 64, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- scanf fonksiyonu işlevsel olarak printf fonksiyonun tersi gibidir. Prototipi şöyledir: int scanf(const char *format, ...); Fonksiyon stdin dosyasından karakterleri tek tek okur. Format karakterlerine uygunsuzluk tespit ettiği noktada uygunsuz olan o karakteri tampona geri yazar ve işlemini sonlandırır. scanf fonksiyonu başarılı bir biçimde yerleştirilen değerin sayısına geri dönmektedir. Tabii bu değer 0 da olabilir. scanf henüz hiçbir karakter okuyamadan EOF ile kaşılaşırsa EOF değerine geri döner. scanf her zaman baştaki boşluk karakterlerini (leading space) ve girişler arasındaki boşluk karakterlerini atmaktadır. Ancak sonraki boşluk karakterlerini ('\n' de dahil olmak üzere) atmamaktadır. Aşağıda scanf kullanımına ilişkin bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } int disp_menu(void) { int option; int result; do { printf("1) Add record \n"); printf("2) Delete record \n"); printf("3) List record \n"); printf("4) Quit\n"); printf("\nChoose an item:"); if ((result = scanf("%d", &option)) != 1 || option < 0 || option > 4) { printf("Invalid option!...\n"); clear_stdin(); } } while (result != 1); return option; } int main(void) { int option; for (;;) { option = disp_menu(); switch (option) { case 1: printf("add record...\n"); break; case 2: printf("delete record...\n"); break; case 3: printf("list record...\n"); break; case 4: goto EXIT; } } EXIT: return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Bir dosyadan okunan karakter beğenilmezse sanki hiç okunmamış gibi bir etki oluşturmak için (yani o karakteri tampona geri bırakmak için) ungetc isimli bir standart C fonksiyonu bulundurulmuştur: #include int ungetc(int c, FILE *stream); Fonksiyon başarı durumunda tampona bırakılan karakterin aynsına, başarısızlık durumunda EOF değerine geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dosyayı byte byte okurken fgetc fonksiyonundan faydalanırız. Standart C fonksiyonları tamponlu çalıştığına göre gereksiz bir biçimde sistem fonksiyonları tekrar tekrar çağrılmayacaktır. Ancak öte yandan fonksiyon çağırmanın da bir maliyeti vardır. İşte C standartlarında fgetc yerine getc isimli alternatif bir fonksiyon da bulundurulmuştur. getc fonksiyonu makro olarak yazılabilmektedir. Yani iki fonksiyon arasındaki tek fark getc fonksiyonunun bir makro biçiminde yazılabilmesidir. getc fonksiyonunun prototipi de şöyledir: #include int getc(FILE *stream); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde her prosesin o anda "sistem genelinde tek olan (unique)" bir "proses id" değeri vardır. Proses id değeri prosesin kontrol bloğuna erişmek için bir anahtar olarak kullanılmaktadır. Yani biz işletim sistemine bu proses id değerini verdiğimizde işletim sistemi çok hızlı bir biçimde bu id değerinden hareketle prosesin kontrol bloğuna erişebilmektedir. Proseslerin id değerleri pid_t türüyle temsil edilmiştir. pid_t türü işaretli bir tamsayı türü olmak koşuluyla ve dosyalarında typedef edilmiş durumdadır. Sistem boot edildiğinde boot kodu 0 numaralı id'ye sahip proses biçimine dönüştürülmektedir. (Buna "swapper" ya da "pager" da denilebilmektedir.) Daha sonra da bu 0 numaralı id bir daha sistemde kullanılmamaktadır. Sistemde ikinci yaratılan proses 1 numaralı id'ye sahip olan "init" isimli prosestir. 0 numaralı pros yok edildiği için sistemdeki bütün proseslerin atası bu "init" posesidir. İşletim sisteminin çekirdeği tipik olarak yeni yaratılan bir proses için proses id değerini bir sayaç ile vermektedir. Her proses yaratıldığında bu sayaç değeri bir artırılır. Sayaç sona geldiğinde yeniden başa geçilir ve bitmiş proseslerin id'leri kullanılır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- O anda çalışmakta olan programa ilişkin proses id değeri getpid isimli POSIX fonksiyonu ile elde edilebilmektedir: #include pid_t getpid(void); Fonksiyon başarısız olamaz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(void) { pid_t pid; pid = getpid(); printf("%jd\n", (intmax_t)pid); /* printf("%lld\n", (long long)pid); */ return 0; } /*-------------------------------------------------------------------------------------------------------------------------- 24. Ders 21/01/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Prosesler, prosesler tarafından sistem fonksiyonlarıyla yaratılmaktadır. Bir prosesi yaratan prosese o prosesin "üst prosesi (parent process)", yaratılan prosese de üst prosesin "alt prosesi (child process)" denilmektedir. Her prosesin bir üst prosesi vardır. Bir prosesin üst prosesi getppid fonksiyonu ile elde edilmektedir. Fonksiyonun prototipi şöyledir: #include pid_t getppid(void); Fonksiyon başarısız olamamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(void) { pid_t pid, ppid; pid = getpid(); printf("pid = %jd\n", (intmax_t)pid); ppid = getppid(); printf("ppid = %jd\n", (intmax_t)ppid); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Bir prosesin üst prosesi sonlanırsa bu tür proseslere "öksüz (orphan) prosesler" denilmektedir. Sistem böyle bir durumda 1 numaralı id'ye sahip olan "init" prosesini öksüz duruma düşmüş prosesin üst prosesi olarak atamaktadır. Dolayısıyla her zaman prosesin bir üst prosesi bulunmaktadır. Sistemlerde prosesler konusunda bazı limitler söz konusu olabilmektedir. Çünkü her proses bir kaynak kullanmaktadır. Bu kaynakların da o makine için bir limiti vardır. Örneğin Linux sistemlerinde, sistem genelinde aynı anda var olabilecek toplam proseslerin sayısı /proc/sys/kernel/threads-max dosyasında belirtilmektedir. Burada belirtilen değer "toplam proseslerin ve thread'lerin" sayısıdır. (Linux sistemlerinde aslında thread'ler de prosesler gibi kaynak kullanmaktadır.) Yine Linux sistemlerinde belli bir kullanıcının yaratabileceği maksimum proses ve thread sayısı da söz konusudur. Bu değer getrlimit fonksiyonuyla ya da ulimit kabuk komutuyla elde edilebilir. Tabii root prosesi (proses id'si 0 olan porsesler ve bu yeterliliğe (capability) sahip olan prosesler) bu sınırlamaya tabi değildir. Linux sistemlerinde "proses id'lerin yeniden başa geçmeden alabileceği maksimum değer de "/proc/sys/kernel/max_pid" dosyasında belirtilmektedir. Aşağıdaki bir Ubuntu makinede bu limitler gösterilmiştir. Ancak bu limitler makineden makineye değişebilmektedir. parallels@ubuntu-linux-20-04-desktop:~$ cat /proc/sys/kernel/threads-max 15071 parallels@ubuntu-linux-20-04-desktop:~$ cat /proc/sys/kernel/pid_max 4194304 parallels@ubuntu-linux-20-04-desktop:~$ ulimit -u 7535 Bu değerler aslında o anda ya da kalıcı olarak değiştirilebilmektedir. Bu değerlerin boot edilene kadar değiştirilmesi bu dosyalara yeni değerlerin yazılmasıyla yapılabilir. Ya da sysctl kabuk komutu ile yapılabilir. Kalıcı değişiklik için sistem boot edilirken başvurulan bazı konfigürasyon dosyalarından faydalanılmaktadır. Örneğin /etc/sysctl.conf dosyasına yeni limitler girilirse sistem her açıldığında bu limitlerle açılacaktır. Aslında bu limitler "kernel parametreleri" yoluyla da değiştirilebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde bir proses yaratmanın yegane yolu "fork" isimli POSIX fonksiyonunu kullanmaktır. fork fonksiyonu, doğrudan işletim sisteminin bu işi yapan sistem fonksiyonunu çağırmaktadır. Linux sistemlerinde sys_fork isimli fonksiyon ve bunun daha genel biçimi olan sys_clone sistem fonksiyonları bu işi yapmaktadır. fork fonksiyonunun prototipi şöyledir: #include pid_t fork(void); fork, Türkçe "çatal" anlamına gelmektedir. "Akışın çatallanması" gibi bir benzetmeyle bu isim verilmiştir. fork, bir prosesin tamamen özdeş bir kopyasını oluşturur. (Bunu klonlama makinesine giren orada klonu çıkartılan bir insan olarak düşünebilirsiniz.) Yani fork fonksiyonu şunları yapmaktadır: 1) Yeni bir proses kontrol blok yaratır. fork işlemini yapan prosesin proses kontrol bloğunun içeriğini, yeni yaratılan prosesin kontrol bloğuna kopyalar. Böylece üst proses ile yeni yaratılan alt proses tamamen aynı özelliklere sahip olmaktadır. 2) fork, bu fonksiyonu çağıran prosesin bellek alanının da kopyasını yeni yaratılan proses için oluşturmaktadır. Böylece her iki proses de aynı koda ve data ve heap alanlarına sahip olacaktır. Ancak bunlar birbirlerinden ayrıdır. Yukarıdaki işlemler fork fonksiyonunun içinde yapılmaktadır. fork fonksiyonundan hem bu fonksiyonu çağıran proses hem de yeni yaratılan proses çıkmaktadır. Ancak bunların bellek alanları ayrı olduğu için artık birinin yapacağı değişikliği diğeri görmeyecektir. fork fonksiyonu, bir klonlama yapmaktadır. Yeni bir prosesi, kendi prosesiyle aynı özelliklere ve aynı bellek alanı ile yaratmaktadır. fork sırasında prosesin kontrol bloğu yeni yaratılan prosese kopyalandığı için üst proses ile alt proses aynı kullanıcı ve grup id'sine sahip olur. (Bir kişi klonlama makinesine girip klonu çıkartıldığında makineden iki kişi çıkacaktır. Bu iki kişinin de anıları aynı olacaktır. Ancak artık bunların yaşamları farklıdır. Birisinin başına gelen şeyler makineden çıktıktan sonra ona özgü olacaktır.) fork işlemini yapan proses üst proses (parent process) durumundadır. Yeni yaratılan proses ise alt proses (child process) durumundadır. Tabii alt proses yeni bir proses id'ye sahip olacaktır. Alt prosesin üst prosesi, fork fonksiyonu uygulayan proses olacaktır. fork fonksiyonu başarısız olabilir. fork başarısızlık durumunda -1 değerine geri dönmektedir. Pekiyi yeni proses hangi noktada yaratılmaktadır? Tabii fork fonksiyonu içerisinde. Yeni yaratılan prosesin (alt prosesin) akışı da fork fonksiyonu içerisinde başlatılacaktır. Bu durumda her iki proses de fork fonksiyonunun içerisinden çıkacaktır. İşte üst proses (yani fork işlemini yapan proses) "alt prosesin id" değeri ile, alt proses ise "0 değeri ile" fork fonksiyonundan çıkacaktır. Böylece programcı fork çıkışında üst proses ile alt prosese farklı işlemler yaptırabilmektedir. Alt prosesin fork içerisinden 0 ile çıkması alt prosesin proses id'sinin 0 olduğu anlamına gelmemektedir. Alt prosesin proses id'si alt proses içerisinden getpid fonksiyonuyla elde edilebilmektedir. fork işleminin tipik kalıbı şöyledir: pid_t pid; ... if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ ... } else { /* child process */ ... } Aşağıdaki örnekte fork fonksiyonu ile bir proses yaratılmış ve çeşitli proses id'ler üst ve alt proseslerde yazdırılmıştır. Bu örnekte üst proseste fork fonksiyonunun alt prosesin proses id değeri ile geri döndüğüne dikkat ediniz. Denemenin yapıldığı makinede şöyle bir sonuç elde edilmiştir: Parent pid: 549376 Parent's parent pid: 536568 fork return value: 549377 common code... Child pid: 549377 Child's parent pid: 549376 Common code... ---------------------------------------------------------------------------------------------------------------------------*/ #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 pid: %lld\n", (long long)getpid()); printf("Parent's parent pid: %lld\n", (long long)getppid()); printf("fork return value: %lld\n", (long long)pid); } else { /* child process */ printf("Child pid: %lld\n", (long long)getpid()); printf("Child's parent pid: %lld\n", (long long)getppid()); } printf("Common code...\n"); sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- fork işleminde en fazla kafa karıştıran noktalardan biri fork fonksiyonundan iki akışın çıkması durumudur. Burada genellikle yeni öğrenenlerin gözden kaçırdığı birkaç nokta vardır: 1) fork sırasında fork işlemini yapan prosesin (yani üst prosesin) tüm bellek alanının yani onun kod, data, stack ve heap alanlarının özdeş bir kopyası oluşturulmaktadır. Yani fork işlemini yapan prosesin kod, data stack ve heap alanlarının hepsi alt proseste de bulunmaktadır. Örneğin: if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { ... } else { ... } Bu kod, bu haliyle hem üst proseste hem de alt proseste bulunacaktır. Yani fork işleminden sonra bu kodlardan aslında iki tane vardır. Bizim buradaki temel amacımız fork çıkışında kodu aynı olan iki farklı prosese farklı şeyleri yaptırmaktır. İşte bunu sağlamanın yolu fork fonksiyonunun geri dönüş değerinden faydalanmaktır. 2) Yeni öğrenen kişilere iki prosesin de fork fonksiyonundan çıkması tuhaf gelebilmektedir. Aslında burada bir tuhaflık yoktur. Şöyle ki: Prosesin yaratılması ve bellek alanlarının kopyalanması zaten fork içerisinde yapılmaktadır. fork fonksiyonunu çağıran proses (üst proses) fork'tan çıkacaktır. Kopyası çıkartılan alt proses de çalışmaya fork içerisinden başlamaktadır. Bu durumda alt proses de fork fonksiyonundan çıkacaktır. Tabii fork fonksiyonundan çıkınca artık üst proses ile alt prosesin yaşamları farklı olabilmektedir. Örneğin üst proses bir global değişkenin değerini değiştirse alt proses bunu değişmiş olarak görmez. Çünkü o global değişkenin üst proseste ve alt proseste farklı kopyaları vardır. Üst proses kendi global değişkenini değiştirmektedir. Yani fork işleminden çıkıldığında üst ve alt prosesin her şeyi aynı olsa da artık bunlar kendi yollarına gideceklerdir. (Bu durumu klon makinesinden çıkan iki kişinin durumuna benzetebiliriz. Klon makinesinden çıkar çıkmaz bu iki kişinin her şeyi aynıdır. Ancak bundan sonra bu kişiler bağımsız kişiler oldukları için başlarına farklı olaylar gelecektir. Birisinin maruz kaldığı bir duruma diğeri maruz kalmayacaktır.) Aşağıdaki örnekte fork işlemi sonrasında üst proses g_x global değişkenine yeni bir değer atamıştır. Sonra alt proseste bu global değişkenin değeri yazdırılmıştır. Tabii alt proses üst prosesin yaptığı bu değişikliği görmeyecektir. Çünkü aslında iki prosesin de bellek alanları tamamen fork içerisinde kopyalama yöntemiyle ayrıştırılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int g_x = 10; int main(void) { pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ g_x = 100; } else { /* child process */ sleep(1); printf("%d\n", g_x); /* 10 */ } printf("Common code...\n"); sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- fork işleminde yeni proses yaratıldığında hangi proses akışının fork fonksiyonundan önce çıkacağının bir garantisi yoktur. Bu işletim sisteminin çizelgeleme algoritmalarına bağlı olarak değişebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte "Common Code" yazısı 8 defa ekranda görünecektir. Çünkü ilk fork işleminden sonra ikinci fork işlemini iki proses yapacaktır. Böylece ikinci fork işleminden sonra aynı koda sahip 4 proses oluşacaktır. Sonra bu 4 proses de üçüncü fork işlemini yapacaktır. O halde üçüncü fork işleminden toplam 8 proses çıkacaktır. Buradaki sleep fonksiyonuna takılmayınız. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include int main(void) { fork(); fork(); fork(); printf("Common code...\n"); sleep(1); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Benzer biçimde yine aşağıdaki kodda ekrana 8 tane 3 sayısı basılacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include int main(void) { int a = 0; fork(); ++a; fork(); ++a; fork(); ++a; printf("%d\n", a); sleep(1); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- fork işlemi sırasında üst prosesin (fork işlemini yapan prosesin) proses kontrol bloğunun yeni yaratılan alt prosesin proses kontrol bloğuna kopyalandığını belirttik. Bu nedenle alt prosesin "kullanıcı id'si, grup id'si, çalışma dizini" ve daha pek çok özellikleri üst prosesle aynı olacaktır. Pekiyi alt proseste dosya betimleyici tablosunun durumu ne olacaktır? Örneğin biz bir dosya açmış olsak sonra fork yapmış olsak alt proseste bu dosyanın durumu ne olacaktır? fork işlemi sırasında işletim sistemi üst prosesin dosya betimleyici tablosu içerisindeki dosya nesnelerinin adreslerini de alt prosesin dosya betimleyici tablosuna kopyalamaktadır. Ancak dosya nesnelerinin kopyalarını çıkartmamaktadır. Böylece fork işleminin sonunda üst prosesin dosya betimleyici tablosunun slotları ile alt prosesin dosya betimleyici tablosunun slotları (yani dosya betimleyicileri) aynı dosya nesnesini gösteriyor durumda olur. Bu tür kopyalamalara "sığ kopyalama (shallow copy)" da denilmektedir. Mademki açık dosyaya ilişkin tüm bilgiler dosya nesnesinde tutulmaktadır, o halde fork işleminden sonra proseslerden biri bir dosyanın dosya göstericisini değiştirirse diğer proses bunu değişmiş olarak görecektir. Tabii fork işlemi sırasında dosya nesnelerinin referans sayaçları da bir artırılmaktadır. Benzer biçimde aslında işin başında açık olan 0, 1 ve numaralı betimleyiciler login işlemi öncesinde yaratılmış durumdadır. Her fork işleminde bu betimleyicilere ilişkin dosya nesnelerinin kopyaları çıkartılmamaktadır. Prosesler aslında genellikle aynı 0, 1 ve 2 numaralı dosya nesnelerini göstermektedir. Aşağıdaki örnekte önce bir dosya açılmış sonra üst proses dosya göstericisini 50'inci offset'e konumlandırmıştır. Üst prosesle alt proses aynı dosya nesnelerini gördüğü için bu durumdan alt proses etkilenecektir. Alt proseste yapılan okuma 50'inci offset'ten itibaren yapılacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; pid_t pid; char buf[10 + 1]; ssize_t result; if ((fd = open("sample.c", O_RDONLY)) == -1) exit_sys("open"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { lseek(fd, 50, SEEK_SET); } else { sleep(1); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); buf[result] = '\0'; puts(buf); } close(fd); sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- C'nin standart dosya fonksiyonlarının bir tamponlama mekanizmasıyla çalıştığını görmüştük. Bu durumda fopen fonksiyonu ile açtığımız bir dosyaya bir şeyler yazıp henüz tampon flush edilmeden fork yaparsak tüm bellek alanının kopyası çıkartılacağı için bu tamponun da flush edilmemiş bir kopyası oluşacaktır. Bu durum tasarımda sorunlara yol açabilir. Programcının bu durumu dikkate alıp fork işleminden önce fflush yapması gerekebilir. Aşağıdaki örnekte printf fonksiyonu Linux sistemlerinde default durumda "satır tamponlamalı" olan stdout dosyasının tamponuna bilgileri yazmıştır. Ancak "\n" karakteri tampona yazılmadığı için flush işlemi de yapılmamıştır. fork işlemi ile birlikte bu tamponun da kopyası çıkarılacağından dolayı ekranda iki tane "Ok" yazısı görünecektir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; printf("Ok"); if ((pid = fork()) == -1) exit_sys("fork"); printf("\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 25. Ders 22/01/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde prosesi sonlandırmak için _exit isimli POSIX fonksiyonu kullanılmaktadır. Bu fonksiyon C'nin standart exit fonksiyonuna benzemektedir. #include void _exit(int status); Fonksiyon, parametre olarak prosesin "exit kodunu" almaktadır. Tabii bir proses sonlanmadan önce prosesin sistem genelinde tahsis etmiş olduğu kaynaklar boşaltılmaktadır. Yani örneğin biz open fonksiyonu ile birtakım dosyalar açmışsak _exit işlemi sırasında bütün bu dosyalar kapatılacaktır. Linux sistemlerinde _exit fonksiyonu doğrudan işletim sisteminin sys_exit_group isimli sistem fonksiyonunu çağırmaktadır. Tabii asıl prosesin sonlandırılması bu sistem fonksiyonu tarafından yapılmaktadır. Yine geleneksel olarak başarılı sonlanmalar için 0 değeri, başarısız sonlanmalar için sıfır dışı değerler kullanılmaktadır. C'nin standart exit fonksiyonun da prototipi şöyledir: #include void exit(int status); C'nin exit fonksiyonu prosesin sonlandırılması için UNIX/Linux sistemlerinde aslında _exit POSIX fonksiyonunu çağırmaktadır: exit ----> _exit ----> sys_exit_group (Linux) exit standart C fonksiyonu, standart C kütüphanesinde yapılan bazı işlemleri de geri almaktadır. Örneğin exit fonksiyonu önce atexit fonksiyonu ile kaydetterilmiş olan fonksiyonları ters sırada çağırır, sonra tmpfile fonksiyonu ile yaratılmış geçici dosyaları siler ve dosya bilgi göstericilerine (streams) ilişkin tamponları flush eder sonra da bunları kapatır. C'de programcı, program içerisinde exit fonksiyonunu hiç çağırmamışsa akış main fonksiyonunu bitirdiğinde main fonksiyonunun geri dönüş değeri ile exit fonksiyonu çağrılmaktadır. Yani C'de main fonksiyonu derleyici tarafından adeta exit(main()) gibi çağrılmaktadır. Yani C'de sonlandırmalar aslında her zaman exit (ya da abort) fonksiyonu ile yapılmaktadır. abort fonksiyonu ise "abnormal" sonlandırmalar için kullanılmaktadır. UNIX/Linux sistemlerinde abort standart C fonksiyonu SIGABRT sinyali oluşturarak programı sonlandırmaktadır. C'de program, standart exit fonksiyonu ile sonlandırılmalıdır. Çünkü exit fonksiyonu yukarıda ele aldığımız bazı gerekli son işlemleri de yapmaktadır. Ancak yine de bazen programın doğrudan _exit POSIX fonksiyonu ile sonlandırılması gerekebilmektedir. Aşağıdaki örnekte program exit fonksiyonu ile değil _exit fonksiyonu ile sonlandırılmıştır. Bu nedenle atexit ile kaydedilen foo fonksiyonu program sonlanırken çağrılmayacaktır. Aynı zamanda dosya tamponları da flush edilmeyeceğinden dolayı printf fonksiyonu ile ekrana yazılmak istenen ancak satır tamponlaması nedeniyle henüz yazılamayan "ok" yazısı da ekranda görülmeyecektir. Burada _exit çağrısını kaldırırsanız. Akış main fonksiyonunu bitirince exit standart C fonksiyonu çağrılacağı için bir sorun kalmayacaktır. Linux'ta aslında _exit fonksiyonu işletim sisteminin sys_exit sistem fonksiyonunu değil sys_exit_group sistem fonksiyonunu çağırmaktadır. Linux'ta sys_exit sistem fonksiyonu yalnızca fonksiyonu çağıran thread'i sys_exit_group fonksiyonu ise tüm thread'leri dolayısıyla da prosesi sonlandırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void foo(void) { fprintf(stderr, "foo\n"); } int main(void) { atexit(foo); printf("ok"); _exit(0); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- _exit fonksiyonunun parametresi olan exit kodunun hangi değerde olduğu işletim sistemini ilgilendirmemektedir. Yani işletim sistemi bu değeri aslında kullanmamaktadır. İşletim sistemi exit kodunu alır ve saklar. Bunu prosesi yaratan üst proses isterse ona verir. Ancak onun hangi değerde olduğu ile ilgilenmez. exit kodunun değeri üst prosesle alt prosesin arasındaki bir anlaşma ile anlam kazanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Üst proses fork fonksiyonu ile alt prosesi yarattıktan sonra onun sonlanmasını bekleyebilir ve alt proses sonlandığında onun exit kodunu alabilir. Bunun için wait ve waitpid isimli POSIX fonksiyonları kullanılmaktadır. waitpid fonksiyonu wait fonksiyonunu işlevsel olarak kapsamaktadır. (Zaten önce wait fonksiyonu vardı, onun yetersizlikleri görülünce waitpid fonksiyonu tasarlandı). wait fonksiyonunun prototipi şöyledir: #include pid_t wait(int *wstatus); wait fonksiyonu herhangi bir alt proses sonlanana kadar "blokede" fonksiyonu çağıran thread'i bekletir. Burada blokede bekleme terimi CPU zamanı harcamadan uykuda kalmayı belirtmektedir. Tabii wait fonksiyonu çağrıldığında alt proseslerden biri sonlanmış da olabilir. Bu durumda wait fonksiyonu blokeye (yani beklemeye) yol açmaz. wait fonksiyonu başarı durumunda exit kodunu aldığı prosesin id değeri ile geri dönmektedir. Böylece programcı çok sayıda alt prosesin söz konusu olduğu durumda hangi alt prosesin exit kodunu aldığını buradan hareketle anlayabilmektedir. Fonksiyon parametresiyle aldığı int nesnesinin içerisine sonlanan prosesin exit kodunu ve sonlanma nedenine ilişkin bazı bilgileri yerleştirmektedir. Normal biçimde sonlanmamış (yani bir sinyal ile sonlanmış) proseslerde exit kodu oluşmamaktadır. O halde programcının prosesin exit kodunu alabilmesi için onun normal bir biçimde sonlanmış olduğunu belirlemesi gerekir. İşte içerisindeki WIFEXITED makrosu ile bu belirleme yapılabilmektedir. Bu makroya wait fonksiyonuna geçirilmiş olan int nesne verilir. Makro bu nesnenin bazı bitlerinden alt prosesin normal sonlanıp sonlanmadığını anlar ve eğer alt proses normal bir biçimde sonlanmışsa sıfır dışı herhangi bir değere, normal bir biçimde sonlanmamışsa sıfır değerine geri döner. Benzer biçimde biz prosesin anormal bir biçimde bir sinyal dolayısıyla sonlanıp sonlanmadığını da WIFSIGNALED makrosuyla tespit edebiliriz. Proses SIGSTOP sinyali ile geçici süre durdurulmuş da olabilir. Bu durum da WIFSTOPPED makrosu ile tespit edilebilmektedir. Prosesin exit kodu ise WEXITSTATUS makrosuyla elde edilmektedir. Yine bu makroya wait fonksiyonuna geçirilen int nesne argüman olarak verilmektedir. Programcı wait fonksiyonuna argüman olarak NULL adres de geçebilir. Bu durumda fonksiyon exit koduyla ilgili bir yerleştirme yapmaz. Ancak yine ilk alt prosesin bitmesini bekler. Eğer wait fonksiyonu çağrıldığında zaten üst prosesin yarattığı herhangi bir alt proses yoksa ya da fonksiyona geçersiz bir adres geçilmişse fonksiyon başarısız olabilmektedir. wait fonksiyonunun tasarımında şu problemler vardır: - wait fonksiyonu ile biz belli bir alt prosesi bekleyememekteyiz. wait çağrıldığında henüz hiçbir alt proses sonlanmamışsa wait fonksiyonu ilk sonlanan alt prosesin exit kodunu alır. - wait fonksiyonu çağrıldığında eğer zaten birden fazla alt proses sonlanmış durumdaysa POSIX standartları hangi alt prosesin exit kodunun elde edileceği konusunda bir garanti vermemektedir. Yani bu durumda wait fonksiyonunun ilk sonlanan alt prosesin exit kodunu alması garanti edilmemiştir. Aşağıdaki örnekte üst proses fork fonksiyonu ile alt prosesi yaratmıştır ve wait fonksiyonu ile onu beklemiştir. Alt proses normal bir biçimde sonlanmışsa onun exit kodunu alıp ekrana yazdırmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); void child_proc(void) { for (int i = 0; i < 10; ++i) { printf("child running: %d\n", i); sleep(1); } exit(100); } int main(void) { pid_t pid; int status; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) child_proc(); printf("parent waiting for child to exit...\n"); if (wait(&status) == -1) exit_sys("wait"); if (WIFEXITED(status)) printf("child exited with exit code %d\n", WEXITSTATUS(status)); printf("Ok, parent continues running...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte ise üst proses wait fonksiyonu çağırmadan alt proses sonlanmıştır. Tabii bu durumda üst proses hiç beklemeden alt prosesin exit kodunu alıp yoluna devam edecektir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); void child_proc(void) { printf("child terminates...\n"); exit(100); } int main(void) { pid_t pid; int status; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) child_proc(); for (int i = 0; i < 10; ++i) { printf("parent running: %d\n", i); sleep(1); } if (wait(&status) == -1) exit_sys("wait"); if (WIFEXITED(status)) printf("child exited with exit code %d\n", WEXITSTATUS(status)); printf("Ok, parent continues running...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Tabii üst proses ne kadar fork yapmışsa o kadar sayıda wait yapmalıdır. Çünkü her wait fonksiyonu bir alt prosesin sonlanma bilgilerini alacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #define NCHILDS 5 void exit_sys(const char *msg); void child_proc(int val) { srand(val); sleep(rand() % 5 + 1); exit(val); } int main(void) { pid_t pids[NCHILDS]; int status; printf("parent is waiting for childs to exit...\n"); for (int i = 0; i < NCHILDS; ++i) { if ((pids[i] = fork()) == -1) exit_sys("fork"); if (pids[i] == 0) child_proc(100 + i); } for (int i = 0; i < NCHILDS; ++i) { if (wait(&status) == -1) exit_sys("wait"); if (WIFEXITED(status)) printf("child exited with exit code %d\n", WEXITSTATUS(status)); } printf("Ok, parent continues running...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 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 alt prosesin proses id değerini belirtir. Bu sayede programcı belli bir alt prosesi bekleyebilmektedir. Bu birinci parametre aslında birkaç biçimde geçilebilmektedir. Eğer bu parametre negatif bir proses id değeri olarak geçilirse bu durumda fonksiyon proses grup id'si bu değerin pozitifi olan herhangi bir alt prosesi beklemektedir. Eğer bu parametre -1 olarak geçilirse bu durumda fonksiyon tamamen wait fonksiyonundaki gibi davranmaktadır. Yani herhangi bir alt prosesi beklemektedir.Eğer bu parametre 0 olarak geçilirse fonksiyon proses grup id'si waitpid fonksiyonunu çağıran prosesin id'si ile aynı olan herhangi bir alt prosesi beklemektedir. Tabii normal olarak bu parametreye programcı pozitif olan bir proses id geçer. Bu durumda fonksiyon o alt prosesi bekleyecektir. (Tabii bu parametreye geçilen proses id, o prosesin bir alt prosesi değilse fonksiyon yine başarısız olmaktadır.) Fonksiyonun ikinci parametresi exit bilgisinin yerleştirileceği int türden nesnenin adresini alır. Üçüncü parametre bazı özel değerlerin bit düzeyinde OR'lanmasıyla oluşturulabilmektedir: WNOHANG: Bu durumda waitpid eğer alt proses henüz sonlanmamışsa bekleme yapmaz, başarısızlıkla sonuçlanır. WUNTRACED, WCONTINUED: Prosesin durdurulması ve devam ettirilmesi ile ilgili bilginin elde edilmesinde kullanılmaktadır. Tabii bu üçüncü parametre genellikle 0 geçilmektedir. 0 geçilmesi bu bayraklardan hiçbirinin kullanılmadığı anlamına gelmektedir. O halde aslında wait(&status) çağrısı ile waitpid(-1, &status, 0) eşdeğerdir. waitpid fonksiyonunda da ikinci parametre NULL adres geçilebilir. Bu durumda proses beklenir ama exit bilgileri elde edilmez. waitpid fonksiyonu da başarı durumunda beklenen proses id değeri ile başarısızlık durumunda -1 değeriyle geri dönmektedir. Aşağıdaki örnekte 5 tane alt proses yaratılmış ancak bunlar herhangi bir sırada değil yaratım sırasına göre waitpid fonksiyonu ile beklenmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #define NCHILDS 5 void exit_sys(const char *msg); void child_proc(int val) { srand(val); sleep(rand() % 5 + 1); exit(val); } int main(void) { pid_t pids[NCHILDS]; int status; printf("parent is waiting for childs to exit...\n"); for (int i = 0; i < NCHILDS; ++i) { if ((pids[i] = fork()) == -1) exit_sys("fork"); if (pids[i] == 0) child_proc(100 + i); } for (int i = 0; i < NCHILDS; ++i) { if (waitpid(pids[i], &status, 0) == -1) exit_sys("wait"); if (WIFEXITED(status)) printf("child exited with exit code %d\n", WEXITSTATUS(status)); } printf("Ok, parent continues running...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Programcının fork fonksiyonu ile her yarattığı alt prosesi wait fonksiyonları ile beklemesi iyi bir tekniktir. Aksi halde sonraki paragrafta ele alacağımız gibi "hortlak (zombie)" proses problemi oluşabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz kabuk üzerinden program çalıştırdığımızda fork işlemini kabuk uygulamaktadır. Dolayısıyla çalıştırılan programın exit kodunu da üst proses olan kabuk almaktadır. İşte biz $? ile kabuk üzerinde son çalıştırılan programın exit kodunu elde edebiliriz. Örneğin: ./sample echo $? 100 ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde prosesler konusunda çokça karşılaşılan "zombie (hortlak)" proses biçiminde bir kavram vardır. Zombie sözcük anlamı olarak "tam ölememiş canlılar" için kullanılmaktadır. Bir alt proses sonlandığında işletim sistemi onun kaynaklarını boşaltmaktadır. Örneğin prosesin bellek alanı tamamen sisteme iade edilmektedir. Prosesin açmış olduğu dosyalar kapatılmaktadır. Ancak işletim sistemi, alt prosesin exit kodunu üst prosese iletebilmek için proses kontrol bloğunu proses bittiğinde hemen serbest bırakmamaktadır. Prosesin exit kodu proses kontrol bloğunda saklanmaktadır. İşletim sistemi bu exit kodunu üst proses herhangi bir zaman isteyebilir diye proses kontrol bloğunu (Linux'taki task_struct yapısı) sisteme iade etmez. Böylece bir alt proses bittiğinde eğer üst proses wait fonksiyonlarıyla alt prosesin exit kodunu henüz almamışsa "kendisi bitmiş ama proses kontrol bloğu boşaltılamamış" bir durum oluşmaktadır. İşte bu duruma UNIX/Linux dünyasında "zombie process" denilmektedir. Zombie proseslerde prosesin id değeri de "üst proses wait ya da waitpid fonksiyonunu kullanabilir" diye sisteme iade edilmemektedir. Alt ve üst proseslerin sonlanması şu biçimlerde olabilmektedir: 1) Üst proses alt prosesten önce sonlanmış olabilir. Bu durumda alt proses "öksüz (orphan)" duruma düşer. Sistem de 1 numaralı id'ye sahip olan "init" prosesini öksüz prosesin üst prosesi olarak atar. Daha sonra alt proses sonlandığında init prosesi alt prosesin exit kodunu alarak onun zombie duruma düşmesini engeller. 2) Alt proses üst prosesten daha önce sonlanmıştır. İşte bu durumda eğer üst proses wait fonksiyonlarını henüz uygulamamışsa alt proses zombie durumda kalır. Tabii üst proses wait fonksiyonlarını uyguladığı anda alt proses zombie olmaktan kurtulur. 3) Alt proses üst prosesten önce sonlanmıştır. Ancak üst proses de wait fonksiyonlarını uygulamadan sonlanmıştır. Bu durumda yine işletim istemi artık exit kodunu alacak bir üst proses kalmadığı için alt prosesi zombie olmaktan çıkartır. Yani onun proses kontrol bloğunu ve id değerini boşaltır. O halde zombie proses yalnızca şu süreçte ortaya çıkmaktadır: "Alt proses sonlanmıştır ancak üst proses wait fonksiyonlarını uygulamadan çalışmasına devam etmektedir." ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 26. Ders 28/01/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Şimdi bir "zombie" proses durumu oluşturalım. Yapacağımız şey alt prosesi sonlandırıp üst prosesin wait fonksiyonlarını uygulamadan yoluna devam etmesini sağlamaktır. Zombie prosesler "ps -l" komutunda "defunct" olarak gösterilmektedir. Bunların "proses durumları da (process state)" "Z" harfi ile belirtilmektedir. Örneğin: $ ps -la F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 S 1000 10612 1868 0 80 0 - 622 hrtime pts/1 00:00:00 sample 1 Z 1000 10613 10612 0 80 0 - 0 - pts/1 00:00:00 sample 4 R 1000 10621 1618 0 80 0 - 3540 - pts/0 00:00:00 ps ---------------------------------------------------------------------------------------------------------------------------*/ #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) { /* üst proses */ for (int i = 0; i < 60; ++i) { printf("parent process continues running: %d\n", i); sleep(1); } } else { /* alt proses */ printf("child terminates...\n"); exit(EXIT_SUCCESS); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Zombie proses oluşmasının şu sorunları vardır: - Üst prosesin ömrü fazla değilse genellikle üst prosesin zombie proses oluşturması ciddi bir soruna yol açmaz. Ancak üst proses uzun süre çalışıyorsa (günlerce, aylarca) zombie prosesler önemli bir sistem kaynağının harcanmasına yol açabilmektedir. - Zombie proseslere ilişkin proses id değerleri o proses zombie'likten kurtulana kadar sistem tarafından kullanılamamaktadır. Sürekli zombie proses üreten bir program proses id'lerin tükenmesine bile yol açabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi zombie proses oluşmasının engellenmesinin tek yolu wait fonksiyonlarını uygulamak mıdır? Çünkü wait fonksiyonları uygulandığında üst proses alt proses bitene kadar blokede bekleyecektir. Halbuki bazı uygulamalarda üst prosesin yoluna devam etmesi ve bloke olmaması istenir. İşte zombie oluşmasının otomatik engellenmesi için iki yöntem kullanılmaktadır: 1) Alt proses bittiğinde SIGCHLD sinyalinde üst proses wait fonksiyonlarını uygularsa üst proses blokede kalmadan zombie durumunu engelleyebilir. 2) Biz alt prosesin exit kodunu almak istemediğimizi işletim sistemine söylersek işletim sistemi alt proses bittiğinde onu zombie duruma sokmadan onun kaynaklarını boşaltabilmektedir. Bu iki zombie engelleme yöntemi de "sinyal (signal)" denilen konu ile ilgildir. Bu konu ileride ele alınacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Modern işletim sistemlerinin büyük çoğunluğunda prosese özgü ismine "çevre değişkenleri (environment variables)" denilen bir veri yapısı bulundurulmaktadır. Çevre değişkenleri anahtar-değer çiftlerini tutan anahtar verildiğinde onun değerini bize veren "sözlük (dictionary)" tarzı bir veri yapısı organizasyonudur. Tabii sözlük tarzı veri yapıları pek çok nesne yönelimli programlama dilinin standart kütüphanesinde "map", "set", "dictionary", "hashtable" gibi isimlerle bulunmaktadır. Ancak çevre değişkenleri, bir sözlük veri yapısının basit bir biçimde işletim sistemi tarafından aşağı seviyeli bir gerçekleştirimidir. Çevre değişkenleri konusunda anahtar-değer çiftlerinin anahtarlarına "çevre değişkeni (environment variable)" denilmektedir. O anahtara karşı gelen değere de "o çevre değişkeninin değeri" denir. Çevre değişkenlerinin anahtarları da değerleri de birer yazı biçimindedir. Örneğin anahtar "ankara" yazısı olabilir, onun değeri de "06" yazısı olabilir. Anahtar "eskisehir" yazısı olabilir onun değeri de "26" yazısı olabilir. Çevre değişkenleri ve değerleri pek çok işletim sisteminde prosesin bellek alanı içerisinde tutulmaktadır. Örneğin Windows sistemleri, UNIX/Linux sistemleri tipik olarak çevre değişkenlerini proses bellek alanı içerisinde özel bir alanda tutmaktadır. Bu konu çerçevesinde programcının şu işlemleri yapabilmesi gerekmektedir: - Bir çevre değişkeni (yani anahtar) verildiğinde onun değerini elde etmek. - Prosesin çevre değişken listesine yeni bir anahtar-değer çifti eklemek - Prosesin tüm çevre değişken listesini elde etmek. UNIX/Linux sistemlerinde prosesin çevre değişkenlerinin (yani anahtarların) büyük harf-küçük harf duyarlılığı vardır. Ancak Windows sistemlerinde çevre değişkenlerinin büyük harf-küçük harf duyarlılığı yoktur. Genel olarak çevre değişkenleri (yani anahtarlar) boşluk karakterleri içermemektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir çevre değişkeni (yani anahtar) verildiğinde onun değerini elde etmek için getenv isimli standart C fonksiyonu kullanılabilir. Fonksiyonun prototipi şöyledir: #include char *getenv(const char *name); Fonksiyon parametre olarak çevre değişkeninin ismini (yani anahtarı) alır geri dönüş değeri olarak onun değerinin bulunduğu bellek adresini verir. Fonksiyonun geri döndürdüğü adres prosesin adres alanı içerisindeki statik düzeyde tahsis edilmiş bir alanın adresidir. Fonksiyon eğer ilgili çevre değişkeni yoksa NULL adrese geri dönmektedir. Fonksiyonun geri dönüş değeri const olmayan bir gösterici olsa da programcı geri döndürülen bu adresteki yazıyı değiştirmeye çalışmamalıdır. C standartlarında bu değiştirme durumu işletim sisteminin isteğine bırakılmış olsa da UNIX/Linux sistemlerinde bu durum tanımsız davranışa yol açmaktadır. getenv fonksiyonu başarısızlık durumunda errno değişkenini herhangi bir değerle set etmemektedir. Aşağıdaki örnekte komut satırından alınan çevre değişkeninin değeri stdout dosyasına yazdırılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #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; } /*-------------------------------------------------------------------------------------------------------------------------- Prosesin çevre değişkenleri, fork işlemi sırasında üst prosesten alt prosese aktarılmaktadır. Zaten çevre değişkenleri prosesin bellek alanında saklandığından fork işlemi de prosesin bellek alanının bir kopyasını oluşturduğundan bu işlemin doğal sonucu olarak üst prosesin çevre değişkenleri alt prosese aktarılmaktadır. Örneğin biz kabuk üzerinden bir program çalıştırdığımızda kabuğun çevre değişkenleri bizim programımıza aktarılacaktır. Kabuğun çevre değişken listesi env kabuk komutuyla her satırda "anahtar=değer" biçiminde görüntülenebilmektedir. Pekiyi kabuk programındaki çevre değişkenleri nasıl oluşturulmuştur? İşte prosesler birbirlerini yaratırken kabuk prosesine gelene kadar bazı prosesler çevre değişkenlerine eklemeler yapmaktadır. Örneğin kabuk programını çalıştıran login programı "HOME", "USER" "SHELL" gibi çevre değişkenlerini prosesin çevre değişken listesine eklemektedir. Benzer biçimde kabuk da pek çok çevre değişkenini çevre değişken listesine eklemiş durumdadır. Yani biz programımızı kabuk üzerinden çalıştırırken kümülatif olarak çeşitli prosesler çevre değişken listesine çeşitli çevre değişkenlerini eklemiş olmaktadır. Örneğin biz kabuk üzerinde "cd" komutunu kullandığımızda kabuk PWD isimli çevre değişkeninin değerini o anda geçilen dizin biçiminde değiştirmektedir. chdir POSIX fonksiyonu bunu yapmaz. Kabuktaki "cd" komutu bunu yapmaktadır. Kabuk üzerinde bir çevre değişkenini başına $ olacak biçimde yazarsak kabuk sanki o yazı yerine onun değerine ilişkin yazıyı oraya yazmışız gibi davranmaktadır. Örneğin biz kabuk üzerinde $PATH yazarsak kabuk bu $PATH yazısını kaldırıp onun yerine onun değerini oraya yerleştirecektir. (Aynı işlem Windows sistemlerinde %NAME% ile yapılmaktadır.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 27. Ders 29/01/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Prosesin çevre değişken listesine yeni bir anahtar-değer çifti eklemek için setenv ve putenv isimli POSIX fonksiyonları kullanılmaktadır. Bu fonksiyonlar standart C fonksiyonları değildir. C'de prosesin çevre değişken listesine ekleme yapan standart bir fonksiyon yoktur. setenv fonksiyonunun prototipi şöyledir: #include int setenv(const char *name, const char *value, int overwrite); Fonksiyonun birinci parametresi çevre değişkeninin ismini ikinci parametresi onun değerini alır. Üçüncü parametre eğer o çevre değişkeni zaten varsa onun değerinin değiştirilip değiştirilmeyeceğini belirtir. Bu parametre sıfır dışı bir değer olarak geçilirse çevre değişkeninin değeri değiştirilir. Sıfır geçilirse değiştirilmez ve fonksiyon yine başarıyla geri döner. 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ğeri uygun biçimde set edilmektedir. Aşağıdaki örnekte komut satırı argümanı ile verilen çevre değişkenleri setenv fonksiyonu ile prosesin çevre değişken listesine eklenmiş ve sonra getenv fonksiyonu ile onların değerleri elde edilmiştir. Girişin aşağıdaki gibi yapılması gerekir: ./sample ali=100 veli=200 selami=300 Program '=' karakterini strchr fonksiyonu ile aramış eğer onu bulursa '=' karakteri yerine '\0' karakterini yerleştirmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(int argc, char *argv[]) { char *str; if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) { if ((str = strchr(argv[i], '=')) == NULL) { fprintf(stderr, "invalid argument: %s\n", argv[i]); continue; } *str = '\0'; if (setenv(argv[i], str + 1, 1) == -1) perror("setenv"); } for (int i = 1; i < argc; ++i) { if ((str = getenv(argv[i])) == NULL) { fprintf(stderr, "environment variable not found: %s\n", argv[i]); continue; } printf("%s ---> %s\n", argv[i], str); } return 0; } /*-------------------------------------------------------------------------------------------------------------------------- putenv fonksiyonu da yine prosesin çevre değişken listesine ekleme yapmak için kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int putenv(char *str); Fonksiyon parametre olarak "anahtar=değer" biçiminde bir yazı almaktadır. Fonksiyon ilgili çevre değişkeni zaten varsa her zaman onun değerini güncellemektedir. Eğer yazıda '=' karakteri kullanılmazsa boş değeri boş olan (yani elde edildiğinde yalnızca null karakter veren bir çevre değişkeni oluşturulmaktadır. Fonksiyon yine 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.) putenv fonksiyonunda verilen adres doğrudan prosesin çevre değişken listesi olarak kullanılmaktadır. Verilen adresteki bilginin program çalıştığı sürece kalıcı olmasına dikkat ediniz. Aşağıdaki örnekte yine program aşağıdakine benzer çalıştırılmalıdır: ./sample ali=100 veli=200 ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(int argc, char *argv[]) { char *str; if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) if (putenv(argv[i]) == -1) perror("putenv"); /* ... */ return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte putenv fonksiyonu ile prosesin çevre değişken listesine bir ekleme yapılmıştır. Sonra buradaki anahtar değer çifti program içerisinde değiştirilmiştir. Prosesin çevre değişken listesinin nasıl organize edildiği izleyen paragrafta ele alınmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { char *value; char env[1024] = "city=istanbul"; if (putenv(env) == -1) exit_sys("setenv"); if ((value = getenv("city")) == NULL) { fprintf(stderr, "cannot find environment variable \"city\"!...\n"); exit(EXIT_FAILURE); } puts(value); strcpy(env, "village=urla"); if ((value = getenv("village")) == NULL) fprintf(stderr, "cannot find environment variable \"village\"!...\n"); puts(value); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde genel olarak çevre değişkenleri bir gösterici dizisi yoluyla tutulmaktadır. Her çevre değişkeni aslında "anahtar=değer\0" biçiminde bir yazı olarak oluşturulmakta ve bu yazıların başlangıç adresleri de bir gösterici dizisinde saklanmaktadır. Bu gösterici dizisinin sonunda da NULL adres bulunmaktadır. Bu gösterici dizisinin başlangıç adresi environ isimli bir global göstericiyi gösteren göstericiyle tutulmaktadır. Yani prosesin çevre değişken listesi aşağıdaki gibi bir veri yapısıyla oluşturulmuştur: environ ----> adres ---> ali=100\0 adres ---> veli=200\0 adres ---> selami=300\0 ... NULL Aslında putenv fonksiyonu bizim "anahtar=değer" biçiminde verdiğimiz yazının adresini eğer anahtar yoksa doğrudan bu gösterici dizisine eklemektedir. Örneğin: char s[] = "ayse=500"; putenv(s); environ ----> adres ---> ali=100\0 adres ---> veli=200\0 adres ---> selami=300\0 ... s dizisinin adresi ----> ayse=500\0 NULL Maalesef bu environ global değişkeninin extern bildirimi herhangi bir başlık dosyasında bulundurulmamıştır. Prosesin çevre değişken listesine erişmek isteyen programcıların bu extern bildirimini kendilerinin yapması gerekmektedir. Örneğin: extern char **environ; O halde prosesin bütün çevre değişkenlerinin listesini almak oldukça kolaydır. Aşağıdaki örnekte prosesin tüm çevre değişkenlerinin listesi elde edilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include extern char **environ; int main(void) { for (int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- getenv fonksiyonu aşağıdaki gibi basit bir biçimde gerçekleştirilebilir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include extern char **environ; char *mygetenv(const char *name) { char *str; for (int i = 0; environ[i] != NULL; ++i) { if ((str = strchr(environ[i], '=')) != NULL) if (!strncmp(name, environ[i], str - environ[i])) return str + 1; } return NULL; } int main(void) { char *val; if ((val = mygetenv("xxx")) == NULL) { fprintf(stderr, "environment variable not found!...\n"); exit(EXIT_FAILURE); } puts(val); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Prosesin çevre değişkenlerine ilişkin gösterici dizisi ve onların gösterdikleri yerler prosesin bellek alanı içerisindedir. fork işlemi sırasında üst prosesin tüm bellek alanının bir kopyası oluşturulduğuna göre alt prosesin çevre değişken listesi üst prosesinkinin aynısı olacaktır. Ancak fork işleminden sonra üst proses ya da alt proses çevre değişken listesinde bir değişiklik yaparsa artık yalnızca o değişiklik o prosese özgü olacaktır. Çünkü fork işlemi sırasında kopyalama yapıldıktan sonra artık üst prosesle alt prosesin bellek alanları birbirinden tamamen ayrılmış olur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir çevre değişkeni unsetenv isimli POSIX fonksiyonuyla prosesin çevre değişken listesinden silinebilmektedir. Fonksiyonun prototipi şöyledir: #include int unsetenv(const char *name); Fonksiyon çevre değişkenin ismini almaktadır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri önder ve errno uygun biçimde set edilir. Eğer ilgili çevre değişkeni zaten yoksa fonksiyon bir şey yapmaz ancak başarılı bir biçimde geri dönmektedir. Aşağıdaki örnekte komut satırından alınan bir çevre değişkeninin önce değeri yazdırılmış sonra o çevre değişkeni silinmiş, sonra da o çevre değişkeninin yeniden değeri elde edilmek istenmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include void exit_sys(const char *msg); 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); if (unsetenv(argv[1]) == -1) exit_sys("ensetenv"); 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); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi biz programımızı çalıştırdığımızda belli bir çevre değişkeninin zaten var olmasını nasıl sağlayabiliriz? Çevre değişkenleri fork işlemi sırasında alt prosese aktarıldığına göre biz eğer kabuk programın (bash) çevre değişken listesine bir ekleme yaparsak kabuk bizim programınızı çalıştırırken fork yapacak ve onun çevre değişkenleri bizim programımıza geçecektir. Pekiyi kabuk programının çevre değişken listesine nasıl ekleme yapabiliriz? İşte bu işlem şöyle yapılabilmektedir: $ CITY=Eskisehir $ export CITY Komut satırında "anahtar=değer" biçiminde bir yazı yazıp ENTER tuşuna basarsak biz kabuk dili için bir kabuk değişkeni yaratmış oluruz. Bu kabuk değişkeninin aynı zamanda kabuğun çevre değişkeni yapılması için export komutu kullanılmaktadır. Tabii bu iki komut tek hamlede de verilebilmektedir: export CITY=Eskisehir Çevre değişkenini kabuktan silmek için de unset komutu kullanılmaktadır. Örneğin: $ unset CITY Bir çevre değişkeninin (aslında genel olarak kabul değişkeninin) değerini elde etmek için UNIX/Linux sistemlerinde çevre değişkeninin başına $ karakteri getirilir. Örneğin: $ echo $CITY Burada biz CITY çevre değişkeninin değerini ekrana yazdırmış olduk. $ karakterinden sonra çevre değişkeni küme parantezlerine de alınabilir. Örneğin: $ echo ${CITY} Buradaki küme parantezine bazı durumlarda gereksinim duyulabilmektedir. Örneğin kabuk üzerinde CITY çevre değişkenini yukarıdaki gibi yaratmış olalım. Şimdi de bu çevre değişkeninden hareketle bu çevre değişkeninin değerine yapışık olarak "CENTER" sözcüğünü de eklemek isteyelim: export OTHER=$CITYCENTER Bu durumda kabuk sanki bizim CITYCENTER isimli bir çevre değişkeninin değerini yazdırmak istediğimizi sanacaktır. Bu durumda mecburen küme parantezleri kullanılmalıdır. Örneğin: export OTHER=${CITY}CENTER Aslında pek çok kabuk programında hiç kabuk programının çevre değişkenlerini set etmeden, doğrudan çalıştırılacak program için çevre değişkenleri belirlenebilmektedir. Bunun için önce "değişken=değer" çiftleri yazılarak program dosyası belirtilir. Örneğin: $ XX=10 YY=20 ./sample Burada XX ve YY kabuğun çevre değişken listesine eklenmemektedir. Doğrudan ./sample prosesinin çevre değişkeni yapılmaktadır. Kabuk bu durumda fork işleminden sonra alt proseste bu çevre değişkenlerini ekleyip exec yapmaktadır. Kabuk üzerinde yukarıdaki gibi çevre değişkeni oluşturduğumuzda bunun kalıcılığı olmaz. Yani bu çevre değişkeni o kabuk programının çevre değişkeni olur. Biz başka terminal açsak o başka bir proses olacağı için bu çevre değişkeni orada bulunmayacaktır. Pekiyi kabuk üzerindeki çevre değişkenlerinin kalıcılığını nasıl sağlayabiliriz? İşte bunu sağlamak için kabukların "startup" dosyaları kullanılmaktadır. Kabukların startup dosyaları kabuğun nasıl çalıştırıldığına bağlı olarak değişmektedir. Kabuk programları üç biçimde çalıştırılabilmektedir: 1) Interactive login shell 2) Interactive Non-login shell 3) Non-interactive shell "Interactive shell" demek "komut satırına düşen kullanıcının komut verip çalıştırdığı shell" demektir. "login shell" demek bize "user name" ve "password" soran shell demektir. "Non-interactive shell" demek ise tek bir komutu çalıştırıp işlemini sonlandıran shell demektir. Değişik kabuk programlarının startup dosyaları farklıdır. Biz burada bash üzerinde duracağız. bash kabuğunun " user manuel" dokümanındaki ilgili bölüm aşağıdaki bağlantıdan incelenebilir: https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html Eğer bash "interactive login shell" biçiminde çalıştırılmışsa önce "/etc/profile" dosyasını çalıştırır, sonra sırasıyla aşağıdaki dosyalardan hangisini ilk bulursa yalnız onun içerisindeki komutları çalıştırmaktadır: ~/.bash_profile ~/.bash_login ~/.profile Eğer bash "interactive non-login shell" olarak çalıştırılırsa (örneğin masaüstünden) bu durumda bash "~/.bashrc" dosyasındaki komutları çalıştırmaktadır. Yani biz "~/.bashrc" dosyasına export ile çevre değişkeni eklersek masaüstünden terminali açtığımızda o çevre değişkeni kabuk üzerinde ekli olarak görünecektir. Tabii programcı hem "interactive login shell" hem de "interactive non-login" shell için aynı komutların çalıştırılmasını isteyebilir. Bunu sağlamanın pratik bir yolu komutları ~/.bashrc dosyasına yazıp ~/.bash_profile içerisinden bu dosyanın çalıştırılmasını sağlamaktır. Bu işlem şöyle yapılabilir: if [ -f ~/.bashrc ]; then . ~/.bashrc; fi Eğer bash interactive olmayan bir biçimde (-c seçeneği ile) çalıştırılırsa bu durumda BASH_ENV isimli bir çevre değişkenini araştırır. Eğer bulursa onun değerinin belirttiği script dosyasını çalıştırır. Ayrıca UNIX/Linux'ta kullanılan kabuk programlarında bir program çalıştırılırken onun soluna "çevre_değişkeni=değer" yazılırsa belirtilen çevre değişkeni çalıştırılan programa aktarılmaktadır. Örneğin: $COUNTRY=Turkey CITY=Ankara ./sample Burada COUNTRY ve CITY çevre değişkenleri sample programına aktarılacaktır. Böyle bir çalıştırmada burada belirtilen değişkenlerin kabuk değişkeni olarak ya da kabuğun çevre değişkeni olarak set edilmediğine dikkat çekmek istiyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi çevre değişkenlerine neden gereksinim duyulmaktadır? Çevre değişkenleri birtakım aşağı seviyeli işlemlerin parametrik hale getirilmesi için kullanılmaktadır. Yani aşağı seviyeli bazı işlemlerin basit bir biçimde dışarıdan değiştirilmesine olanak sağlamaktadır. Bazı çevre değişkenleri bazı POSIX fonksiyonları tarafından kullanılmaktadır. Örneğin exec fonksiyonlarının p'li biçimleri prosesin PATH çevre değişkenine başvurmaktadır. Ya da örneğin dinamik bir kütüphane yüklenirken dinamik yükleyici yükleyicisi prosesin LD_LIBRARY_PATH çevre değişkenine başvurmaktadır. Bazen çevre değişkenleri uygulama programcıları tarafından da kullanılmaktadır. Örneğin biz programımız içerisinde bir dosyanın yerini belirlemek isteyelim. Ancak kullanıcı bu dosyayı farklı bir yere yerleştirebiliyor olsun. Bunu bir çevre değişkeni ile ayarlanabilir hale getirebiliriz: char *data_path = "datafile.dat"; char *value; FILE *s; if ((value = getenv("DATA_LOCATION")) != NULL) data_path = value; if (f = fopen(data_path, "r")) == NULL) { fprintf(stderr, "cannot open file!...\n"); exit(EXIT_FAILURE); } ... Örneğin gcc derleyicisi <...> biçiminde include edilmiş dosyaların yerlerini aynı zamanda C_INCLUDE_PATH isimli bir çevre değişkeninde de aramaktadır. Yani derleyici standart include dosyalarının bulunduğu yerin dışında bu çevre değişkeni ile belirtilen dizinlere de bakmaktadır. Tabii birden fazla dizin belirtilebilir bu durumda ':' ile onları ayırmak gerekir. Örneğin: export C_INCLUDE_PATH=/home/kaan:/home/kaan/Study ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir program dosyasını çalıştırmak için ismine "exec fonksiyonları" denilen bir grup POSIX fonksiyonu kullanılmaktadır. Bu fonksiyonların yaptıkları işlemler birbirine benzerdir. Ancak fonksiyonların parametrik yapıları arasında bazı farklılıklar vardır. exec fonksiyonlarını şunlardır: execl execle execlp execv execve (Linux'ta sistem fonksiyonu olarak yazılmıştır) execvp fexecve (Linux'ta execveat sistem fonksiyonu) Ayrıca POSIX standartlarında tanımlı olmasa da da GNU C kütüphanesinde execvpe isimli bir exec fonksiyonu da bulunmaktadır. Yine Linux sistemlerine özgü bir biçimde execveat isimli bir fonksiyon da vardır. Aslında UNIX/Linux sistemleri bu exec fonksiyonlarının hepsini sistem fonksiyonu biçiminde bulundurmamaktadır. Örneğin Linux sistemlerinde execve fonksiyonu bir sistem fonksiyonu biçiminde yazılmıştır. Taşınabilir exec fonksiyonları bu sistem fonksiyonunu çağıran kütüphane fonksiyonları biçiminde gerçekleştirilmiştir. Benzer biçimde Linux'a özgü biçimde "execveat" fonksiyonu da bir sistem fonksiyonu biçimine gerçekleştirilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- exec fonksiyonları prosesin yaşamına başka bir kodla devam etmesini sağlamaktadır. exec fonksiyonlarına biz "çalıştırılabilen bir program dosyasını" parametre olarak veririz. exec fonksiyonları o anda çalışmakta olan programın kodlarını bellekten atıp onun yerine bizim vediğimiz program dosyasının kodlarını belleğe yükler ve o kodu çalıştırır. exec işlemi ile prosesin kontrol bloğunda ciddi bir değişiklik yapılmaz. Yani prosesin id'si, kullanıcı ve grup id'leri, prosesin çalışma dizini vs. değişmez. exec işlemleriyle prosesin yalnızca kodu değiştirilmektedir. Örneğin "sample" programının içerisinde biz exec fonksiyonlarıyla "mample" programını çalıştırmak istediğimizde "sample" programının bellekteki kodu kaldırılır onun yerine "mample" programının kodu belleğe yüklenir ve "mample" programının kodu çalıştırılır. Ancak prosesin kontrol bloğundaki bilgiler değişmez. Yani exec fonksiyonları uygulandığında proses yaşamına başka bir kodla devam etmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 28. Ders 04/02/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- exec fonksiyonlarının isimlerinin sonlarında bulunan "l" harfi (execl, execlp) komut satırı argümanlarının tek tek bir liste biçiminde verileceğini belirtmektedir. Fonksiyonların isimlerinin sonundaki "v" harfi ise komut satırı argümanlarının bir dizi (vector) biçiminde verileceğini belirtir. Fonksiyonların isimlerinin sonlarındaki "p" harfi (path) aramanın PATH çevre değişkenine bakılarak yapılacağını, "e" harfi (environment) ise prosesin çevre değişkenlerinin exec sırasında değiştirileceği anlamına gelmektedir. Yukarıda da belirtildiği gibi Linux sistemlerinde execl, execv, execlp, execvp, execle fonksiyonları aslında execve sistem fonksiyonu fonksiyonu çağrılarak gerçekleştirilmiştir. fexecve ise Linux sistemlerinde execveat sistem fonksiyonu çağrılarak gerçekleştirilmiştir. Biz burada bu fonksiyonların üzerinde tek tek duracağız. Tüm exec fonksiyonları başarı durumunda geri dönmezler. Çünkü zaten başarı durumunda bu fonksiyonlar başka bir programı yüklemiş ve çalıştırmış durumda olurlar. Bu fonksiyonlar başarısızlık durumunda yine -1 değerine geri dönerler ve errno değeri uygun biçimde set edilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- execl fonksiyonunun prototipi şöyledir: #include int execl(const char *path, const char *arg0, ... /*, (char *)0 */); Fonksiyonun birinci parametresi çalıştırılacak olan programın yol ifadesini almaktadır. Bu yol ifadesi mutlak ya da göreli olabilir. Fonksiyonun diğer parametreleri sırasıyla çalıştırılacak programa geçirilecek komut satırı argümanlarının listesini belirtir. Birinci komut satırı argümanının (argv[0]) her zaman program ismi olacak biçimde oluşturulması genel bir beklenti ve C standartlarında öngörülen bir durumdur. Programcı exec uygularken bunu sağlamak zorunda değildir. Ancak bunun sağlanmaması kötü bir tekniktir. Fonksiyon değişken sayıda (... parametresine dikkat ediniz) argüman aldığı için argüman listesinin sonunda NULL adresin bulunması gerekmektedir. Ancak C'de "default argüman dönüştürmesi (default argument conversion)" denilen kurala göre eğer argümanın karşılığında bir parametre türü belirtilmemişse "int türünden küçük türler int türüne, float ise double türüne dönüştürülerek" fonksiyona yollanmaktadır. Burada programcının NULL adres sabitini yalnızca NULL sembolik sabiti biçiminde belirtmemesi gerekir. Çünkü NULL sembolik sabiti düz sıfır olarak da define edilmiş olabilir. Benzer biçimde programcı burada NULL adres sabiti için düz sıfır da geçmemelidir. Uygun olan durum düz sıfır değerinin ya da NULL sembolik sabitinin bir adres türüne (tipik olarak char * türüne) dönüştürülerek aktarılmasıdır. Yukarıda da belirtildiği exec fonksiyonları başarı durumunda zaten geri dönmezler. Başarısızlık durumunda -1 değerine geri dönerler ve errno değişkeni uygun biçimde set edilir. Örneğin: if (execl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1) exit_sys("execl"); /* unreachable code */ Burada execl ile prosesin çalışma dizininde bulunan "mample" programı çalıştırılmak istenmiştir. Diğer argümanlar mample programının main fonksiyonunun argv parametresine geçirilecek komut satırı argümanlarını belirtmektedir. exec fonksiyonları çeşitli nedenlerle başarısız olabilir. Örneğin çalıştırılacak program dosyası bulunamayabilir, bulunduğu halde proses dosya için "x" hakkına sahip olmayabilir, çalıştırılabilen dosyanın formatı bozulmuş olabilir. Bu durumlarda errno değeri uygun biçimde set edilir. Aşağıdaki örneği inceleyiniz. Aşağıdaki sample programı çalıştırıldığında ekranda şu yazıların çıkması gerekir: sample running... mample running... mample ali veli selami ---------------------------------------------------------------------------------------------------------------------------*/ /* sample.c */ #include #include #include void exit_sys(const char *msg); int main(void) { printf("sample running...\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[]) { printf("mample running...\n"); for (int i = 0; i < argc; ++i) puts(argv[i]); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Mademki exec fonksiyonları başarılı olduğunda zaten geri dönmemektedir. O halde exec işlemi aşağıdaki gibi de yapılabilir: exec(...); exit_sys("exec); Burada exec zaten başarılı olduğunda akış aşağıya geçmeyecektir. Başarısız olduğunda akış aşağıya geçecektir. Bu durumda kontrol yapmaya aslında gerek yoktur. Fakat biz kursumuzda genel olarak exec işlemini aşağıdaki gibi yapacağız: if (exec(...) == -1) exit_sys("exec"); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- fork işlemi ile yeni bir proses yaratılıp yeni prosesin üst proses ile aynı kodu çalıştırması sağlanıyordu. exec işleminde ise prosesin kodu atılıp başka bir programın kodu çalıştırılıyordu. Pekiyi biz hem kendi programımız devam etsin hem de başka bir programı da çalıştıralım istiyorsak bunu nasıl yapabiliriz? İşte bu durumda yalnızca fork ya da yalnızca exec işe yaramamaktadır. fork ve exec fonksiyonlarının birlikte kullanılması gerekmektedir. Şöyle ki: Programcı önce fork yapar, sonra alt proseste exec işlemini uygular. Yani başka bir programın kodunu alt proses çalıştırmış olur. Bu işlem tipik olarak şöyle yapılabilir: pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) if (exec(...) == -1) exit_sys("exec"); /* Yalnızca üst prosesin akışı buraya gelir */ Burada exec başarılı olursa zaten artık alt prosesin bellek alanı boşaltılıp yeni program yüklenecektir. exec başarısız olursa zaten alt proses sonlandırılmıştır. Tabii yukarıdaki kalıp && operatörüyle de şöyle oluşturulabilir: pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && exec(...) == -1) exit_sys("exec"); /* Yalnızca üst prosesin akışı buraya gelir */ Tabii üst prosesin yine alt prosesi wait fonksiyonlarıyla beklemesi gerekmektedir. exec işlemi yapılmış olsa da bu bakımdan değişen bir şey yoktur. Çalıştırılan programa ilişkin prosesin üst prosesi yine fork işlemi yapan prosestir. Örneğin: pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && exec(...) == -1) exit_sys("exec"); /* Yalnızca üst prosesin akışı buraya gelir */ if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); Bazen fork işleminden sonra üst proses alt proseste bazı ayarlamalar yaptıktan sonra exec uygulamak isteyebilir. Örneğin: pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { /* alt proseste bazı işlemler */ if (exec(...) == -1) exit_sys("exec"); /* unreachable code */ } /* Yalnızca üst prosesin akışı buraya gelir */ if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); Aslında fork ve exec nadiren tek başına uygulanmaktadır. Genellikle fork ve exec bir arada yukarıdaki kalıp eşiliğinde kullanılmaktadır. Aşağıdaki örnekte üst proses "/bin/ls" programını çalıştırıp yoluna devam etmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; printf("sample running...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execl("/bin/ls", "/bin/ls", "-l", (char *)0) == -1) exit_sys("execl"); printf("ok, parent continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- fork/exec işlemlerinde kişilerin kafasını karıştıran bir durum oluşmaktadır. Kişiler haklı olarak şöyle düşünmektedir: "fork işlemi ile üst prosesin bellek alanı alt proses için kopyalandığına göre ve alt proseste de exec yapıldığında alt prosesin bellek alanı hemen boşaltılacağına göre burada üst prosesin bellek alanı gereksiz biçimde alt prosese kopyalanmış olmuyor mu?" Gerçekten de ilk bakışta böyle bir durum söz gibi gözükmektedir. Ancak modern işlemcilerin "sayfalama (paging)" mekanizmaları sayesinde aslında fork işlemi sırasında "copy on write" mekanizması işletilmektedir. Yani aslında bugün kullandığımız işlemcilerde fork işlemi sırasında işletim sistemi üst prosesin bellek alanını zaten alt prosese bütünsel olarak kopyalamamaktadır. Kopyalama işlemi aslında "gerektiğinde" yapılmaktadır. Bu mekanizmaya "copy on write" denilmektedir. Bu konuda bilgiler ileride verilecektir. Ancak eski sistemlerde "copy on write" mekanizması ya yoktu ya da etkin olarak gerçekleştirilemiyordu. Yani eski sistemlerde yukarıdaki sorudaki durum gerçekten etkinlik bakımından bir problem oluşturuyordu. Bu nedenle bu eski sistemler zamanında fork fonksiyonunun bellek kopyalamasını yapmayan (ya da minimal düzeyde yapan) vfork isminde bir benzeri de bulundurulmuştur. vfork fonksiyonu eskiden POSIX standartlarında bulunuyordu. 2008'den itibaren POSIX standartlarından kaldırılmıştır. Fakat glibc kütüphanesi bu fonksiyonu bulundurmaya devam etmektedir. Zaten yukarıda da belirttiğimiz gibi modern sistemlerde artık vfork fonksiyonuna gereksinim de kalmamıştır. vfork tamamen fork işlemi yapar. Ancak üst prosesin bellek alanını alt prosese kopyalamaz. Çünkü vfork exec için düşünülmüştür. Yani vfork işleminden sora exec yapılmalıdır. Eğer vfork işleminden sonra exec yapılmayıp sanki fork yapılmış gibi program devam ettirilirse "tanımsız davranış (undefined behavior)" oluşmaktadır. vfork fonksiyonunun prototipi fork ile aynı biçimdedir: #include pid_t vfork(void); Eski POSIX standartlarına göre vfork işleminden sonra yalnızca _exit fonksiyonu ya da exec fonksiyonları çağrılabilir. Bunun dışında başka bir fonksiyon çağrılamaz. Yani vfork başarılı ise biz ya _exit ile prosesi sonlandırmalıyız ya da exec uygulamalıyız. Tabii exec de başarısız olursa _exit ile (exit ile değil) alt prosesi sonlandırmalıyız. Başka bir fonksiyonun kullanılamamasının nedeni o fonksiyonların kodlarının alt prosese kopyalanmamış olmasındandır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- execv fonksiyonu işlevsel olarak execl fonksiyonu ile aynıdır. Ancak bu fonksiyon çalıştırılacak program için komut satırı argümanlarını bir gösterici dizisi biçiminde ister. 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 belirtir. İkinci parametresi ise komut satırı argümanlarının bulunduğu char türden gösterici dizisinin başlangıç adresini almaktadır. Yani bizim komut satırı argümanlarını bir gösterici dizisine yerleştirip onun adresini vermemiz gerekir. Bu gösterici dizisinin son elemanı NULL adres olmalıdır. Tabii bu durumda tür dönüştürmesi yapmaya gerek yoktur. Örneğin: char *argv[] = {"/bin/ls", "-l", NULL}; ... execv("/bin/ls", argv); exit_sys("execv"); execv fonksiyonun ikinci parametresindeki const niteleyicisinin yerine dikkat ediniz. Burada const niteleyicisi adresi geçirilen gösterici dizisinin const olduğunu belirtmektedir. O gösterici dizilerinin gösterdiği adresteki yazıların const olduğunu belirtmemektedir. Aşağıdaki execv fonksiyonun kullanımına bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; char *argv[] = {"/bin/ls", "-l", NULL}; printf("sample running...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execv("/bin/ls", argv) == -1) exit_sys("execl"); printf("ok, parent continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- execv ne zaman tercih edilebilir? İşte bazen execl fonksiyonu yerine execv fonksiyonunun kullanılması daha uygun olabilmektedir. Örneğin biz "sample" isimli bir program yazalım. Bu program da komut satırı argümanlarıyla aldığı programı çalıştırsın. Yani "sample" programı şöyle çalıştırılsın: $ ./sample /bin/ls -l Eğer böyle bir programı execl ile yazamaya çalışırsak bunu pratik bir biçimde başaramayız. Çünkü çalıştıracağımız programın kaç komut satırı argümanı ile çalıştırılacağını baştan bilmemekteyiz. Aşağıda böyle bir programa örnek verilmiştir. Programı şöyle edebilirsiniz: $ ./sample /bin/ls -l $ ./sample /bin/cp sample.c x.c $ ./sample mample ali veli selami ---------------------------------------------------------------------------------------------------------------------------*/ /* sample.c */ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { pid_t pid; if (argc == 1) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } printf("sample running...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execv(argv[1], &argv[1]) == -1) exit_sys("execv"); printf("ok, parent continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- exec fonksiyonlarının iki p'li versionu vardır: execlp ve execvp. Bu p'li versiyonların prototipleri p'siz versiyonlarla aynıdır. Yalnızca ilk parametrenin semantik anlamı farklıdır. Fonksiyonların prototipleri şöyledir: #include int execlp(const char *file, const char *arg0, ... /*, (char *)0 */); int execvp(const char *file, char *const argv[]); exec fonksiyonlarının p'li versiyonları şöyle çalışmaktadır: - Eğer bu fonksiyonların birinci parametrelerinde belirtilen dosya isminde hiç "/" karakteri kullanılmamışsa bu fonksiyonlar önce PATH çevre değişkeninin değerini getenv fonksiyonuyla alıp buradaki yazıyı ':' karakterlerinden parçalara ayırırlar. Bu ':' karakterlerinin arasındaki yazıların dizin belirttiğini varsayarlar. Sonra exec yapılacak dosyayı sırasıyla bu dizinlerde ararlar. Eğer bulurlarsa onu exec yaparlar, bulamazlarsa bu fonksiyonlar başarısız olur. Tabii bu fonksiyonlar PATH çevre değişkeninde belirtilen dizinlerdeki aramayı baştan sona doğru yaparlar ve ilk bulduğu dizindeki programı exec işlemine sokarlar. PATH çevre değişkeninin değerinin aşağıdakine benzer bir biçimde bulunması gerekmektedir: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin" - Eğer p'li exec fonksiyonlarının birinci parametresiyle belirtilen dosya isminde en az bir "/" karakteri varsa bu durumda fonksiyonlar PATH çevre değişkenine başvurmazlar. Birinci parametresiyle belirtilen göreli ya da mutlak yol ifadesinden hareketle dosyanın yerini belirlemeye çalışırlar. Başka bir deyişle bu durumda fonksiyonların p'li versiyonlarının p'siz versiyonlarından hiçbir farkı kalmamaktadır. Örneğin: execlp("ls", ...); /* PATH çevre değişkenine başvurulur */ execlp("./mample", ...); /* PATH çevre değişkenine başvurulmaz */ execlp("a/mample", ...); /* PATH çevre değişkenine başvurulmaz */ exec fonksiyonlarının p'li versiyonları eğer dosya isminde hiç "/" karakteri yoksa ve PATH dizinlerinde de dosyayı bulamazlarsa prosesin çalışma dizinine bakmamaktadır. Yani bu durumda bu fonksiyonlar yalnızca PATH çevre değişkenindeki dizinlere bakmaktadır. Tabii PATH eçvre değişkeninde o andaki prosesin çalışma dizini "." ile de belirtilebilir. Örneğin: "/bin:/usr/bin:/:." Buradaki "." prosesin çalışma dizinini belirtmektedir. Biz PATH çevre değişkeninin sonuna dizinler ekleyebiliriz. Örneğin: PATH=$PATH:/home/kaan Tabii bunun kalıcı hale getirilmesi için kabuk programının startup dosyalarına yerleştirilmesi gerekir. Prosesin çalışma dizininin PATH çevre değişkenine eklenmesi güvenlik zafiyeti nedeniyle iyi bir teknik kabul edilmemektedir. Örneğin: PATH=$PATH:. Pekiyi exec fonksiyonlarının p'li versiyonları PATH çevre değişkenini bulamazsa ne olur? POSIX standartları bu durumdaki davranışın sistemden sisteme değişebileceğini (implementation dependent) belirtmektedir. Pek çok sistem (örneğin Linux ve BSD) bu durumda sanki PATH çevre değişkeni "/bin:/usr/bin" biçimindeymiş gibi davranmaktadır. exec fonksiyonlarının p'li versiyonları (execlp ve execvp) aramayı PATH dizinlerinde sırasıyla yapmaktadır. Ancak bu fonksiyonlar dosyayı bir dizinde bulduğunda ve onu sistem fonksiyonuyla (execve) çalıştırmaya çalıştığında EINVAL ve ENOEXEC errno değerini alırsa dosyanın bir kabuk dosyası (shell script) olduğundan çalıştırılamadığı sonucunu çıkartmaktadır ve bu durumda dosyayı /bin/sh (default shell) programı ile çalıştırmaktadır. Ancak exec fonksiyonlarının diğer versiyonları EINVAL ve ENOEXEC hatalarında bunu yapmamaktadır. Bunu yalnızca exec fonksiyonlarının p'li versiyonları yapmaktadır. Exec fonksiyonlarının p'li versiyonları PATH dizinlerinin birinde dosyayı sistem fonksiyonuyla (execve) çalıştırmaya çalıştığında EACCESS errno değeri ile başarısız olurlarsa dosyayı sonraki PATH dizinlerinde aramaya devam ederler. Ancak bu arama sırasında bu fonksiyonlar artık dosyayı diğer PATH dizinlerinde bulamazlarsa EACCESS errno değeri ile başarısız olurlar. Aşağıda execlp fonksiyonuna bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; printf("sample running...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execlp("ls", "ls", "-l", (char *)0) == -1) exit_sys("execv"); printf("ok, parent continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda execvp kullanımına örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { pid_t pid; if (argc == 1) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } printf("sample running...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execvp(argv[1], &argv[1]) == -1) exit_sys("execv"); printf("ok, parent continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Şimdi kabuk üzerinden programları neden "./sample" biçiminde çalıştırdığımız artık anlaşılabilir. Kabuk programları önce fork yapıp alt proseste exec fonksiyonlarının p'li versiyonlarıyla programları çalıştırmaktadır. Dolayısıyla biz programı "sample" biçiminde çalıştırmak istediğimizde bu p'li versiyonlar bu programı PATH çevre değişkeninin belirttiği dizinlerinde bulamayacaktır. Ancak biz programı "./sample" biçiminde çalıştırmak istediğimizde bu fonksiyonlar artık PATH çevre değişkenine bakmayacaktır. Pekiyi kabuk programları neden exec fonksiyonlarının p'li versiyonlarını kullanmaktadır? Bunun birinci sebebi kolaylık sağlamak içindir. Örneğin "ls" komutunu biz "/bin/ls" biçiminde kullanmak istemeyiz. Bunun ikinci sebebi güvenliktir. Eskiden durum böyle değilken programın çalışma dizinine gerçek komutlarla aynı isimli komutlar yerleştirerek hileli işlemler yapmaya yaltenenler olmuştur. İşte bu nedenle PATH dizinlerinin içerisinde prosesin çalışma dizini de yerleştirilmez. Eğer durum böyle olmasaydı bazen hatalı yazılmış komutlarla istenmeden başka programlar da çalıştırılabilirdi. Örneğin dizinimizde "co" diye bir program olsun biz "cp" yerine yanlışlıkla "co" yazarsak bu programımızı istemeden de çalıştırabiliriz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 29. Ders 05/02/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Daha önce yapmış olduğumuz myshell kabuk programına fork/exec işlemini ekleyelim. Programın bu versiyonu önce "internal" komutlara bakacak, eğer internal komutlarda verilen komutu bulmazsa onu fork/exec ile program dosyası gibi çalıştıracaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 typedef struct tagCMD { char *name; void (*proc)(void); } CMD; void parse_cmd_line(char *cmdline); void cd_proc(void); void exit_sys(const char *msg); char *g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[PATH_MAX]; CMD g_cmds[] = { {"cd", cd_proc}, {NULL, NULL} }; int main(void) { char cmdline[MAX_CMD_LINE]; char *str; int i; pid_t pid; if (getcwd(g_cwd, PATH_MAX) == NULL) exit_sys("fatal error (getcwd)"); for (;;) { printf("CSD:%s>", g_cwd); if (fgets(cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(cmdline); if (g_nparams == 0) continue; if (!strcmp(g_params[0], "exit")) break; for (i = 0; g_cmds[i].name != NULL; ++i) if (!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if (g_cmds[i].name == NULL) { if ((pid = fork()) == -1) { perror("fork"); continue; } if (pid == 0 && execvp(g_params[0], &g_params[0]) == -1){ fprintf(stderr, "command not found or cannot execute!\n"); continue; } if (wait(NULL) == -1) exit_sys("wait"); } } return 0; } void parse_cmd_line(char *cmdline) { char *str; g_nparams = 0; for (str = strtok(cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) g_params[g_nparams++] = str; g_params[g_nparams] = NULL; } void cd_proc(void) { char *dir; if (g_nparams > 2) { printf("too many arguments!\n"); return; } if (g_nparams == 1) { if ((dir = getenv("HOME")) == NULL) exit_sys("fatal error (getenv"); } else dir = g_params[1]; if (chdir(dir) == -1) { printf("%s\n", strerror(errno)); return; } if (getcwd(g_cwd, PATH_MAX) == NULL) exit_sys("fatal error (getcwd)"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- exec fonksiyonlarının başarısızlığının nedeni olabilecek çeşitli errno değerleri vardır. Bunların en önemlilerinden birkaçı şunlardır: ENOENT (No such file or directory): Dosya bulunamamıştır. EACCESS (Permission denied): Dosya bulunmuştur ancak proses dosyaya "x" hakkına sahip değildir. ENOEXEC (Exec format error): Dosya bulunmuştur. Prosesin dosyaya "x" hakkı da vardır. Ancak dosyanın formatı çalıştırmaya uygun değildir. Yani dosya çalıştırılabilir bir dosya değildir ya da dosyanın başında "shebang" yoktur. EINVAL (Invalid argument): Dosya bulunmuştur, proses dosyaya "x" hakkına sahiptir. Ancak dosya bu sistem tarafından desteklenen "çalıştırılabilir (executable)" bir formata sahip değildir. Yukarıdada belirttiğimiz gibi exec fonksiyonlarının p'li versiyonları (execlp ve execvp) PATH dizinlerinde tek tek dosyayı aramaktadır. Ancak bu fonksiyonlar dosyayı bir dizinde bulduğunda ve onu sistem fonksiyonuyla (execve) çalıştırmaya çalıştığında EINVAL ve ENOEXEC errno değerini alırsa dosyanın bir kabuk dosyası olduğundan çalıştırılamadığı sonucunu çıkartmaktadır ve bu durumda dosyayı "/bin/sh (default shell)" programı ile çalıştırmaktadır. Ancak exec fonksiyonlarının diğer versiyonları EINVAL ve ENOEXEC hatalarında bunu yapmamaktadır. Bunu yalnızca exec fonksiyonlarının p'li versiyonları yapmaktadır. exec fonksiyonlarının p'li versiyonları PATH dizinlerinin birinde dosyayı sistem fonksiyonuyla (execve) çalıştırmaya çalıştığında EACCESS errno değeri ile başarısız olurlarsa dosyayı sonraki PATH dizinlerinde aramaya devam ederler. Ancak bu arama sırasında bu fonksiyonlar artık dosyayı diğer PATH dizinlerinde bulamazlarsa EACCESS errn değeri ile başarısız olurlar. Bu davranışın anlamı izleyen bölümlerde başka paragraflarda açıklanacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- exec fonksiyonlarının iki tane e'li biçimleri vardır: execle ve execve. Buradaki "e" harfi "environment" yani çevre değişkenleri anlamında isme eklenmiştir. Anımsanacağı gibi çevre değişkenleri tipik olarak prosesin bellek alanında bulunduruluyordu ve fork işlemi sırasında üst prosesin bellek alanının alt prosese kopyalanmasıyla alt prosese geçiriliyordu. Ancak exec işlemleri prosesin bellek alanını ortadan kaldırıp yeni bir program kodunu yüklediğine göre prosesin çevre değişkenleri ne olacaktır? İşte exec işlemi sırasında prosesin bellek alanı boşaltılıp yeni program için prosesin bellek alanı yeniden oluşturulurken çevre değişkenleri de sıfırdan oluşturulabilmektedir. Bunu exec fonksiyonlarının e'li versiyonları yapmaktadır. exec fonksiyonlarının e'siz versiyonları o andaki prosesin çevre değişkenlerinin aynısını exec yapılan programın bellek alanına taşımaktadır. Yani biz exec fonksiyonlarının e'siz versiyonlarını kullandığımızda exec yapmadan önceki çevre değişkenleriyle exec yapıldıktan sonraki programın çevre değişkenleri aynı olacaktı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[]); execle fonksiyonun birinci parametresi yine çalıştırılacak dosyanın yol ifadesini almaktadır. Diğer parametreler programa geçirilecek komut satırı argümanlarını belirtir. Bu argüman listesinin sonu yine NULL adresle bitirilmelidir. Bu NULL adresten sonra son parametre char türden bir gösterici dizisi olmalıdır. Bu gösterici dizisi çevre değişkenlerini "anahtar=değer" biçiminde tutan yazıların başlangıç adreslerinden oluşmalıdır (yani environ global değişkeninde olduğu gibi). Bu fonksiyonlardaki çevre değişkenleri için oluşturulan gösterici dizilerinin sonunda NULL adres olmalıdır. execve fonksiyonu da benzerdir. Bu fonksiyon da önce çalıştırılacak programın yol ifadesini alır. Sonra komut satırı argümanlarını bir gösterici dizisi olarak sonra da çevre değişkenlerini bir gösterici dizisi olarak almaktadır. Aşağıdaki execve fonksiyonunun kullanımına bir örnek verilmiştir. Burada komut satırı argümanlarıyla verilen program execve fonksiyonu ile çalıştırmaktadır. execve için çevre değişken listesi bir gösterici dizisi biçiminde oluşturulmuştur. mample programı içerisinde hem komut satırı argümanları hem de prosesin çevre değişkenleri yazdırılmıştır. Testi şöyle yapabilirsiniz: ./sample mample ali veli selami Şöyle bir çıktı elde edeceksiniz: sample running... ok, parent continues... mample running... command line arguments: mample ali veli selami Envirionment variables: city=eskisehir PATH=/bin:/usr/bin name=ali ---------------------------------------------------------------------------------------------------------------------------*/ /* sample.c */ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { pid_t pid; char *env[] = {"city=eskisehir", "PATH=/bin:/usr/bin", "name=ali", NULL}; if (argc == 1) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } printf("sample running...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execve(argv[1], &argv[1], env) == -1) exit_sys("execve"); printf("ok, parent continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); 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("mample running...\n\n"); printf("command line arguments:\n\n"); for (int i = 0; i < argc; ++i) puts(argv[i]); printf("\nEnvirionment variables:\n\n"); for (int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki execle fonksiyonunun kullanımına bir örnek verilmiştir. execle fonksiyonunda komut satırı argümanlarından sonra çevre değişkenlerini belirten gösterici dizisi argüman olarak geçilmelidir. ---------------------------------------------------------------------------------------------------------------------------*/ /* sample.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; char *env[] = {"city=eskisehir", "PATH=/bin:/usr/bin", "name=ali", NULL}; printf("sample running...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execle("mample", "mample", "ali", "veli", "selami", (char *)0, env) == -1) exit_sys("exece"); printf("ok, parent continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); 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("mample running...\n\n"); printf("command line arguments:\n\n"); for (int i = 0; i < argc; ++i) puts(argv[i]); printf("\nEnvirionment variables:\n\n"); for (int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirtildiği gibi UNIX türevi sistemlerde yalnızca execve fonksiyonu sistem fonksiyonu olarak işletim sistemi içerisinde bulunmaktadır. Aslında execl, execlp, execv, execvp, execle fonksiyonları, execve fonksiyonunu çağıracak biçimde birer kütüphane fonksiyonu biçiminde bulundurulmaktadır. Yani burada "taban (base)" fonksiyon execve fonksiyonudur. Aşağıda execv fonksiyonunun execve kullanılarak basit biçimde yazımına örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); extern char **environ; int myexecv(const char *path, char * const *argv) { return execve(path, argv, environ); } int main(int argc, char *argv[]) { pid_t pid; if (argc == 1) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } printf("sample running...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && myexecv(argv[1], &argv[1]) == -1) exit_sys("myexecv"); printf("ok, parent continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte de execl fonksiyonunun execve kullanılarak nasıl yazıldığı hakkında bir fikir verilmiştir. Burada komut satırı argümanlarının sayısı MAX_ARGS ile sınırlandırılmıştır. Değişken sayıda argüman alan fonksiyonların yazımını inceleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* sample.c */ #include #include #include #include #include #define MAX_ARG 4096 void exit_sys(const char *msg); extern char **environ; int myexecl(const char *path, const char *arg0, ...) { va_list vl; char *args[MAX_ARG + 1]; char *arg; int i; va_start(vl, arg0); args[0] = (char *)arg0; for (i = 1; (arg = va_arg(vl, char *)) != NULL && i < MAX_ARG; ++i) args[i] = arg; args[i] = NULL; va_end(vl); return execve(path, args, environ); } int main(int argc, char *argv[]) { pid_t pid; if (argc == 1) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } printf("sample running...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && myexecl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1) exit_sys("myexecl"); printf("ok, parent continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); 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("mample running...\n\n"); printf("command line arguments:\n\n"); for (int i = 0; i < argc; ++i) puts(argv[i]); printf("\nEnvirionment variables:\n\n"); for (int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- fexecve isimli POSIX fonksiyonu execve fonksiyonu gibidir. Ancak bunun tek farkı dosyayı yol ifadesini alarak değil dosya betimleyicisini alarak çalıştırmasıdır. Yani biz çalıştırmak istediğimiz dosyayı zaten open fonksiyonu ile açmışsak bu durumda doğrudan fexecve fonksiyonunu kullanabiliriz. Fonksiyonun prototipi şöyledir: #include int fexecve(int fd, char *const argv[], char *const envp[]); Fonksiyonun birinci parametresi çalıştırılacak dosyanın dosya betimleyicisini almaktadır. Diğer parametreler execve fonksiyonu ile tamamen aynıdır. Bu fonksiyonun birinci parameresinde belirtilen betimleyiciye ilişkin dosya hangi modda açılmış olmalıdır? POSIX standartlarında olan ancak Linux tarafından desteklenmeyen O_EXEC bayrağı bunun için kullanılabilir. (O_EXEC bayrağında dosyanın "x" hakkına sahip olup olmadığına bakılmaktadır.) Ancak POSIX standartlarında da dosyanın O_RDONLY modunda açılabileceği belirtilmiştir. Yani dosya bu modlarda da açılmış olabilir. Linux sistemlerinde O_EXEC bayrağı olmasa da benzer amaçlarla kullanılan standart olmayan O_PATH bayrağı bulunmaktadır. Linux sistemlerinde bu dosya O_PATH bayrağı ile de açılmış olabilir. O halde Linux sistemleri de göz önüne alındığında buradaki dosyanın taşınabilirlik bakımından O_RDONLY modda açılması uygun olmaktadır. Programcı bu dosyayı fork işleminden sonra alt proseste exec işleminden önce açabilir. Bu durumda dosyanın alt proseste kapatılması işlemi exec sırasında otomatik yapılmaktadır. Yani exec edilen kodda artık bu dosya açık görülmeyecektir. Aşağıda fexecve fonksiyonun kullanımına bir örnek verilmiştir. Burada dosya alt proseste açılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); extern char **environ; int main(int argc, char *argv[]) { pid_t pid; int fd; if (argc == 1) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } printf("sample running...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { if ((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); if (fexecve(fd, &argv[1], environ) == -1) exit_sys("fexecve"); } printf("ok, parent continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- exec işlemi yapıldığında o ana kadar açık olan dosyaların akibeti ne olacaktır? Anımsanacağı gibi açık dosyaların dosya nesnelerinin adresleri "dosya betimleyici tablosu" denilen bir tabloda tutuluyordu. Örneğin bir program 100 tane dosya açıp sonra exec işlemi uygulasa yeni çalıştırılacak kod bu 100 dosyanın farkında olmayacaktır. Ancak dosya betimleyici tablosunda bu 100 betimleyici çoğu kez gereksiz bir biçimde bulunmaya devam edecektir. İşte UNIX/Linux sistemlerinde her açık dosya için "close on exec" isminde bir bayrak da tutulmaktadır. Eğer bu bayrak "set" edilmişse bu durumda exec işlemi sırasında bu dosya işletim sistemi tarafından otomatik olarak kapatılır. Eğer bu bayrak "reset" durumdaysa bu durumda exec işlemi sırasında dosya kapatılmaz, exec yapılan program kodu dosyanın betimleyicisini bilirse onu kullanmaya devam edebilir. Bu bayrak default olarak "reset" durumdadır. open fonksiyonuyla dosya açılırken açış modunda O_CLOEXEC bayrağı belirtilirse bu bayrak set edilmiş olur. Örneğin: fd = open("test.txt", O_RDONLY|O_CLOEXEC); Programcı isterse herhangi bir zaman fcntl fonksiyonu ile de bu bayrağı set ya da reset edebilir. Biz bu fcntl fonksiyonunu henüz görmedik. Ancak bu bayrağın set edilmesi işlem şöyle yapılabilmektedir: if (fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC)) == -1) exit_sys("fcntl"); Benzer biçimde bu bayrak şöyle de reset edilebilir: if (fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) & ~FD_CLOEXEC)) == -1) exit_sys("fcntl"); Close on exec bayrağı dosya nesnesinin içerisinde tutulmamaktadır. Aynı dosya nesnesini gösteren farklı betimleyiciler olabilir. Bu betimleyicilerden birinin close on exec bayrağı set edilmişken diğerinin set edilmemiş olabilir. Yani close on exec bayrağı dosya nesnesinin içerisinde değil proses kontrol blok içerisinde başka bir yerdedir. Aşağıdaki örnekte "sample" programı execl ile "mample" programını çalıştırmıştır. Ancak "mample" programı "sample" programının açmış olduğu dosyanın betimleyici numarasını bilmediği için "sample" programı komut satırı argümanıyla bu bilgiyi "mample" programına iletmiştir. Aşağıdaki programı daha sonra dosyanın close-on-exec bayrağı set set ederek yeniden deneyiniz: if ((fd = open("sample.c", O_RDONLY|O_CLOEXEC)) == -1) exit_sys("open"); Tabii aynı işlem şöyle de yapılabilirdi: if ((fd = open("sample.c", O_RDONLY)) == -1) exit_sys("open"); if (fcntl(fd, F_SETFD, fcntl(fd, F_GETFD)|FD_CLOEXEC) == -1) exit_sys("fcntl"); close on exec bayrağı bazı işlemler sırasında işletim sistemi tarafından set ya da reset edilebilmektedir. Örneğin dup ve dup2 fonksiyonları ile dosya betimleyicisinin kopyası çıkartılırken her zaman yeni betimleyicinin close on exec bayrağı reset durumda olur. ---------------------------------------------------------------------------------------------------------------------------*/ /* sample.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { pid_t pid; int fd; char fd_str[64]; if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); if ((pid = fork()) == -1) exit_sys("fork"); sprintf(fd_str, "%d", fd); if (pid == 0 && execl("mample", "mample", fd_str, (char *)0) == -1) exit_sys("execve"); printf("parent process continues...\n"); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include #include #define BUFFER_SIZE 4096 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); } fd = atoi(argv[1]); lseek(fd, 0, SEEK_SET); 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); } /*-------------------------------------------------------------------------------------------------------------------------- 30. Ders 18/02/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- exec fonksiyonları ile script dosyaları da (yani text dosyalar da) çalıştırılabilmektedir. Bu özellik tamamen çekirdekte bulunan sistem fonksiyonları (Linux'ta execve) tarafından sağlanmaktadır. exec fonksiyonları (aslında Linux'ta execve sistem fonksiyonu) eğer çalıştırılmak istenen dosya "çalıştırılabilir bir dosya değilse (örneğin Linux'ta ELF formatı ya da a.out formatı değilse)" bu dosyanın birinci satırını okuyarak onunla özel bir işlem yapmaktadır. Çalıştırılabilir formata sahip olmayan bir dosyanın (tipik olarak bir text dosya) birinci satırı aşağıdaki gibi ise exec fonksiyonları burada özel bir işlem uygulamaktadır: #! [optional SPACE'ler] [isteğe bağlı argüman(lar)] Burada #! karakterlerine genellikle "shebang" denilmektedir. Bu karaktrerler hemen satırın başında olmak zorunadadır. Shebang karakterlerinden sonra isteğe bağlı bir ya da birden fazla SPACE karakteri bulundurulabilmektedir. Bundan sonra gerçekten çalıştırılacak olan "çalıştırılabilir bir dosyanın" mutlak yol ifadesi olmalıdır. Bunu isteğe bağlı argümanlar izleyebilir. Örneğin: #! /bin/bash #!/bin/bash #!/usr/bin/python #!make -f exec işlemini yapan sistem fonksiyonları eğer exec yapılmak istenen dosya çalıştırılabilir bir dosya değilse (muhtemelen bir text dosya) onun birinci satırını okuyarak orada belirtilen çalıştırılabilir dosyayı çalıştırmaktadır. Ancak exec fonksiyonlarının bu işlemi yapabilmesi için exec yapılan dosyanın yine de (text dosyası olmasına karşın) "x" hakkına sahip olması gerekmektedir. Aksi takdirde exec fonksiyonları başarısız olur ve yine errno değeri EACCESS biçiminde set edilir. Yukarıdaki biçimde biz bir script dosyasını çalıştırmaya çalıştığımızda aslında asıl çalıştırılan dosya shebang'te belirtilen dosya olmaktadır. Pekiyi shebang'te belirtilen dosya çalıştırılırken ona komut satırı argümanları olarak ne geçirilecektir? Shebang'te belirtilen programın çalıştırılması sırasında bu programa geçirilen komut satır argümanları şöyledir: argv[0] ---> shebang'te belirtilen program dosyasına ilişkin yol ifadesi argv[1] ---> Eğer shebang'te çalıştırılabilen programın yanında isteğe bağlı argüman varsa o argüman argv[2] ---> exec fonksiyonunda belirtilen çalıştırılabilir olmayan dosyanın (yani script dosyasının) yol ifadesi argv[3] ve sonrası ---> exec fonksiyonunda belirtilen komut satırı argümanları ancak ilk argüman dahil değil Eğer shebang'in yanındaki programın yanında isteğe bağlı argüman verilmemişse bu durumda shebang'te belirtilen programın komut satırı argümanları şöyle olacaktır: argv[0] ---> shebang'te belirtilen program dosyasına ilişkin yol ifadesi argv[1] ---> exec fonksiyonunda belirtilen çalıştırılabilir olmayan dosyanın (yani script dosyasının) yol ifadesi argv[2] ve sonrası ---> exec fonksiyonunda belirtilen komut satırı argümanları ancak ilk argüman dahil değil Burada dikkat edilmesi gereken bir nokta şudur: exec fonksiyonunda belirtilen argv[0] için girilen argüman shebang'te belirtilen programa aktarılmamaktadır. Şimdi çeşitli denemelerle argüman aktarımını anlamaya çalışalım. "test.txt" şöyle olsun: #! /home/kaan/Study/Unix-Linux-SysProg/mample ankara Burada görüldüğü gibi shebang'te belirtilen programın yanında bir isteğe bağlı argüman vardır. Şimdi "mample" programının da şöyle yazılmış olduğunu kabul edelim: #include int main(int argc, char *argv[]) { printf("mample running...\n"); for (int i = 0; i < argc; ++i) puts(argv[i]); return 0; } Şimdi aşağıdaki gibi exec yapılmış olsun: execl("test.txt", "test.txt", "ali", "veli", "selami", (char *)0); Ekranda şunları görmeliyiz: mample running... /home/kaan/Study/Unix-Linux-SysProg/mample ankara test.txt ali veli selami exec işlemi şöyle yapılmış olsun: execl("test.txt", "ali", "veli", "selami", (char *)0); Ekrana şunlar çıkacaktır: mample running... /home/kaan/Study/Unix-Linux-SysProg/mample ankara test.txt veli selam Şimdi de shebang satırı şöyle olsun: #!/home/kaan/Study/Unix-Linux-SysProg/mample Görüldüğü gibi burada arık shebang'te belirtilen programın yanında isteğe bağlı argüman yoktur. Şimdi exec işlemini şöyle yapmış olalım: execl("test.txt", "test.txt", "ali", "veli", "selami", (char *)0); mample running... /home/kaan/Study/Unix-Linux-SysProg/mample test.txt ali veli selami Sistemlerde genellikle shebang satırları için maksimum bir uzunluk belirlenmiş olmaktadır. Örneğin eski Linux sistemlerinde eğer shebang satırı uzunsa kernel bunun ilk 127 karakterini dikkate almaktadır. Ancak Linux'ta 5.1 kernel'ı ile birlikte bu uzunluk 255'e yükseltilmiştir. Shebang'te belirtilen çalıştırılabilir program genellikle "mutlak yol ifadesi" ile belirtilmektedir. Ancak Linux'ta buradaki program "göreli yol ifadesi" ile de belirtilebilmektedir. Örneğin: #!mample Bu durumda burada belirtilen program exec işlemini yapan prosesin çalışma dizini temel alınarak aranmaktadır. Shebang'te belirtilen programın yanına birden fazla argüman yazabilir miyiz? Örneğin: #! /home/kaan/Study/Unix-Linux-SysProg/mample ankara izmir istanbul Maalesef bu durumda UNIX türevi sistemler arasında bazı farklılıklar söz konusu olmaktadır. Bu durum POSIX standartlarında açık biçimde belirtilmemiş ve işletim sistemini yazanların isteğine bırakılmıştır. Linux ve pek çok sistem bu durumda shebang'te belirtilen programın sağındaki türm argümanları tek bir argümanmış gibi aktarmaktadır. "test.txt" dosyasının yukarıdaki gibi olduğunu varsayalım. Bu dosya aşağıdaki gibi exec yapılmış olsun: execl("test.txt", "test.txt", "ali", "veli", "selami", (char *)0); Linux sistemlerinde aşağıdaki gibi bir çıktı elde edilmiştir: mample running... /home/kaan/Study/Unix-Linux-SysProg/mample ankara istanbul izmir test.txt ali veli selami Bazı UNIX türevi sistemler bu durumda yalnızca boşlukla ayrılmış ilk argümanı (örneğimizde "ankara") programa aktarıp diğerlerini ihmal edebilmektedir. Bu durumda programcının taşınabilirliği sağlamak için shenag dosyasının yanına tek bir argüman yerleştirmesi uygun olmaktadır. Tabii biz bir script dosyasını doğrudan kabuk üzerinden de çalıştırabiliriz. Fark eden bir şey yoktur. Bu durumda zaten kabuk exec işlemini uygulamaktadır. Örneğin "test.txt" dosyası şöyle olsun: #! /home/kaan/Study/Unix-Linux-SysProg/mample ankara Şimdi bunu kabuk üzerinden çalıştıralım: ./test.txt ali veli selami mample running... /home/kaan/Study/Unix-Linux-SysProg/mample ankara ./test.txt ali veli selami Görüldüğü gibi burada exec işlemini kabuk uygulamıştır. Kabuk exec uygularken dosya ismini yine exec'te ilk komut satırı argümanı olarak kullanır. Ancak exec bunu shebang'te belirtilen programa aktarmamaktadır. Aşağıda shebang programına parametre aktarımının test edilmesi için bir örnek verilmiştir. Burada script (test.txt) programına chmod ile "x" hakkı vermeyi unutmayınız. ---------------------------------------------------------------------------------------------------------------------------*/ /* sample.c */ #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) { if (execl("test.txt", "test.txt", "ali", "veli", "selami", (char *)0) == -1) exit_sys("execl"); /* Unreachable code */ } if (wait(NULL) == -1) exit_sys("wait"); 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) puts(argv[i]); return 0; } /* test.txt */ #! /home/kaan/Study/Unix-Linux-SysProg/mample ankara /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi bütün bunların anlamı nedir? Yani shebang ile bir script dosyasının aslında başka bir programı çalıştırmasının ne faydası olabilir? Bu mekanizma sayesinde yorumlayıcı yoluyla çalıştırılan dosyaların doğrudan çalıştırılabilmesine olanak sağlanmaktadır. Örneğin aşağıdaki gibi "sample.sh" isimli bir bash script dosyası olsun: #!/bin/bash for i in {1..10} do echo $i done Bu program 1'den 10'a kadar sayıları ekrana yazdırmaktadır. Normalolarakbir bash programı aşağıdaki gibi çalıştırılabilir: /bin/bash sample.sh Burada "sample.sh" dosyasının "x" hakkına sahip olması gerekmez. Ancak biz dosyası doğrudan aşağıdaki gibi çalıştırmak isteyebiliriz: ./sample.sh Bu durumda dosyanın "x" hakkına sahip olması gerekir. Dosyayı böyle çalıştırmak istediğimizde kabuk program exec işlemi uygulayıp "sample.sh" programını çalıştırmak isteyecektir. Sistem fonksiyonu da "sample.sh" programının çalıştırılabilir bir dosya formatına sahip olmadığını anladığında shebang satırına bakıp orada belirtilen "/bin/bash" programını çalıştıracaktır. Ancak bu programa scipt dosyasının kendisini argüman olarak geçircektir. Yani program adeta şöyle çalıştırılmış olacaktır: /bin/bash sample.sh Pekiyi /bin/bash programı buradaki "sample.sh" programını çalıştırırken onun başında shebang satırı bir soruna yol açmayacak mı? İşte script dillerinin hemen hepsinde # özellikle bu shebang kullanımını desteklemek içn yorum satırı biçiminde ele alınmaktadır. Aynı durum python, perl, sed, awk gibi dillerde de böyledir. Şimdi bir Python programını shebang ile çalıştıralım. Programın ismi "sample.py" olsun: #!/usr/bin/python3 for i in range(10): print(i) Bu dosyaya "x" vererek biz artık onu komut satırından çalıştırabiliriz: ./sample.py Aşağıdaki örnekte ekrana 1'den 10'a kadar sayıları yazan "sample.py" isimli bir python programı verilmiştir. Bu program shebang içerdiğinden dolayı komut satırında "x" verilmişse doğrudan aşağıdaki gibi çalıştırılabilmektedir: ./sample.py ---------------------------------------------------------------------------------------------------------------------------*/ #!/usr/bin/python3 for i in range(10): print(i) /*-------------------------------------------------------------------------------------------------------------------------- Bazen shebang yanında isteğe bağlı argüman gerekebilmektedir. Örneğin make programına bir dosyayı verebilmek için -f seçeneğinin kullanılması gerekir. O halde make için shebang oluşturulurken -f seçeneğinin shebang satırında isteğe bağlı argüman olarak belirtilmesi gerekir. Örneğin: #! /bin/make -f sample: sample.o gcc -o sample sample.o sample.o: sample.c gcc -c sample.c clean: rm -f *.o rm -f sample Burada dosyanın "sample.mak" isminde olduğunu düşünelim. Bu dosyaya "x" hakkını verdikten sonra onu aşağıdaki gibi çalıştırmış olalım: ./sample.mak Bu çalıştırma aslında aşağıdakiyle eşdeğer olacaktır: /bin/make -f sample.mak ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Shebang satırında bazı şeylere dikkat etmek gerekir. Örneğin shebang karakterlerinin hemen ilk satırın başından başlatılması gerekir. Aksi taktirde exec fonksiyonları ENOEXEC ile başarısız olacaktır. Eğer shebang karakterlerinin yanındaki dosya bulunamazsa bu durumda exec fonksiyonları ENOENT ile başarısız olur. Eğer dosyanın ilk satırında shebang yoksa yine bu fonksiyonlar errno değerini ENOEXEC ile set ederek başarısız olmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- exec fonksiyonlarının p'li versiyonları (yani execlp ve execvp) özel bir davranışa sahiptir. Bilindiği gibi bu fonksiyonlar PATH çevre değişkeninde belirtilen dizinlerde exec yapılan dosyayı tek tek aramaktadır. Eğer bunlar çalıştırılabilir olmayan dosyayı "x" hakkına sahip olarak bulup ancak dosyanın başında "shebang" görmezlerse sanki dosyanın başında aşağıdaki gibi bir shebang olduğunu varsaymaktadır: #!/bin/sh Buradan şu sonuç çıkmaktadır: exec fonksiyonlarının p'li versiyonları ile bir shell script dosyasını biz başında shebang satırı olmadan da çalıştırabiliriz. Ancak exec fonksiyonlarının p'siz versiyonlarında bunu yapamayız. Öte yandan Linux sistemlerinde zaten execve dışındaki exec fonksiyonlarının sistem fonksiyonu olmadığını anımsayınız. O halde exec fonksiyonlarının p'li versiyonları tamamen user mode'da script dosyasını execve yaptıktan sonra ENOEXEC errno değeri ile fonksiyonun başarısız olduğunu gördüklerinde bu kez "/bin/sh" dosyasını execve ile exec yapmaktadır. Dosya isminin içerisinde "/" karakteri kullanılsa bile exec fonksiyonlarının p'li versiyonlarının davranışı yine bu biçimdedir. exec fonksiyonlarının p'li versiyonlarının bu davranışı POSIX'te eskiden isteğe bağlı bırakılmıştı. Ancak sonra standartlarda bu davranış zorunlu tutulmuştur. Ancak POSIX standartları çalıştırılacak shell programının ne olacağı konusunda bir belirlemede bulunmamıştır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi shebang'te belirtilen dosyanın kendisi de bir script dosyası olabilir mi? Yani bu shebang işlemi özyinelemeli midir? Aslında POSIX standartları bu konuda bir şey söylememiştir. Bu durumda böyle bir işlemin özyinelemeli yapılacağının bir garantisi yoktur. Linux çekirdeği bu tür durumlarda dört kademeye kadar özyineleme yapabilmektedir. Siz de deneme yoluyla bu özyinelemeyi yapıp sonucuna bakabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 31. Ders 18/02/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- system isimli bir standart C fonksiyonu vardır. Bu fonksiyon ilgili sistemdeki kabuk programını (command interpreter) interaktif olmayan modda çalıştırarak bizim verdiğimiz bir kabuk komutunun kabuk tarafından çalıştırılmasını sağlar. Böylece biz kabuk üzerinde yazabildiğimiz tüm komutları bir C programının içerisinde bu yolla çalıştırabiliriz. system fonksiyonunun prototipi şöyledir: #include int system(const char *command); Fonksiyon parametre olarak kabuğa işletilecek komut yazısını almaktadır. Tabii system bir standart C fonksiyonu olduğuna göre yalnızca UNIX/Linux sistemlerinde değil diğer tüm sistemlerde de kullanılabilmektedir. Örneğin system fonksiyonu UNIX/Linux sistemlerinde "/bin/sh" programını çalıştırırken, Windows sistemlerinde "cmd.exe" programını çalıştırmaktadır. Tabii bir sistemde kabuk programı bulunuyor olmak zorunda değildir. Örneğin pek çok gömülü sistemde bir işletim sistemi olmadığı için kabuk programı da yoktur. İşte programcı ilgili sistemde kabuk programının olup olmadığını fonksiyonun parametresine NULL adres geçerek test edebilir. Bu durumda system fonksiyonu eğer ilgili sistemde kabuk programı varsa sıfır dışı bir değere yoksa 0 değerine geri dönmektedir. Tabii programcı Windows, UNIX/Linux ve macOS gibi sistemlerde çalışıyorsa böyle bir kontrol yapmaz. Pek çok sistemde kabuk programları "interaktif olmayan (noninteractive)" bir modda çalıştırılabilmektedir. Örneğin UNIX/Linux kabuk programları "-c" seçeneği ile çalıştırılırsa yalnızca bir komutu çalıştırıp sonlanmaktadır. Örneğin: $ bash -c "ls -l; cat sample.c" Benzer biçimde Windows sistemlerinde de "cmd.exe" kabuk programı /C seçeneği ile benzer biçimde çalıştırılabilmektedir. O halde UNIX/Linux sistemlerinde system fonksiyonu kabuk programını "-c" seçeneği ile fork/exec yoluyla çalıştırmaktadır. Eğer system fonksiyonu fork ya da wait işlemini yapamazsa -1 değeri ile geri dönmektedir. Eğer fork yapıp exec yapamazsa sanki _exit(127) biçiminde oluşturulan ve waitpid fonksiyonu ile elde edilen değere (status) geri dönmektedir. Diğer durumlarda (yani fork ve exec başarılı bir biçimde yapılmışsa) system fonksiyonu çalıştırdığı kabuk pogramının waitpid fonksiyonuyla elde edilen değerine (status) geri dönmektedir. Tabii kabuk programları da interaktif olmayan modda çalıştırılan komutun waitpid fonksiyonu ile elde edilen (status) değere geri dönerler. Örneğin: $ system("ls -l"); Burada system fork/exec ile "/bin/sh" programını -c seçeneği ile çalıştırmaktadır. Tabii kabuk programı da "ls" programını fork/exec ile çalıştıracaktır. (Bazen kabuk programları interaktif modda eğer tek bir komut işletiliyorsa boşuna fork yapmayabilir). Burada kabuk programı aslında "ls" programının waitpid fonksiyonu ile elde edilen değeri ile (status) sonlanmaktadır. Dolayısıyla biz aslında system fonksiyonun geri dönüş değeri olarak çalıştırdığımız "ls" programının waitpid fonksiyonu ile elde edilen (status) değeri elde etmiş oluruz. UNIX/Linux sistemlerinde genel olarak kabuk komutları (yani programları) başarı durumunda exit kod olarak 0 değerini oluşturmaktadır. Pekiyi system fonksiyonunun başarısını nasıl tespit edebiliriz? Biz fonksiyonun geri dönüş değerini -1 ve 127 ile test edebiliriz. Örneğin: result = system("ls -l") ; if (result == -1 || (WIFEXITED(result) && WEXITSTATUS(result) != 0) { fprintf(stderr, "command failed!\n"); exit(EXIT_FAILURE); } Biz kabuk programını system fonksiyonu ile çalıştırırken komutlar arasına ";" koyarak birden fazla komutun çalıştırılmasını sağlayabiliriz. Genel olarak kabuk bu durumda son komutun exit kodunu bize vermektedir. Aslında programcılar genellikle system fonksiyonu için yalnızca -1 kontrolünü yapmaktadır. Yani çalıştırdıkları komutun başarısını kontrol etmemektedir. Örneğin: if (system("any command") == -1) exit_sys("system"); Aşağıda system fonksiyonun kullanımına bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include void exit_sys(const char *msg); int main(void) { int result; if (system("ls -l > test.txt") == -1) exit_sys("system"); printf("Ok\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi system fonksiyonunu nasıl yazabiliriz? Aşağıda buna bir örnek verilmiştir. Ancak aşağıdaki örnekte bazı noktalar henüz kursumuzda o konu anlatılmadığı için ihmal edilmiştir. Bu noktalar şunlardır: - waitpid fonksiyonu sinyalle kesilirse yeniden çalıştırılması gerekir. - Üst prosesin işlemler sırasında SIGCHLD sinyalini, SIGINT ve SIGQUIT sinyallerini bloke etmesi gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int mysystem(const char *command) { pid_t pid; int status; if (command == NULL) return 1; if ((pid = fork()) == -1) return -1; if (pid == 0) { if (execl("/bin/sh", "/bin/sh", "-c", command, (char *)0) == -1) _exit(127); /* unreachable code */ } if (waitpid(pid, &status, 0) == -1) return -1; return status; } int main(void) { int result; result = mysystem("ls"); if (result == -1 || WIFEXITED(result) && WEXITSTATUS(result) != 0) { fprintf(stderr,"command failed!...\n"); exit(EXIT_FAILURE); } printf("Ok\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi mademki system fonksiyonu bizim için zaten fork/exec işlemlerini yapmaktadır, bu durumda örneğin bir programı çalıştırmak için biz fork/exec kullanmak yerine bu işlemi system fonksiyonu ile yapamaz mıyız? Bu sorunun yanıtı genel olarak bu işlemlerin system fonksiyonu ile yapılabileceğidir. Ancak bu konudaki her türlü gereksinimimizi system fonksiyonu karşılayamaz. Örneğin fork işleminden sonra alt proseste ayarlamalar yapıp exec yapmak isteyebiliriz. Ayrıca system fonksiyonu kendi içerisinde kabuk programını çalıştırdığı için daha yavaş ve daha fazla kaynak kullanır durumdadır. Bizim tavsiyemiz bir programı açıkça fork/exec ile çalıştırmak ancak karmaşık işlemleri (örneğin IO redirection, boru vs. gibi) system fonksiyonuyla yapmaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte kabuk programı system fonksiyonu sayesinde sarmalanmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(void) { char cmd[4096]; char *str; for (;;) { printf("CSD>"); fflush(stdout); if (fgets(cmd, 4096, stdin) != NULL) if ((str = strchr(cmd, '\n')) != NULL) *str = '\0'; if (!strcmp(cmd, "exit")) break; if (system(cmd) == -1) perror("system"); } return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar dosyalar için 9 tane erişim bayrağı gördük: S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH. Bu bayraklar dosyanın "rwx rwx rwx" erişim haklarını belirtmektedir. Ancak aslında dosyaların erişim hakları 9 tane değil 12 tanedir. Henüz görmediğimiz üç erişim hakkına "set-user-id", "set-group-id" ve "sticky" hakları denilmektedir. Şimdi dikkatimizi bu üç erişim hakkına çevireceğiz. Bu üç erişim hakkı open fonksiyonunda ya da chmod fonksiyonunda sırasıyla S_ISUID, S_ISGID ve S_ISVTX sembolik sabitleriyle kullanılabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Komut satırında dosyanın set-user-id bayrağını set etmek için u+s seçeneği kullanılabilir. Örneğin: $ ls -l sample -rwxrwxrwx 1 kaan study 17176 Şub 19 11:46 sample $ chmod u+s sample $ ls -l sample -rwsrwxrwx 1 kaan study 17176 Şub 19 11:46 sample Görüldüğü gibi dosyanın set-user-id bayrağı set edildiğinde "x" harfinin yerinde "s" harfi gözükmektedir. Yani eğer dosyanın hem "x" bayrağı hem de set-user-id bayrağı set edilmişse "x" hakkının bulunduğu yerde "s" harfi gözükmektedir. Ancak dosyanın yalnızca set-user-id bayrağı set edilmişse "x" hakkının bulunduğu yerde "S" harfi gözükür. Örneğin: $ ls -l test.txt -rw-rw-rw- 1 kaan study 2087 Şub 19 10:49 test.txt $ chmod u+s test.txt $ ls -l test.txt -rwSrw-rw- 1 kaan study 2087 Şub 19 10:49 test.txt Dosyanın set-grup-id bayrağının set edilmesi de chmod komutunda g+s ile yapılmaktadır. İşlem sonrasında yine grup bilgisinde "x" hakkı yerinde "s" ya da "S" görünür. chmod komutunda +s kullanılırsa bu durumda dosyanın hem set-user-id hem de set-group-id bayrakları set edilir. Dosyanın sticky bayrağını set etmek için chmod komutunda +t kullanılmaktadır. Bu işlem yapıldığında görüntü olarak grup hakkında "x" varsa x hakkının olduğu yerde "t", yoksa orada "T" görülmektedir. Benzer biçinde istersek yine chmod komutunda octal digit'lerle set-user-d, set-group-id ve sticky bitlerini set edebiliriz. Örneğin: $ chmod 4755 sample Burada en soldaki octal digit 4 olduğu için dosyanın set-user-id bayrağı da set edilmiştir. chmod fonksiyonunda yukarıda belirttiğimiz bayrakları kullanarak set-user-id, set-group-id ve stick bayraklarını set edebiliriz. Örneğin biz "sample" programını rwsrwxrwx haline şöyle getirebiliriz: if (chmod("sample", S_IRWXU|S_IRWXG|S_IRWXO|S_ISUID) == -1) exit_sys("chmod"); Pekiyi zaten var olan bir dosyaya bu özellikleri programlama yoluyla nasıl ekleyebiliriz? Bunun için önce dosyanın erişim haklarının stat, lstat ya da fstat fonksiyonuyla elde edilmesi gerekmektedir. Ondan sonra bu erişim haklarına biz S_ISUID, S_ISGID ve S_ISVTX bayraklarını OR işlemiyle ekleyebiliriz. Ancak stat fonksiyonun bize verdiği st_mode değeri dosyanın türünü de içermektedir. Gerçi chmod fonksiyonu bu ekstra bitleri dikkate almamaktadır. Ancak yine de stat yapısının st_mode elemanındaki değeri S_IFMT ile maskelemek daha uygundur. (Stevens "Advanced Programming in the UNIX Environment" kitabında böyle yapmamıştır.) O halde bu işlem şöyle yapılabilir: struct stat finfo; if (stat("sample", &finfo) == -1) exit_sys("stat"); if (chmod("sample", (finfo.st_mode & ~S_IFMT) | S_ISUID) == -1) /* ~S_IMT maskelemesi yapılmasa da genel olarak bir sorun oluşmaz */ exit_sys("chmod"); ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { struct stat finfo; if (stat("sample", &finfo) == -1) exit_sys("stat"); if (chmod("sample", (finfo.st_mode & ~S_IFMT) | S_ISUID) == -1) exit_sys("chmod"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi bir dosyanın set-user-id ve set-group-id bayraklarının set edilmiş olmasının ne anlamı vardır? Öncelikle bu bayrakların yalnızca "çalıştırılabilir dosyalar için" anlamlı olduğunu belirtelim. Yani bu bayraklar tasarımda "çalıştırılabilir dosyalar" için düşünülmüştür. Çalıştırılabilir dosyanın set-user-id bayrağı set edilmişse bu dosya exec yapıldığında prosesin etkin kullanıcı id'si işletim sistemi tarafından dosyanın kullanıcı id'si olacak biçimde değiştirilmektedir. Benzer biçimde çalıştırılabilir dosyanın set-group-id bayrağı set edilmişse bu dosya exec yapıldığında prosesin etkin grup id'si dosyanın grup id'si olarak değiştirilmektedir. Başka bir deyişle örneğin biz set-user-id bayrağı set edilmiş bir programı çalıştırdığımızda artık prosesimiz etkin kullanıcı id'si sanki o dosyanın sahibiymiş gibi olmaktadır. "/bin/passwd" programı ile biz kendi parolamızı değiştebilmekteyiz. Ancak bu program sıradan kullanıcılara yazma hakkı verilmemiş olan "/etc/passwd" ve "/etc/shadow" dosyalarında değişikler yapabilmektedir. İşte biz /bin/passwd programının set-user-id bayrağı set edildiği için onu çalıştırdığımızda sanki program "root" önceliğinde çalışmaktadır. Böylece program bu dosyalarda değişiklik yapabilmektedir. "/bin/passwd" programının erişim hakları şöyledir: -rwsr-xr-x 1 root root 68208 May 28 2020 /bin/passwd Aşağıdaki örnekte "sample" programı "mample" programını exec yaparak çalıştırmıştır. Biz bu örnekte mample programını derledikten sonra chwon komutuyla sudo ile birlikte kullanıcı id'sini ve group id'sini root olarak değiştirdik. Bu mample programı prosesin gerçek ve etkin kullanıcı ve grup id'lerini ekrana isim olarak yazdırmaktadır. Bu deneyi önce mample programının set-user-id bayrağı set edilmeden ve set edildikten sonra yineleyiniz. Aşağıda yapılanlar özetlenmiştir: $ gcc -o sample sample.c $ gcc -o mample mample.c $ ls -l mample -rwxr-xr-x 1 kaan study 17088 Şub 19 13:42 mample $ sudo chown root:root mample $ ls -l mample -rwxr-xr-x 1 root root 17088 Şub 19 13:42 mample $ ./sample Real user id: kaan Effective user id: kaan Real group id: study Effective group id: study $ sudo chmod u+s mample $ ls -l mample -rwsr-xr-x 1 root root 17088 Şub 19 13:42 mample $ ./sample Real user id: kaan Effective user id: root Real group id: study Effective group id: study $ sudo chmod g+s mample $ ls -l mample -rwsr-sr-x 1 root root 17088 Şub 19 13:42 mample $ ./sample Real user id: kaan Effective user id: root Real group id: study Effective group id: root Biz bir programı sudo ile root önceliğinde (etkin proses id'si 0 olacak biçimde) çalıştırıyor olalım. Dosyanın set-user-id bayrağı da set edilmiş olsun. Bu durumda program çalışırken prosesin etkin kullanıcı id'si root değil program dosyasının kullanıcı id'si olacaktır. Yani set-user-id bayrağı set edilmiş olan programların sudo ile root önceliğinde çalıştırılmasının bir anlamı kalmamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* sample.c */ #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 && execl("mample", "mample", (char *)0) == -1) exit_sys("execl"); if (wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { struct passwd *pass; struct group *gr; if ((pass = getpwuid(getuid())) == NULL) exit_sys("getpwuid"); printf("Real user id: %s\n", pass->pw_name); if ((pass = getpwuid(geteuid())) == NULL) exit_sys("getpwuid"); printf("Effective user id: %s\n", pass->pw_name); if ((gr = getgrgid(getgid())) == NULL) exit_sys("getgrgid"); printf("Real group id: %s\n", gr->gr_name); if ((gr = getgrgid(getegid())) == NULL) exit_sys("getgrgid"); printf("Effective group id: %s\n", gr->gr_name); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 32. Ders 25/02/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Normal olarak set-user id ve set-group-d bayrakları çalıştırılabilen dosyalar için söz konusudur. Ancak dizinler için de set-group-d bayrağının bir anlamı vardır. Pek çok UNIX türevi sistemde (Linux da buna dahil) bir dizin'in set-group-id bayrağı set edilirse Linux o dizin içerisinde open fonksiyonuyla (zaten başka yolu yoktur) ya da mkdir fonksiyonu ile bir dosya ya da dizin yaratıldığında dosyanın grup id'sini prosesin etkin grup id'si olarak değil, o dizin'in grup id'si olarak set etmektedir. BSD sistemlerinde zaten default olarak bir dosya ya da dizin yaratıldığında dosyanın ya da dizin'in grup id'si o dosyanındosya ya da dizin'in içinde bulunduğu dizin'in grup id'si olarak set edilir. O halde Linux'ta open fonksiyonu ile bir dosya ya da dizin yaratılırken dosya ya da dizin'in grup id'si eğer o dosyanın içinde bulunduğu dizin'in set-group-id bayrağı set edilmemişse prosesin etkin group id'si olarak eğer set edilmişse dizin'in grup id'si olarak set edilmektedir. Dizinlerin set-user-id bayraklarının set edilmesi benzer bir etkiye yol açmamaktadır. (Bu durum bazı eski UNIX sistemlerinde denenmiştir ancak modern sistemlerde böyle bir semantik yoktur.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi script dosyaları (shebang içeren text dosyalar) için set-user-id ve set-group-id bayrakları set edilebilir mi? Bu işlem ilk zamanlar uygulanmışır. Ancak güvenlik açığı nedeniyle sonra uygulamadan kaldırılmıştır. Bugünkü modern UNIX/Linux sistemleri script dosyalarının set-user-id ve set-group-id bayrakları set edilmiş olsa bile onları dikkate almamaktadır. Buradaki güvenlik açığı ilginç bir biçimde aşağıdaki gibi oluşmaktadır: - Script dosyasının ismi "x.txt" olsun. Eğer bu dosyanın set-user-id ve set-group-id bayrakları dikkate alınsaydı bu durumda exec fonksiyonları bu dosyayı açıp shebang satırında bulunan programı çalıştırırken prosesin etkin kullanıcı ve/veya grup id'sini "x.txt" dosyasının kullanıcı ve/veya grup id'si olarak set ederdi. Bu durumda da eğer birisi örneğin "y.txt" sembolik bağlantı dosyası oluşturup bu dosyanın "x.txt" dosyasını göstermesini sağlarsa ve bu "y.txt" ile exec yaparsa bu durumda aslında exec fonksiyonu sembolik bağlantıyı izleyecek ve "x.txt" dosyasını çalıştıracaktır. Ancak exec fonksiyonları bu "x.txt" dosyasının shebang satırındaki programı çalıştırırken yine komut satırı argümanı olarak "y.txt" dosyasını kullanacaktır. İşte tam bu sırada birisi bu "y.txt" dosyasınının sembolik bağlantısını değiştirirse maalesef prosesin etkin kullanıcı id'si "x.txt"nin kullanıcı id'si olarak buradaki başka dosyayı çalıştırır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi dosyaların sticky bayraklarının ne işlevi vardır? Aslında sticky bayrağı tasarımda başka bir amaçla düşünülmüştür. Eski sistemlerde çalıştırılabilen dosyaların çalıştırılması sonrasında programın bellekten atılmaması gibi bir ip ucu oluşturmaktadır. Ancak modern sistemlerde böyle bir etkinin bir anlamı kalmadığı için sticky bayrağı da ilk tasarlandığı zamanki işlevinden tamamen kopmuştur. Bugün sticky bayrağı değişik sistemlerde değişik amaçlarla kullanılabilmektedir. POSIX standartları eskiden sticky bayrağı üzerinde açıklama yapmıyordu. Ancak belli zamandan sonra sticky için şöyle bir işlevsellik tanımlanmıştır: Bir dizin'in sticky bayrağı set edilirse dizine prosesin yazma hakkı olsa bile dizin içerisindeki başkalarına ait (yani kullanıcı id'si başka) olan dosyalar silinememekte ve ismi değiştirilememektedir. Bugünkü sistemlerde dizin dışında diğer dosyaların sticky bayraklarının set edilmiş olup olmamasının işlevsel bir anlamı yoktur. Örneğin Linux sistemlerinde "/tmp" dizininin sticky bayrağı et edilmişir ve bu dizine yazma hakkı verilmiştir. Bu durumda biz bu dizinde dosya yaratabiliriz, kendi dosyamızı silebiliriz. Ancak başkalarının dosyalarını silemeyiz. "/tmp" dizininin erişim hakları şöyledir: drwxrwxrwt 19 root root 65536 Şub 25 10:05 /tmp ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi bir prosesin "gerçek kullanıcı id'si (real user id)" ile "etkin kullanıcı id'si (effective user id)", "gerçek grup id'si (real group id)" ile de "etkin grup id'si (effective group id)" aynı olmaktadır. Ancak set-user-id ve set-group-id bayrakları set edilmiş çalıştırılabilir programlar çalıştırıldığında bu id'ler farklı hale gelebilmektedir. Örneğin prosesimizin gerçek kullanıcı id'si ve etkin kullanıcı id'si "kaan" olsun. Biz set-user-id bayrağı set edilmiş "/bin/passwd" programını exec yaptığımızda prosesimizin gerçek kullanıcı id'si "kaan" olmaya devam eder ancak etkin kullanıcı id'si "root" olur. Dosya işlemlerinde teste her zaman etkin id'ler sokulmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Gerçek kullanıcı id'si ve gerçek grup id'si, etkin kullanıcı id'si ve etkin grup id'si dışında bir de "saklı kullanıcı id'si (saved set user id)" ve "saklı grup id'si (saved set group id)" denilen iki id daha vardır. Bir proses exec uyguladığında programın set-user-id ve set-group-id bayrakları set edilmiş olsun ya da olmasın her zaman kernel yeni etkin kullanıcı id'sini ve yeni etkin grup id'sini saklı kullanıcı id'si ve saklı grup id'si olarak set etmektedir. Örneğin prosesimizin gerçek kullanıcı id'si "kaan" ve etkin kullanıcı id'si "kaan", gerçek grup id'si "study" ve "etkin grup id'si "study" olsun. Şimdi biz set-user-id bayrağı set edilmiş olan "/bin/passwd" programını exec ile çalıştıralım. Arık prosesimizin gerçek kullanıcı id'si "kaan", etkin kullanıcı id'si "root" olacaktır. Gerçek group id'si "study" ve etkin grup id'si de "study" olarak kalacaktır. İşte kernel aynı zamanda bu yeni etkin kullanıcı ("root" id'sini kastediyoruz) ve grup id'sini prosesin "saklı kullanıcı id'si (saved set user id)" ve "saklı grup id'si" olarak da set etmektedir. O halde prosesimizin id'leri artık şöyle olacaktır: gerçek kullanıcı id'si: kaan etkin kullanıcı id'si: root saklı kullanıcı id'si: root gerçek grup id'si: study etkin grup id'si: study saklı grup id'si: study Bu işlem set-user id ya da set-group-id bayrağı set edilmemiş programlar çalıştırılıken de yürütülmektedir. Örneğin prosesimizin gerçek kullanıcı id'si "kaan", etkin kullanıcı id'si "kaan", gerçek grup id'si "study" ve etkin grup id'si "study" olsun. Biz de set-user-id bayrağı set edilmemiş olan bir programı exec yapmış olalım. Yeni id'ler şöyle olacaktır: gerçek kullanıcı id'si: kaan etkin kullanıcı id'si: kaan saklı kullanıcı id'si: kaan gerçek grup id'si: study etkin grup id'si: study saklı grup id'si: study Tabii saklı kullanıcı id'si ve saklı grup id'si yine porses kontrol bloğu içerisinde (Linux'taki task_struct yapısı) saklanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- O anda çalışmakta olan prosesin (yani kendi prosesimizin) gerçek kullanıcı id'si getuid isimli POSIX fonksiyonuyla, etkin kullanıcı id'si de geteuid isimli POSIX fonksiyonuyla elde edilebilmektedir. Fonksiyonların prototipleri şöyledir: #include uid_t getuid(void) uid_t geteuid(void) uid_t türü ve dosyaları içerisinde bir tamsayı türü olacak biçimde typedef edilmiştir. Bu fonksiyonlar başarısız olamamaktadır. O anda çalışmakta olan prosesin gerçek grup id'si getgid POSIX fonksiyonu ile, etkin grup id'si ise getegid POSIX fonksiyonu ile elde edilebilmektedir. Fonksiyonların prototipleri şöyledir: #include gid_t getgid(void); gid_t getegid(void); Bu fonksiyonlar da başarısız olamamaktadır. POSIX standartlarında saklı id'leri alan fonksiyonlar yoktur. Ancak Linux sistemlerinde bu işlemi yapacak fonksiyon bulunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void exit_sys(const char *msg); int main(void) { uid_t ruid, euid; gid_t rgid, egid; struct passwd *pass; struct group *grp; ruid = getuid(); if ((pass = getpwuid(ruid)) == NULL) exit_sys("getpwuid"); printf("Real User Id: %s (%ju)\n", pass->pw_name, (uintmax_t)ruid); euid = geteuid(); if ((pass = getpwuid(euid)) == NULL) exit_sys("getpwuid"); printf("Effective User Id: %s (%ju)\n", pass->pw_name, (uintmax_t)euid); rgid = getgid(); if ((grp = getgrgid(rgid)) == NULL) exit_sys("getgrgid"); printf("Real Group Id: %s (%ju)\n", grp->gr_name, (uintmax_t)rgid); egid = getegid(); if ((grp = getgrgid(egid)) == NULL) exit_sys("getgrgid"); printf("Effective Group Id: %s (%ju)\n", grp->gr_name, (uintmax_t)egid); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Prosesin kullanıcı ve grup id'lerini set etmek için setuid, setgid, seteuid ve setegid POSIX fonksiyonları bulundurulmuştur. Ancak bu fonksiyonların sematiği kişiler biraz karmaşık gelmektedir. Biz burada bu fonksiyonları tek tek açıklayacağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- setuid fonksiyonunun prototipi şöyledir: #include int setuid(uid_t uid); Her ne kadar fonksiyonun ismi sanki yalnızca prosesin "gerçek kullanıcı id'sini" değişterecek gibi görünüyorsa da aslında fonksiyon şöyle davranmaktadır: 1) Prosesin önceliği uygunsa (appropriate privilege) yani prosesin etkin kullanıcı id'si 0 ise ya da Linux sistemlerinde proses bu işi yapacak yeterliliğe (capability) sahipse (CAP_SETUID) fonksiyon prosesin hem gerçek kullanıcı id'sini, hem etkin kullanıcı id'sini hem de saklı kullanıcı id'sini parametresi ile belirtilen kullanıcı id'si yapar. 2) Eğer prosesin önceliği uygun değilse (yani root değilse ya da Linux'ta CAP_SETUID yeterliliği yoksa) bu durumda setuid fonksiyonu eğer parametresi ile belirtilen uid değeri prosesin gerçek ya da saklı kullanıcı id'si ile aynı ise prosesin yalnızca etkin kullanıcı id'sini parametresi ile belirtilen id olarak değiştirir. Bu koşullar sağlanmıyorsa fonksiyon başarısız olmaktadır. Pekiyi bunun anlamı nedir? Bu işlem set-user-id prgramların kısmen geri daha sonra dönüp yeniden önceliği geri almasını sağlamak için düşünülmüştür. Saklı id'lerin kullanılmasının tek nedeni de budur. Şöyle ki: - Biz kaan prosesi olarak set-user-id bayrağı set edilmiş root programını çalıştırdığımızı düşünelim. Şimdi bizim id'lerimiz şöyle olacaktır: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: root Saklı kullanıcı id'si: root Şimdi biz sonraki paragrafta göreceğimiz seteuid fonksiyonu ile geri dönüp prosesin yetkisini azaltarak onun etikin kullanıcı id'sinin kaan olmasını sağlayalım. Bu durumdaki id'ler şöyle olacaktır: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: kaan Saklı kullanıcı id'si: root İşte şimdi bu noktada biz setuid fonksiyonu ile yeniden root önceliğine dönebiliriz. Çünkü bizim şu anda uygun önceliğimiz olmasa da saklı kullanıcı id'miz hala root durumundadır. Yani programlar önceliği yükse birtakım işlemler yapıp sonra önceliği düşürüp sonra da yüksek önceliğe geri dönmek için saklı id'ler uydurulmuştur. setuid fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner ve errno uygun biçimde set edilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- seteuid fonksiyonu setuid fonksiyonunun yalnızca etkin kullanıcı id'sini set eden biçimidir. Genellikle setuid yerine bu fonksiyon tercih edilmektedir. Fonksiyonun prototipi şöyledir: #include int seteuid(uid_t uid); Fonksiyonun çalışması şöyledir: 1) Prosesin önceliği uygunsa (appropriate privilege) yani prosesin etkin kullanıcı id'si 0 ise ya da proses Linux sistemlerinde bu işi yapacak yeterliliğe (capability) sahipse (CAP_SETUID) fonksiyon prosesin yalnızca etkin kullanıcı id'sini parametresiyle belirtilen id olarak değiştirir. Gerçek kullanıcı id'si ve saklı kullanıcı id'si değiştirilmez. 2) Eğer prosesin önceliği uygun değilse (yani root değilse ya da Linux'ta CAP_SETUID yeterliliği yoksa) bu durumda seteuid fonksiyonu eğer parametresi ile belirtilen uid değeri prosesin gerçek ya da saklı kullanıcı id'si ile aynı ise prosesin yalnızca etkin kullanıcı id'sini parametresi ile belirtilen id olarak değiştirir. Görüldüğü gibi eğer prosesin önceliği uygun değilse zaten setuid fonksiyonu ile seteuid fonksiyonu arasında bir fark kalmamaktadır. Bu durumda geri dönüş senaryosu tam olarak şöyle gerçekleştirilir. Yine prosesimizin id'leri şöyle olsun: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: kaan Saklı kullanıcı id'si: kaan Şimdi biz set-user-id bayrağı set edilmiş bir programı exec yapalım. Id'lerimiz şöyle olacaktır: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: root Saklı kullanıcı id'si: root Şimdi çalıştırdığımız program kaan olarak bazı şeyleri yapmak istesin o zaman şu çağrıyı yapacaktır: seteuid(getuid()); Şimdi prosesin id'leri şöyle olacaktır: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: kaan Saklı kullanıcı id'si: root Burada saklı kullanıcı id'sinin değişmediğine dikkat ediniz. Şimdi proses kaan olarak bazı şeyleri yaptıktan sonra yeniden root olmak istesin: seteuid(0); Tabii aynı işlem setuid(0) ile de yapılabilirdi. Şimdi prosesin id'leri şöyle olacaktır: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: root Saklı kullanıcı id'si: root Eğer saklı kullanıcı id'si diye bir kavram uydurulmuş olmasaydı geri dönüş yapılamazdı. seteuid fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri önmektedir. errno uygun biçimde set edilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- setgid fonksiyonun temel semantiği setuid fonksiyonunda olduğu gibidir. Fonksiyonun prototipi şöyledir: #include int setgid(gid_t gid); Fonksiyon şöyle çalışmaktadır: 1) Prosesin önceliği uygunsa (appropriate privilege) yani prosesin etkin kullanıcı id'si 0 ise ya da Linux sistemlerinde proses bu işi yapacak yeterliliğe (capability) sahipse (CAP_SETGID) fonksiyon prosesin hem gerçek grup id'sini, hem etkin grup id'sini hem de saklı grup id'sini parametresi ile belirtilen grup id'si yapar. 2) Eğer prosesin önceliği uygun değilse (yani root değilse ya da Linux'ta CAP_SETGID yeterliliği yoksa) bu durumda setgid fonksiyonu eğer parametresi ile belirtilen gid değeri prosesin gerçek ya da saklı grup id'si ile aynı ise prosesin yalnızca etkin grup id'sini parametresi ile belirtilen grup id olarak değiştirir. Buradaki amaç tamamen set-group-id bayrağı set edilmiş programların grup id'lerini geri döndürüp yeniden eski değere set edebilmesidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- setegid fonksiyonun temel semantiği de seteuid fonksiyonunda olduğu gibidir. Fonksiyonun prototipi şöyledir: #include int setegid(gid_t gid); Fonksiyon şöyle çalışmaktadır: 1) Prosesin önceliği uygunsa (appropriate privilege) yani prosesin etkin kullanıcı id'si 0 ise ya da Linux sistemlerinde proses bu işi yapacak yeterliliğe (capability) sahipse (CAP_SETGID) fonksiyon prosesin yalnızca etkin grup id'sini parametresiyle belirtilen grup id'i olarak değiştirmektedir. 2) Eğer prosesin önceliği uygun değilse (yani root değilse ya da Linux'ta CAP_SETGID yeterliliği yoksa) bu durumda setegid fonksiyonu eğer parametresi ile belirtilen gid değeri prosesin gerçek ya da saklı grup id'si ile aynı ise prosesin yalnızca etkin grup id'sini parametresi ile belirtilen grup id olarak değiştirir. Yani eğer prosesin önceliği uygun değilse bu durumda setgid ile setegid fonksiyonları arasında bir fark kalmamaktadır. setgid ve setegid fonksiyonları yine grup id bakımından geriye dönüşü sağlamak için düşünülmüştür. Şöyleki, prosesimizin işin başında id'leri şöyle olsun: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: kaan Saklı kullanıcı id'si: kaan Gerçek grup id'si: study Etkin grup id'si: study Saklı grup id'si: study Şimdi biz set-group-id bayrağı set edilmiş bir programı çalıştıralım. Program dosyasının kullanıcı id'si "ali", grup id'si ise "test" olsun. Biz exec yaptığımızda id'lerimiz şöyle olacaktır: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: kaan Saklı kullanıcı id'si: kaan Gerçek grup id'si: study Etkin grup id'si: test Saklı grup id'si: test Şimdi biz grup olarak geçmişe dönüp bazı şeyleri yapmak isteyelim: setegid(getgid()); Tabii burada setgid fonksiyonunu da kullanabilirdik. Şimdi prosesimizin id'leri şöyle olacaktır: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: kaan Saklı kullanıcı id'si: kaan Gerçek grup id'si: study Etkin grup id'si: study Saklı grup id'si: test Şimdi eski grup id'ye yeniden geri dönmek isteyelim: setegid(test_gid); Tabii burada yine setgid fonksiyonu da kullanılabilirdi. Görüldüğü gibi grup temelinde geri dönüp yeniden aynı etkin grup id'ye sahip olabilmek için saklı grup id'den faydalanılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bizim prosesimizin kullanıcı id'leri şöyle olsun: Gerçek kullanıcı id'si: root Etkin kullanıcı id'si: root Saklı kullanıcı id'si: root Şimdi biz aşağıdaki gibi bir çağrı yaparsak artık geri dönüş olanağımız kalmaz: setuid(uid_kaan); Çünkü uygun öceliğe sahip olan program setuid fonksiyonunu uyguladığında yalnızca kullanıcı etkin id'si değil, gerçek kullanıcı id'si ve saklı kullanıcı id'si de değişmektedir. Yani bu işlem sonucunda kullanıcı id'leri şöyle olacaktır: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: kaan Saklı kullanıcı id'si: kaan Ancak biz şu çağrıyı yapmış olsaydık geri dönebilirdik: seteuid(uid_kaan); Şimdi kullanıcı id'leri şöyle olacaktır: Gerçek kullanıcı id'si: root Etkin kullanıcı id'si: kaan Saklı kullanıcı id'si: root Şimdi artık setuid ya da seteuid fonksiyonu ile geri dönüş mümkündür. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki fonksiyonlara ek olarak setreuid ve setregid isimli iki yardımcı fonksiyon da bulundurulmuştur. Aslında bu fonksiyonlara mutlak anlamda gerek yoktur. Ancak bazı işlemleri kolaytırmaktadır. Bu fonksiyonlar ilk kez BSD UNIX sistemlerinde kullanılmış daha sonra POSIX standartlarına dahil edilmiştir. Bu fonksiyonların ana amacı tek hamlede gerçek kullanıcı ve etkin kullanıcı id'lerini, gerçek grup id ve etkin grup id'lerini değiştirebilmektedir. Genellikle gerçek id'lerle etkin id'leri yer değiştirmek amacıyla bu fonksiyon kullanılmaktadır. setreuid fonksiyonunun prototipi şöyledir: #include int setreuid(uid_t ruid, uid_t euid); Fonksiyon parametre olarak değiştirilmek istenen gerçek kullanıcı id'sini ve grup id'sini almaktadır. Eğer bunlardan herhangi biri değiştirilmek istenmiyorsa bu durumda o parametre için -1 girilir. Fonksiyon şöyle çalışmaktadır: 1) Eğer proses uygun önceliğe sahipse (yani root ise ya da Lİnux sistemlerinde CAP_SETUID yeteneğine sahipse) her iki id'yi de değiştirir. 2) Eğer proses uygun önceliğe sahip değilse fonksiyon yalnızca etkin kullanıcı id'sini değiştirir. Ancak bunu yapılabilmesi için prosesin gerçek kullanıcı id'sinin ya da saklı kullanıcı id'sinin argüman olarak geçilen id ile aynı olması gerekmektedir. 3) POSIX standartlarında bu fonksiyon ile gerçek kullanıcı id'sinin etkin kullanıcı id'si ya da saklı kullanıcı id'si biçiminde değiştirilip değiştirilemeyeceği "belirsiz (unspcified)" bırakılmıştır. Linux sistemlerinde setreuid fonksiyonu "prosesin gerçek kullanıcı id'sini etkin kullanıcı id'si olarak" değiştirebilmektedir. Ancak saklı kullanıcı id'si olarak değiştirememektedir. Bu fonksiyon eğer set edilmek istenen etkin kullanıcı id'si (yani hedef etkin kullanıcı id'si) prosesin o andaki gerçek kullanıcı id'sine eşit değilse bu durumda saklı kullanıcı id'sini de etkin kullanıcı id'si olarak set etmektedir. Aynı zamanda fonksiyon yine eğer gerçek kullanıcı id'si set ediliyorsa saklı kullanıcı id'sini yine hedef etkin kullanıcı id'si olarak set etmektedir. Fonksiyonun parametrelerinden biri bile uygunsuzsa fonksiyon başarısız olmaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner ve errno değeri uygun biçimde set edilir. Örneğin: setreuid(getuid(), getuid()); Burada çağrı sonucunda prosesin gerçek kullanıcı id'si değişmeyecektir. Ancak prosesin etkin kullanıcı id'si ve saklı kullanıcı id'si gerçek kullanıcı id'si olarak set edilecektir. Artık geri dönüş mümkün değildir. Örneğin: setreuid(geteuid(), getuid()); Burada prosesin kullanıcı id'si ile etkin kullanıcı id'si yer değiştirilmiştir. Örneğin prosesin çağrı öncesindeki kullanıcı id'leri şöyle olsun: Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: root Saklı kullanıcı id'si: root Şimdi şu çağrıyı yapalım: setreuid(geteuid(), getuid()); Şimdi id'ler şöyle olacaktır: Gerçek kullanıcı id'si: root Etkin kullanıcı id'si: kaan Saklı kullanıcı id'si: root Buradan eski duruma şöyle dönebiliriz: setreuid(geteuid(), getuid()); Gerçek kullanıcı id'si: kaan Etkin kullanıcı id'si: root Saklı kullanıcı id'si: root Tabii yukarıda da belirttiğimiz gibi bu fonksiyonda prosesin uygun önceliği yoksa kullanıcı id'sinin etkin kullanıcı id'si olarak değiştirilip değiştirilemeyeceği POSIX sistemlerinde belirsiz bırakılmıştır. Ancak Linux ve BSD bunu yapabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- setregid fonksiyonunun prototipi şöyledir: #include int setregid(gid_t rgid, gid_t egid); setregid fonksiyonu da setereuid fonksiyonu ile grup temelinde benzer semantiğe sahiptir: 1) Eğer proses uygun önceliğe sahipse (yani root ise ya da Lİnux sistemlerinde CAP_SETGID yeteneğine sahipse) fonksiyon her iki id'yi de değiştirir. 2) Eğer proses uygun önceliğe sahip değilse fonksiyon etkin grup id'sini değiştirir. Ancak bunu yapılabilmesi için prosesin gerçek grup id'sinin ya da saklı grup id'sinin argüman olarak geçilen id ile aynı olması gerekmektedir. 3) Fonksiyon uygun önceliğe sahip değilse gerçek grup id'sini saklı grup id'si olarak değiştirebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi POSIX standartlarında saklı kullanıcı ve grup id'lerini almanın bir yolu yoktur. Ancak Linux sistemleri bir sistem fonksiyonu yoluyla buna izin vermektedir. getresuid ve getresgid fonksiyonları bütün id'leri tek hamlede elde etmeye izin vermektedir. #define _GNU_SOURCE /* See feature_test_macros(7) */ #include int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid); int getresgid(gid_t *rgid, gid_t *egid, gid_t *sgid); Fonksiyonlar ilgili nesnelerin adreslerini alıp değerleri oraya yerleştirmektedir. Başarı durumunda 0, başarısızlık durumunda -1 değerine geri dönmektedir. Aşağıdaki örnekt prosesin gerçek, etkin ve saklı kullanıcı id'leri getresuid fonksiyonuyla elde edilip ekrana yazdırılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #define _GNU_SOURCE #include #include #include #include #include void exit_sys(const char *msg); int main(void) { uid_t ruid, euid, ssuid; struct passwd *pass; if (getresuid(&ruid, &euid, &ssuid) == -1) exit_sys("getresuid"); if ((pass = getpwuid(ruid)) == NULL) exit_sys("getpwuid"); printf("Real User Id: %s (%ju)\n", pass->pw_name, (uintmax_t)ruid); if ((pass = getpwuid(euid)) == NULL) exit_sys("getpwuid"); printf("Effective User Id: %s (%ju)\n", pass->pw_name, (uintmax_t)euid); if ((pass = getpwuid(ssuid)) == NULL) exit_sys("getpwuid"); printf("Saved Set User Id: %s (%ju)\n", pass->pw_name, (uintmax_t)ssuid); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 33. Ders 26/02/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux sistemlerinde getresuid ve getresgid fonksiyonlarının bir de set'li versiyonları vardır. Bu fonksiyonlar da birer sistem fonksiyonu olarak gerçekleştirilmiştir. Bu fonksiyonlar POSIX standartlarında bulunmamaktadır: #define _GNU_SOURCE /* See feature_test_macros(7) */ #include int setresuid(uid_t ruid, uid_t euid, uid_t suid); int setresgid(gid_t rgid, gid_t egid, gid_t sgid); Fonksiyonlar sırasıyla gerçek, etkin ve saklı id'leri alarak proses için set işlemi yapmaktadır. Set işlemi için şu koşullar bulunmaktadır: Uygun önceliğe sahip olmayan prosesler gerçek, etkin ve saklı id'lerini ancak o andaki gerçek, etkin ve saklı id'lerinden biri olarak değiştirebilirler. Uygun önceliğe sahip olan prosesler bunları herhangi bir biçimde değiştirebilirler. Yine fonksiyon herhangi bir parametrede uygunsuz bir durumla karşılaşırsa tüm işlem başarısız olur ve -1 değeri ile geri döner. Başarı durumunda fonksiyon 0 ile geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İlk zamanlar UNIX sistemlerinde prosesler tek bir grup ile ilişkilendirilmişti. Sonra bir prosesin birden fazla grup ile ilişkili olması gerektiği anlaşıldı ve bu durum "ek gruplar (supplementary grorups)" kavramı ile sisteme dahil edilerek gerçekleştirildi. Ek gruplar konusu uzun süredir POSIX standartlarının içerisinde var olan ve neredeyse tüm UNIX türevi sistemlerin desteklediği bir özelliktir. Bir kullanıcının gerçek bir grubu vardır. Dolayısıyla işin başında gerçek grup id'si ve etkin grup id'si bu gruptur. Anımsanacağı gibi dosya erişimlerinde test işlemlerine etkin kullanıcı id'si ve etkin grup id'si girmektedir. Yine anımsanacağı gibi bir dosyanın rwx biçimindeki hangi üçlü kısmının open fonksiyonunda ve diğer fonksiyonlarda dikkate alınacağı şöyle belirleniyordu: if (prosesin etkin kullanıcı id'si == 0) else if (prosesin etkin kullanıcı id'si == dosyanın kullanıcı id'si) else if (prosesin etkin grup id'si ya da ek gruplarından birinin id'si == dosyanın grup id'si) else Buradan görüldüğü gibi grup kontrolü yapılırken yalnızca prosesin etkin grup id'si değil aynı zamanda ek grup id'leri de eşdeğer düzeyde etkili olmaktadır. Yani örneğin prosesimizin etkin grup id'si "study" olsun. Ancak ek grupları da "work" ve "test" olsun. Erişmeye çalıştığımız dosyanın grup id'si "test" ise her ne kadar bizim etkin grup id'miz "test" değilse de ek gruplarımızdan biri "test" olduğu için biz bu dosya ile aynı gruptan proses kabul ediliriz. Pekiyi ek gruplara neden gereksinim duyulmuştur? Bunun gerekçesini şöyle bir örnekle açıklayabiliriz. "ali", "veli" ve "selami" ortak bir proje üzerinde çalışıyor olsunlar. Bunların proje dosyalarına ortak biçimde erişebilmek için aynı gruba dahil olmaları gerekir. Ancak "ali" kullanıcısnın "ayse" ve "fatma" ile başka bir proje üzerinde de çalıştığını varsayalım. Eğer bu ikinci proje grubundaki üyeler de aynı grup id'ye sahip olurlarsa bu durumda bu ikinci proje grubuna dahil olmayanlar da ikinci proje grubunun dosyalarına erişebilecektir. Yani bir kullanıcı birden fazla proje üzerinde çalışacaksa birden fazla gruba üye olabilmelidir. Bir prosesin gerçek kullanıcı ve gerçek grup id'leri login prosesi tarafından "/etc/passwd" dosyasına başvurularak belirlenmektedir. İşte login programı prosesin ek gruplarını da "/etc/group" dosyasına bakarak belirlemektedir. Anımsanacağı gibi "/etc/group" dosyasındaki her satır bir gruba ilişkin bilgileri barındırıyordu. Satırın sonundaki son eleman ise o gruba ek grup olarak dahil olan kullanıcıları belirtmektedir. Örneğin: ... sys:x:3: adm:x:4:syslog,kaan tty:x:5:syslog disk:x:6: ... Burada syslog kullanıcısı hem adm grubuna hem de tty grubuna ek grup olarak üye biçimdedir. kaan kullanıcısı da ek grup olarak adm grubuna üyedir. Tabii login prosesinin bir kullanıcının ek gruplarını belirleyebilmesi içn /etc/group dosyasındaki tüm satırları gözden geçirmesi gerekmektedir. Bu durumda pseudo kod olarak login programı şöyle yazılmıştır: 1) user name ve password iste (bunu terminal programı da yapıyor olabilir) 2) /etc/passwd ve /etc/shadow dosyalarına başvurarak doğrulamayı yap 3) Kullanıcının ek grup'larını tespit etmek için /etc/group dosyasını dolaş ve kullanıcının ek gruplarını elde et 4) setuid fonksiyonuyla prosesin gerçek, etkin ve saklı kullanıcı id'sini /etc/passwd dosyasında belirtilen biçimde set et. 5) setgid fonksiyonuyla prosesin gerçek, etkin ve saklı grup id'sini /etc/passwd dosyasında belirtilen biçimde set et. 6) chdir fonksiyonuyla prosesin çalışma dizinini /etc/passwd dosyasında belirtildiği gibi set et 7) exec ile /etc/passwd dosyasında belirtilen programı çalıştır. Genel olarak login programı fork/exec değil yalnızca exec yapmaktadır. Biz shell'den çıkınca normalde yeniden login programı çalıştırışmaktadır. Tabii prosesin ek grupları da diğer bilgilerde olduğu gibi prosesin kontrol bloğunda saklanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- O anda çalışmakta olan prosesin ek grup id'leri getgroups isimli POSIX fonksiyonu ile elde edilebilmektedir. Fonksiyonun prototipi şöyledir: #include int getgroups(int gidsetsize, gid_t grouplist[]); Fonksiyonun ikinci parametresi ek grup id'lerinin yerleştirileceği gid_t türünden dizini başlangıç adresini belirtir. Birinci parametre ise bu dizinin uzunluğunu belirtmektedir. Fonksiyon başarı durumunda diziye yerleştirdiği eleman sayısına, başarısızlık durumunda -1 değerine geri dönmektedir. Pekiyi biz bu fonksiyona geçireceğimiz dizinin uzunluğunu nasıl belirleyebiliriz? Daha ileride göreceğimiz gibi içerisinde NGROUPS_MAX isimli bir sembolik sabit vardır. Bu sembolik sabit ilgili sistemindeki proseslerin sahip olabileceği maksimum grup sayısını belirtmektedir. Ancak maalesef bu sembolik sabit "Runtime Increasable Values" grubundadır. Yani bu değer sistem açıldıktan sonra artırılmış olabilir. Gerçek değer ise sysconf fonksiyonuyla elde edilmektedir. Ancak sysconf fonksiyonun çağrılması zahmetlidir. Bu fonksiyonun özel bir durum olarak birinci parametresi 0 geçilirse zaten fonksiyon bize o kullanıcının ek gruplarının sayısını vermektedir. Biz de bu sayı kadar alanı malloc ile tahsis edebiliriz. Fonksiyondaki diğer önemli bir nokta fonksiyonun aynı zamanda prosesin etkin grup id'sini de verdiğimiz diziye yerleştirip yerleştirmeyeceğinin sistemdem sisteme değişebileceğidir. Linux sistemleri her zaman prosesin etkin kullanıcı id'sini de bu diziye yerleştirmektedir. NGROUPS_MAX sembolik sabitine bu değer dahil değildir. Yani buradan hareketle dizi uzunlu belirlenecekse bu değerden bir fazla değer kadar alan malloc ile tahsis edilmelidir. Ancak birinci parametre 0 geçilirse zaten geri döndürülen değere bu değer dahildir. Özetle fonksiyonun çağrılmasında şu yöntemler izlenebilir: 1) Dizi NGROUPS_MAX + 1 uzunlukta açılıp fonksiyon başarısız olursa sysconf fonksiyonu ile gerçek değer elde edilebilir. Ya da doğrudan sysconf fonksiyonu kullanılabilir. 2) Dizi uzunluğu büyük bir değer olarak tespit edilebilir. Ancak fonksiyonun başarısı kontrol edilebilir. 3) Fonksiyonun birinci parametresine 0 geçilerek fonksiyon çağrılabilir. Elde edilen değerden hareketle malloc fonksiyonu ile tam istenen uzunlukta alan tahsis edilebilir. Aşağıdaki örnekte prosesin ek grup id'leri alınıp ekrana (stdout dosyasına) yazdırılmıştır. Linuz sistemlerinde bu listeye prosesin etkin grup id'sinin de dahil edildiğine dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { gid_t *sgids; int ngroups; struct group *grp; if ((ngroups = getgroups(0, NULL)) == -1) exit_sys("getgroups"); if ((sgids = (gid_t *)malloc(ngroups * sizeof(gid_t))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if (getgroups(ngroups, sgids) == -1) exit_sys("getgroups"); for (int i = 0; i < ngroups; ++i) { if ((grp = getgrgid(sgids[i])) == NULL) exit(EXIT_FAILURE); if (i != 0) printf(", "); printf("%ju (%s)", (uintmax_t)sgids[i], grp->gr_name); } printf("\n"); free(sgids); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- POSIX standartlarında prosesin ek gruplarını set eden bir fonksiyon bulundurulmamıştır. Ancak işletim sistemlerinde bunu yapan mecburen bir sistem fonksiyonu bulundurulmak zorudadır. Linux sistemlerinde ve diğer pek çok UNIX türevi sistemde setgroups isimli fonksiyon ilgili sistem fonksiyonunu çağırarak bu işi yapmaktadır. Fonksiyonun prototipi şöyledir: #include int setgroups(size_t size, const gid_t *list); Fonksiyonun Fonksiyonun ikinci parametresi set edilecek ek grupların listesini belirtmektedir. Bu listeye prosesin etkin grup id'si dahil edilmemelidir. Birinci parametres ise bu dizinin uzunluğunu belirtir. Fonksiyon başarı durumunda 0, başarısızlık durumunda -1 değerine geri döner. Tabii fonksiyonu herkes çağıramaz. Fonksiyonun başarılı olması için prosesin uygun önceliğe (appropriate priveleges)" sahip olması gerekmektedir. (Yani prosesin etkin kullanıcı id'si 0 (root) olması ya da Linux sistemlerinde CAP_SETGID yeterliliğine sahip olması gerekir.) Aşağıdaki program set-user-id bayrağı set edilmiş sahibi root olan bir program dosyasına dönüştürülmüştür. Program aşağıdaki gibi derlenmiştir: $ gcc -o sample sample.c Sonra program dosyasının sahibi root olarak değiştirilmiştir. Ondan sonra da program dosyasının set-user-id bayrağı set edilmiştir: $ sudo chown root sample $ ls -l sample -rwxr-xr-x 1 root study 17400 Şub 26 12:18 sample $ sudo chmod u+s sample $ ls -l sample -rwsr-xr-x 1 root study 17400 Şub 26 12:18 sample Burada önce dosyanın modunun değiştirildiğine daha sonra set-user-id bayrağının set edildiğine dikkat ediniz. Çünkü chown POSIX fonksiyonu dosyanın set-user-id ve set-group-id bayraklarını reset etmektedir. chown fonksiyonun dokümantasyonunu dikkatlice bir kez daha okuyunuz. Aşağıdaki programda önce prosesin ek grupları elde edilmiş sonra ek gruplara bir grup dahil edilip setgroups fonksiyonuyla prosesin ek grupları set edilmiştir. setgroups işleminde prosesin ekin kullanıcı id'si 0 olduğu için başarılı olmaktadır. En sonunda program setuid fonksiyonu ile asıl kullanıcının gerçeki etkin ve saklı kullanıcı id'leriyle çalışmaya devam ettirilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { gid_t *sgids; int ngroups; struct group *grp; uid_t euid; euid = geteuid(); if ((ngroups = getgroups(0, NULL)) == -1) exit_sys("getgroups"); if ((sgids = (gid_t *)malloc((ngroups + 1) * sizeof(gid_t))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if (getgroups(ngroups, sgids) == -1) exit_sys("getgroups"); for (int i = 0; i < ngroups; ++i) { if ((grp = getgrgid(sgids[i])) == NULL) exit(EXIT_FAILURE); if (i != 0) printf(", "); printf("%ju (%s)", (uintmax_t)sgids[i], grp->gr_name); } printf("\n"); sgids[ngroups] = 0; if (setgroups(ngroups + 1, sgids) == -1) exit_sys("setgroups"); free(sgids); if (setuid(getuid()) == -1) /* prosesin gerçek, etkin ve saklı kullanıcı id'leri gerçek kullanıcı id'si haline getiriliyor */ exit_sys("setuid"); printf("success...\n") ; return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bir kullanıcının (yani kullanıcı id'sinin) ilişkin olduğu tüm grupları görebilmek için kabuk üzerinde "id" komutu ya da "groups" komutu uygulanabilir. Örneğin: $ id uid=1000(kaan) gid=1000(study) gruplar=1000(study),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),115(lpadmin),136(sambashare) $ groups study adm cdrom sudo dip plugdev lpadmin sambashare Var olan bir kullanıcıya bir ek grup atamak için "adduser" komutu kullanılabilir. Aslında bu "adduser" komutu yeni bir kullanıcı yaratmak için kullanılmaktadır. Ancak bu komuta biz var olan bir kullanıcı ve grup verirsek bu komut kullanıcıyı ek grup olarak da ilgili gruba eklemektedir. Ancak bu komut o andaki kabuk prosesinde bir değişiklik yaratmaz. Yalnızca "/etc/group" dosyasında güncelleme yapmaktadır. Örneğin: $ sudo adduser kaan work [sudo] kaan için parola: "kaan" kullanıcısı "work" grubuna ekleniyor ... kaan kullanıcısı work grubuna ekleniyor Tamamlandı. Aynı şey "usermod" kabuk komutuyla da yapılabilmektedir: $ sudo usermod -G work kaan Bir kullanıcının ek gruptan silinmesi de benzer komutlarla yapılabilmektedir. Örneğin: $ sudo deluser kaan work `kaan' kullanıcısı `work' grubundan siliniyor ... Tamamlandı. Burada "kaan" kullanıcısı "work" isimli gruptan çıkarılmıştır. Tabii bu komut da yalnızca "etc/group" dosyası üzerinde güncelleme yapmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- fork işlemi sırasında üst prosesin tüm ek grupları alt prosese aktarılmaktadır. Yani örneğin biz kabuk üzerinden bir program çalıştırdığımızda kabuk prosesinin ek grupları bizim prosesimize kabuğun uyguladığı fork neticesinde aktarılmış olacaktır. Tabii kabuk prosesinin de ek grupları aslında login programı tarafından "/etc/group" dosyasına başvurularak set edilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında bir POSIX fonksiyonu olmasa da Linux, BSD ve pek çok UNIX türevi sistemlerde initgroups isimli bir fonksiyon da bulunmaktadır. Bu fonksiyon /etc/group dosyasını dolaşarak belli bir kullanıcının bütün ek grup bilgilerini elde eder ve setgroups fonksiyonu ile bunları set eder. Yani tipik olarak login programı aslında bu initgroups fonksiyonunu çağırmaktadır. Fonksiyonun prototipi şöyledir: #include int initgroups(const char *user, gid_t group); Fonksiyon birinci parametresiyle kullanıcının ismini alır. /etc/group dosyasına başvurarak kullanıcının ek grup id'lerini elde eder. Sonra da setgroups fonksiyonunu uygulayarak prosesin ek gruplarını set eder. Tabii fonksiyonun bu işlemi yapabilmesi için yine uygun önceliğe sahip olması gerekmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine döner ve errno uygun biçimde set edilir. Fonksiyonun ikinci parametresi /etc/group dosyasından elde edilen ek gruplara dahil edilecek ekstra bir grubu belirtmektedir. Tipik olarak login prosesi prosesin etkin grup id'sini bu listeye eklemek için fonksiyonun ikinci parametresini prosesin etkin grup id'si ile çağırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bugünkü masaüstü işletim sistemleri "zaman paylaşımlı (time sharing)" bir çalışma ortamı oluşturmaktadır. Zaman paylaşımlı çalışma fikri ilk kez 1957 yılında uygulanmış ve sonra aktif bir biçimde işletim sistemlerine sokulmuştur. Dolayısıyla bugün kullandığımı UNIX/Linux, Windows ve macOS sistemleri zaman paylaşımlı çalışma uygulamaktadır. Proses terimi çalışmakta olan programın bütün bilgilerini içermektedir. Programın bağımsız çizelgelenen akışlarına "thread" denilmektedir. Thread'ler 90 yılların ortalarına doğru işletim sistemlerine sokulmuştur. Bir proses tek bir thread'le çalışmaya başlatılır. Buna prosesin "ana thread'i (main thread)" denir. Diğer thread'ler sistem fonksiyonlarıyla ya da sistem fonksiyonlarını çağıran kütüphane fonksiyonlarıyla programcı tarafından yaratılmaktadır. Zaman paylaşımlı çalışmada proseslerin thread'leri işletim sistemi tarafından CPU'ya atanır. O thread'in CPU'da belli bir süre çalışmasına izin verilir. O süre dolduğunda thread'in çalışmasına ara verilip başka thread benzer biçimde CPU'ya atanmaktadır. Tabii çalışmasına ara verilen thread'in bilgileri proses kontrol bloğuna kaydedilmekte ve çalışma sırası yeniden o thread'e geldiğinde thread en son kesilen noktadan çalışmasına devam etmektedir. Bir thread'in zaman paylaşımlı bir biçimde çalıştırıldığı parçalı çalışma süresine "quanta süresi" ya da İngilizce "time quantum" denilmektedir. Quanta süresinin ne kadar olacağı işletim sisteminin tasarımına bağlıdır. Bir thread'in çalışmasına ara verilmesi ve sıradaki thread'in CPU'ya atanması sürecine ise İngilizce "task switch" ya da "context switch" denilmektedir. Tabii bu işlem de belli bir zaman çerçevesinde yapılabilmektedir. Eğer quanta süresi uzun tutulursa interaktivite azalır. Quanta süresi tutulursa zamanın önemli kısmı "context switch" için harcanır dolayısıyla "birim zamanda yapılan iş miktarı (throughput)" düşer. Quanta süresi çeşitli faktörlere bağlı olarak değişebilmektedir. UNIX/Linux sistemleri ortalama 60 ms. civarında Windows sistemleri ortalama 20 ms. civarında bir quanta süresi uygulamaktadır. Zaman paylaşımlı bir sistemde kullanıcı sanki tüm proseslerin aynı anda çalıştığını sanmaktadır. Halbuki bu bir illüzyondur. Aslında programlar sürekli ara verilip çalıştırılmaktadır. Bu işlem çok hızlı yapıldığı için sanki programlar aynı anda çalışıyromuş gibi bir algı oluşmaktadır. Pekiyi bir thread CPU'ya atanmışken onun quanta süresini doldurması ve CPU'dan kopartılması nasıl sağlanmaktadır? İşte bu işlem hemen her zaman donanım kesmeleri yoluyla yapılmaktadır. Sistem donanımında periyodik kesme oluşturan bir mekanizma vardır. Buna "timer kesmesi" ya da UNIX/Linux dünyasında "jiffy" denilmektedir. Eski Linux sistemleri makineler yavaş olduğu için timer kesme periyodunu 10 ms. olarak ayarlamaktaydı. Ancak makineler hızlanınca artık bu periyot uzun süredir 1 ms. biçiminde ayarlanmaktadır. Yani her 1 milisaniyede bir aslında donanım kesmesi yoluyla kernel kodu devreye girmektedir. Bu kesme kodu da 60 ms. gibi bir zaman dolduğunda threadler arası geçiş (context switch) yapmaktadır. Thread akışının bu biçimde quanta süresi dolduğunda donanım kesmesi yoluyla zorla ara verilmesine işletim sistemleri dünyasında "preemptive" işletim sistemleri denilmektedir. UNIX/Linux, Windows ve macOS sistemleri preemptive işletim sistemleridir. Artık pek çok işlemci ailesi bu biçimdeki donanım kesmeleri oluşturan timer devrelerini CPU'nun içerisine de dahil etmiştir. Ancak x86 ve x64 sistemlerinde timer sistemi için genel olarak eskiye uyum bakımından Intel 8254 ve onun ileri versiyonları olan ve ismine "PIT (Programmable Interval Timer)" denilen devreler aktif olarak kullanılmaktadır. Preemptive sistemlere bir alternatif olarak "non-preemptive" ya da "cooperative multitask" da denilen sistemler bulunmaktadır. Bu sistemlerde bir thread çalıştığında kendi rızası ile CPU'yu birakır. Eğer CPU bırakmazsa diğer threadler çalışma fırsatı bulamazlar. Bu patolojik duruma "diğer thread'lerin açlıktab ölmesi (starvation)" denilmektedir. Tabii bu sistemler artık çok kısıtı kullanılmaktadır. PalmOS, eski Windows 3.X sistemleri böyleydi. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 34. Ders 04/03/2023 Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi sistemimizde birden fazla CPU (ya da çekirdek) varsa zaman paylaşımlı çalışma nasıl yürütülmektedir? Aslında değişen bir şey yoktur. Bu durum tıpkı yemek verilen bir kurumda yemeğin birden fazla koldan fazla verilmesi gibidir. İşletim sisteminin zaman paylaşımlı çalışma için oluşturduğu kuyruğa işletim sistemleri dünyasında "çalıştırma kuyruğu (run queue)" denilmektedir. Bu çalıştırma kuyruğu çok CPU söz konusu olduğunda her CPU için oluşturulmaktadır. Böylece her CPU yine zaman paylaşımlı bir biçimde çalıştırma kuyruğundaki thread'leri çalıştırmaktadır. Yani yukarıda açıkladığımız temel prensip değişmemektedir. Tabii burada işletim sisteminin bazı kararları da vermesi gerekir. Örneğin yeni bir thread (ya da proses) yaratıldığında bunun hangi CPU'ya atanacağı gibi. Bazen işletim sistemi thread'i bir CPU'nun çalıştırma kuyruğuna atar. Ancak diğer kuyruklar daha boş hale gelirse (çünkü o sırada çeşitli prosesler ve thread'ler sonlanmış olabilir) işletim sistemi başka bir CPU'nun çalıştırma kuyruğundaki thread'i kuyruğu daha boş olan CPU'nun çalıştırma kuyruğuna atayabilir. (Biz bir süper markette işin başında boş bir kasanın kuyruğuna girmiş olabiliriz. Sonra başka bir kasadaki kuyruk çok azalmış duruma gelebilir. Biz de o kuyruğa geçmeyi tercih ederiz. İşletim sistemi de buna benzer davranmaktadır.) Linux işletim sistemi, Windows sistemleri ve macOS sistemleri buna benzer bir çizelgeleme algoritması kullanmaktadır. Bir ara Linux O(1) çizelgelemesi denilen bir yöntem denemiştir. Bu yöntemde işletim sistemi tek bir çalıştırma kuyruğu kullanıyordu. Hangi CPU'daki parçalı çalışma süresi biterse bu kuyruktan seçme yapılıyordu. Çok CPU'lu zaman paylaşımlı çalışmada CPU sayısı artırıldıkça total performans artacaktır. Çünkü CPU'lar için düzenlenen çalıştırma kuyruklarında daha az thread bulunacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Zaman paylaşımlı çalışmada en önemli kavramlaran biri de "bloke olma (blocking)" denilen kavramdır. İşletim sistemi bir thread'i CPU'ya atadığında o thread "dışsal bir olaya ilişkin bir işlem başlattığı zaman" uzun süre bekleme yapabileceğinden dolayı işletim sistemi o thread'i çalıştırma kuyruğundan (run queue) çıkartır, "bekleme kuyruğu (wait queue)" denilen bir kuyruğa ekler. Böylece zaten bekleyecek olan thread boşuna CPU zamanı harcamadan pasif bir biçimde bekletilmiş olur. Örneğin bir thread klavyeden bir şey okumak istesin. İşletim sistemi thread'i bloke ederek çalıştırma kuyruğundan çıkartır ve onu bekleme kuyruğuna alır. Artık o thread çalıştırma kuyruğunda olmadığından zaman paylaşımlı çalışmada işletim sistemi tarafından ele alınmaz. Beklenen dışsal olay (örneğin klavye okuması) gerçekleştiğinde thread yeniden çalıştırma kuyruğuna yerleştirilir. Böylece çalışma aynı prensiple devam ettirilir. İşletim sistemi bekleme kuyruklarındaki thread'lere ilişkin olayların gerçekleştiğini birkaç biçimde anlayabilmektedir. Örneğin bir soket okuması yapıldığında eğer sokete henüz bilgi gelmemişse işletim sistemi thread'i bloke eder. Sonra network kartına paket geldiğinde network kartı bir donanım kesmesi oluşturur. İşletim sistemi devreye girer ve eğer gelen paket soketten okuma yapmak isteyen thread'e ilişkinse bu kesme kodunda (interrupt hanler) aynı zamanda o thread'i blokeden kurtarır. Ya da örneğin wait gibi bir işlemde işletim sistemi wait işlemini yapan thread'i bloke ederek wait kuyruğuna yerleştirir. Alt proses bittiğinde _exit sistem fonksiyonunda bu wait kuyruklarına bu sistem fonksiyonu bakar ve ilgili thread'in blokesini çözer. sleep gibi bir fonksiyonda ise işletim sistemi bekleme zamanını kendisi hesaplamaktadır. İşletim sistemi bekleme zamanı dolunca thread'in blokesini çözer. Genel olarak işletim sistemleri her olay için ayrı bir wait kuyruğu oluşturmaktadır. Örneğin aygıt sürücüler kendi wait kuyruklarını oluşturup bloke işlemlerini kendileri yapmaktadır. Thread'in çalıştırma kuyruğundan çıkartılıp wait kuyruğuna alınması nasıl ve kimin tarafından yapılmaktadır? Böyle bir işlem user mode'da sıradan prosesler tarafından yapılamaz. Hemen her zaman kernel mode'da işletim sisteminin sistem fonksiyonları tarafından ya da aygıt sürücüler tarafından yapılmaktadır. Yani thread'in bloke olması programın çalışması sırasında çağrılan bir sistem fonksiyonu (ya da aygıt sürücü fonksiyonu) tarafından yapılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread'ler "IO yoğun (IO bound)" ve "CPU yoğun (CPU bound)" olmak üzere ikiye ayrılmaktadır. IO yoğun thread'ler kendisine verilen quanta süresini çok az kullanıp hemen bloke olan thread'lerdir. CPU yoğun thread'ler ise kendisine verilen quanta süresini büyük ölçüde kullanan thread'lerdir. Örneğin bir döngü içerisinde sürekli hesap yapan bir thread CPU yoğun bir thread'tir. Ancak aşağıdaki gibi bir thread IO yoğun thread'tir: for (;;) { scanf("%d", &val); if (val == 0) break; printf("%d\n", val); } Burada bu thread aslında çok az CPU zamanı harcamaktadır. Zamanının büyük kısmını uykuda geçirecektir. IO yoğun ve CPU yoğun thread kavramı işletim sistemi için değil durumun insanlar tarafından anlaşılması için uydurulmuş kavramlardır. Yani işletim sistemi bu biçimde thread'leri ayırmamaktadır. Bir sistemde yüzlerde IO yoğun thread olsa bile bu durum sistemi çok fazla yormaz. Ancak çok sayıda CPU yoğun thread sistemi yavaşlatacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir programda iki nokta arasında geçen zaman sistemin o anki yüküne bağlı olarak değişebilmektedir. Örneğin sistemde çok sayıda CPU yoğun thread varsa iki nokta arasındaki zaman eskisine göre uzayabilir. Aşağıdaki programı önce bir kez çalıştırınız sonra kabuk üzerinden komut satırının sonuna & koyarak çok sayıda çalıştırınız. Programın döngüde harcadığı gerçek zaman uzayacaktır. Biz bu programda C'nin clock_gettime fonksiyonunu kullandık. Bu fonksiyon nano saniye temelinde çözünürlüğe sahiptir. clock isimli standart C fonksiyonu Linux sistemlerinde CPU zamanını verdiği için bu deneyde kullanılamamaktadır. Ancak ileride zaman ölçme konusu ayrı başlık altında değerlendirilecektir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { struct timespec ts1, ts2; long long elapsed_time; if (clock_gettime(CLOCK_MONOTONIC, &ts1) == -1) exit_sys("clock_gettime"); for (long i = 0; i < 1000000000; ++i) for (int k = 0; k < 30; ++k) ; if (clock_gettime(CLOCK_MONOTONIC, &ts2) == -1) exit_sys("clock_gettime"); elapsed_time = (ts2.tv_sec * 1000000000LL + ts2.tv_nsec) - (ts1.tv_sec * 1000000000LL + ts1.tv_nsec); printf("Elapsed Nanosecond: %lld\n", elapsed_time); printf("Elapsed Second: %f\n", elapsed_time / 1000000000.); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aslında bir programın toplam çalışma zamanı "time" isimli kabuk komutuyla da ölçülebilmektedir. Komut basit bir biçimde şöyle kullanılabilir: $ time ./sample time komutundan şöyle bir çıktı elde edilmektedir: real 1m38,360s user 0m18,597s sys 0m0,052s Görüldüğü gibi time komutu programın hep toplam çalışma zamanını hem de kernel ve user mode'da harcadığı zamanı rapor etmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kabuk üzerinde satırın sonuna & yerleştirilirse bu durumda kabuk fork/exec yapar ancak wait ile bekleme yapmaz. Dolayısıyla yeniden hemen kabuk promptuna düşülür. Bu komutta çalıştırılan programlara birer numara verilmektedir. "fg" komutuyla bu numara veridliğinde ilgili program yeniden "foreground" hale getirilebilmektedir. Satırın sonuna & yerleştirilmesi yalnızca wait yapılmamayı sağlamaz aynı zamanda bu işlemin henüz görmediğimiz sinyal konusuyla ilgili etkileri de vardır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Modern kapasiteli mikroişlemcilerde "sayfalama (paging)" denilen önemli bir mekanizma vardır. Örneğin Intel işlemcileri bu sayfalama mekanizmasına 80386 modelleriyle birlikte sahip olmuştur. ARM Cortex A serisi ve Cortex M serisi işlemcilerin de bu mekanizmaları vardır. Itanium, PowerPC gibi işlemcilerde de sayfalama mekanizması bulunmaktadır. Genellikle koruma mekanizmasına sahip işlemciler sayfalama mekanizmasına da sahip olurlar. Ancak koruma mekanizmasına sahip olduğu halde sayfalama mekanizmasına sahip olmayan işlemciler de vardır. Sayfalama mekanizması güçlü işlemcilerde bulunan bir mekanizmadır. Genel olarak mikrodenetleyicilerde bu mekanizma yoktur. Örneğin ARM'ın Cortex M serisi mikrodenetleyicilerinde sayfalama mekanizması bulunmamaktadır. Ayrıca işlemcilerdeki bu sayfalama mekanizması aktif ve pasif duruma getirilebilmektedir. Yani işlemci sayfalama mekanizmasına sahip olduğu halde sistem programcısı bu mekanizmayı açmayabilir ve kullanmayabilir. İşlemciler reset edildiğinde sayfalama mekanizması pasif durumdadır. İşletim sistemleri bazı ön hazırlıkları yaptıktan sonra bu mekanizmayı açmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sayfalama mekanizmasında fiziksel RAM aynı zamanda "sayfa (page)" denilen ardışıl bloklara ayrılır. Sayfa uzunluğu sistemden sisteme hatta aynı işlemcide işlemcinin modundan moduna değişebilir. Ancak en tipik kullanılan sayfa uzunluğu 4096 (4K) byte'tır. Gerçekten de bugün Linux, Windows ve macOS sistemleri 4K'lık sayfalar kullanmaktadır. Sayfalama mekanizması etkin hale getirildiğinde işlemci RAM'deki her sayfaya bir sayfa numarası karşılık getirir. Örneğin ilk 4096 byte 0'ıncı sayfaya, sonraki 4096 byte 1'inci sayfaya ilişkindir. Sayfalar bu biçimde ilk sayfa 0'dan başlatılarak ardışıl biçimde numaralandırılmaktadır. Yani her byte aslında bir sayfa içerisinde bulunur. Bir program içerisinde kullanılan yani derleyicinin ürettiği adresler aslında gerçek fiziksel adresler değildir. Bu adreslere "sanal adresler (virtual addresses)" denilmektedir. Derleyiciler kodları sanki geniş bir RAM'de program tek başına çalışacakmış gibi üretmektedir. Örneğin 32 bit işlemcilerin kullanıldığı bir Linux sisteminde derleyici sanki program 4 GB'lik RAM'de tek başına 4 MB'den itibaren yüklenecekmiş gibi kod üretmektedir. Yani örneğin 32 bit Linux sistemlerinde (Windows ve macOS'te de böyle) sanki derleyiciler program 4 GB bellekte 4 MB'den itibaren tek başlarına yüklenecekmiş gibi bir kod üretmektedir. Her program derlendiğinde aynı biçimde kod üretilmektedir. Çünkü derleyicinin ürettiği bu adresler sanal adreslerdir. Pekiyi her program aynı biçimde sanki RAM'in 4 MB'sinden başlanarak ardışıl bir biçimde yüklenecekmiş gibi bir koda sahipse bu programlar nasıl çalışmaktadır? İşte sayfalama mekanizmasına sahip olan CPU'lar aslında "sayfa tablosu (page table)" denilen bir taloya bakarak çalışırlar. Sayfa tablosu sanal sayfa numaralarını fiziksel sayfa numaralarına eşleyen bir tablodur. Sayfa tablosunun görünümü aşağıdaki gibidir: Sanal Sayfa No Fiziksel Sayfa No ... ... 4562 17456 4563 18987 4564 12976 ... ... Şimdi Intel işlemcisinin aşağıdaki gibi bir makine kodunu çalıştırdığını düşünelim: MOV EAX, [05C34782] Burada makine komutu bellekte 05C34782 numaralı adresten başlayan 4 byte erişmek istemektedir. İşlemci önce bu adres değerinin kaçıncı sanal sayfaya karşılık geldiğini hesaplar. Bu hesap işlemci tarafından oldukça kolay bir biçimde yapılır. Sayı 12 kere sağa ötelenirse başka bir deyişle sayının sağındaki 3 hex digit atılırsa bu sanal adresin kaçıncı sanal sayfaya karşılık geldiği bulunabilir: 05C34782 >> 12 = 05C34 (sanal sayfa no, decimal 23604) Artık işlemci sayfa tablosunda 0x5C34 yani desimal 23604 numaralı girişe bakar. Sayfa tablosunun ilgili kısmı şöyle olsun: Sanal Sayfa No (decimal/hex) Fiziksel Sayfa No (desimal/hex) ... ... 23603 (5C33) 47324 (B8DC) 23604 (5C34) 52689 (CDD1) 23605 (5C35) 29671 (73E7) ... ... Burada 23604 (5C34) numaralı sanal sayfa 52689 (CDD1) fiziksel sayfasına yönlendirilmiştir. Pekiyi işlemci hangi fiziksel adrese erişecektir? İşte bizim sanal adresimiz 05C34782 idi. Bu adres iki kısma ayrıştırılabilir: 05C24 Sanal sayfa no (hex) 782 Sayfa offset'i (hex) Bu durumda işlemci aslında fiziksel RAM'de 52689 (CDD1)'uncu fiziksel sayfanın 1922 (782) byte'ına erişecektir. O zaman gerçek bellekteki erişim adresi 52689 (CDD1) * 4096 (1000) + 1922 (782) olacaktır. Burada özetle anlatılmak istenen şey şudur: İşlemci her bellek erişiminde erişilecek sanal adresi iki kısma ayırır: "Sanal Sayfa No" ve "Sayfa Offset'i". Sonra sayfa tablosuna giderek sanal sayfa numarasına karşı gelen fiziksel sayfa numarasını elde eder. O fiziksel sayfanın sayfa offet'i ile belirtilen byte'ına erişir. Örneğin şöyle bir fonksiyon çağırmış olalım: foo(); Derleyicimiz de şöyle bir kod üretmiş olsun: CALL 06F14678 (hex) Burada 06F14678 foo fonksiyonunun sanal bellek adresidir. Derleyici bu adresi üretmiştir. Ancak program çalışırken işlemci bu adresi ikiye ayırır (hex olarak konuşacağız): 06F146 Sanal Sayfa No (hex) 678 Sayfa Offset'i (hex) Sonra sayfa tablosuna gider ve 06F146 sayfasının hangi fiziksel sayfaya yönlendirildiğini tespit eder. Bu fiziksel sayfanın hex olarak 7C45 olduğuna düşünelim. O zaman işlemcinin erişeceği fiziksel adres 7C45000 + 678 hex adresi olacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Buraya kadar şunları anladık: - Derleyici 32 bit bir sistemde sanki program 4 GB'lik bir RAM'de tek başına 4 MB'ye yüklenerek çalıştırılacakmış gibi bir kod üretmektedir. - İşlemci kodu çalıştırırken her bellek erişiminde sayfa tablosuna bakıp aslında o sanal adresleri fiziksel adreslere dönüştürmektedir. Pekiyi sayfa tablosunu kim oluşturmaktadır? Sayfa tablosu işletim sistemi tarafından proses belleğe yüklenirken (exec fonksiyonları tarafından) oluşturulmaktadır. İşletim sisteminin yükleyicisi (loader) programı 4K'lık parçalara ayırarak sanal sayfa numaraları ardışıl ancak fiziksel sayfa numaraları ardışıl olmayacak biçimde fiziksel RAM'e yüklemektedir. Yani işletim sistemi fiziksel RAM'deki boş sayfalara bakar. Programın 4K'lık kısımlarını bu fiziksel RAM'deki boş sayfalara yükler ve sayfa tablosunu buradan hareketle oluşturur. Aslında sayfa tablosu bir tane değildir. İşletim sistemi her proses için ayrı bir sayfa tablosu oluşturmaktadır. CPU'lar sayfa tablolarını belli bir yazmacın gösterdiği yerde ararlar (Örneğin Intel işlemcilerinde sayfa tablosu CR3 yazmacının gösterdiği yerdedir.) İşletim sistemi thread'ler arası geçiş (context switch) yapıldığında çalışmasına ara verilen thread ile yeni geçilen thread'in aynı prosesin thread'leri olup olmadığına bakar. Eğer yeni geçilen thread ile çalışmasına ara verilen thread aynı prosese ilişkinse sayfa tablosu değiştirilmez. Çünkü aynı prosesin thread'leri aynı sanal bellek alanını kullanmaktadır. Ancak yeni geçilen thread kesilen thread'le farklı proseslere ilişkinse işletim sistemi CPU'nun gördüğü sayfa tablosunu da değiştirmektedir. Böylece aslında bir prosesin thread'i çalışırken CPU o prosesin sayfa tablosunu gösterir durumda olur. Her prosesin sayfa tablosu birbirinden farklı olduğu için iki farklı prosesteki sanal adresler aynı olsa bile bu adreslerin fiziksel karşılıkları farklı olacaktır. Örneğin aynı programı iki kez çalıştıralım. Bu durumda bu iki proses için işletim sistemi iki farklı sayfa tablosu kullanıp aynı sanal adresleri farklı fiziksel sayafalara yönlendirecektir. Böylece aslında aynı sanal adreslere sahip olan programlar farklı fiziksel adreslere sahip olacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Konu ile ilgili sorular ve kısa cevapları şöyledir: Soru: Bir programı debugger ile inceliyorum. Orada bir nesnenin adresini görüyorum. Bu adres nasıl bir adresitir? Yanıt: Bu adres sanal bir adrestir. İşlemci bu adrese erişmek istediğinde aslında sayfa tablosu yoluyla fiziksel olan başka bir adrese erişecektir. Soru: İki farklı programda sanal 5FC120 adresi kullanılıyorsa bunlar fiziksel RAM'de aynı yeri mi gösteriyordur? Yanıt: Hayır, çünkü işletim sistemi her proses için farklı bir sayfa tablosu oluşturmaktadır. Bir thread çalışırken işlemci o thread'e ilişkin prosesin sayfa tablosunu kullanıyor durumdadır. Dolayısıyla bu iki farklı proseste işletim sistemi sayfa tablolarının ilgili sayfalarını aslında farklı fiziksel sayfalara yönlendirmiş durumdadır. Soru: 32 bit bir derleyicinin ürettiği kodun aslında sanki 4 GB belleğe tek başına 4 MB'den itibaren yüklenecekmiş gibi üretildiği söylendi. Sanal bellek alanındaki bu 4 MB boşluğun anlamı nedir? Yanıt: Bunun çok özel bir anlamı yoktur. Bir kere NULL adres için en az bir sayfa gerekmektedir. Pek çok işletim sistemi güvenlik amacıyla ve bazı başka nedenlerden dolayı sanal bellek alanının belli bir bölümünü boş bırakmaktadır. Windows'ta da bu alan 4 MB'dir. Ancak programın minimal yüklenme adresi 64K'ya kadar düşürülebilmektedir. Soru: CPU sayfa tablosunun yerini nereden bilmektedir? Yanıt: CPU'lar sayfa tablosunu özel bir yazmacın gösterdiği yerde arayacak biçimde tasarlanmıştır. Dolayısıyla context switch sırasında aslında işletim sistemi yazmacın değerini değiştirmektedir. Yani işletim sistemi aslında tüm proseslerin sayfa tablolarını fiziksel RAM'da oluşturur. Context switch sırasında yalnızca sayfa tablosunun yerini belirten ilgili yazmacın değerini değiştirir. Soru: Sayfalama mekanizması CPU'nun çalışmasını yavaşlatmaz mı? Yanıt: Teorik olarak sayfalama mekanizması CPU'nun çalışmasını yavaşlatabilir. Ancak bugünkü CPU'ların çalışma hızları zaten bu sayfalama mekanizmasının aktif olduğu durumla belirlenmektedir. Dolayısıyla donanımsal olarak sayfalama mekanizması iyi bir biçimde oluşturulduğu için buradaki hız kaybı önemsenecek ölçüde değildir. Ayrıca işlemciler sayfa tablosuna erişimi azaltmak için zaten onun bazı bölümlerini kendi içlerindeki bir cache sisteminde tutabilmektedir. Ayrıca sayfa girişlerine hızlı erişim için işlemciler TLB (Translation Lookaside Buffer) denilen bir cache mekanizması da oluşturmaktadır. Soru: Sayfalama mekanizmasına ne gerek vardır? Yanı: Bu durum izleyen paragraflarda ele alınacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 35. Ders 05/03/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İşletim sistemi her proses için ayrı bir sayfa tablosu oluşturduğuna göre ve bu sayfa tablosunda aynı sanal sayfa numaralarını zaten farklı fiziksel sayfalara yönlendirdiğine göre aslında hiçbir proses diğerinin alanına erişemez. Yani proseslerin birbirlerinin alanlarına erişmesi zaten sayfalama mekanizmasıyla engellenmiş olmaktadır. Bu duruma "sayfalama mekanizması ile proseslerin fiziksel bellek alanlarının izole edilmesi" denilmektedir. Örneğin aşağıdaki gibi iki prosesin sayfa tablosu söz konusu olsun: Proses-1 Sanal Sayfa No (decimal/hex) Fiziksel Sayfa No (desimal/hex) ... ... 23603 (5C33) 47324 (B8DC) 23604 (5C34) 52689 (CDD1) 23605 (5C35) 29671 (73E7) ... ... Proses-2 Sanal Sayfa No (decimal/hex) Fiziksel Sayfa No (desimal/hex) ... ... 23603 (5C33) 84523 (14A2b) 23604 (5C34) 62981 (F605) 23605 (5C35) 42398 (A59E) ... ... İki prosesin sayfa tablosunda Fiziksel Sayfa Numaraları birbirinden ayrıldığında zaten bu iki proses asla birbirlerinin alanlarına erişemeyecektir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi 32 bit bir mimaride işletim sisteminin sayfa tablosu yukarıdaki şekillere göre ne kadar yer kaplar? 32 bit mimaride fiziksel RAM en fazla 4 GB olabilir. Proseslerin sanal bellek alanları da 4 GB'dir. O halde toplam sayfa sayısı 4GB/4K = 2^32/2^12 = 2^20 = 1 MB olur. Her sayfa tablosu girişi Intel mimarisinde 4 byte'tır. Dolayısıyla yukarıdaki şekillere göre bir prosesin sayfa tablosu 4 MB yer kaplar. Bu alan sayfa tablosu için çok büyüktür. Bu nedenle işlemcileri tasarlayanlar sayfa tablolarının kapladığı alanı küçültmek için sanal adresleri iki parçaya değil, üç ya da dört parçaya ayırma yoluna gitmişlerdir. Gerçekten de örneğin Intel'in 32 bit mimarisinde bir sanal adres üç parçaya ayrılmaktadır. Bu ayrıntı kursumuzun konusu dışındadır ve Derneğimizde "80x86 ve ARM Sembolik Makine Dilleri" kursunda ele alınmaktadır. Biz bu kursumuzda çeşitli gösterimlerde sanal adresleri ikiye ayıracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz yukarıda 32 bit sistemlere göre örnekler verdik. Pekiyi 64 bit sistemlerde durum nasıldır? 64 bit sistemlerde fiziksel RAM'in teorik büyüklüğü 2^64 = 16 exabyte olmaktadır. Dolayısıyla prosesin sanal bellek alanı da bu kadar olacaktır. Burada eğer sanal adres iki parçaya ayrılırsa sayfa tablolarının aşırı büyük yer kaplaması kaçınılmazdır. Bu nedenle 64 bit sistemlerde genellikle işlemcileri tasarlayanlar sanal adresleri dört parçaya ayırmaktadır. Bu konu yine kursumuzun kapsamı dışındadır. Ancak 64 bit sistemlerde değişen bir şey yoktur. Program yine çok geniş bir sanal belleğe sanki tek başına yüklenecekmiş gibi derlenir. Yine işletim sistemi proses için sayfa tablosu oluşturarak sanal sayfa numaralarını gerçek fiziksel sayfa numaralarına yönlendirir. Tabii pek çok işletim sistemi 16 exabyte sanal bellek alanı çok büyük olduğu için bunu kısıtlama yoluna gitmektedir. Örneğin Linux yalnızca 256 TB alanı kullanmaktadır. Windows ise yalnızca 16 TB alan kullanır. Bu alanlar bile bugün için çok büyüktür. Sayfa tablolarının gerçek organizasyonu için kurs dokümanlarında /doc/ebooks klasöründe Intel'in AMD'nin ve ARM işlemcilerinin orijinal dokümanları bulundurulmuştur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi sayfalama (paging) mekanizmasının ne faydası vardır? İşte sayfalama mekanizmasının iki önemli işlevi vardır: 1) Sayfalama mekanizması programların fiziksel RAM'e ardışıl yüklenmesinin zorunluluğunu ortadan kaldırır. Böylece "bölünme (fragmentation)" denilen olgunun olumsuz etkisini azaltır. 2) Sayfalama mekanizması "sanal bellek (virtual memory)" denilen olgunun gerçekleştirimi için gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bölünme (fragmentation) bellek yönetimi konusunda önemli bir problemdir. Bir nesnenin belleğe yüklenmesi ardışıl bir biçimde yapılırsa zamanla yükleme boşaltma işlemlerinin sonucunda bellekte çok sayıda küçük alan oluşmaktadır. Bu küçük alanlar ardışıl olmadığı için genellikle bir işe yaramamaktadır. Küçük alanların toplamı oldukça büyük miktarlara varabilmekte ve toplam belleğin önemli miktarını kaplayabilmektedir. Bu olguya "bölünme (fragmentation)" denilmektedir. Bölünmenin engellenmesi için ardışıl yükleme zorunluluğunun ortadan kaldırılması gerekir. Bu durumda bellek bloklara ayrılır. Yüklenecek nesne bloklara bölünerek ardışıl olmayacak biçimde boş bloklara atanır. Ancak nesnenin hangi parçasının hangi bloklarda olduğu da bir biçimde kaydedilir. Bu teknik hem RAM yönetiminde hem de disk yönetiminde benzer biçimde kullanılmaktadır. Ancak bloklama yöntemiyle bölünme ortadan kaldırılmaya çalışıldığında bu sefer başka bir problem ortaya çıkmaktadır. Nesnelerin son bloklarında kullanılmayan alanlar kalabilmektedir. Bu da bir çeşit bölünmedir. Bu bölünme durumuna "içsel bölünme (internal fragmentation)" denilmektedir. İçsel bölünmede yapılabilecek bir şey yoktur. Ancak içsel bölünmenin etkisi diğerine göre daha az olmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sanal bellek (virtual memory) bir programın tamamının değil belli kısmının belleğe yüklenerek disk ile RAM arasında yer değiştirmeli bir biçimde çalıştırılmasına yönelik bir mekanizmadır. Bu mekanizma sayesinde örneğin 100 MB'lık bir programın başlangıçta yalnızca 64K'lık kısmı RAM'e yüklenebilir. Sonra program çalışmaya başlar. Çalışma sırasında programın bellekte olmayan bir kısmına erişildiğinde işletim sistemi programın bellekte olmayan kısmını o anda diskten belleğe yükler ve çalışma kesintisiz devam ettirilirir. Sanal bellek kullanımında yine fiziksel RAM sayfalara ayrılır. Her sayfaya bir numara verilir. İşletim sistemi RAM'in hangi sayfasının hangi programın neresini tuttuğunu bir biçimde oluşturduğu veri yapılarıyla bilir duruma gelir. Bir programın RAM'de olmayan bir sayfasının diskten RAM'e yüklenmesine "swap in" denilmektedir. Ancak zamanla RAM'deki tüm fiziksel sayfalar dolu duruma gelebilir. Bu durumda işletim sistemi bir programın bir parçasını RAM'e çekebilmek için RAM'deki bir sayfayı da RAM'dan atmak durumunda kalır. Bu işleme ise "swap out" denilmektedir. Tabii işletim sistemi hangi programın RAM'deki hangi sayfasının boşaltılacağı konusunda iyi bir karar vermek durumundadır. İşletim sistemine göre "gelecekte kullanılma olasılığı en düşük olan sayfanın" RAM'den atılması en iyi stratejidir. Bu durumda bir program çalışırken aslında sürekli bir biçimde disk ile RAM arasında yer değiştirmeler yapılmaktadır. Bu yer değiştirmelere genel olarak işletim sistemi dünyasında "swap" işlemi denilmektedir. Şüphesiz swap işlemi yavaş bir işlemdir ve toplam performans üzerinde en önemli zayıflatıcı etkilerden birini oluşturmaktadır. Swap işlemlerinin olumsuz etkisini azaltmak için ilk akla gelen şey fiziksel RAM'i büyütmektir. Ancak fiziksel RAM'in büyütülmesi maliyet oluşturmaktadır. Bugünkü SSD'ler hard disklere göre oldukça iyi performans göstermektedir. Dolayısıyla bilgisayarımızda hard disk yerine SSD varsa swap işlemleri daha hızlı yürütülecektir. Şüphesiz en önemli unsur aslında sayfaların yer değiştirilmesi konusunda uygulanan algoritmalardır. Bunlara "page replacement" algoritmaları denilmektedir. Tabii bugünkü işletim sistemleri bilinen en iyi algoritmaları zaten kullanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi işletim sistemi programın RAM'de olmayan bir sayfasını yüklemek istediğinde RAM'den sayfa boşaltacağı zaman ya boşaltılacak sayfa üzerinde daha önce yazma işlemleri (update) yapıldıysa ne olacaktır? İçeriği değiştirilmiş olan sayfanın RAM'den atılırken mecburen diskte saklanması gerekir. İşte işletim sistemleri bu işlemler için diskte ismine "swap file" ya da "page file" denilen dosyalar tutmaktadır. Değiştirilmiş olan sayfaları bu dosyalara yazmaktadır. Linux işletim sistemi swap alanı olarak genellikle ayrı bir disk bölümünü kullanmaktadır. Ancak herhangi bir dosya da swap dosyası olarak kullanılabilmektedir. Kullanılacak swap disk alanının ya da dosyalarının toplamı bazen önemli labilir. Çünkü sistemin toplam sanal bellek kapasitesi bu swap dosyalarıyla da ilgilidir. Linux sistemlerinde o andaki toplam swap alanları "/proc/swaps" dosyasından elde edilebilir. Buradaki değer Kilo Byte cinsindendir. Ya da "swapon -s" komutuyla aynı bilgi elde edilebilir. Pekiyi sistemin kullandığı swap alanı dolarsa ne olur? İşte bu durumda sistemin sanal bellek limiti dolmuş kabul edilir. Yapılacak şey sisteme yeni swap alanları eklemektir. Bunun Linux'ta nasıl yapılacağını ilgili kaynaklardan öğrenebilırsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi işletim sistemi programı belleğe yüklerken baştan kaç sayfayı yüklemektedir? İşte buna "minimum working set" denilmektedir. İşletim sistemleri genel olarak bir program için en az yüklenecebilecek sayfa sayısını belirlemiş durumdadır. Böylece yüklenmiş her programın en azından "minimum working set" kadar sayfası RAM'de bulunmak zorundadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi sanal bellek mekanizması nasıl gerçekleştirilmektedir? İşte işlemciler sanal bellek mekanizmasını oluşturabilmek için özel bir biçimde tasarlanmıştır. İşlemci ne zaman sanal adresi fiziksel adrese dönüştürmek için sayfa tablosuna başvursa, eğer sayfa tablosunda o sanal adrese bir fiziksel sayfa karşılık getirilmemişse ismine "page fault" denilen bir içsel kesme (intterupt) oluşturmaktadır. Örneğin: Sanal Sayfa No (decimal/hex) Fiziksel Sayfa No (desimal/hex) ... ... 23603 84523 23604 - 23605 42398 23606 - 23607 73245 ... ... Burada Fiziksel Sayfa Numarasındaki "-" sembolleri o sanal sayfaya bir fiziksel sayfanın karşı getirilmediğini belirtmektedir. Dolayısıyla örneğin işlemci 23604 numaralı, 23606 numaralı sanal sayfalar için dönüştürme yapmak istediğinde "page fault" oluşturacaktır. İşte "page fault"" denilen kesme oluştuğunda işletim sisteminin kesme kodu devreye girer. Buna "page fault handler" denilmektedir. Bütün swap mekanizması bu işletim sisteminin kesme kodu tarafından yapılmaktadır. İşletim sisteminin bu kesme kodu (page fault handler) önce hangi prosesin hangi sayfaya erişmek istediğini tespit eder. Sonra onun diskteki karşılığını bulur ve yer değiştirme işlemini yapar. Tabii bu kesme kodu yer değiştirme işlemini yaptıktan sonra artık sayfa tablosunu da güncellemektedir. İşletim sisteminin kesme kodu bittiğinde kesmeye yol açan makine komutu yeniden çalıştırılarak akış devam ettirilmektedir. Bu komut yeniden çalıştırıldığında artık sayfa tablosu düzeltildiği için page fault oluşmayacaktır. Bu durumda bir program çalıştırılmak istendiğinde işletim sistemi aslında programın az sayıda sayfasını RAM'e yükleyip sayfa tablosunun o sayfalar dışındaki fiziksel sayfa numaralarını "-" haline getirir. Böylece yukarıda açıklanan mekanizma eşliğinde kesiksiz çalışma sağlanacaktır. Pekiyi ya erişilmek istenen sanal adres uydurma bir adresse ne olacaktır? İşte işletim sisteminin page fault kesme kodu (handler) öncelikle erişilmek istenen adresin o proses için legal bir adres olup olmadığına bakmaktadır. Eğer erişilmek istenen adres legal bir adres değilse artık hiç swap işlemi yapılmadan proses cezalandırılır ve sonlandırılır. Yani her türlü sanal adresin diskte bir karşılığı yoktur. Biz bir göstericiye rastgele bir adres yerleştirip oraya erişmek istesek aslında proses bu page fault kesme kodu tarafından sonlandırılmaktadır. O halde sanal bellek mekanizması tipik olarak işlemci ve işletim sistemi tarafından olarak şöyle gerçekleştirilmektedir: 1) Proses bir sanal adrese erişmeye çalışır 2) İşlemci sanal adresi parçalarına ayırır ve sayfa tablosuna başvurularak 3) Sayfa tablosunda ilgili sayfaya bir fiziksel sayfa karşı getirilmişse sorun oluşmaz çalışma normal olarak devam eder. Ancak sanal sayfaya bir fiziksel adres karşı getirilmemişse (şekilde onu "-" ile gösterdik) bu durumda işlemci "page fault" denilen içsel kesmeyi oluşturur. 4) Page fault kesmesi için kesme kodunu işletim sistemini yazanlar bulundurmuştur. Bu kod önce erişilmek istenen adresin geçerli bir adres olup olmadığına bakar. Eğer erişilmek istenen adres geçerli bir adres değilse proses sonlandırılır. Eğer geçerli bir adresse page fault kesme kodu "swap mekanizması" ile programın o kısmını RAM'e yükler, sayfa tablosunu günceller ve kesme kodundan çıkar. Artık işlemci fault oluşturan makine komutuyla çalışmasına devam eder. Ancak sayfa tablosu düzeltildiği için bu kez fault oluşturmaz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi işletim sisteminin "bellek yönetimi (memory management)" kısmını yazanlar hangi bilgileri tutmak zorundadır? İşte işletim sistemleri tipik olarak bu mekanizma için şu bilgileri kernel alanı içerisinde oluşturmak zorundadır: 1) Tüm fiziksel RAM'deki tüm sayfaların "free" olup olmadığına ilişkin tablo 2) Bir fiziksel sayfanın free değilse hangi proses tarafından kullanıldığına ilişkin bilgi 3) Swap dosyalarının yerleri ve organizasyonu 4) Hani proseslerin hangi sayfalarının o anda fiziksel RAM'de hangi fiziksel sayfalarda bulunduğu 5) Diğer başka bilgiler Bellek yönetimi (memory management) bir işletim sisteminin en önemli ve en zor yazılan alt sistemlerden biridir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi sanal bellek toplamda bize ne sağlamaktadır? Şüphesiz sanal bellek mekanizmasının en önemli faydası RAM yeterli olmasa bile çok sayıda büyük programın aynı anda çalışır durumda tutulabilmesidir. Bizim elimizde 8 GB RAM olsa bile biz onlarca büyük programı çalışır durumda tutabiliriz. Ancak yukarıda da belirtildiği gibi işletim sistemi bir swap alanı bulundurmaktadır. Eğer bu swap alanı dolarsa başka bir limit nedeniyle "out of memory" durumu oluşabilmektedir. Bu nedenle eğer programlar çok fazla bellek kullanıyorsa bu swap alanlarının büyütülmesi de gerekebilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sayfalama ve sanal bellek mekanizmasında işletim sistemi de o anda sanal bellek alanı içerisinde bulunmak zorundadır. Pekiyi işletim sisteminin kodları sayfa tablosunda sanal belleğin neresindedir? İşte genellikle işletim sistemi tasarımcıları sanal bellek alanını "user sapace" ve "kernel space" olarak ikiye ayırmaktadır. "user space" genellikle sanal bellek alanının düşük anlamlı kısmında, kernel space ise yüksek anlamlı kısmında bulundurulur. Örneğin 32 bit Linux sistemleri 4 GB'lik sanal bellek alanını şöyle ayırmıştır: 32 Bit Linux Proses Sanal Bellek Alanı 3 GB User Space 1 GB Kernel Space Bu durumda 32 bit Linux sistemlerinde bir programın kullanabileceği maksimum sanal bellek 3 GB'dir. (Windows ta 2 GB user space için, 2 GB kernel space için kullanılmıştır.) 64 bit Linux sistemlerinde ise prosesin sanal bellek alanı şöyle organize edilmiştir: 64 Bit Linux Proses Sanal Bellek Alanı 128 TB User Space 128 TB Kernel Space Görüldüğü gibi aslında teorik sanal bellek 16 exabyte olduğu halde 64 bit Linux sistemleri yalnızca 256 TB sanal belleğe izin vermektedir. Proseslerin sayfa tablolarında kernel alanınının içeriği hep aynıdır. Yani context switch yapılsa bile kernel kodları hep aynı sanal adreslerde bulunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz bir program içerisinde yüksek miktarda dinamik tahsisat yaptığımızda ne olur? Linux sistemlerinde malloc fonksiyonu brk ya da sbrk denilen sistem fonksiyonunu çağırabilmektedir. Ancak arka planda sanal bellek bakımından şunlar gerçekleşir: - İşletim sistemi malloc ile tahsis edilen alanı sayfa tablosunda oluşturur. Oluştururken de tahsis edilen alanın toplam swap alanından küçük olduğunu garanti etmeye çalışır. Çünkü malloc ile tahsis edilen alan eninde sonunda swap dosyası içerisinde bulundurulacaktır. - İşletim sistemi swap dosyasının boyutu yeterliyse tahsisatı kabul etmektedir. Ancak sistemden sisteme değişebilecek biçimde bu sırada swap dosyasında tahsisat yapılabilir ya da yapılmayabilir. Eğer swap dosyasında o anda tahsisat yapılırsa bu durumda swap alanı ciddi biçimde azalacak ve belki de başka proses artık aynı tahsisatı yapamayacaktır. Ancak işletim sistemi swap dosyasında tahsisatı henüz yapmayabilir. Bu işlemi dinamik alan kullanıldığında yapabilir. Genellikle Linux sistemleri bu yola başvurmaktadır. Literatürde dinamik alan için swap dosyasında baştan yer ayrılmasına "alanın commit edilmesi" denilmektedir. Aşağıdaki 3 GB RAM olan 2 GB swap alanına sahip 64 bit Linux sisteminde 5 GB alan dinamik olarak tahsis edilmek istenmiştir. Burada tahsisat başarılı gibi gözükse de tahsis edilen alan kullanılırken swap alanı yetersizliğinden dolayı sinyal oluşacak ve proses sonlandırılacaktır. O halde 64 bit Linux sistemlerinde biz teorik olarak her biri 128 TB olan onlarca programı bir arada çalıştırabiliriz. Ancak bunun için swap alanımızın da yeterli büyüklükte diskte oluşturulmuş olması gerekir. Swap alanının yetersizliği durumunda bir sinyal ile proses sonlandırılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include int main(void) { char *pc; pc = (char *)malloc(5000000000); if (pc == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } for (long i = 0; i < 5000000000; ++i) pc[i] = 0; printf("ok\n"); getchar(); free(pc); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- 36. Ders 11/03/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi işletim sistemi sayfa tabloları yoluyla proseslerin bellek alanlarını tam olarak birbirinden izole etmektedir. Dolayısıyla bir proses istese de başka bir prosesin bellek alanına erişememektedir. Ancak ismine "paylaşılan bellek alanları (shared memory)" denilen bir teknik ile işletim sistemi farklı proseslerin aynı fiziksel sayfaya erişimini sağlayabilmektedir. Şöyle ki: İşletim sistemi iki prosesin sayfa tablosunda farklı sanal sayfaları aynı fiziksel sayfaya eşlerse bu iki proses farklı sanal adreslerle aslında aynı fiziksel sayfayı görüyor durumda olur. Örneğin: Proses-1 Sayfa Tablosu Sanal Sayfa Numarası Fiziksel Sayfa Numarası ... ... 987 1245 988 1356 999 1412 ... ... Proses-2 Sayfa Tablosu Sanal Sayfa Numarası Fiziksel Sayfa Numarası ... ... 356 7645 357 1356 358 489 ... ... Görüldüğü gibi birinci prosesin 988'inci sanal sayfa numarası ikinci prosesin 357'inci sanal sayfa numarasıyla aynı fiziksel adrese yönlendirilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında sayfa tablolarında her bir sayfanın da ayrıca bir "özellik bilgisi (attribute)" vardır. Yani sayfa tablolarının formatı daha gerçekçi bir biçimde şöyledir: Sanal Sayfa No Fiziksel Sayfa No Sayfa özelliği ... ... ... Sayfa özelliği o fiziksel sayfanın "read only" mi, "read/write" mı "execute" özelliğine sahip mi olduğunu belirtmektedir. Ayrıca bir fiziksel sayfa "user mode" ya da "kernel mode" sayfa olarak belirlenebilmektedir. İşletim sistemi prosesin tüm fiziksel sayfalarını "user mode" olarak ancak kernel'ın tüm sayfalarını "kernel mode" olarak ayarlamaktadır. User mode bir proses yalnızca user mode sayfalara erişebilmektedir. Kernel mode sayfalara erişememektedir. Eğer user mode bir proses kernel mode sayfaya erişmek isterse işlemci bir "içsel kesme (fault)" oluşturmakta ve işletim sistemi devreye girerek prosesi sonlandırmaktadır. Ancak kernel mode bir proses hem kernel mode sayfalara hem de user mode sayfalara erişebilmektedir. Bizim prosesimiz user mode'da çalışmaktadır. User mode prosesler bir user mode sayfaya erişirken işlemci erişim biçimine bakar ve duruma göre yine içsel kesme oluşturur. User mode bir proses user mode ancak read-only bir sayfaya yazma yaparsa içsel kesme (page fault) oluşturulmaktadır. Bu durumda işletim sistemi prosesi cezalandırarak sonlandırma yoluna gitmektedir. Ayrıca pek çok işlemci ailesinde bir kodun bir fiziksel sayfada çalışabilmesi için o kodun "execute" özelliğine sahip bir fiziksel sayfada bulunması gerekmektedir. Bu mekanizma altında örneğin bir proses "execute" olmayan bir fiziksel sayfadaki bir fonksiyonu çağırmak isterse yine işlemci içsel kesme (page fault) oluşturmaktadır. Örneğin C derleyicileri string'leri ELF formatında özel bir bölüme (section) yerleştirirler ve işletim sisteminin yükleyicisi de (UNIX/Linux sistemlerindeki exec fonksiyonları) bu sayfaları sayfa tablosunda oluştururken bu sayfaların özelliklerini "read-only" yaparlar. Böylece biz bir string'i değiştirmek istediğimizde koruma mekanizması yüzünden prosesimiz sonlandırılır. Zaten C'de string'lerin güncellenmesi "tanımsız davranış (undefined behavior)" olarak belirtilmektedir. Benzer biçimde derleyiciler genellikle global const nesneleri de yine "read-only" bölümlere (sections) yerleştirmektedir. (Ancak yerel const nesneler stack'te olduğu için read-only yerleştirilememektedir.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aynı programın ikinci kez çalıştırıldığını düşünelim. Bu durumda her şeyi aynı olan iki program çalışıyor durumda olacaktır. Ancak bu iki proses birbirinden bağımsız olduğuna göre bu iki prosesin farklı sayfa tabloları vardır ve aslında bu iki prosesin bellek alanı tamamen izole edilmelidir. İşte işletim sistemleri bu tür durumlarda "copy on write" denilen bir mekanizma uygulamaktadır. Bu mekanizmada işletim sistemi bir program ikinci kez çalıştırıldığında sayfa tablosunda önceki çalıştırma ile aynı fiziksel sayfaları eşler. Ancak bu sayfaları "read-only" biçimde işaretler. Proseslerden biri bu sayfaya yazma yaptığında içsel kesme (page fault) oluşur, işletim sistemi devreye girer tam yazma yapıldığı sırada o sayfanın bir kopyasını oluşturup iki prosesin fiziksel sayfalarını birbirinden ayırır. Böylece iki proses baştan aynı fiziksel sayfaları paylaşırken daha sonra bu fiziksel sayfalar birbirinden ayrıştırılmaktadır. Bu mekanizma sayesinde aslında hiç yazma yapılmayan fiziksel sayfaların boşuna bir kopyası oluşturulmamış olur. Örneğin ikinci çalıştırılan programın makine kodlarının bulunduğu sayfalar aslında hiç güncellenmemektedir. Bu durumda iki kopyanın aynı fiziksel sayfayı görmesinde bir sakınca yoktur. İşletim sistemleri dinamik kütüphanelerde de benzer tekniği kullanmaktadır. Bir dinamik kütüphane iki farklı proses tarafından kullanıldığında mümkün olduğunca bu proseslerin sayfa tabloları aynı fiziksel sayfaları gösterir. Ancak proseslerden biri dinamik kütüphanedeki bir sayfada değişiklik yaparsa o noktada bu sayfanın kopyasından oluşturulmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İşletim sistemleri dünyasında bir prosesin başka bir prosese bilgi göndermesi ve başka bir prosesten bilgi almasına "proseslerarası haberleşme (interprocess communication - IPC)" denilmektedir. Burada bilgi gönderip almadan kastedilen şey bir grup byte'ın gönderilip alınmasıdır. Proseslerarası haberleşme önemli bir konudur. Çünkü sayfa tabloları yoluyla birbirinden izole edilmiş proseslerin başka yöntemlerle birbirleriyle haberleşmesi gerekebilmektedir. Bir program başka bir programa bir şeyler gönderebilir, o program da bunları işleyebilir. Proseslerarası haberleşmenin en bariz örneği "client-server" sistemlerdir. Bu sistemlerde client program server programa bir istek gönderir. Server program da bu isteği yerine getirip sonuçları client programa iletir. Client-server tarzda haberleşmenin sağlanabilmesi için proseslerarası haberleşme denilen mekanizmanın kullanılması gerekir. Benzer biçimde "dağıtık sistemlerde (distributed systems)" de esas olarak proseslerarası haberleşme mekanizmaları kullanılmaktadır. Proseslerarası haberleşme mekanizmaları iki bölümde ele alınıp incelenmektedir: 1) Aynı makinenin prosesleri arasında haberleşme 2) Farklı makinelerin prosesleri arasında haberleşme Aynı makinenin prosesleri arasında haberleşme işletim sisteminin sağladığı özel yöntemlerle gerçekleştirilmektedir. Farklı makinelerin prosesleri arasında haberleşme için ortak uyulması gereken bazı kuralların belirlenmiş olması gerekir. Bu ortak kurallara "protokol (protocol)" denilmektedir. Farklı makinelerin prosesleri arasında haberleşme için çeşitli protokol aileleri oluşturulmuştur. Ancak bunlardan en yaygın kullanılanı "IP (Internet Protocol)" isimli protokol ailesidir. Biz kursumuzda önce aynı makinenin prosesleri arasındaki haberleşme yöntemlerini inceleyeceğiz daha sonra farklı makinelerin prosesleri arasında haberleşme yöntemleri üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aynı makinenin prosesleri arasında haberleşme için çeşitli işletim sistemlerinde birbirine benzer yöntemler geliştirilmiştir. Örneğin "boru (pipe)" haberleşmesi yöntemi UNIX/Linux sistemleriyle Windows sistemleri arasında benzer biçimde yürütülür. Paylaşılan bellek alanları (shared memory)" denilen haberleşme yöntemine yine benzer biçimde uygulanmaktadır. Ancak bazı işletim sistemlerinde o sisteme özgü özel yöntemler de olabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Proseslerarası haberleşme mekanizmasının iki yönü vardır. Birincisi iki prosesin haberleşeceği bir ortamın oluşturulmasıdır. İkincisi ise bu ortamın senkronize bir biçimde kullanılmasıdır. Yani proseslerarası haberleşme bir senkronizasyon sağlanarak gerçekleştirilmelidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- En yaygın kullanılan proseslerarası haberleşme yöntemi "boru (pipe)" haberleşmesi denilen yöntemdir. Boru haberleşmesi hem bir ortam sunarken hem de senkronizasyonu kendi içerisinde sağlamaktadır. Bu nedenle boru haberleşmesi kolay kullanılabilen bir IPC yöntemidir. Boru haberleşmeleri "isimsiz boru haberleşmeleri" ve "isimli boru haberleşmeleri" olmak üzere ikiye ayrılmaktadır. İsimsiz boru haberleşmelerine İngilizce "unnamed pipe" ya da "anonymous pipe", isimli boru haberleşmelerine ise "named pipe" ya da "fifo" denilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Boru aslında FIFO tarzında çalışan bir kuyruk sistemidir. Bir proses boruya yazma yapar, diğeri de borundan yazılanları okur. UNIX/Linux sistemlerinde borular birer dosya gibi ele alınmaktadır. Dolayısıyla boruya yazma işlemi "write" fonksiyonu ile borudan okuma işlemi ise "read" fonksiyonu ile yapılmaktadır. Yazan taraf boruya bir grup byte'ı yazdığında okuyan taraf bunları aynı sırada okur. Boruların belli bir uzunlukları vardır. Eskiden BSD ve Linux sistemlerinde boru uzunlukları 4096 byte (bir sayfa) büyüklüğündeydi. Linux daha sonra default boru uzunluğunu 65536'ya (16 sayfaya) yükseltmiştir. UNIX/Linux sistemlerinde borular tek yönlüdür. Bu nedenle proseslerden biri boruya yazma yaparken diğeri borudan okuma yapar. (Halbuki örneğin Windows sistemlerinde borular çift yönlüdür.) Borunun tek yönlü olması demek iki prosesin de yazdıklarının aynı boruya yazılması demektir. P1 prosesi boruya yazma yapıp yine P1 prosesi borudan okuma yaparsa kendi yazdığını okur. Bunun da bir anlamı olmaz. Halbuki çift yönlü borularda borudan hem okuma hem yazma yapılabilmektedir. Borudan okuma yapıldığında karşı tarafın yazdığı okunur. Ancak UNIX/Linux sistemlerindeki yukarıda da belirttiğimiz gibi borular tek yönlüdür. Dolayısıyla haberleşmede bir taraf yazma yaparken diğer taraf okuma yapmalıdır. Eğer borularla karşılıklı okuma yazma yapılmak isteniyorsa iki boru kullanılmalıdır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Borulara write POSIX fonksiyonu ile yazma yapılırken yazılmak istenen byte kadar boruda yer yoksa write işlemi blokeye yol açar ve boruda yazılmak istenen miktar kadar boş alan oluşana kadar ilgili thread blokede bekletilir. Örneğin biz boruya 50 byte yazmak isteyelim. Eğer boruda en az 50 byte'lık boş yer varsa biz bloke olmadan bu işlemi yaparız ve write fonksiyonu 50 değeri ile geri döner. Ancak boruda örneğin 40 byte boş yer varsa bu durumda write fonksiyonu 50 byte'lık yer açılana kadar blokede bekler. 50 byte'lık yer açıldığında bu 50 byte'ı boruya yazar ve 50 değeri ile geri döner. Böylece bir proses sürekli boruya yazma yapar ancak karşı taraf borudan okuma yapmazsa boru dolar en sonunda write fonksiyonu blokede bekler. Karşı taraf borudan okuma yaptığında boruda yer açılmaktadır. Borulara yazma işlemi atomik düzeyde yapılmaktadır. Atomik yazma demekle sanki yazma işleminin tek hamlede araya hiçbir akış girmeden yazılması kastedilmektedir. Farklı prosesler aynı boruya aynı anda yazma yapsalar bile bu yazılanlar iç içe geçmez. Ancak borularda PIPE_BUF denilen bir sembolik sabit değeri vardır. Eğer iki prosesten en az biri bu PIPE_BUF değerinden daha yüksek miktarda byte'ı boruya yazmak isterse bu durumda iç içe geçme oluşabilir. Yani birden fazla prosesin aynı boruya aynı zamanda yazma yapması durumunda iç içe geçmenin olmaması için yazılanların PIPE_BUF sembolik sabitinden daha küçük ya da ona eşit olması gerekir. PIPE_BUF pek çok sistemde (Linux'ta da böyle) 4096 değerindedir. Burada önemli bir nokta şudur: Biz PIPE_BUF değerinden daha fazla byte'ı boruya yazmak istediğimizde yine tüm bilgi boruya yazılana kadar bloke oluşmaktadır. Ancak bu durumda iç içe geçmeme garanti edilememektedir. Ayrıca proses borunun uzunluğundan fazla bilgiyi boruya yazmaya çalışabilir. Bu durumda yine tüm bilgi boruya yazılana kadar bloke oluşmaktadır. Normal olarak borulara yazma yaparken write fonksiyonu tüm byte'lar yazılana kadar bloke oluşturduğuna göre write fonksiyonun yazılmak istenen byte sayısı ile geri dönmesi beklenir. Gerçekten de hemen her zaman böyle olmaktadır. Ancak write fonksiyonu ile boruya yazma yapılırken bir sinyal oluşursa bu durumda POSIX standartları kısmi yazma yapılabileceğini söylüyorsa da Linux sistemlerinde bu durumda boruya hiçbir şey yazmadan write fonksiyonu başarısız olur ve -1 değeri ile geri döner. errno değişkeni de EINTR değeriyle set edilir. Linux sistemlerinde yine write fonksiyonu boruda yeterince yer yoksa ve blokede bekliyorsa sinyal oluştuğunda boruya bir şey yazmadan -1 ile geri dönmektedir. (Yani blokeli boru yazımlarında POSIX standartlarına göre kısmi yazım söz konusu olabilirse de Linux sistemlerinde "kısmi yazım (partial write)" işlemi yapılmamaktadır.) Boruya 0 byte yazılmak istenirse bu durum POSIX standartlarında "unspecified" bırakılmıştır. Ancak pek çok UNIX türevi sistem (Linux'ta da böyle) 0 byte yazma durumunda boruya bir şey yazmamakta basit bazı kontroller yapıp write fonksiyonunu sonlanmaktadır. Eğer bu kontrollerde bir sorun yoksa write fonksiyonu 0 ile geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- read fonksiyonu ile borudan okuma yapılırken read fonksiyonu eğer boru tamamen boşsa en az 1 byte boruda olana kadar blokeye yol açar. Ancak boruda en az 1 byte bilgi varsa read okuyabildiği kadar byte'ı okur, blokeye yol açmadan hemen okuyabildiği byte sayısı ile geri döner. Yani read tüm byte'lar okunana kadar değil, en az bir byte okunana kadar beklemeye yol açmaktadır. Bu bakımdan write gibi davranmamaktadır. Örneğin biz borudan 50 byte okumak isteyelim. Ancak boruda 10 byte bulunuyor olsun. read fonksiyonu 50 byte okunana kadar beklemez. O 10 byte'ı okur, bu 10 değeri ile geri döner. Ancak boruda hiç bilgi yoksa read blokede en az 1 byte boruda olana kadar blokede bekleyecektir. Örneğin biz borudan 50 byte okumak isteyelim. Ancak boruda hiç bilgi olmasın. read fonksiyonu blokede bekler. O sırada bir proses boruya 10 byte yazmış olsun. Şimdi read bu 10 byte'ı alarak işlemini sonlandırır ve 10 değeri ile geri döner. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi boru haberleşmesi nasıl sonlandırılmaktadır? Boruyu kesinlikle önce yazan tarafın kapatması gerekir. Bu durumda okuyan taraf önce boruda kalanları okur. Artık boruda okunacak bir şey kalmamışsa ve yazan taraf da boruyu kapatmışsa read fonksiyonu 0 değeri ile geri döner. Yukarıda da belirttiğimiz gibi normalde read fonksiyonu boruda hiç bilgi yoksa blokede beklemektedir. Ancak eğer boruya yazma potansiyelinde hiçbir betimleyici kalmadıysa bu durumda read fonksiyonu bloke olmadan 0 özel değeri ile geri döner. O halde sonlandırma şöyle yapılmalıdır: Önce yazan taraf boruyu kapatır. Sonra okuyan taraf boruda kalanları okur ve read fonksiyonu 0 ile geri döndüğünde okuyan taraf da boruyu kapatır. Boru haberleşmesinde eğer boruyu yanlış bir biçimde önce okuyan taraf kapatırsa yazan taraf boruya yazma yaptığında SIGPIPE isimli bir sinyal oluşmaktadır. Bu sinyal de prosesin sonlanmasına yol açacaktır. Yani okuma tarafı kapatılmış bir boruya yazma yapmak normal bir durum değildir. Ancak yazma tarafı kapatılmış bir borudan okuma yapmaya çalışmak normal bir durumdur. Yukarıda da belirttiğimiz gibi bu durumda read boruda bir şey kalmadıysa 0 ile geri dönecektir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İsimsiz boru haberleşmesi üst ve alt prosesler arasında yapılabilen bir haberleşmedir. İsimsiz boru haberleşmesi tipik olarak şu aşamalardan geçilerek gerçekleştirilir: 1) Üst proses henüz fork ile alt prosesi yaratmadan önce pipe isimli POSIX fonksiyonu ile isimsiz boruyu yaratmalıdır. pipe fonksiyonunun prototipi şöyledir: #include int pipe(int pipefd[2]); pipe fonksiyonu int türden bir dizinin başlangıç adresini parametre olarak alır. Prototipteki dekleratör bir gösterici dekleratörüdür. pipe fonksiyonu boruyu yaratır. Borudan okuma yapmak ve boruya yazma yapmak için iki dosya betimleyicisi oluşturur. O betimleyicilerin numaralarını da bizim fonksiyona geçirdiğimiz iki elemanlı int diziye yerleştirir. Dizinin ilk elemanına borudan okuma yapmak için kullanılacak betimleyiciyi, ikinci elemanına ise boruya yazma yapmak için kullanılacak betimleyiciyi yerleştirmektedir. Tabii bu betimleyiciler dosya betimleyici tablosunda tahsis edilmiş durumdadır. Bu betimleyicilerin gösterdiği dosya nesneleri boruya erişmek için gereken bilgileri tutmaktadır. Bize verilen dizinin ilk elemanına yerleştirilen betimleyici read-only, dizinin ikinci elemanına yerleştirilen betimleyici ise write-only bir betimleyicidir. Yani biz ilk betimleyici ile yalnızca read işlemi ikinci betimleyici ile yalnızca write işlemi yapabiliriz. pipe fonksiyonunun dosya betimleyici tablosundaki en düşük numaralı boş betimleyicileri vereceği POSIX standartlarında garanti edilmiştir. Ancak pipe fonksiyonun verdiği iki betimleyicinin hangisinin düşük numaralı betimleyici olacağının bir garantisi yoktur. 2) Boru yaratıldıktan sonra üst prosesin fork işlemi ile alt prosesi yaratması gerekir. fork işlemiyle birlikte üst prosesteki tüm betimleyiciler alt proseste de aynı değerlerle aynı dosya nesnelerini gösterir biçimde oluşturulacaktır. Yani artık üst ve alt prosesler aynı numaralı betimleyicilerle boruya erişebilir durumda olur. 3) Artık üst ve alt prosesler arasında haberleşme için bir karar verilmelidir. Kim yazacak kim okuyacaktır? Bundan sonra boruya yazma ve okuma potansiyelinde olan birer betimleyici bırakılmalıdır. Yani yazan taraf okuma betimleyicisini okuyan taraf yazma betimleyicisini kapatmalıdır. 4) Artık okuma ve yazma işlemleri read ve write fonksiyonlarıyla gerçekleştirilebilir. 5) Haberleşmeyi sonlandırmak için yazan taraf boruyu kapatır. Okuyan taraf önce boruda kalanları okur, sonra read fonksiyonu 0 ile geri döner. Böylece okuyan taraf da boruyu kapatır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 37. Ders 12/03/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki programda üst proses boruya yazma yapmakta alt proses de borudan okuma yapmaktadır. Alt proses üst prosesin yazdığı int sayıları okumaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { int pid; int fdpipe[2]; int result; if (pipe(fdpipe) == -1) exit_sys("pipe"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent writes */ close(fdpipe[0]); for (int i = 0; i < 1000000; ++i) if (write(fdpipe[1], &i, sizeof(int)) == -1) exit_sys("write"); close(fdpipe[1]); if (wait(NULL) == -1) exit_sys("wait"); } else { /* child reads */ int val; close(fdpipe[1]); while ((result = read(fdpipe[0], &val, sizeof(int))) > 0) { printf("%d ", val); fflush(stdout); } if (result == -1) exit_sys("read"); close(fdpipe[0]); printf("\n"); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi isimsiz boru haberleşmesinde neden okuyan taraf yazma betimleyicisini, yazan taraf da okuma betimleyicisini kapatmaktadır? İşte read fonksiyonunun 0 ile geri dönmesi için boruya yazma potansiyelinde olan tek bir betimleyicinin bulunuyor olması gerekir. Eğer okuyan taraf yazma betimleyicisini kapatmazsa yazan taraf yazma betimleyicisini kapatsa bile boruya hala yazma potansiyelinde olan bir betimleyici kaldığı için read fonksiyonu 0 ile geri dönmeyecektir. Bu durumda haberleşmenin sonlandırılması sorunlu hale gelecektir. Yazan tarafın okuma betimleyicisini kapatmasının diğeri gibi kritik bir önemi yoktur. Ancak prensip olarak kullanılmayan betimleyicilerin kapatılması iyi bir tekniktir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Üst ve alt prosesler arasında haberleşme yapmak kişilere biraz tuhaf gelebilmektedir. Çünkü fork işleminden sonra zaten her iki kod da programı yazan kişi tarafından yazılmış olmaktadır. Ancak üst ve alt prosesler arasında boru haberleşmelerinin gerektiği önemli durumlar vardır. Eskiden thread'ler yokken bir işi daha hızlı yapmak için prosesler kullanılıyordu. Üst proses fork işlemi yapıp alt prosesle koordineli bir biçimde işleri paylaşıyordu. O günlerde bu koordinasyonun sağlanması için üst ve alt prosesler arasında haberleşme de gerekiyordu. Thread'lerden sonra artık bu tür işlemler prosesler yerine thread'lerle yapılmaya başlanmıştır. Üst ve alt prosesler arasında exec sonrasına da haberleşmeler yapılabilmektedir. Tabii bu durumda exec yapılan alt prosesin exec sonrasında boru betimleyicilerinin numaralarını biliyor olması gerekir. Ancak stdin, stdout ve stderr dosyalarının betimleyicileri sabit olduğuna göre exec sonrasında haberleşme bu betimleyiciler yoluyla yapılabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki programda kabuktakine benzer bir boru yönlendirmesi yapılmıştır. Program komut satırı argümanı olarak aşağıdaki gibi bir yazı almaktadır: "prog1 arg1 arg2 ... | prog2 arg1 arg2 ..." Program buradaki "|" sembolünün yerini bulur. Sonra bunun iki tarafını strtok ile parse ederek iki ayrı gösterici dizisine yerleştirir. Sonra üst proses pipe fonksiyonuyla boruyu yaratır. Soldaki ve sağdaki programlar için fork işlemi yapar. Soldaki programı henüz exec ile çalıştırmadan o alt prosesin stdout dosyasını boruya yönlendirir. Benzer biçimde sağdaki programı henüz çalıştırmadan o prosesin de stdin dosyasını boruya yönlendirir. Sonra da exec işlemlerini uygular. Kabuk aşağıdaki gibi bu boru işlemini yapmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #define MAX_ARG 1024 void exit_sys(const char *msg); void exit_sys_child(const char *msg); int main(int argc, char *argv[]) { char *ppos, *str; char *cmdl[MAX_ARG + 1], *cmdr[MAX_ARG + 1]; int pipefds[2]; pid_t pidl, pidr; int n; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((ppos = strchr(argv[1], '|')) == NULL) { fprintf(stderr, "invalid argument: %s\n", argv[1]); exit(EXIT_FAILURE); } *ppos = '\0'; n = 0; for (str = strtok(argv[1], " \t"); str != NULL; str = strtok(NULL, " \t")) cmdl[n++] = str; cmdl[n] = NULL; if (n == 0) { fprintf(stderr, "invalid argument!...\n"); exit(EXIT_FAILURE); } n = 0; for (str = strtok(ppos + 1, " \t"); str != NULL; str = strtok(NULL, " \t")) cmdr[n++] = str; cmdr[n] = NULL; if (n == 0) { fprintf(stderr, "invalid argument!...\n"); exit(EXIT_FAILURE); } if (pipe(pipefds) == -1) exit_sys("pipe"); if ((pidl = fork()) == -1) exit_sys("fork"); if (pidl == 0) { close(pipefds[0]); if (pipefds[1] != 1) { /* bu kontrol normal durumda yapılmayabilir */ if (dup2(pipefds[1], 1) == -1) exit_sys_child("dup2"); close(pipefds[1]); } if (execvp(cmdl[0], cmdl) == -1) exit_sys_child("execvp"); /* unreachable code*/ } if ((pidr = fork()) == -1) exit_sys("fork"); if (pidr == 0) { close(pipefds[1]); if (pipefds[0] != 0) { /* bu kontrol normal durumda yapılmayabilir */ if (dup2(pipefds[0], 0) == -1) exit_sys_child("dup2"); close(pipefds[0]); } if (execvp(cmdr[0], cmdr) == -1) exit_sys_child("execvp"); /* unreachable code*/ } close(pipefds[0]); close(pipefds[1]); if (waitpid(pidl, NULL, 0) == -1) exit_sys("waitpid"); if (waitpid(pidr, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_child(const char *msg) { perror(msg); _exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Borularla ilgili iki yardımcı ve ilginç POSIX fonksiyonu vardır: popen ve pclose. popen fonksiyonu bir boru yaratır sonra kabuk programını interaktif olmayan bir biçimde (-c seçeneği ile) çalıştırır. Sonra kabuk programının stdin ya da stdout dosyalarını bu yarattığı boruya yönlendirir. Borunun diğer ucunu da fdopen fonksiyonundan faydalanarak bir dosya bilgi göstericisine (stream) dönüştürüp bize vermektedir. Fonksiyonun prototipi şöyledir: #include FILE *popen(const char *command, const char *mode); Fonksiyonun birinci parametresi çalıştırılacak kabuk komutunu belirtir. İkinci parametre yalnızca "r" ya da "w" olabilir. Eğer bu parametre "r" girilirse kabuk komutunun stdout dosyası boruya yönlendirilir. Borunun okuma ucu da bize verilir. Yani biz, bize verilen dosyadan okuma yaptığımızda aslında kabuk komutunun stdout dosyasına yazdıklarını okumuş oluruz. Eğer ikinci parametre "w" olarak girilirse bu durumda biz bu dosyaya yazma yaptığımızda aslında kabukta çalıştırdığımız program bunu stdin dosyasından okuyacaktır. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir. popen ile yaratılmış olan boru ve dosya bilgi göstericisi pclose fonksiyonu ile yok edilmektedir. pclose fonksiyonunun prototipi de şöyledir: #include int pclose(FILE *stream); Fonksiyon dosya bilgi göstericisini parametre olarak alır ve daha önce yapılan işlemleri sonlandırır. pclose fonksiyonu aynı zamanda wait işlemini de uygulamaktadır. (Yani thread pclose çağrısında bloke olabilmektedir.) Fonksiyon başarı durumunda çalıştırdığı kabuk programının wait fonksiyonu ile elde edilen durum bilgisine, başarısızlık durumunda -1 değerine geri dönmektedir. Kabuk programlarının interaktif olmayan modda çalıştırıldıklarına çalıştırdıkları programın durum koduyla (yani wait fonksiyonlarından elde edilen değerleri kastediyoruz) sonlandığını anımsayınız. pclose kontrolü şöyle yapılabilir: if ((status = pclose(f)) == -1) exit_sys("pclose"); Ancak eğer programcı çalıştırmış olduğu programın da başarısını dikkate almak isterse kontrolü şöyle yapabilir: if ((status = pclose(f)) == -1) exit_sys("pclose"); if (WIFEXITED(status)) printf("Shell exit status: %d\n", WEXITSTATUS(status)); else printf("shell abnormal terminated!...\n"); Aşağıdaki programda "ls -l" komutu çalıştırılıp onun çıktısı elde edilmiştir. Programda pclose fonksiyonun geri dönüş değerinin alınıp kontrol edilmesine genellikle gerek yoktur. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include void exit_sys(const char *msg); int main(void) { FILE *f; int ch; if ((f = popen("ls -l", "r")) == NULL) exit_sys("popen"); while ((ch = fgetc(f)) != EOF) putchar(ch); if ((status = pclose(f)) == -1) exit_sys("pclose"); pclose(f); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte popen fonksiyonu "w" moduyla çağrılmıştır. Bu durumda bizim dosyaya yazdıklarımız aslında boruya yazılacak ve "wc" programı da stdin yerine borudan okuma yapacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include void exit_sys(const char *msg); int main(void) { FILE *f; int ch; if ((f = popen("wc", "w")) == NULL) exit_sys("popen"); for (int i = 0; i < 100; ++i) fprintf(f, "%d\n", i); pclose(f); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- popen fonksiyonu ile aldığımız dosya bilgi göstericisinin (stream) tamponlu çalıştığına dikkat ediniz. Dolayısıyla yazdığımız şeyler önce tampona aktarılıp oradan boruya aktarılacaktır. Uygulamaya göre gerekirse fflush ile tamponu tazeleyebilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İsimsiz borular yalnızca üst ve alt prosesler arasındaki haberleşmelerde kullanılmaktadır. Halbuki isimli borular herhangi iki proses arasında haberleşmede kullanılabilmektedir. İsimli borularla çalışma tipik olarak şu aşamalardan geçilerek yapılır: 1) Önce ismine "boru dosyası" ya da "fifo dosyası" denilen özel bir dosyanın yaratılması gerekir. Boru dosyaları "ls -l" komutunda "p" dosya türü ile gösterilmektedir. Boru dosyaları gerçek disk dosyaları değildir. Yalnızca bir dizin girişi içerirler. Bunların diskte bir içerik olarak karşılıkları yoktur. Bu nedenle boru dosyaları hep 0 uzunlukta görüntülenmektedir. Boru dosyaları open fonksiyonuyla yaratılmaz. Bunları yaratmak için mkfifo isimli POSIX fonksiyonu kullanılmaktadır. mkfifo fonksiyonunun prototipi şöyledir: #include int mkfifo(const char *path, mode_t mode); Fonksiyonun birinci parametresi yaratılacak boru dosyasının yol ifadesini, ikinci parametresi ise erişim haklarını almaktadır. Bu erişim hakları yine prosesin umask değeri ile maskelenmektedir. Fonksiyon başarı durumunda 0 başarısızlık durumunda -1 değerine geri dönmektedir. Örneğin: if (mkfifo("testfifo", S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH) == -1) exit_sys("mkfifo"); Boru dosyaları manuel olarak da mkfifo isimli kabuk komutuyla yaratılabilmektedir. Örneğin: mkfifo myfifo Erişim hakları -m seçeneği ile verilebilir. Örneğin: mkfifo -m 666 myfifo Tabii komut uygulanırken kabuğun umask değeri etkili olmamaktadır. 2) İki proses de boru dosyasını open fonksiyonuyla açar. Açım sırasında tipik olarak O_RDONLY ve O_WRONLY modları kullanılmalıdır. Boru dosyaları O_RDWR modunda açılabilirse de bu durum genellikle uygun değildir. (Linux boru dosyalarının O_RDWR modunda açılmasına izin vermektedir. Ancak POSIX standartları bu durumu "undefined" bırakmıştır.) Örneğin: if ((fd = open("myfifo", O_WRONLY)) == -1) exit_sys("open"); Bir proses isimli boruyu O_WRONLY modunda açmışsa başka bir proses boruyu O_RDONLY (ya da O_RDWR modunda) açana kadar open blokede beklemektedir. Benzer biçimde bir proses boruyu O_RDONLY modunda açmışsa diğer bir proses boruyu O_WRONLY (ya da O_RDWR modunda) açana kadar open fonksiyonu blokede bekler. Tabii boru O_RDWR modunda açılmışsa bloke oluşmaz. Ancak bu modda açım genel olarak uygun değildir ve POSIX standartları bunu "undefined" olarak ele almaktadır. open fonksiyonunun dosya betimleyici tablosundaki en düşük betimleyiciyi verdiğini anımsayınız. 3) Artık haberleşecek iki proses de boruyu açmıştır. Haberleşme write ve read fonksiyonlarıyla yukarıda belirtildiği gibi yapılır. Yani buradaki haberleşmenin isimsiz boru haberleşmesinden bir farkı yoktur. write ve read fonksiyonları tamamen isimsiz boru haberleşmesinde olduğu gibi davranmaktadır. 4) Haberleşmenin sonunda yine yazan taraf boruyu kapatır, okuyan tarafın read fonksiyonu 0 ile geri döner. Böylece okuyan taraf da boruyu kapatır. İsimli boruyu kullanan hiçbir betimleyici kalmadığında isimli borunun içi silinmektedir. Örneğin isimli boruyu iki proses de açmış olsun. Birinin yazdığını diğerinin okuduğunu varsayalım. Yazan taraf boruyu kapattıktan sonra okuyan taraf borudakilerin hepsini okumadan boruyu kapatırsa boru içinde kalan bilgiler silinmektedir. 5) Eğer boru dosyası proseslerden biri tarafından mkfifo fonksiyonuyla yaratılmışsa unlink ya da remove fonksiyonuyla yine ilgili proses tarafından silinebilir. Eğer boru komut satırından mkfifo komutuyla yaratılmışsa bu durumda yine komut satırından rm komutu ile silme yapılabilir. Tabii boru dosyası başka bir haberleşme için de kullanılacaksa silinmeden bekletilebilir. Örneğin: if (unlink("mypipe") == -1) exit_sys("mypipe") ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 38. Ders 18/03/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte "prog1" programı zaten komut satırında yaratılmış olan "mypipe" dosyasına yazma yapıp, "prog2" programı da bu borudan okuma yapmaktadır. "prog1" programı klavyeden (stdin dosyasından) bir yazı alır. Bu yazıyı borudan "prog2" programına borudan yollar. Ancak bu örnekte prog2 programı borudan ne kadar bilgi okuyacağını bilmemektedir. Dolayısıyla 4096 byte kadar bilgiyi okumak ister. Tabii read fonksiyonu borudan okuma yaparken 4096 byte okunanan kadar blokeye yol açmaz. Okuyabildiği kadar bilgiyi okur okuyabildiği byte sayısına geri döner. Yani böylede prog2 programındaki read fonksiyonu karşı tarafın atomik bir biçimde boruya yazdığı kadar bilgiyi okumaktadır. Haberleşme "prog1" programının klavyeden (stdin dosyasından) "quit" okumasıyla sonlanmaktadır. Bu durumda "prog1" döngüden çıkar boruyu kapatır."prog2"de ise read fonksiyonu 0 ile geri döner. Böylece "proc2" de döngüden çıkar ve boruyu kapatır. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fdpipe; char buf[BUFFER_SIZE]; char *str; if ((fdpipe = open("mypipe", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) != NULL) if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; *str = '\0; if (*buf == '\0') continue; if (write(fdpipe, buf, strlen(buf)) == -1) exit_sys("write"); if (!strcmp(buf, "quit")) break; } close(fdpipe); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fdpipe; ssize_t result; char buf[BUFFER_SIZE + 1]; if ((fdpipe = open("mypipe", O_RDONLY)) == -1) exit_sys("open"); while ((result = read(fdpipe, buf, BUFFER_SIZE)) > 0) { buf[result] = '\0'; puts(buf); } if (result == -1) exit_sys("read"); close(fdpipe); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi boru dosyası haberleşecek proseslerden biri tarafından yaratılabilir. Bu durumda bu boru dosyasının onu yaratan proses tarafından silinmesi uygun olur. Fakat boru dosyalarının dışarıdan yaratılması çoğu kez tercih edilmektedir. Çünkü bu durumda iki programın çalıştırma sırasının bir önemi olmaktadır. Halbuki boru dosyası dışarıda yaratılırsa programların çalıştırma sıralarının bir önemi olmaz. Anımsanacağı gibi UNIX/Linux sistemlerinde bir dosya remove ya da unlink fonksiyonu ile silindiğinde açık betimleyiciler normal olarak çalışmaya devam eder. Son betimleyici kapatıldığında dosya gerçek anlamda silinmektedir. Aynı durum boru dosyaları için de geçerlidir. Aşağıdaki örnekte "prog1" programı isimli boruyu mkfifo fonksiyonuyla yaratmış ve sonlanmadan önce unlink fonksiyonu ile boruyu silmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fdpipe; char buf[BUFFER_SIZE]; char *str; if (mkfifo("mypipe", S_IRUSR|S_IWUSR|S_IRGRP|S_IRGRP) == -1) exit_sys("mypipe"); if ((fdpipe = open("mypipe", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) != NULL) if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; *str = '\0'; if (*buf == '\0') continue; if (write(fdpipe, buf, strlen(buf)) == -1) exit_sys("write"); if (!strcmp(buf, "quit")) break; } close(fdpipe); if (unlink("mypipe") == -1) exit_sys("unlink"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fdpipe; ssize_t result; char buf[BUFFER_SIZE + 1]; if ((fdpipe = open("mypipe", O_RDONLY)) == -1) exit_sys("open"); while ((result = read(fdpipe, buf, BUFFER_SIZE)) > 0) { buf[result] = '\0'; puts(buf); } if (result == -1) exit_sys("read"); close(fdpipe); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Stream tabanlı haberleşmelerde önemli problemlerden biri de "değişken uzunlukta kayıtların" aktarımıdır. Yani örneğin boruya yazan taraf sürekli farklı uzunluklarda kayıtları boruya yazsa, okuyan taraf bu yazılanları nasıl birbirinden ayrıştıracaktır? İşte bu tür durumda iki yöntem akla gelmektedir: 1) Yazan taraf önce değişken uzunluktaki kaydın byte miktarını boruya yazar, sonra değişken uzunluktaki kaydı boruya yazar. Okuyan taraf da önce uzunluğu okur sonra o uzunluk kadar yeniden okuma yapar. Bu yöntem etkin ve çoğu zaman tercih edilen yöntemdir. 2) Yazan taraf kayıtların sonuna özel bir byte yerleştirir. (Örneğin kayıtlar yazısalsa '\n' gibi bir karatker yerleştirilebilir.) Böylece okuyan taraf o özel byte'ı görene kadar okuma yapabilir. Buradaki problem bu özel byte'ın okuyan taraf tarafından nasıl tespit edileceğidir. read fonksiyonu ile sürekli 1 byte okumak etkin bir yöntem değildir. O zaman bir blok bilginin okunup bir tampona yerleştirilmesi ve o tampondan akıllıca kontrol yapılması yoluna gidilir. Buna benzer yazısal aktarımlarda bazen programcılar kaydın sonuna '\n' karakterini yerleştirip boru betimleyicisinden fdopen fonksiyonu ile dosya bilgi gösterici (stream) elde edip fgets gibi bir fonksiyonla okuma yapma yoluna gidebilmektedir. Ne de olsa fgets tamponlu biçimde çalıştığı için bizim yapmamız gerekenleri kendisi yapmaktadır. Tabii aslında önce boruyu açıp sonra fdopen fonksiyonunu kullanmak yerine doğrudan fopen fonksiyonuyla da boru açılabilir. Aşağıdaki örnekte "prog1" programı bir dizin'in yol ifadesini komut satırı argümanıyla almış o dizindeki dosyaları elde edip önce onların uzunluklarını sonra da isimlerini boruya yazmıştır. Okuyan taraf da (prog2 programı) önce uzunluğu sonra içeriği okuyup kayıtları birbirinden ayırabilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fdpipe; DIR *dir; struct dirent *de; size_t len; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((dir = opendir(argv[1])) == NULL) exit_sys("opendir"); if ((fdpipe = open("mypipe", O_WRONLY)) == -1) exit_sys("open"); while (errno = 0, (de = readdir(dir)) != NULL) { len = strlen(de->d_name); if (write(fdpipe, &len, sizeof(size_t)) == -1) exit_sys("write"); if (write(fdpipe, de->d_name, len) == -1) exit_sys("write"); } if (errno != 0) exit_sys("readdir"); close(fdpipe); closedir(dir); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fdpipe; ssize_t result; char buf[BUFFER_SIZE + 1]; size_t len; if ((fdpipe = open("mypipe", O_RDONLY)) == -1) exit_sys("open"); for (;;) { if ((result = read(fdpipe, &len, sizeof(size_t))) == -1) exit_sys("read"); if (result == 0) break; if (read(fdpipe, buf, len) == -1) exit_sys("read"); buf[len] = '\0'; puts(buf); } close(fdpipe); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte yukarıda belirttiğimiz yöntemlerin ikincisi kullanılmıştır. Yani her kayıttan sonra özel bir sonlandırıcı karakter boruya eklenmiştir. Biz burada sonlandırıcı karakter için '\0' değil '\n' karakterini tercih ettik. Çünkü C'de '\0' görene kadar okuma yapan standart bir fonksiyon yoktur. Halbuki '\n' görene kadar okuma yapan bir fgets gibi bir standart fonksiyon vardır. Aşağıdaki program hakkında şu özet açıklamaları yapmak istiyoruz: - Boruya yazan taraf dosya ismini boruya yazdıktan sonra '\n' karakterini de sonlandırıcı oluşturmak amacıyla boruya yazmıştır. - Okuyan taraf tek tek karakterleri read fonksiyonuyla borudan okumak yerine standart fgets fonksiyonundan faydalanmıştır. Tabii bunun için boruyu açtıktan sonra fdopen fonksiyonuyla ondan bir stream elde etmiştir. Şüphesiz bu tür durumlarda boru doğrudan fopen fonksiyonuyla da açılabilir. fopen fonksiyonuyla açım alternatifi "prog3.c" dosyasında verilmiştir. - Yazan taraf boruyu kapattığında fgets fonksiyonu NULL adresle geri dönecektir. Anımsanacağı gibi fgets en az bir byte okursa ikinci parametresi ile belirtilen adrese, hiç byte okuyamadan EOF ile karşılaşırsa NULL adrese geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fdpipe; DIR *dir; struct dirent *de; char delim = '\n'; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((dir = opendir(argv[1])) == NULL) exit_sys("opendir"); if ((fdpipe = open("mypipe", O_WRONLY)) == -1) exit_sys("open"); while (errno = 0, (de = readdir(dir)) != NULL) { if (write(fdpipe, de->d_name, strlen(de->d_name)) == -1) exit_sys("write"); if (write(fdpipe, &delim, 1) == -1) exit_sys("write"); } if (errno != 0) exit_sys("readdir"); close(fdpipe); closedir(dir); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fdpipe; char buf[BUFFER_SIZE]; FILE *fpipe; if ((fdpipe = open("mypipe", O_RDONLY)) == -1) exit_sys("open"); if ((fpipe = fdopen(fdpipe, "r")) == NULL) exit_sys("fdopen"); for (;;) { if (fgets(buf, BUFFER_SIZE, fpipe) == NULL) break; printf("%s", buf); } fclose(fpipe); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog3.c */ #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { char buf[BUFFER_SIZE]; FILE *fpipe; if ((fpipe = fopen("mypipe", "r")) == NULL) exit_sys("fdopen"); for (;;) { if (fgets(buf, BUFFER_SIZE, fpipe) == NULL) break; printf("%s", buf); } fclose(fpipe); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Client-Server haberleşme konusunda bir tasarım mimarisidir. Genellikle "client-server" denildiğinde konu TCP/IP soket haberleşmesi ile ilişkilendirilmektedir. Ancak client-server haberleşme aslında genel bir konudur. Haberleşmedeki ortam değişebilir. Haberleşecek birimler aynı makinenin prosesleri olabildiği gibi farklı makinelerin prosesleri de olabilmektedir. İşte client-server haberleşme aynı makinenin prosesleri arasında borular kullanılarak da yapılabilmektedir. Client-Server haberleşmede iki program söz konusudur: Client program ve server program. Burada asıl işi yapan program server programdır. Client program bir istekte bulunur. Server program da bu isteği karşılar. Client program istekte bulunurken server programa "mesaj" adı altında bir bilgi gönderir. Server da client için bir isteği gerçekleştirdikten sonra yine mesaj adı altında client'a bir bilgi göndermektedir. Client-Server tarzı haberleşme en çok TCP/IP protokolü ile yapılıyor olsa da borular yoluyla da yapılabilmektedir. Pekiyi borular yoluyla çok client'lı (multi-client) bir client-server uygulama yapmak için kaç boruya ihtiyaç vardır? Client programların isteklerini farklı borularla server programa iletmesine gerek yoktur. Client ptogramlar ortak tek bir boru kullanarak isteklerini server programa iletebilirler. Bu durumda server program da belli uzunluktaki kayıtları okuyarak istekleri elde eder. O isteklerin hangi client'tan geldiğini mesajın içerisinden öğrenebilir. Ancak server programın isteğin yanıtını tek bir boruyla client programlara iletmesi mümkün değildir. Mecburen her client için farklı borunun kullanılması gerekir. O halde şunlar söylenebilir: - Client programlar tek ve ortak bir boru ile server programa istekte bulunurlar. Bu borunun işin başında yaratılmış olması uygundur. - Server program da her client için farklı bir boru ile client'a istek sonucunu gönderir. Server'ın client'a mesaj gönderdiği o client'a özgü boruların client tarafından yaratılması ancak "CONNECT" mesajında server programa iletilmesi daha uygun bir çözümdür. Bu boru yine "DISCONNECT" mesajı sonrasında client program tarafından yok edilir. - Server programın kendisine bağlanmış olan tüm client'ların bilgilerini tutması gerekir. Client'ları birbirinden ayıran "sistem genelinde tek olan (unique)" bir değerin id olarak belirlenmesi uygundur. Bunun için akla ilk gelen proses id'lerin client'ları birbirinden ayırmak için kullanılmasıdır. Bu durumda server program bir id'ye göre client bilgilerine hızlı erişim sağlamalıdır. Bunun için en uygun veri yapısı şüphesiz "dengelenmiş ikili ağaçlar (balanced binary trees)" ya da "hash tabloları (hash tables)". Ancak bilindiği gibi C'de bu veri yapıları C'nin standart kütüphanesinde bulunmamaktadır. Halbuki nesne yönelimli dillerin neredeyse hemen hepsinde "sözlük (dictionary)" veri yapısı denilen bu veri yapıları o dillerin standart kütüphanelerinde hazır biçimde bulunmaktadır. Örneğin C++'taki "map" ya da "unordered_map" isimli sınıflar bu iş için idealdir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 39. Ders 19/03/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki isimli borular kullanılarak bir client-server haberleşme örneği verilmiştir. Programla ilgili bazı açıklamaları veriyoruz: - Programda client'ların server'a mesaj göndermesi için kullandıkları borunun işin başında "serverpipe" ismiyle yaratılmış olması gerekmektedir. - Client ile server arasındaki haberleşmeler yazısal biçimde şöyle yapılmaktadır: "KOMUT " Client ve server bu yazıyı parse edip komut ve parametrelerini birbirinden ayırmaktadır. - Server'ın client'a göndereceği mesajlar için kullanılacak borular server tarafından yaratılmaktadır. Bu sırada client "boru yaratılmış mı" diye beklemektedir. - Server her client'a bir id numarası vermektedir. Id numarası olarak proses id'ler değil client borulara ilişkin betimleyici numaraları kullanılmıştır. Server bağlantıyı sağladığında client borusunu yaratır, onun betimleyici numarasını boruya gönderir. Ancak client borusunun ismini client program belirlemektedir. Yani borunun ismi client program tarafından belirlenmekte ancak yaratımı server program tarafından yapılmaktadır. - Client'tan server'a gönderilen mesajlar şunlardır: CONNECT DISCONNECT_REQUEST DISCONNECT CMD Burada CMD client'ın server'a işlettireceği kabuk komutunu belirtmektedir. Yani server client'ın gönderdiği kabuk komutunu işletir. Onun sonucunu client'a yollar. - Server programın client programa gönderdiği mesajlar da şunlardır: CMD_RESPONSE DISCONNECT_ACCEPTED INVALID_COMMAND - Client'ın bağlantıyı sonlandırması şöyle bir el sıkışmayla sağlanmıştır: 1) Önce client server'a DISCONNECT_REQUESTED mesajını gönderir. 2) Sonra server bu mesajı alınca eğer disconnect'i kabul ederse DISCONNECT_ACCEPTED mesajını gönderir. 3) Client son olarak server'a DISCONNECT mesajını göndererek el sıkışmayı sonlandırır. - Client program bir komut satırı oluşturup komutların kullanıcı tarafından uygulanmasını sağlamaktadır. Ancak bağlantının sonlandırılması bir dizi el sıkışma gerektirdiği için "quit" komutu sırasında yaptırılmıştır. - Server programda client'ların bilgileri için bir veri yapısı oluşturulmamıştır. Zaten client id server'daki dosya betimleyicisi olduğu için böyle bir sözlük veri yapısına ihtiyaç duyulmamıştır. Tabii aslında client'ın pek çok bilgisini server saklamak isteyebilir. Genel olarak böyle bir veri yapısının oluşturulması uygundur. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include /* Symbolic Constants */ #define SERVER_PIPE "serverpipe" #define MAX_CMD_LEN 1024 #define MAX_MSG_LEN 32768 #define MAX_PIPE_PATH 1024 /* Type Declaration */ typedef struct tagCLIENT_MSG { int msglen; int client_id; char msg[MAX_MSG_LEN]; } CLIENT_MSG; typedef struct tagSERVER_MSG { int msglen; char msg[MAX_MSG_LEN]; } SERVER_MSG; typedef struct tagMSG_CONTENTS { char *msg_cmd; char *msg_param; } MSG_CONTENTS; typedef struct tagMSG_PROC { const char *msg_cmd; int (*proc)(const char *msg_param); } MSG_PROC; /* Function Prototypes */ void sigpipe_handler(int sno); int putmsg(const char *cmd); int get_server_msg(int fdp, SERVER_MSG *smsg); void parse_msg(char *msg, MSG_CONTENTS *msgc); void check_quit(char *cmd); int connect_to_server(void); int cmd_response_proc(const char *msg_param); int disconnect_accepted_proc(const char *msg_param); int invalid_command_proc(const char *msg_param); void clear_stdin(void); void exit_sys(const char *msg); /* Global Data Definitions */ MSG_PROC g_msg_proc[] = { {"CMD_RESPONSE", cmd_response_proc}, {"DISCONNECT_ACCEPTED", disconnect_accepted_proc}, {"INVALID_COMMAND", invalid_command_proc}, {NULL, NULL} }; int g_client_id; int g_fdps, g_fdpc; /* Function Definitions */ int main(void) { char cmd[MAX_CMD_LEN]; char *str; SERVER_MSG smsg; MSG_CONTENTS msgc; int i; if (signal(SIGPIPE, sigpipe_handler) == SIG_ERR) exit_sys("signal"); if ((g_fdps = open(SERVER_PIPE, O_WRONLY)) == -1) exit_sys("open"); if (connect_to_server() == -1) { fprintf(stderr, "cannot connect to server! Try again...\n"); exit(EXIT_FAILURE); } for (;;) { printf("Client>"); fflush(stdout); fgets(cmd, MAX_CMD_LEN, stdin); if ((str = strchr(cmd, '\n')) != NULL) *str = '\0'; check_quit(cmd); if (putmsg(cmd) == -1) exit_sys("putmsg"); if (get_server_msg(g_fdpc, &smsg) == -1) exit_sys("get_client_msg"); parse_msg(smsg.msg, &msgc); for (i = 0; g_msg_proc[i].msg_cmd != NULL; ++i) if (!strcmp(msgc.msg_cmd, g_msg_proc[i].msg_cmd)) { if (g_msg_proc[i].proc(msgc.msg_param) == -1) { fprintf(stderr, "command failed!\n"); exit(EXIT_FAILURE); } break; } if (g_msg_proc[i].msg_cmd == NULL) { /* command not found */ fprintf(stderr, "Fatal Error: Unknown server message!\n"); exit(EXIT_FAILURE); } } return 0; } void sigpipe_handler(int sno) { printf("server down, exiting...\n"); exit(EXIT_FAILURE); } int putmsg(const char *cmd) { CLIENT_MSG cmsg; int i, k; for (i = 0; isspace(cmd[i]); ++i) ; for (k = 0; !isspace(cmd[i]); ++i) cmsg.msg[k++] = cmd[i]; cmsg.msg[k++] = ' '; for (; isspace(cmd[i]); ++i) ; for (; (cmsg.msg[k++] = cmd[i]) != '\0'; ++i) ; cmsg.msglen = (int)strlen(cmsg.msg); cmsg.client_id = g_client_id; if (write(g_fdps, &cmsg, 2 * sizeof(int) + cmsg.msglen) == -1) return -1; return 0; } int get_server_msg(int fdp, SERVER_MSG *smsg) { if (read(fdp, &smsg->msglen, sizeof(int)) == -1) return -1; if (read(fdp, smsg->msg, smsg->msglen) == -1) return -1; smsg->msg[smsg->msglen] = '\0'; return 0; } void parse_msg(char *msg, MSG_CONTENTS *msgc) { int i; msgc->msg_cmd = msg; for (i = 0; msg[i] != ' ' && msg[i] != '\0'; ++i) ; msg[i++] = '\0'; msgc->msg_param = &msg[i]; } void check_quit(char *cmd) { int i, pos; for (i = 0; isspace(cmd[i]); ++i) ; pos = i; for (; !isspace(cmd[i]) && cmd[i] != '\0'; ++i) ; if (!strncmp(&cmd[pos], "quit", pos - i)) strcpy(cmd, "DISCONNECT_REQUEST"); } int connect_to_server(void) { char name[MAX_PIPE_PATH]; char cmd[MAX_CMD_LEN]; char *str; SERVER_MSG smsg; MSG_CONTENTS msgc; int response; printf("Pipe name:"); fgets(name, MAX_PIPE_PATH, stdin); if ((str = strchr(name, '\n')) != NULL) *str = '\0'; if (access(name, F_OK) == 0) { do { printf("Pipe already exists! Overwrite? (Y/N)"); fflush(stdout); response = tolower(getchar()); clear_stdin(); if (response == 'y' && remove(name) == -1) return -1; } while (response != 'y' && response != 'n'); if (response == 'n') return -1; } sprintf(cmd, "CONNECT %s", name); if (putmsg(cmd) == -1) return -1; while (access(name, F_OK) != 0) usleep(300); if ((g_fdpc = open(name, O_RDONLY)) == -1) return -1; if (get_server_msg(g_fdpc, &smsg) == -1) exit_sys("get_client_msg"); parse_msg(smsg.msg, &msgc); if (strcmp(msgc.msg_cmd, "CONNECTED")) return -1; g_client_id = (int)strtol(msgc.msg_param, NULL, 10); printf("Connected server with '%d' id...\n", g_client_id); return 0; } int cmd_response_proc(const char *msg_param) { printf("%s\n", msg_param); return 0; } int disconnect_accepted_proc(const char *msg_param) { if (putmsg("DISCONNECT") == -1) exit_sys("putmsg"); exit(EXIT_SUCCESS); return 0; } int invalid_command_proc(const char *msg_param) { printf("invalid command: %s\n", msg_param); return 0; } void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include /* Symbolic Constants */ #define SERVER_PIPE "serverpipe" #define MAX_MSG_LEN 32768 #define MAX_PIPE_PATH 1024 #define MAX_CLIENT 1024 /* Type Declaration */ typedef struct tagCLIENT_MSG { int msglen; int client_id; char msg[MAX_MSG_LEN]; } CLIENT_MSG; typedef struct tagSERVER_MSG { int msglen; char msg[MAX_MSG_LEN]; } SERVER_MSG; typedef struct tagMSG_CONTENTS { char *msg_cmd; char *msg_param; } MSG_CONTENTS; typedef struct tagMSG_PROC { const char *msg_cmd; int (*proc)(int, const char *msg_param); } MSG_PROC; typedef struct tagCLIENT_INFO { int fdp; char path[MAX_PIPE_PATH]; } CLIENT_INFO; /* Function Prototypes */ int get_client_msg(int fdp, CLIENT_MSG *cmsg); int putmsg(int client_id, const char *cmd); void parse_msg(char *msg, MSG_CONTENTS *msgc); void print_msg(const CLIENT_MSG *cmsg); int invalid_command(int client_id, const char *cmd); int connect_proc(int client_id, const char *msg_param); int disconnect_request_proc(int client_id, const char *msg_param); int disconnect_proc(int client_id, const char *msg_param); int cmd_proc(int client_id, const char *msg_param); void exit_sys(const char *msg); /* Global Data Definitions */ MSG_PROC g_msg_proc[] = { {"CONNECT", connect_proc}, {"DISCONNECT_REQUEST", disconnect_request_proc}, {"DISCONNECT", disconnect_proc}, {"CMD", cmd_proc}, {NULL, NULL} }; CLIENT_INFO g_clients[MAX_CLIENT]; /* Function Definitions */ int main(void) { int fdp; CLIENT_MSG cmsg; MSG_CONTENTS msgc; int i; printf("Server running...\n"); if ((fdp = open(SERVER_PIPE, O_RDWR)) == -1) exit_sys("open"); for (;;) { if (get_client_msg(fdp, &cmsg) == -1) exit_sys("get_client_msg"); print_msg(&cmsg); parse_msg(cmsg.msg, &msgc); for (i = 0; g_msg_proc[i].msg_cmd != NULL; ++i) if (!strcmp(msgc.msg_cmd, g_msg_proc[i].msg_cmd)) { if (g_msg_proc[i].proc(cmsg.client_id, msgc.msg_param)) { } break; } if (g_msg_proc[i].msg_cmd == NULL) if (invalid_command(cmsg.client_id, msgc.msg_cmd) == -1) continue; } close(fdp); return 0; } int get_client_msg(int fdp, CLIENT_MSG *cmsg) { if (read(fdp, &cmsg->msglen, sizeof(int)) == -1) return -1; if (read(fdp, &cmsg->client_id, sizeof(int)) == -1) return -1; if (read(fdp, cmsg->msg, cmsg->msglen) == -1) return -1; cmsg->msg[cmsg->msglen] = '\0'; return 0; } int putmsg(int client_id, const char *cmd) { SERVER_MSG smsg; int fdp; strcpy(smsg.msg, cmd); smsg.msglen = strlen(smsg.msg); fdp = g_clients[client_id].fdp; return write(fdp, &smsg, sizeof(int) + smsg.msglen) == -1 ? -1 : 0; } void parse_msg(char *msg, MSG_CONTENTS *msgc) { int i; msgc->msg_cmd = msg; for (i = 0; msg[i] != ' ' && msg[i] != '\0'; ++i) ; msg[i++] = '\0'; msgc->msg_param = &msg[i]; } void print_msg(const CLIENT_MSG *cmsg) { printf("Message from \"%s\": %s\n", cmsg->client_id ? g_clients[cmsg->client_id].path : "", cmsg->msg); } int invalid_command(int client_id, const char *cmd) { char buf[MAX_MSG_LEN]; sprintf(buf, "INVALID_COMMAND %s", cmd); if (putmsg(client_id, buf) == -1) return -1; return 0; } int connect_proc(int client_id, const char *msg_param) { int fdp; char buf[MAX_MSG_LEN]; if (mkfifo(msg_param, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH) == -1) { printf("CONNECT message failed! Params = \"%s\"\n", msg_param); return -1; } if ((fdp = open(msg_param, O_WRONLY)) == -1) exit_sys("open"); g_clients[fdp].fdp = fdp; strcpy(g_clients[fdp].path, msg_param); sprintf(buf, "CONNECTED %d", fdp); if (putmsg(fdp, buf) == -1) exit_sys("putmsg"); return 0; } int disconnect_request_proc(int client_id, const char *msg_param) { if (putmsg(client_id, "DISCONNECT_ACCEPTED") == -1) return -1; return 0; } int disconnect_proc(int client_id, const char *msg_param) { close(g_clients[client_id].fdp); if (remove(g_clients[client_id].path) == -1) return -1; return 0; } int cmd_proc(int client_id, const char *msg_param) { FILE *f; char cmd[MAX_MSG_LEN] = "CMD_RESPONSE "; int i; int ch; if ((f = popen(msg_param, "r")) == NULL) { printf("cannot execute shell command!...\n"); return -1; } for (i = 13; (ch = fgetc(f)) != EOF; ++i) cmd[i] = ch; cmd[i] = '\0'; if (putmsg(client_id, cmd) == -1) return -1; return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Blokesiz boru işlemlerini ele almadan önce fcntl isimli POSIX fonksiyonunu (aynı zamanda bir sistem fonksiyonu olarak bulundurulmaktadır) ele almak istiyoruz. fcntl zaten açılmış olan bir dosyanın çeşitli açım özelliklerini değiştirmek için kullanılmaktadır. fcntl fonksiyonu sayesinde biz open fonksiyonu ile açım sırasında belirlediğimiz bazı açış bayraklarını daha sonra değiştirebilmekteyiz. fcntl fonksiyonu aynı zamanda open fonksiyonu kullanılmadan yaratılmış olan betimleyicilerin özelliklerini değiştirmekte de kullanılabilmektedir. Örneğin pipe fonksiyonu boruyu yarattıktan sonra bize iki betimleyici vermektedir. Bu iki betimleyiciyi open fonksiyonuyla biz açmadığımız için onun bazı özelliklerini ancak fcntl fonksiyonu ile set edebiliriz. fcntl fonksiyonu yalnızca betimleyici bayraklarının set edilmesi için değil onların elde edilmesi için (get edilmesi için) de kullanılmaktadır. Biz daha önce close on exec bayrağını set etmek için fcntl fonksiyonunu zaten kullanmıştık. Ancak orada fonksiyonun genel tanıtımını yapmamıştık. fcntl fonksiyonunun prototipi şöyledir: #include int fcntl(int fd, int cmd, ...); Fonksiyonun birinci parametresi bayrakları set ya da get edilecek betimleyicinin numarasını almaktadır. İkinci parametre ne yapılacağını belirtir. Bu parametreye önceden belirlenmiş sembolik sabitlerle define edilmiş bazı değerler girilmektedir. Fonksiyon get etme amacıyla kullanılıyorsa iki argümanla çağrılmaktadır. Ancak set etme amacıyla kullanılıyorsa üç argümanla çağrılmaktadır. Fonksiyon başarı durumunda get amaçlı kullanıldıysa ilgili get edilen değere, set amaçlı kullanıldıysa -1 dışındaki herhangi bir değere ve başarısızlık durumunda -1 değerine geri dönmektedir. Tabii eğer fonksiyon get amaçlı kullanılıyorsa ve betimleyici ile komut parametresi doğru bir biçimde verilmişse fonksiyonun başarısız olma olasılığı yoktur. Fonksiyonun ikinci parametresi F_GETFL ve F_SETFL biçiminde girilirse bu durumda fonksiyon "dosya durum bayraklarını (file status flags)" ve "erişim modunu (access modes)" get ve set etmektedir. Dosya durum bayrakları (file status flags) şunlardır: O_APPEND O_DSYNC O_NONBLOCK O_RSYNC O_SYNC Biz bu bayraklardan yalnızca O_APPEND bayrağını open fonksiyonunu anlatırken açıklamıştık. Dosya erişim modları da şunlardan oluşmaktadır: O_EXEC O_RDONLY O_RDWR O_SEARCH O_WRONLY Eğer programcı dosya durum bayraklarını F_GETFL ile elde etmek istemişse fonksiyonun geri dönüş değerini dosya durum bayraklarıyla bit AND işlemine sokarak ilgili bayrağın set edilip edilmediğini anlayabilir. Örneğin: result = fcntl(fd, F_GETFL); if (result & O_APPEND) { /* O_APPEND bayrağı set edilmiş */ } Eğer programcı dosya erişim modlarını elde etmek istiyorsa erişim modları ile bit AND işlemini kullanmamlıdır. Bunun için önce fonksiyonun geri dönüş değeri O_ACCMODE değeri ile bit AND işlemine sokulup erişim modlarıyla == karşılaştıması yapılmalıdır. Örneğin biz dosyanın O_RDWR modunda açılıp açılmadığını anlam isteyelim. Bu işlemi şöyle yapmamalıyız: result = fcntl(fd, F_GETFL); if (result & O_RDWR) { /* dikkat! hatalı kullanım */ ... } Bu işlemin şöyle yapılması gerekir: result = fcntl(fd, F_GETFL); if ((result & O_ACCMODE) == O_RDWR) { /* doğru kullanım */ /* O_RDWR bayrağı set edilmiş */ } Şimdi F_SETFL komutuyla O_NONBLOCK bayrağını set etmek isteyelim. İşlemi şöyle yapabiliriz: result = fcntl(fd, F_GETFL); if (fcntl(fd, F_SETFL, result|O_NONBLOCK) == -1) exit_sys("fcntl"); F_SETFL işleminde hem dosya durum bayraklarının hem de erişim modunun set edilmeye çalışıldığına dikkat ediniz. Diğer bayraklara dokunmadan yalnızca O_NONBLOCK bayrağının set edilmesi için önce get işleminin yapılması gerekir. Şimdi de O_NONBLOCK bayrağını clear edelim: result = fcntl(fd, F_GETFL); if (fcntl(fd, F_SETFL, result & ~O_NONBLOCK) == -1) exit_sys("fcntl"); fcntl fonksiyonunda F_GETFD ve F_SETFD komut kodları "dosya betimleyici bayraklarını" get ve set etmekte kullanılmaktadır. POSIX standartları dosya betimleyici bayrağı olarak yalnızca tek bir bayrak tanımlamıştır. Bu bayrak FD_CLOEXEC bayrağıdır. Dolayısıyla biz bu komut kodu ile yalnızca dosyanın "close on exec" bayrağını get ve set edebiliriz. Biz de zaten daha önce dosyanın close on exec bayrağını alıp set etmiştik. Dosyanın close on exec bayrağı şöyle alınabilir: result = fcntl(fd, F_GETFD); if (result & FD_CLOEXEC) { /* close on exec bayrağı set edilmiş durumda */ } Dosyanın close on exec bayrağını şöyle set edebiliriz: result = fcntl(fd, F_GETFD); if (fcntl(fd, F_SETFD, result | FD_CLOEXEC) == -1) exit_sys("fcntl"); Dosyanın close on exec bayrağını şöyle clear edebiliriz: result = fcntl(fd, F_GETFD); if (fcntl(fd, F_SETFD, result & ~FD_CLOEXEC) == -1) exit_sys("fcntl"); Biz dosya betimleyicisini çiftlemek için daha önce dup ve dup2 fonksiyonlarını kullanmıştık. Aslında dosya betimleyicilerinin çiftlenmesi fcntl fonksiyonuyla da yapılabilmektedir. Bunun fcntl fonksiyonunda komut kodu olarak F_DUPFD kullanılır. Bu komut kodunda üçüncü parametre de girilmelidir. Fonksiyon üçüncü parametrede belirtilen betimleyici değerine eşit ya da ondan büyük olan düşük boş betimleyiciyi bize verir. Bu bakımdan fcntl ile dosya betimleyicisinin çiftlenmesi dup ve dup2 fonksiyonlarından farklıdır. Anımsanacağı gibi dup fonksiyonu bize ilk boş betimleyiciyi, dup2 fonksiyonu ise ikinci parametresiyle belirttiğimiz betimleyiciyi vermektedir. Oysa fcntl fonksiyonu F_DUPFD komut koduyla bize belli bir değerden büyük en düşük betimleyici verir. Örneğin: if ((fd2 = fcntl(fd, F_DUPFD, 50)) == -1) exit_sys("fcntl"); Burada fd'nin çiftlenmiş betimleyicisi 50 ya da ilk büyük betimleyicidir. O halde fcntl ile dup fonksiyonun eşdeğeri şöyledir: if ((fd2 = fcntl(fd, F_DUPFD, 0)) == -1) /* dup ile eşdeğer */ exit_sys("fcntl"); Ayrıca fcntl fonksiyonunun F_DUPFD_CLOEXEC biçiminde bir komut daha vardır. Bu komut kodu hem F_DUPFD hem de close on exec bayrağının set edilmesini birlikte yapmaktadır. Yani özetle bu komut kodu hem dosya betimleyicisini çiftler hem de çiftlenmiş olan yeni betimleyicinin close on exec bayrağını set eder. Örneğin: if ((fd2 = fcntl(fd, F_DUPFD_CLOEXEC, 0)) == -1) exit_sys("fcntl"); fd2 betimleyicisinin aynı zamanda close on exec bayrağı set edilmiştir. fcntl fonksiyonunun diğer komut kodları dosya kilitleme gibi özel bazı konulara ilişkindir. Bu bayraklar o konuların anlatıldığı bölümde ele alınmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- mypipe bir isimli boru dosyası olsun. Aşağıdaki gibi bir işlem yapsak ne olur? $ ls > mypipe Bu durumda kabuk programı IO yönlendirmesi yapmak için "mypipe" dosyasını "write" modda open fonksiyonu ile açmak isteyecektir. Ancak "mypipe" isimli boru dosyası olduğu için açım sırasında bloke oluşacaktır. Pekiyi bu blokeden nasıl kurtulunabilir? Tabii başka bir terminalden biz boruyu bu kez "read" modda açarak. Örneğin: $ cat < mypipe Burada yine kabuk programı "mypipe" dosyasını yönlendirme için "read" modda açacaktır. Bu durumda diğer kabuk prosesi blokeden kurtulacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dosyayı (boru dosyaları da diğer aygıt sürücü dosyaları da dahil olmak üzere) açarken kullanılan bayraklardan biri de O_NONBLOCK bayrağıdır. Bu bayrağın normal dosyalarda (regular file) bir etkisi yoktur. Ancak borularda, soketlerde ve özel bazı aygıt sürücülerde bu bayrak önemli bir işlevselliğe sahiptir. Bu işlevselliğe "blokesiz IO işlemleri (Nonblocking IO)" denilmektedir. Blokesiz IO işlemlerinin temel fikri şudur: Bazı aygıtlardan (boru ve soketlerde dahil olmak üzere) okuma yazma yapılırken uzun süre beklemeye yol açabilecek bir bloke durumu oluşabilmektedir. Örneğin biz bir borudan okuma yapmak isteyelim. Ancak boruda hiç byte olmasın. Bu durumda read fonksiyonu blokeye yol açacak ve boruya bilgi gelene kadar program akışı kesilecektir. İşte blokesiz işlemlerde eğer ilgili işlem blokeye yol açabilecekse bloke oluşturulmamakta read ve write fonksiyonları başarısızlıkla geri dönmekte errno değeri EAGAIN denilen özel bir değerle set edilmektedir. Örneğin biz içerisinde hiç byte olmayan bir borudan read fonksiyonu ile 10 byte okumak isteyelim. Eğer boru default durumda olduğu gibi "blokeli modda" ise read fonksiyonu en az 1 byte boruya yazılana kadar blokede kalır. Ancak eğer blokesiz modda isek bu durumda read bloke olmaz -1 değeriyle geri döner ve errno değeri EAGAIN ile set edilir. Böylece programcı arka planda "mademki boruda bir şey yok o zaman ben de başka bir şey yapayım" diyebilmektedir. Aynı durum write sırasında da olmaktadır. Örneğin blokesiz modda biz bir boruya write işlemi yapmak isteyelim ancak boru tam olarak dolu olsun. Bu durumda write fonksiyonu -1 ile geri döner ve errno değeri EAGAIN değeri set edilir. Blokesiz modda işlemler blokeli moddaki işlemlere göre oldukça seyrek kullanılmaktadır. İsimli boru dosyaları open fonksiyonuyla O_NONBLOCK bayrağı kullanılarak açılırken artık open fonksiyonunda bloke oluşmaz. Anımsanacağı gibi blokesiz modda open karşı taraf boruyu ters modda açana kadar bloke oluşturuyordu. open fonksiyonunda O_NONBLOCK bayrağı kullanıldığında proses boruyu read modda açtığında henüz karşı taraf boruyu write modda açmamışsa read fonksiyonu boruyu yazma potansiyelinde olan hiçbir betimleyici olmadığı için 0 ile geri döner. İsimli borularda proses boruyu "write" modda açarken normalde blokeli modda open fonksiyonu karşı taraf boruyu "read" modda açana kadar bloke oluşuyordu. Halbuki isimli boruları O_NONBLOCK bayrağı ile "write" modda açmaya çalıştığımızda karşı taraf boruyu henüz "read" modda açmamışsa open başarısız olmaktadır. Bu durumda open fonksiyonu errno değerini ENXIO ile set etmektedir. İsimli borularda iki taraf da boruyu O_NONBLOCK bayrağı ile açarsa yukarıda anlattığımız nedenden dolayı senkronizasyona dikkat etmek gerekir. Bu tür durumlarda işlemleri kolaylaştırmak için isimli borular blokeli modda açılıp (O_NONBLOCK kullanılmadan) sonrasında fcntl fonksiyonu ile blokesiz moda geçilebilir. Prosesler biri boruyu blokeli modda diğeri blokesiz modda açabilir. Bu da herhangi bir soruna yol açmaz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 40. Ders 25/03/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda isimsiz borularda blokesiz IO örneği verilmiştir. Bu örnekte borular blokesiz moda sokulmuş sonra read ve write işlemleri başarısız olduğunda errno değerine bakılmıştır. Eğer başarısızlığın nedeni blokesiz IO yüzündense errno EAGAIN özel değerine set edileceği için bu sırada prosesler arka planda başka işlemler yapmıştır. Örnekte kasten üst proseste biraz bekleme yapılmıştır. Dolayısıyla program çalıştırıldığında alt proses read işleminde blokesiz IO yüzünden başarısız olup birtakım arka plan işlemleri yapacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void exit_sys(const char *msg); void exit_sys_child(const char *msg); int main(int argc, char *argv[]) { int pipefds[2]; pid_t pid; ssize_t result; int val; int i; if (pipe(pipefds) == -1) exit_sys("pipe"); if (fcntl(pipefds[1], F_SETFL, fcntl(pipefds[1], F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); if (fcntl(pipefds[0], F_SETFL, fcntl(pipefds[0], F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent writes */ close(pipefds[0]); sleep(1); i = 0; while (i < 100000) { if (write(pipefds[1], &i, sizeof(int)) == -1) if (errno == EAGAIN) { printf("parent background processing...\n"); usleep(500); continue; } else exit_sys("write"); ++i; } close(pipefds[1]); if (wait(NULL) == -1) exit_sys("wait"); } else { /* child reads */ close(pipefds[1]); while ((result = read(pipefds[0], &val, sizeof(int))) != 0) { if (result == -1) if (errno == EAGAIN) { printf("child background processing...\n"); usleep(500); continue; } else exit_sys_child("read"); printf("%d\n", val); } close(pipefds[0]); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_child(const char *msg) { perror(msg); _exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda isimli boruların blokesiz modda kullanımına bir örnek verilmiştir. Burada "mypipe" isimli borusunun dışarıda yaratılmış olması gerekir. İki proses de boruyu önce blokeli modda açmış sonra fcntl fonksiyonu ile blokesiz moda geçmiştir. Yukarıda da belirttiğimiz gibi open fonksiyonu ile O_NONBLOCK bayrağı kullanılarak isimli boruların blokesiz modda açılması sırasında senkronizasyona dikkat edilmesi gerekir. Halbuki önce iki prosesin open fonksiyonu ile boruları blokeli modda açıp sonra fcntl fonksiyonu ile blokesiz moda geçirmesi bu senkronizasyon problemini bertaraf etmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /* proc1.c */ #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fdpipe; int i; if ((fdpipe = open("mypipe", O_WRONLY)) == -1) exit_sys("open"); if (fcntl(fdpipe, F_SETFL, fcntl(fdpipe, F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); i = 0; while (i < 100000) { if (write(fdpipe, &i, sizeof(int)) == -1) if (errno == EAGAIN) { printf("parent background processing...\n"); usleep(500); continue; } else exit_sys("write"); ++i; } close(fdpipe); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* proc2.c */ #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fdpipe; ssize_t result; int val; if ((fdpipe = open("mypipe", O_RDONLY)) == -1) exit_sys("open"); if (fcntl(fdpipe, F_SETFL, fcntl(fdpipe, F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); while ((result = read(fdpipe, &val, sizeof(int))) != 0) { if (result == -1) if (errno == EAGAIN) { printf("child background processing...\n"); usleep(500); continue; } else exit_sys("read"); printf("%d\n", val); } close(fdpipe); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aslında POSIX standartları çeşitli uyum kategorilerini içermektedir. Yani POSIX standartdını destekleyen sistemler onun bazı uyum kategorilerini destekleyip bazılarını desteklemiyor olabilirler. Örneğin XSI (X/Open ya da Open Group anlamına gelmektedir) önemli bir uyum kategorisidir. Bu uyum kategorisi X/Open denilen ya da yeni ismi ile "Single Unix Specification" denilen uyum kategorisini anlatır. Biz kursumuzda POSIX standartları demekle tüm bu uyum kategerilerini içerecek biçimde bu terimi kullanıyoruz. Halbuki yukarıda da belirtildiği gibi bazı uyum kategorileri dışlanarak da POSIX standartları desteklenebilmektedir. POSIX standartlarında, fonksiyonların yanında bu uyum kategorileri belirtilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux dünyasında "IPC fonksiyonları" demekle geleneksel olarak "mesaj kuyrukları, paylaşılan bellek alanları ve semaphore'lar" anlaşılmaktadır. "IPC fonksiyonları" yerine "IPC nesneleri" terimi de kullanılabilmektedir. Borular tipik bir IPC mekanizması olduğu halde bu dünyada IPC fonksiyonları denildiğinde borular anlaşılmamaktadır. (Semaphore'lar senkronizasyon konusu ile ilgilidir. Ancak bir anlamda IPC konusuyla da ilgilidir. Biz semaphore'ları "thread'lerin anlatıldığı" bölümde ele alacağız.) Yinelemek gerekirse UNIX dünyasında -onların terminolojisiyle- üç IPC mekanizması vardır: - Mesaj kuyrukları - Paylaşılan Bellek Alanları - Semaphore'lar Borular birer IPC mekanizması olduğu halde UNIX/Linux dünyasında IPC mekanizması denildiğinde genellikle borular ayrı biçimde ele alınmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux dünyasında IPC mekanizması (onların terimi ile) için iki farklı arayüz (fonksiyon grubu) kullanılmaktadır: 1) Eskiden beri var olan geniş bir taşınabilirliğe sahip olan ismine "Sistem 5 IPC fonksiyonları" ya da "XSI IPC fonksiyonları" denilen klasik IPC fonksiyonları. 2) 1990'lı yılların ortalarında "Real Time Extensions" eklemeleriyle UNIX/Linux dünyasına katılan ismine "POSIX IPC fonksiyonları" denilen yeni ve daha modern IPC fonksiyonları. Her ne kadar ikinci grup fonksiyonlara "POSIX IPC fonksiyonları" deniliyorsa da her iki fonksiyon grubu da POSIX standartlarında bulunmaktadır. Klasik IPC fonksiyonlarının yanında XSI uyum kategorisi belirteçleri yerleştirilmiştir. Her iki arayüzdeki fonksiyonların parametrik yapıları ve isimleri o arayüze uygun biçimde belirlenmiştir. Bu nedenle örneğin Sistem 5 (XSI) klasik IPC fonksiyonlarının isimlendirmeleri ve kullanımları birbirine benzerdir ve POSIX IPC fonksiyonlarının isimlendirmeleri ve kullanımları birbirine benzerdir. Aşağıda her iki arayüzdeki yaratıcı fonksiyonların isimleri verilmiştir: Sistem 5 IPC Fonksiyonları POSIX IPC Fonksiyonları msgget (mesaj kuyruğu mq_open (mesaj kuyruğu) shmget (paylaşılan bellek alanları) shm_open (paylaşılan bellek alanları) semget (semaphore'lar) sem_open (semaphore'lar) Pekiyi mademki klasik (Sistem 5) IPC nesneleri 70'lerden beri kullanılmaktadır ve oldukça taşınabilir durumdadır. O halde neden 90'lı yıllarda yeni bir arayüz gereksinimi duyulmuştur? İşte bunun nedeni klasik IPC nesnelerinin genel tasarımında olan bazı problemlerdir. POSIX IPC nesneleriyle bu problem giderilmeye çalışılmıştır. POSIX IPC nesneleri Linux çekirdeğine çok sonraları eklenmiştir. Hala bazı UNIX türevi sistemlerde bu fonksiyonlar tam desteklenmemektedir. Yani POSIX IPC nesneleri taşınabilirlik konusunda daha problemlidir. Gerçi seneler içerisinde bu IPC nesneleri daha çok sistem tarafından desteklenmiştir ve bugünlerde bu taşınabilirlik problemi büyük ölçüde ortadan kalkmıştır. Bu IPC mekanizmalarının incelenmesi işlemi iki biçimde yapılabilir. Önce Klasik Sistem 5 IPC nesneleri görülüp sonra bunların POSIX IPC karşılıkları incelenebilir ya da her IPC mekanizması ayrı ayrı klasik Sistem 5 ve POSIX karşılıkları incelenebilir. Biz bu kursumuzda ikinci yöntemi izleyeceğiz. Pekiyi biz programcı olarak hangi grup IPC fonksiyonlarını kullanmalıyız? İşte bu iki grubun birbilerine göre amaca yönelik bazı avantajları ve dezavantajları söz konusu olmaktadır. Ancak artık programcıların özel bir durum yoksa modern POSIX IPC fonksiyonlarını tercih etmesi daha uygundur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Haberleşmeler biçimsel olarak "stream tabanlı" ve "mesaj (datagram) tabanlı" olmak üzere ikiye ayrılmaktadır. Stream tabanlı haberleşme denildiğinde yazan tarafın tek bir hamlede (örneğin write ile) yazdığını okuyan tarafın tek hamlede (tek bir read ile) okumasının zorunlu olmadığı kastedilmektedir. Örneğin bir proses tek hamlede haberleşme kanalına 100 byte göndermiş olabilir. Stream tabanlı haberleşmede okuma yapan taraf bu 100 byte'ı tek hamlede değil birden fazla okuma yaparak elde edebilir. Yani stream tabanlı haberleşmede okuyan taraf haberleşme kanalından istediği kadar byte okuyabilmektedir. Burada "stream" terimi "dere, pınar, su akışı" anlamlarından hareketle uydurulmuştur. Mesaj (datagram) haberleşmelerde ise gönderen taraf bir grup byte'ı mesaj adı altında tek hamlede gönderir. Okuyan taraf bunu tek hamlede okumak zorundadır. Örneğin yazan taraf mesaj adı altında haberleşme kanalına 100 byte göndermiş olabilir. Bu 100 byte'lık paketi diğer taraf parça parça okuyamaz. Tek hamlede okumak zorundadır. Yani okuyan taraf bu paketi ya okumaz ya da okursa hepsini okur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Borulardaki haberleşme "stream tabanlı" haberleşmeye tipik bir örnektir. Örneğin biz boruya 100 byte yazdığımızda okuyan taraf bu 100 byte'ı tek hamlede okumak zorunda değildir. Örneğin bunu 10'ar 10'ar byte okuyabilir. Oysa mesaj kuyrukları ismi üzerinde mesaj tabanlı haberleşme sağlamaktadır. Yani mesaj kuyruklarında kuyruğa yazan taraf "mesaj" adı altında bir grup byte'ı bir paket olarak yazar. Okuyan taraf da bunu bir paket olarak tek hamlede okur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Klasik Sistem 5 IPC mekanizmasında iki farklı prosesin aynı IPC nesnesi üzerinde anlaşabilmesi için "anahtar (key)" ve "id" kavramları kullanılmaktadır. Bu arayüzlerde xxxget fonksiyonlarına programcı bir anahtar verir. Bu anahtar sayısal bir değerdir. İki proses aynı anahtarı verirse aynı nesneyi kullanırlar. Bu xxxget fonksiyonları verilen anahtara ilişkin bir id geri döndürmektedir. Bu id değeri aslında diğer fonksiyonlarda handle olarak kullanılmaktadır. Buradaki bir problem verilen anahtarın tesadüfen sistemdeki başka bir prosesle çakışabilmesidir. Bunun için bazı kontrollerin yapılması gerekebilmektedir. xxxget fonksiyonlarının verdiği id değerleri sistem genelinde "tek (unique)" bir değerdir. Yani aslında bu id değeri, diğer prosese proseslerarası haberleşme yöntemleriyle gönderilse diğer proses hiç xxxget işlemi yapmadan doğrudan bu id değerini kullanabilir. Başka bir deyişle birden fazla proses xxxget fonksiyonlarında aynı anahtarı verdiklerinde aslında aynı id'yi elde etmektedir. Klasik Sistem 5 IPC mekanizmasında xxxget fonksiyonlarında kullanılan anahtarlar aslında IPC nesnesinin türüne göre ayrı bir isim alanındadır. Yani 12345 gibi bir anahtar paylaşılan bellek alanları için ayrı bir anahtar, mesaj kuyrukları için ayrı bir anahtar durumundadır. Klasik Sistem 5 IPC nesneleri yaratıldıktan sonra xxxctl fonksiyonu ile yok edilene kadar ya da sistem reboot edilene kadar yaşamaktadır. Bu duruma bu terminolojide "kernel persistancy" denilmektedir. Halbuki örneğin borular öyle değildir. Bir isimli boru yaratılmış ve bir proses onu açıp onun içerisine bir şeyler yazmış olsun. Prosesler boruyu kapatınca artık borunun içerisindekiler yok olur. Halbuki klasik Sistem 5 IPC nesnelerinde bir proses IPC nesnesini silmedikten sonra reboot edilene kadar nesnenin içerisindeki bilgiler kalmaya devam etmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Klasik Sistem 5 (XSI) mesaj kuyruklarında dört fonksiyon vardır: msgget, msgsnd, msgrcv ve msgctl fonksiyonları. msgget fonksiyonu mesaj kuyruğunu yaratır ya da yaratılmış olanı açar. msgget fonksiyonunu open fonksiyonuna benzetebiliriz. Mesaj kuyruğuna bir grup byte'ı mesaj olarak yerleştirmek için msgsnd fonksiyonu, mesaj kuyruğundan mesaj almak için msgrcv fonksiyonu ve mesaj kuyruğu üzerinde silme de dahil olmak üzere bazı diğer işlemleri yapabilmek için msgctl fonksiyonu kullanılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Klasik Sistem 5 mesaj kuyrukları msgget fonksiyonuyla yaratılabilir ya da zaten var olan mesaj kuyrukları msgget fonksiyonuyla kullanım için açılabilir. Fonksiyonun prototipi şöyledir: #include int msgget(key_t key, int msgflg); Fonksiyonun birinci parametresi tamsayı biçiminde bir anahtar belirtmektedir. Fonksiyonun ikinci parametresinde şu bayraklar kullanılabilir: IPC_CREAT: Burada ilgili anahtara ilişkin bir mesaj kuyruğu varsa olan mesaj kuyruğu açılır ancak yoksa yeni bir mesaj kuyruğu yaratılır. Buradaki semantik open fonksiyonundaki O_CREAT semantiğine benzemektedir. IPC_EXCL: Bu bayrak tek başına değil IPC_CREAT ile birlikte IPC_CREAT|IPC_EXCL biçiminde kullanılabilir. Semantik open fonksiyonundaki O_EXCL bayrağındaki gibidir. Yani daha önce başka kişiler tarafından aynı anahtara ilişkin bir mesaj kuyruğu zaten yaratılmışsa msgget fonksiyonu bu durumda başarısız olur ve EEXIST ile set edilir. Eğer IPC_CREAT kullanılmazsa zaten var olan anahtara ilişkin mesaj kuyruğunun açılmak istendiği anlaşılmaktadır. Bu parametre 0 olarak da girilebilir. Eğer mesaj kuyruğunun yaratılma olasılığı varsa (yani IPC_CREAT bayrağı belirtilmişse) aynı zamanda oluşturulacak mesaj kuyruğu için erişim haklarının da programcı tarafından belirtilmesi gerekir. Mesaj kuyrukları bir dosya olmasa da sanki dosya gibi erişim haklarına sahiptir. Dolayısıyla erişim hakları için yine içerisindeki S_IXXX sembolik sabitleri ya da bunların sayısal değerleri kullanılır. Bu erişim haklarının IPC_CREAT ve IPC_EXCL ile bitsel OR işlemine sokulması gerekmektedir. Örneğin: msgid = msgget(0x12345, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); Anımsanacağı gibi POSIX 2008 standardı ile bu S_IXXX sembolik sabitlerine sayısal değerler de karşılık getirilmişti. O halde POSIX 2008 ve sonrasında bu işlemi şöyle de yapabiliriz: msgid = msgget(0x12345, IPC_CREAT|0644); Tabii anahtara ilişkin mesaj kuyruğu zaten varsa buradaki erişim haklarının ve O_CREAT bayrağının hiçbir etkisi yoktur. Erişim hakları, nesne gerçekten yaratılacaksa kullanılmaktadır. msgget fonksiyonunda (diğer xxxget fonksiyonlarında da böyle) birinci parametre olan anahtar IPC_PRIVATE olarak girilirse bu durumda kullanılmayan bir anahtar oluşturulup mesaj kuyruğu yaratılır. msgget fonksiyonu da bu mesaj kuyruğunun id değerine geri döner. Örneğin: msgid = msgget(IPC_PRIVATE, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); Burada olmayan bir anahtardan hareketle mesaj kuyruğu yaratıldığı için bir çakışma söz konusu olmayacaktır. Tabii bu durumda bu id değerinin (anahtarı bilmiyoruz) diğer prosese aktarılması gerekmektedir. Bu aktarım da olsa olsa komut satırı argümanıyla ya da başka bir proseslerarası haberleşme yöntemiyle olabilir. IPC_PRIVATE anahtarı makul gibi gözükse de genel olarak kullanışsızdır. Bir program mesaj kuyruğunu yaratıp sonlanabilir. Bu durumda yukarıda da belirttiğimiz gibi mesaj kuyruğu yaratılmış biçimde kalmaya devam eder. Ta ki msgctl fonksiyonu ile silinene kadar ya da sistem reboot edilene kadar. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 41. Ders 26/03/2023 Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte program belli bir anahtar kullanarak klasik Sistem 5 mesaj kuyruğunu yaratmıştır. Aynı anahtarı kullanan başka bir proses aynı mesaj kuyruğunu açabilir. Bu durumda aynı anahtarı kullanan proseslerin hepsi aynı id'yi elde eder. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char *msg); int main(void) { int msgid; if ((msgid = msgget(MSG_KEY, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- O anda yaratılmış olan klasik Sistem 5 IPC nesnelerini (mesaj kuyrukları, paylaşılan bellek alanları ve semaphore'lar) komut satırından görüntülemek için "ipcs" isimli komut kullanılmaktadır. Örneğin: ----- İleti Kuyrukları ----- anahtar iltkiml sahibi izinler kull-bayt ileti-sayısı 0x01234567 0 kaan 644 0 0 ----- Paylaşımlı Bellek Bölütleri ----- anahtar shmid sahibi izinler bayt ekSayısı durum 0x00000000 18 kaan 600 4194304 2 hedef 0x00000000 21 kaan 600 524288 2 hedef 0x00000000 22 kaan 600 67108864 2 hedef 0x00000000 26 kaan 600 524288 2 hedef 0x00000000 458815 kaan 600 4194304 2 hedef ----- Semafor Dizileri ----- anahtar semkiml sahibi izinler semSayısı Bu komut anahtarları hex sistemde yazdırmaktadır. Bizim anahtarları hex olarak vermemize gerek yoktur. ipcs komutu default olarak tüm IPC nesnelerini görüntülemektedir. Ancak -q komut satırı argümanıyla yalnızca mesaj kuyrukları, -m komutuyla yalnızca paylaşılan bellek alanları ve -s komutuyla da yalnızca semaphore nesneleri görüntülenebilir. Örneğin: $ ipcs -q ----- İleti Kuyrukları ----- anahtar iltkiml sahibi izinler kull-bayt ileti-sayısı 0x01234567 0 kaan 644 0 0 Aslında Linux çekirdeği IPC nesneleri yaratıldığında onların bilgilerini proc dosya sisteminde "/proc/sysvipc" dizini içerisinde msg, sem, shm dosyalarının içerisine yazmaktadır. ipcs komutu da zaten bu dosyaların içindekilerini görüntülemektedir. Not: proc dosya sistemi bellekte oluşturulan, kernel tarafından bir dosya sistemi gibi sürekli güncellenen ve çekirdeğin yaptığı önemli işlemleri dış dünyaya bildirmek amacıyla kullanılan özel bir dosya sistemidir. Kursumuzda proc dosya sistemi ileride ayrı bir bölümde ele alınacaktır. proc dosya sisteminde bazı dosyalar yazılabilir durumda da olabilmektedir. Bu durumda bu dosyalara yazma yapıldığında çekirdeğin bazı ayarları da değiştirilmiş olmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Mesaj kuyruğuna bir mesaj göndermek için msgsnd POSIX fonksiyonu kullanılır. Fonksiyonun prototipi şöyledir: #include int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); Fonksiyonun birinci parametresi mesaj kuyruğunun msgget fonksiyonundan elde edilen id değeridir. (Tabii id değerleri sistem genelinde tektir (unique). Dolayısıyla proses mesaj kuyruğunun id değerini biliyorsa anahtardan hareketle id elde etmek zorunda değildir.) Fonksiyonun ikinci ve üçüncü parametreleri mesajı oluşturan byte yığınının adresini ve uzunluğunu almaktadır. Son parametre, gönderim ile ilgili bazı ayrıntıları belirten flag değerleridir. Normal olarak mesaj kuyruğu doluysa (mesaj kuyruğunun bazı limitleri vardır) msgsnd bloke oluşturmaktadır. Eğer mesaj kuyruğu dolu olduğu halde bloke oluşmasın isteniyorsa (yani blokesiz işlem yapılmak isteniyorsa) bu durumda fonksiyonun son parametresine IPC_NOWAIT özel değeri geçirilir. Eğer bu değer geçilmeyecekse bu parametre 0 olarak girilebilir. (Sistem 5 mesaj kuyruklarında işlem yapmak O_NONBLOCK gibi bir bayrakla değil, IPC_NOWAIT bayrağı ile yapılmaktadır.) Fonksiyonun ikinci parametresine, mesajı oluşturan byte topluluğunun adresinin girileceğini belirtmiştik. Üçüncü parametre de mesaj uzunluğunu belirtmekteydi. İşte mesaj aslında şöyle oluşturulmak zorundadır: Mesajın başında long bir alan olmalıdır. Bu long alan mesajın türünü (önceliğini de diyebiliriz) belirtmektedir. Bu mesaj türü 0'dan büyük pozitif bir değer biçiminde girilmelidir. Bu long alandan sonra mesajın byte'ları gelmelidir. Bu long alan ile mesaj byte'ları ardışıl olmalıdır. Bunu sağlamak için programcı tipik olarak bir yapı oluşturur. Örneğin: struct MSG { long mtype; char msg[1024]; }; struct MSG msg; Burada yapının msg elemanı (yani long kısımdan sonra gelen kısmını temsil eden kısım) için 1024 byte yer ayrılmıştır. Göncerilecek mesaj 1024 byte'tan daha kısa ise burada gereksiz boş alan kalır. 1024'ten daha yüksek bir mesajı bu yapıya yerleştiremeyiz. Bu sorun için ilk akla gelen yöntem yapının mesaj uzunluğuna bağlı olarak dinamik bir biçimde tahsis edilmesidir. Tabii long alandan sonraki adresi pratik bir biçimde elde edebilmemiz için yine long alandan sonra 1 byte uzunlukta bir dizi bulundurabiliriz: struct MSG { long mtype; char msg[1]; }; Şimdi mesajın n byte uzunlukta olduğunu tespit etmiş olalım (kontroller koddan kaldırılmıştır): struct MSG *msg; msg = (struct MSG *)malloc(sizeof(long) + n) msg->mtype = 1; memcpy(msg->msg, mesage_content); ... free(msg); C99 ve sonrasında bir yapının son elemanı dizi ise o elemanda uzunluk belirtilmeyebilmektedir. Buna C standartlarında "flexible array member" denilmektedir. Örneğin: struct MSG { long mtype; char msg[]; }; Tabii derleyici için bu msg elemanı yalnızca bir yer tutucudur. Bunun için bir yer tahsis etmez. Tabii bu işlem biraz zahmetlidir. Bu nedenle genellikle programcılar baştan maksimum mesaj uzunluğunu tespit edip buna uygun bir yapı bildirirler. Örneğin kullanılacak maksimum mesaj uzunluğu 8192 olsun: struct MSG { long mtype; char msg[8192]; }; msgsnd fonksiyonundaki üçüncü parametre olan mesaj uzunluğuna mesajın başındaki long alan dahil değildir. Yalnızca mesajın kendi uzunluğu dahildir. Yani msgsnd fonksiyonunu kullanırken biz mesaj uzunluğu olarak gerçek mesajın uzunluğunu veririz. Pekiyi mesajın türü ne anlamakta gelmektedir? Mesajın türü iki nedenden dolayı kullanılmaktadır: 1) Mesajı alan taraf spesifik bir türe ilişkin mesajları alabilir. Örneğin alan taraf türü 10 olan mesajları alırsa kuyruktaki diğer mesajları pas geçer ve ilk 10 türüne sahip olan mesajı alır. Bu da client-server tarzı uygulamalarda tek bir mesaj kuyruğunun kullanılmasını mümkün hale getirmektedir. 2) Mesajın türü istenirse "öncelik kuyruğu (priority queue)" gibi de kullanılmaktadır. Mesaj kuyruklarının tıpkı borularda olduğu gibi belli bir limiti vardır. Bu limite gelindiğinde msgsnd fonksiyonu default olarak bloke olur ve mesaj kuyruğunda yer açılana kadar blokede kalır. Böylece tıpkı borularda olduğu gibi bir senkronizasyon da sağlanmış olmaktadır. Ancak yukarıda da belirttiğimiz gibi msgsnd fonksiyonunun son parametresi IPC_NOWAIT girilirse bu durumda mesaj kuyruğunun limiti dolmuşsa msgsnd fonksiyonu blokeye yol açmaz -1 değeri ile geri döner ve errno değişkeni de EAGAIN değeriyle set edilir. msgsnd fonksiyonu başarısızlık durumunda -1 değeri ile geri döner. Mesaj kuyruğu doluysa ve bloke oluşmuşsa zaten henüz geri dönmez. Başarı durumunda fonksiyon 0 ile geri dönmektedir. Aşağıdaki programda 1000000 tane int değer mesaj kuyruğuna bir mesaj biçiminde msgsnd fonksiyonuyla yazılmak istenmiştir. Tabii mesaj kuyruğunun limitleri dolacağı için bu mesajların hepsi kuyruğa yazılamayacaktır. Dolayısıyla IPC_NOWAIT parametresi de geçilmediğine göre bloke oluşacaktır. Bu programda mesaj türü (yapının mtype alanı) 1 olarak alınmıştır. Mesaj türü zaten 0 olamaz. Eğer mesaj türüyle programcı ilgilenmeyecekse onu herhangi bir değerde tutabilir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #define MSG_KEY 0x1234567 struct MSG { long mtype; int val; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; if ((msgid = msgget(MSG_KEY, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); for (int i = 0; i < 1000000; ++i) { msg.mtype = 1; msg.val = i; if (msgsnd(msgid, &msg, sizeof(int), 0) == -1) exit_sys("msgsnd"); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Klasik Sistem 5 mesaj kuyruklarından mesaj okumak için msgrcv fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); Fonksiyonun birinci parametresi mesaj kuyruğunun id'sini belirtmektedir. Fonksiyonun ikinci ve üçüncü parametreleri kuyruktan alınan mesajın yerleştirileceği alanın adresini ve uzunluğunu almaktadır. Yine buraya geçirilecek adresin ilk byte'larında long bir alan bulunmalıdır. Mesaj bu long alandan sonraki yere yerleştirilir. Dolayısıyla programcı genellikle yine ikinci parametreye geçirilecek adresi bir yapı olarak oluşturur. Örneğin: struct MSG { long mtype; char msg[8192]; }; struct MSG msg; Burada fonksiyonun ikinci parametresine bu yapı nesnesinin adresi geçirilir. Fonksiyon kuyruktaki mesajın türünü ve içeriğini bu yapıya yerleştirecektir. Fonksiyonun üçüncü parametresinde belirtilen uzunluk long alandan sonraki gerçek mesajın uzunluğudur. Örneğin: result = msgrcv(msgid, &msg, 8192, ...); Fonksiyonun dördüncü parametresi hangi türe ilişkin mesajların alınacağını belirtmektedir. Kuyrukta farklı tür numaralarına (mtype) sahip mesajlar bir arada bulunuyor olabilir. Dördüncü parametre bunların alınış biçimini belirtmektedir. Bu parametre 0 olarak girilirse bu durumda msgrcv kuyruktaki tür değeri ne olursa olsun ilk mesajı alır. (0 değerinin geçerli bir tür değeri belirtmediğine dikkat ediniz.) Eğer bu parametreye biz 0'dan büyük bir değer girersek bu durumda o tür değerine sahip ilk mesaj kuyruktan alınır. Örneğin biz bu parametreye 100 girmiş olalım. Kuyrukta da 100 tür değerine sahip kuyruğun farklı yerlerinde 4 tane mesaj olsun. Biz bu durumda 100 tür değerine sahip kuyrukta en önceki mesajı alırız. Bu sayede biz kuyrukta belli bir tür değerine sahip mesajları elde edebilmekteyiz. Eğer bu parametre negatif bir değer olarak girilirse bu durumda bu negatif değerin mutlak değerine eşit ya da ondan küçük olan en küçük mesaj tür değerine sahip kuyruktaki ilk mesaj elde edilir. Örneğin kuyruktaki mesajların da mesaj tür değerleri şöyle olsun: 20 5 30 2 8 40 Şimdi biz msgrcv fonksiyonu bir döngü içerisinde dördüncü parametresi -10 olacak biçimde çağrımış olalım. -10 değerinin mutlak değeri 10'dur. 10'dan küçük ya da 10'a eşit olan en küçük tür değerine sahip olan mesaj 2 tür değerine sahip mesajdır. O halde kuyruktan önce bu mesaj alınacaktır. Sonra 5 tür değerine sahip olan mesaj sonra da 8 tür değerine sahip olan mesaj alınacaktır. Sonra koşula uygun kuyrukta mesaj kalmayacağına göre msgrcv artık bloke olacaktır. Fonksiyonun dördüncü parametresi negatif girildiğinde, artık kuyruk adeta bir öncelik kuyruğu gibi ele alınmaktadır. Ancak burada mesaj türü değeri küçük olanlar daha öncelikli kabul edilmektedir. msgrcv fonksiyonunun son parametresi (msgflg) mesajın alımına ilişkin bazı özellikleri belirtir. POSIX standartlarına göre buradaki bayrak değerleri iki bayraktan oluşabilir (bu iki bayrak birlikte bulunabilir): IPC_NOWAIT: Bu durumda blokesiz alım yapılmaktadır. Yani kuyrukta uygun mesaj yoksa msgrcv başarısız olur ve -1 değeri ile geri döner, errno değeri EAGAIN olarak set edilir. MSG_NOERROR: Normal olarak alınan mesajın uzunluğu msgrcv fonksiyonunun üçüncü parametresinde belirtilen tampon uzunluğundan büyükse msgrcv başarısız olur ve errno değeri E2BIG değeri ile set edilir. Ancak son parametrede MSG_NOERROR bayrağı kullanılırsa bu durumda msgrcv başarılı olur ancak kuyruktaki mesaj kırpılarak verdiğimiz tampona yerleştirilir. Programcı son parametre için özel bir bayrak belirlemek istemiyorsa bu parametreyi 0 olarak geçebilir. msgrcv fonksiyonu başarı durumunda okunan mesajın byte uzunluğu ile başarısızlık durumunda -1 ile geri dönmektedir. Okunan mesajın byte uzunluğuna long alan dahil değildir. Burada özellikle bir noktayı vurgulamak istiyoruz: Klasik Sistem 5 mesaj kuyruklarında karşı tarafın mesaj kuyruğunu kapatması diye bir durum yoktur. Dolayısıyla borularda olduğu gibi 0 byte okunana kadar yinelenen bir döngü oluşturulamamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Mesaj kuyruğundan sürekli mesaj okuyan programları yazarken dikkat ediniz. Çünkü bu programlar kuyrukta uygun mesaj olmadığı zaman blokeye yol açarlar. Örneğin: for (;;) { if (msgrcv(msgid, &msg, sizeof(int), 0, 0) == -1) exit_sys("msgrcv"); printf("Message type: %ld\n", msg.mtype); printf("Message content: %d\n", msg.val); } Burada msgrcv kuyruktaki mesajları alıp bitirdikten sonra artık kuyrukta mesaj yoksa bloke oluşacaktır. Pekiyi bu durumda bu program nasıl döngüden çıkacaktır? Tabii en normal durum kuyruktan alınan mesajın incelenip özel bir mesaja göre döngüden çıkmak olabilir. Eğer kuyruktaki tüm mesajların alınıp döngüden çıkılması isteniyorsa bu durumda blokesiz okuma yoluna gidilebilir. Örneğin: while (msgrcv(msgid, &msg, sizeof(int), 0, IPC_NOWAIT) != -1) printf("Message type: %ld\n", msg.mtype); printf("Message content: %d\n", msg.val); } if (errno != EAGAIN) exit_sys("msgrcv"); Aşağıdaki örnekte prog1 programı mesaj kuyruğuna 0'dan 1000000'a kadar değerleri mesaj olarak yerleştirmektedir. Mesajın tür değeri programlar tarafından kullanılmadığı için 1 olarak geçilmiştir. prog2 programı bu değerleri kuyruktan almış 1000000 değerini gördüğünde o da kendini sonlandırmıştır. Mesaj kuyruklarında close tarzı bir işlem yoktur. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #define MSG_KEY 0x1234567 struct MSG { long mtype; int val; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; if ((msgid = msgget(MSG_KEY, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); msg.mtype = 1; for (int i = 0; i <= 1000000; ++i) { msg.val = i; if (msgsnd(msgid, &msg, sizeof(int), 0) == -1) exit_sys("msgsnd"); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #define MSG_KEY 0x1234567 struct MSG { long mtype; int val; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; if ((msgid = msgget(MSG_KEY, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); for (;;) { if (msgrcv(msgid, &msg, sizeof(int), 0, 0) == -1) exit_sys("msgrcv"); if (msg.val == 1000000) break; printf("Message type: %ld\n", msg.mtype); printf("Message content: %d\n", msg.val); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte prog1.c programı klavyeden (stdin dosyasından) alınan yazıları sonunda null karakter dahil olmak üzere mesaj kuyruğuna yazmaktadır. Klavyeden "quit" girildiğinde bu mesaj da mesaj kuyruğuna yazılır ve prog1.c programı sonlanır. prog2.c programı da kuyruktan mesajları alarak onları ekrana (stdout dosyasına) yazdırmaktadır. Programlarda yine mesajın tür değeri kullanılmamaktadır, 1 biçiminde kodlanmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #define MSG_KEY 0x1234567 struct MSG { long mtype; char buf[8192]; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; char *str; if ((msgid = msgget(MSG_KEY, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); msg.mtype = 1; for (;;) { printf("Message text:"); fflush(stdout); if (fgets(msg.buf, 8192, stdin) == NULL) continue; if ((str = strchr(msg.buf, '\n')) != NULL) *str = '\0'; if (msgsnd(msgid, &msg, strlen(msg.buf) + 1, 0) == -1) exit_sys("msgsnd"); if (!strcmp(msg.buf, "quit")) break; } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #define MSG_KEY 0x1234567 struct MSG { long mtype; char buf[8192]; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; if ((msgid = msgget(MSG_KEY, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); for (;;) { if (msgrcv(msgid, &msg, 8192, 0, 0) == -1) exit_sys("msgrcv"); if (!strcmp(msg.buf, "quit")) break; printf("Message text: %s\n", msg.buf); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte prog1.c programı döngü içerisinde önce mesaj tür değerini sonra da mesaj yazısını alıp kuyruğa yollamıştır. prog2.c ise msgrcv fonksiyonunda dördüncü parametrede -100 kullanarak en düşük mesaj tür değeri önce alınacak biçimde mesajları kuyruktan almıştır. Bu örnekte önce prog1 programını çalıştırınız. quit mesajına düşük bir öncelik (yüksek değer) veriniz. Sonra prog2 programını çalıştırarak sonuçları gözden geçiriniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #define MSG_KEY 0x1234567 struct MSG { long mtype; char buf[8192]; }; void exit_sys(const char *msg); void clear_stdin(void); int main(void) { int msgid; struct MSG msg; char *str; if ((msgid = msgget(MSG_KEY, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); for (;;) { printf("Message type:"); scanf("%ld", &msg.mtype); clear_stdin(); printf("Message text:"); fflush(stdout); if (fgets(msg.buf, 8192, stdin) == NULL) continue; if ((str = strchr(msg.buf, '\n')) != NULL) *str = '\0'; if (msgsnd(msgid, &msg, strlen(msg.buf) + 1, 0) == -1) exit_sys("msgsnd"); if (!strcmp(msg.buf, "quit")) break; } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void clear_stdin(void) { while (getchar() != '\n') ; } /* prog2.c */ #include #include #include #include #include #define MSG_KEY 0x1234567 struct MSG { long mtype; char buf[8192]; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; if ((msgid = msgget(MSG_KEY, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); for (;;) { if (msgrcv(msgid, &msg, 8192, -100, 0) == -1) exit_sys("msgrcv"); if (!strcmp(msg.buf, "quit")) break; printf("Message type: %ld Message text: %s\n", msg.mtype, msg.buf); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Mesajların type değerinin kullanıldığı tipik örnekler client-server tarzı haberleşme örnekleridir. Şimdi mesaj kuyrukları kullanılarak client-server tarzı bir haberleşme yapmak isteyelim. Bu haberleşmede client'ların server'dan istekte bulunması için tek bir mesaj kuyruğu yeterlidir. Benzer biçimde server'ın da client'lara yanıtı geri döndürmesi için toplamda tek bir mesaj kuyruğu yeterlidir. (Anımsanacağı isimli borularda, server yanıtları iletirken mecburen her client için ayrı bir isimli boru kullanıyordu.) Haberleşmenin sistematiği şöyle olabilir: - Client programlar kendi proses id'lerini mesaj tür bilgisi olarak kullanıp mesajı önceden belirlenmiş mesaj kuyruğuna gönderir. Bu mesaj kuyruğuna "server mesaj kuyruğu" diyelim. - Server program server mesaj kuyruğundan sıraki mesajı okur. (msgrcv fonksiyonunun dördüncü parametresi 0 olarak girilirse kuyruktan sıradaki mesajlar okunmaktadır.) Bunun hangi client'tan geldiğini anlar. İşlemi yapar. İşlemin sonucunu "client mesaj kuyruğu" diye isimlendirdiğimiz mesaj kuyruğuna bir mesaj olarak yollar. Ancak yolladığı mesajın tür değeri client'ın proses id değeridir. - Mesajı gönderen client program, client mesaj kuyruğundan mesaj tür bilgisi kendi proses id'si olacak biçimde msgrecv fonksiyonu ile yanıtı elde eder. Burada da görüldüğü gibi client programların hepsi ortak bir mesaj kuyruğundan server yanıtlarını alabilmektedir. Eğer mesaj kuyruğunun böyle bir tür değeri olmasaydı ve mesaj kuyruğundan belli bir tür değerine ilişkin mesaj okunamasaydı bu durum mümkün olamazdı. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 42. Ders 01/04/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında klasik Sistem 5 mesaj kuyruklarıyla client-server haberleşme programı yazılırken tek bir kuyruk da kullanılabilir. Örneğin bu durumda client'lar yine kendi proses id'lerini mesaj tür değeri yaparak kuyruğa server için mesaj bırakır. Server da mesajı işledikten sonra bu kez yanıtı ilgili prosesin id'sini mesaj tür bilgisi yaparak kuyruğa yazar. Client da kuyruktan kendi mesaj tür bilgisiyle okuma yapabilir. Ancak tek bir kuyruğun kullanılması aslında iyi bir yöntem değildir. Bu durum haberleşmeyi çok kırılgan hale getirmektedir. Şöyle ki: - Bu durum "kilitlenme (deadlock)" denilen soruna kolaylıkla yol açabilmektedir. Mesaj kuyruklarının bir kapasitesi vardır. Kuyruk dolduğunda blokeli yazmalarda kuyruğa mesaj yazacak taraf bloke olmaktadır. Bu senaryoda server bir client'ın isteğini karşılamak için mesaj kuyruğuna yanıt mesajını yazacağı zaman diğer client'lar istek yaparak mesaj kuyruğunu doldurmuş olabilir. Bu durumda ne client'lar kuyruğa mesaj yazabilir ne de server herhangi bir yanıtı kuyruğa yazabilir. İşte bu durum tipik bir "kilitlenme (deadlock)" durumudur. - Bu modelde client ilk kez server'a kendi proses id'si ile mesaj gönderirken aynı kuyruğa mesajı yazacağı için birazdan aynı proses id ile kuyruktan mesaj almaya çalışırken kendi yazdığı mesajı alabilir. Tabii bunu engellemenin çeşitli yolları da vardır. Örneğin böylesi bir durumda server elde ettiği proses id'nin başına bir bilgi ekleyebilir. Client da okumasını buna göre yapabilir. - Bu model, kilitlenme durumu aşılsa bile yavaş olmaktadır. Çünkü kuyrukların bir kapasitesi vardır. Bu kapasite dolduğunda blokeler haberleşmeyi yavaşlatabilmektedir. İlk açıkladığımız, iki kuyruklu modelde her ne kadar "kilitlenmeye daha dirençliyse" kırılgan bir yapı oluşturabilmektedir. Şöyle ki, belli bir client kuyruktan bilgi okumazsa (kasten bunu yapabilir ama client programı da biz yazıyorsak bunu kasten yapmayız) kuyruk dolabilir yine bir kilitlenme oluşabilir. Tabii client program neden kuyruktan mesaj okumayacaktır? Client program da bizim tarafımızdan yazıldığına göre genellikle böyle bir şey olmaz. Ancak client programların başka yerlerinde yanlış bir teknik yüzünden bir bloke oluşsa onlar mesajlarını kuyruktan alamayabilirler. Bu durum diğer client'ları olumsuz etkileyebilir. O halde mesaj kuyruklarıyla client-server haberleşme daha sağlam yapılacaksa yine her client için ayrı bir mesaj kuyruğu kullanılabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında mesaj kuyruklarına 0 byte'lık mesajlar da gönderilebilir. Bu durumda kuyruğa yine mesaj yazılır. Mesajın tür değeri yine 0 byte'lık mesajı gönderen kişinin belirlediği tür değeri olarak mesajı okuyan kişi tarafından elde edilmektedir. Yani 0 byte'lık mesajlar yine kuyruğa bir mesaj gibi yazılmaktadır. Bu tür mesajların uzunluğu çekirdek tarafından 1 byte mesajlarmış gibi ele alınmaktadır. Böylece çekirdek sonsuz döngü içerisinde 0 byte mesaj gönderme durumunda mesaj kuyruğunun belli bir süre sonra dolmasına yol açmaktadır. Pekiyi 0 byte'lık mesajlar neden gönderilebilir? İşte genellikle 0 byte uzunluğunda mesajlar "iletişimi sonlandırmak amacıyla" gönderilmektedir. Tabii bazen gönderilecek mesaj gerçekten bir bilgi içermeyip yalnızca bir tür değeri de içeriyor olabilir. Aşağıdaki örnekte prog1 programı klavyeden (stdin dosyasından) aldığı yazıyı mesaj kuyruğuna yazmakta prog2 programı da bunu mesaj kuyruğundan okumaktadır. İletişim sonlandırılacağı zaman prog1 kuyruğa 0 uzunlukta özel bir mesaj tür değeri olan QUIT_MSG mesajını bırakır. prog2 programı da bu özel mesaj tür değerini aldığında işlemini sonlandırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #define MSG_KEY 0x1234567 #define NORMAL_MSG 1 #define QUIT_MSG 2 struct MSG { long mtype; char buf[8192]; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; char *str; size_t len; if ((msgid = msgget(MSG_KEY, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); for (;;) { printf("Message text:"); fflush(stdout); if (fgets(msg.buf, 8192, stdin) == NULL) continue; if ((str = strchr(msg.buf, '\n')) != NULL) *str = '\0'; if (!strcmp(msg.buf, "quit")) { msg.mtype = QUIT_MSG; len = 0; } else { msg.mtype = NORMAL_MSG; len = strlen(msg.buf) + 1; } if (msgsnd(msgid, &msg, len, 0) == -1) exit_sys("msgsnd"); if (msg.mtype == QUIT_MSG) break; } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #define MSG_KEY 0x1234567 #define NORMAL_MSG 1 #define QUIT_MSG 2 struct MSG { long mtype; char buf[8192]; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; if ((msgid = msgget(MSG_KEY, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); for (;;) { if (msgrcv(msgid, &msg, 8192, 0, 0) == -1) exit_sys("msgrcv"); if (msg.mtype == QUIT_MSG) break; printf("Message type: %ld Message text: %s\n", msg.mtype, msg.buf); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Sistem 5 IPC nesnelerinin önemli sorunlarından biri bunların anahtar (key) değerlerinin bir çakışma yaratabilmesidir. Yani bizim belirlediğimiz anahtara ilişkin bir mesaj kuyruğu başkaları tarafından yaratılmış olabilir. Bu tür durumlarda anahtar yerine IPC_PRIVATE özel değerinin kullanılabileceğini belirtmiştik. Ancak bu durumda da elde edilen id değerinin diğer prosese iletilmesi gerekmekteydi. İşte anahtar çakışmasını azaltmak için ftok isimli bir POSIX fonksiyonundan faydalanılmaktadır. Fonksiyonun prototipi şöyledir: #include key_t ftok(const char *path, int id); Fonksiyonun birinci parametresi olan bir dosyanın yol ifadesini belirtmelidir. İkinci parametre kombine edilecek değeri belirtir. İkinci parametrenin yalnızca düşük anlamlı bir byte'ı kombine işleminde kullanılmaktadır. Bu parametrenin düşük anlamlı byte'ının 0 olmaması gerekir. Fonksiyon eğer biz ona aynı yol ifadesini ve aynı id değerini veriyorsak bize aynı geri dönüş değerini verir. Eğer biz ona farklı bir yol ifadesi ve/veya farklı bir id veriyorsak o bize başka bir geri dönüş değeri verir. Fonksiyonun amacı, Sistem 5 IPC nesneleri için bir yol ifadesinden hareketle bir anahtar değerin sistem genelinde tek olacak biçimde (unique) oluşturulmasıdır. Böylece her proses anahtar üretmekte bu ftok fonksiyonunu kullanırsa ve ftok fonksiynuna kendisine ilişkin bir yol ifadesi ve id değeri verirse sistem genelinde çakışma olmaz. Her ne kadar ftok fonksiyonu sistem genelinde bir anahtar çakışmasını engellemek için düşünülmüşse de maalesef böyle bir garantinin verilmesi mümkün değildir. Dosyanın yol ifadesi ve id değerinden hareketle bir tamsayı değerin (key_t değerinin) sistem genelinde tek olacak biçimde üretilmesi mümkün değildir. Standartlar bile bunu garanti altına almamaktadır. Yani iki program farklı yol ifadesi ve id değeri verdiğinde bile aynı anahtar değerini elde edebilir (her ne kadar bu olasılık çok düşükse de). Libc kütüphanesindeki ftok fonksiyonu dosyanın inode numarasını, dosyanın içinde bulunduğu aygıtın numarasını ve bizim verdiğimiz id değerini kombine ederek bir değer üretmektedir. Bu üretim de çakışmama olasılığını sıfırlayamamaktadır. ftok fonksiyonunu kullanırken dosyayı silip aynı isimle yeniden yaratırsak dosyanın inode numarası değişebileceğinden dolayı aynı değeri elde edemeyebiliriz. ftok fonksiyonu başarı durumunda ürettiği anahtar değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Pekiyi ftok fonksiyonu farklı yol ifadeleri ve/veya id'ler için farklı anahtar değeri üretmeyi garanti etmiyorsa bu fonksiyonun kullanımının anlamı var mıdır? Aslında bu soruya "yoktur" yanıtını verebiliriz. Ancak ftok fonksiyonundan hedeflenen şeylerden biri de anahtarın sayısal olmasından kurtulunup yazısal hale getirilmesidir. Fakat ne olursa olsun ftok fonksiyonun verdiği değer programcının uyduracağı değerden daha iyi olma eğilimindedir (tabii herkes ftok kullanırsa). Aşağıda ftok kullanımına bir örnek verilmiştir. İki program da aynı yol ifadesi ve aynı ftok id'si ile ftok uygulamış ve aynı değeri elde etmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 #define NORMAL_MSG 1 #define QUIT_MSG 2 struct MSG { long mtype; char buf[8192]; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; char *str; size_t len; key_t key; if ((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if ((msgid = msgget(key, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); for (;;) { printf("Message text:"); fflush(stdout); if (fgets(msg.buf, 8192, stdin) == NULL) continue; if ((str = strchr(msg.buf, '\n')) != NULL) *str = '\0'; if (!strcmp(msg.buf, "quit")) { msg.mtype = QUIT_MSG; len = 0; } else { msg.mtype = NORMAL_MSG; len = strlen(msg.buf) + 1; } if (msgsnd(msgid, &msg, len, 0) == -1) exit_sys("msgsnd"); if (msg.mtype == QUIT_MSG) break; } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 #define NORMAL_MSG 1 #define QUIT_MSG 2 struct MSG { long mtype; char buf[8192]; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; key_t key; if ((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if ((msgid = msgget(key, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); for (;;) { if (msgrcv(msgid, &msg, 8192, 0, 0) == -1) exit_sys("msgrcv"); if (msg.mtype == QUIT_MSG) break; printf("Message type: %ld Message text: %s\n", msg.mtype, msg.buf); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Mesaj kuyruklarının sistemlerde o sisteme özgü çeşitli kapasite limitleri söz konusu olabilmektedir. POSIX standartları bu kapasite limitleri konusunda bir açıklama yapmamıştır. Ancak Linux sistemlerinde çekirdek mesaj kuyrukları için üç kapasite limiti belirlemektedir: 1) MSGMAX Limiti: Bu limit bir mesaj kuyruğuna yazılabilecek mesajın, mesaj tür değeri haricindeki kısmının maksimum byte uzunluğudur. Herhangi bir proses, bu değerin yukarısındaki uzunluktaki bir mesajı kuyruğa yerleştirememektedir. Linux sistemlerinde bu değer default olarak şimdilik 8192'dir. Bu değer proc dosya sisteminde /proc/sys/kernel/msgmax dosyasında belirtilmektedir. Bu değer bu dosyanın içeriği değiştirilerek değiştirilebilmektedir. Ancak bunu yapabilmek için prosesin uygun önceliğe sahip olması (proses id'sinin 0 olması) gerekir. 2) MSGMNI (Maximum Number of Id): Bu limit tüm sistemde yaratılcak farklı mesaj kuyruklarının maksimum sayısını belirtmektedir. Bu sayı "/proc/sys/kernel/msgmni" dosyasından elde edilebilir. Linux sistemlerinde bunun default değeri şimdilik 32000'dir. 3) MSGMNB (Maximum Number of Bytes): Bu değer bir mesaj kuyruğundaki mesajın tür değeri dışındaki tüm mesajlarının toplam maximum byte sayısıdır. msgsnd fonksiyonu bu değer aşıldığında blokeli işlemlerde bloke olur. Bu değer /proc/sys/kernel/msgmnb dosyasından elde edilebilir. Linux sistemlerinde bu değer şimdilik default durumda 16384'tür. Yani msgsnd fonksiyonu bu değer aşıldığında bloke olur. Ancak bu değer eldeki RAM miktarına bağlı olarak çekirdek tarafından ayarlanabilmektedir. Buradaki limit değerleri çekirdek parametreleriyle ya da çekirdek derlenirken konfigürasyon parametreleriyle değiştirilebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Klasik Sistem 5 mesaj kuyruklarına ilişkin bazı kontrol işlemleri msgctl isimli POSIX fonksiyonu ile yapılmaktadır. Fonksiyonun prototipi şöyledir: #include int msgctl(int msqid, int cmd, struct msqid_ds *buf); Fonksiyonun birinci parametresi mesaj kuyruğunun id değeridir. İkinci parametre uygulanacak işlemi belirtir. Bu uygulanacak işlem şunlardan biri olabilir: IPC_STAT IPC_SET IPC_RMID Fonksiyonun üçüncü parametresi msqid_ds isimli yapı nesnesinin adresini almaktadır. Bu yapı içerisinde tanımlanmıştır. POSIX bu yapının olması gereken elemanlarını belirtmiştir. Linux sistemlerinde yapı şöyle bildirilmiştir: struct msqid_ds { struct ipc_perm msg_perm; /* Ownership and permissions */ time_t msg_stime; /* Time of last msgsnd(2) */ time_t msg_rtime; /* Time of last msgrcv(2) */ time_t msg_ctime; /* Time of creation or last modification by msgctl() */ unsigned long msg_cbytes; /* # of bytes in queue */ msgqnum_t msg_qnum; /* # number of messages in queue */ msglen_t msg_qbytes; /* Maximum # of bytes in queue */ pid_t msg_lspid; /* PID of last msgsnd(2) */ pid_t msg_lrpid; /* PID of last msgrcv(2) */ }; Yapının msg_perm elemanı ipc_perm isimli yapı türünden nesnedir. ipc_perm yapısı da şöyle bildirilmiştir: struct ipc_perm { uid_t uid; /* Effective UID of owner */ gid_t gid; /* Effective GID of owner */ uid_t cuid; /* Effective UID of creator */ gid_t cgid; /* Effective GID of creator */ unsigned short mode; /* Permissions */ }; Buradaki cuid ve cgid elemanları mesaj kuyruğunu ilk yaratan prosesin etkin kullanıcı ve grup id'sini belirtmektedir. Normal olarak uid ve gid elemanları cuid ve cgid elemanlarıyla aynı değerdedir. Ancak daha sonra msgctl fonksiyonu ile bu değerler değiştirilebilmektedir. Yapının mode elemanı da mesaj kuyruğunun erişim haklarını belirtmektedir. msqid_ds yapısının msg_stime, msg_rtime ve msg_ctime elemanları mesaj kuyruğuna en son yapılan msgsnd, msgrcv ve msgctl işlemlerinin zamanlarını belirtmektedir. Yapının msg_cbytes elemanı (POSIX standartlarında bu eleman belirtilmemiştir, ancak Linux sistemlerinde bulunmaktadır) mesaj kuyruğundaki tüm mesajların mesajın tür değeri haricindeki kısımlarının byte uzunluğunu belirtmektedir. Yapının msg_qnum elemanı ise mesaj kuyruğundaki o anda bulunan mesaj sayısını belirtir. Yapının msg_qbytes elemanı mesaj kuyruğunun tutabileceği mesaj tür değeri haricindeki kısımların toplam byte sayısını belirtmektedir. Bu değer, mesaj kuyruğu yaratılırken Linux sistemlerinde MSGMNB değeri olarak yapıya aktarılmaktadır. Yapının msg_lspid ve msg_lrpid elemanları sırasıyla kuyruğa son msgsnd yapan prosesin id değerini ve kuyruktan son msgrcv yapan prosesin id değerini belirtmektedir. Yapı bildirimindeki msgqnum_t ve msglen_t tür isimleri içerisinde işaretsiz bir tamsayı türü olarak (ama en azından unsigned short uzunlukta) bildirilmelidir. Bizim mesaj kuyruğuna mesaj yazabilmemiz için mesaj kuyruğuna "write" hakkına, mesaj kuyruğundan mesaj okuyabilmemiz için mesaj kuyruğundan "read" hakkına sahip olmamız gerekir. Buradaki erişim kontrolü tıpkı dosyalarda olduğu gibi yapılmaktadır. Ancak "owner" kontrolü burada msqid_ds yapısının msg_perm yapı elemanının uid elemanı ile ve "group" kontrolü ise aynı yapının gid elemanı ile belirlenmektedir. Normal olarak yukarıda da belirttiğimiz gibi mesaj kuyruğu yaratıldığında ipc_perm yapısının uid ve gid elemanları cuid ve cgid elemanlarıyla aynı değerdedir. Ancak daha sonra izleyen paragraflarda ele alacağımız üzere IPC_SET işlemi ile bunlar farklılaşabilirler. Tabii mesaj kuyruğunun yaratıcısına ilişkin cuid ve cgid değerleri değiştirilememektedir. Fonksiyon başarı durumunda 0 değeri ile başarısızlık durumunda -1 değeri ile geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- msgctl fonksiyonunun ikinci parametresi IPC_STAT geçilirse bu durumda mesaj kuyruğu bilgileri elde edilerek msqid_ds yapı nesnesinin içine yerleştirilmektedir. Örneğin: struct msqid_ds msginfo; ... if (msgctl(msgid, IPC_STAT, &msgino) == -1) exit_sys("msgctl"); Aşağıda bir mesaj kuyruğu yaratılıp (ya da açılıp) içerisine iki mesaj yerleştirilmiş sonra da msgctl fonksiyonu ile IPC_STAT parametresi kullanılarak mesaj kuyruğu bilgileri elde edilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 321 struct MSG { long mtype; char buf[8192]; }; void exit_sys(const char *msg); int main(void) { int msgid; struct MSG msg; key_t key; struct msqid_ds msginfo; if ((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if ((msgid = msgget(key, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); msg.mtype = 1; strcpy(msg.buf, "test"); if (msgsnd(msgid, &msg, 4, 0) == -1) exit_sys("msgsnd"); if (msgsnd(msgid, &msg, 4, 0) == -1) exit_sys("msgsnd"); if (msgctl(msgid, IPC_STAT, &msginfo) == -1) exit_sys("msgctl"); printf("Maximum number of bytes: %ju\n", (uintmax_t)msginfo.msg_qbytes); /* 16384 ama değişebilir */ printf("Maximum number of bytes: %ju\n", (uintmax_t)msginfo.msg_qnum); /* 2 */ return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- msgctl fonksiyonunun ikinci parametresi IPC_SET olarak girilirse bu durumda mesaj kuyruğuna ilişkin bazı değerler set edilebilmektedir. Ancak bu durumda msqid_ds yapısının tüm elemanları değil yalnızca aşağıda belirtilen elemanları değiştirilmektedir: msg_perm.uid msg_perm.gid msg_perm.mode msg_qbytes IPC_SET işleminin yapılabilmesi için fonksiyonu çağıran prosesin etkin kullanıcı id'sinin mesaj kuyruğunda belirtilen msg_perm.uid ya da msg_perm.cuid değerine eşit olması ya da prosesin uygun önceliğe sahip olması (root) gerekmektedir. IPC_SET işlemi yapılırken msqid_ds yapısının diğer elemanları zaten dikkate alınmamaktadır. Yani diğer elemanlarda geçersiz değerlerin olması önemli değildir. Tabii programcı yalnızca belli bir değeri değiştirecekse önce IPC_STAT yapıp ondan sonra IPC_SET uygulamalıdır. Çünkü IPC_SET yukarıdaki elemanların hepsini değiştirmektedir. msqid_ds yapısının msg_qbytes elemanının değiştirilmesi yalnızca uygun önceliğe sahip (örneğin root) prosesler tarafından yapılabilmektedir. Aşağıdaki örnekte mesaj kuyruğuna ilişkin durum bilgisi IPC_STAT parametresiyle elde edilip IPC_SET parametresiyle kuyruğun msg_qbytes elemanı değiştirilmiştir. Ancak bunun yapılabilmesi için programın sudo ile çalıştırılması gerekir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 321 void exit_sys(const char *msg); int main(void) { int msgid; key_t key; struct msqid_ds msginfo; if ((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if ((msgid = msgget(key, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); if (msgctl(msgid, IPC_STAT, &msginfo) == -1) exit_sys("msgctl"); printf("Maximum number of bytes: %ju\n", (uintmax_t)msginfo.msg_qbytes); /* muhtemelen 16384 */ msginfo.msg_qbytes = 30000; if (msgctl(msgid, IPC_SET, &msginfo) == -1) exit_sys("msgctl"); if (msgctl(msgid, IPC_STAT, &msginfo) == -1) exit_sys("msgctl"); printf("Maximum number of bytes: %ju\n", (uintmax_t)msginfo.msg_qbytes); /* 30000 */ return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Mesaj kuyruğunu silmek için msgctl fonksiyonu IPC_RMID parametresiyle çağrılmalıdır. Mesaj kuyruğunun bu biçimde silinebilmesi için prosesin uygun önceliğe sahip olması (örneğin root olması) ya da prosesin etkin kullanıcı id'sinin msqid_ds yapısındaki ipc_perm.uid ya da ipc_perm.cuid değerine eşit olması gerekmektedir. Yani özetle biz root değilsek başkasının mesaj kuyruğunu silemeyiz. Tabii mesaj kuyrukları bu biçimde silinmezse zaten reboot işleminde otomatik olarak silinmektedir. Mesaj kuyrukları silindiğinde artık o anda mesaj kuyruğunu kullanmakta olan prosesler hemen error ile geri dönmektedir. (Yani dosya sisteminde olduğu gibi, gerçek silme tüm kullanan proseslerin kaynağı bırakmasıyla değil, o anda yapılmaktadır.) IPC_RMID parametresi kullanılırken artık üçüncü parametreye gereksinim duyulmamaktadır. Bu parametre NULL olarak geçilebilir. Örneğin: if (msgctl(msgid, IPC_RMID, NULL) == -1) exit_sys("msgctl"); Aşağıda daha önce oluşturulmuş olan mesaj kuyruğu msgctl fonksiyonu ve IPC_RMID parametresi ile silinmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 void exit_sys(const char *msg); int main(void) { int msgid; key_t key; if ((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if ((msgid = msgget(key, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("msgget"); if (msgctl(msgid, IPC_RMID, NULL) == -1) exit_sys("msgctl"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Linux'a özgü bir biçimde msgctl fonksiyonunda ikinci parametrede bazı özel değerler de kullanılabilmektedir. Bu özel değerler şunlardır: IPC_INFO MSG_INFO MSG_STAT MSG_STAT_ANY Biz kursumuzda standart olmayan bu parametreler üzerinde durmayacağız. Ancak ilgili man sayfalarından ya da başka dokümanlardan bu parametrelerin işlevleri konusunda bilgi edinebilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Klasik Sistem 5 IPC mekanizması için iki önemli kabuk komutu vardır: ipcs ve ipcrm. Biz daha önce ipcs komutunu görmüştük. Bu komutun /proc/sysvipc dizinindeki msg, sem ve shm dosyalarını okuyarak işlem yaptığını belirtmiştik. İşte ipcrm komutu ise belli bir ipc nesnesini silmek için kullanılmaktadır. ipcrm silme işlemini anahtara göre (büyük harfler) ya da id'lere göre (küçük harfler) yapabilmektedir. Örneğin mesaj kuyruklarının ipcrm ile silinmesi anahtar belirtilerek -Q seçeneği ile id belirtilerek -q seçeneği ile yapılabilmektedir. Tabii bu komut aslında msgctl fonksiyonunu kullanarak yazılmıştır. Örneğin: ipcrm -Q 0x7b050002 ya da örneğin: ipcrm -q 3 ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 43. Ders 02/04/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi klasik Sistem 5 IPC nesnelerinin 1990'lı yıllarda alternatif yeni biçimleri oluşturulmuştur. Bunlara halk arasında "POSIX IPC nesneleri" denilmektedir. Bu nesneler sonradan UNIX/Linux dünyasına katıldığı için belli bir süre taşınabilirlik problemlerine sahipti. Ancak artık bu nesneler de yaygın tüm UNIX/Linux sistemlerinde bulunmaktadır. POSIX IPC mekanizmasının klasik Sistem 5 IPC mekanizmasından en önemli farklılıklarından biri IPC nesnesinin belirlenmesi için bir anahtarın değil doğrudan bir dosya isminin kullanılmasıdır. Yani adeta isimli borularda olduğu gibi iki proses bir dosya isminde anlaşmaktadır. POSIX IPC nesneleri birer dosya ismiyle temsil edilmiş olsa da bu isimli dosyalar bir dizin girişi biçiminde bulundurulmamaktadır. Daha doğrusu böyle bir zorunluluk yoktur. POSIX standartlarına göre POSIX IPC nesnelerine ilişkin dosya isimleri "kök dizinde bir dosya" belirtmelidir. Örneğin "/my_message_queue" gibi. POSIX buradaki dosya isminin kök dizinde olmasını zorunlu hale getirmemiştir. Ancak bunun işletim sistemine bağlı olduğunu belirtmiştir. Yani programcı eğer ilgili işletim sistemi kabul ediyorsa buradaki dosya ismini başka bir dizindeki dosya ismi gibi verebilir. Ancak taşınabilir programların bu isimleri kök dizinde uydurması gerekmektedir. Her ne kadar isimlerin bile çakışabileceği mümkünse de bu olasılık sayısal anahtarların çakışmasından çok daha düşüktür. POSIX IPC mekanizmasının Sistem 5 IPC mekanizmasından diğer önemli farklılığı POSIX IPC mekanizmasının "dosya işlemlerine" benzetilmesidir. Yani bu işlemler adeta dosya işlemleri gibi ele alınmaktadır. Örneğin klasik Sistem 5 mesaj kuyruğunu birisi msgctl fonksiyonu ile sildiğinde o anda bu kuyruk üzerinde işlem yapan fonksiyonlar başarısızlıkla geri dönerler. Ancak POSIX mesaj kuyruklarında birisi dışarıdan bu mesaj kuyruğunu silse bile, son proses bu mesaj kuyruğunu kapatmadan gerçek silme yapılmamaktadır. Bu durum adeta dosya silmedeki "unlink/remove" fonksiyonlarına benzemektedir. Hem klasik Sistem 5 hem de POSIX IPC nesneleri silinene kadar ya da reboot işlemine kadar hayatta kalmaktadır (kernel persistant). Bu bakımdan bu iki nesne grubu birbirine benzemektedir. Klasik Sistem 5 ve POSIX IPC mekanizmasını yöneten fonksiyonlarda da arayüz bakımından bazı farklılıklar vardır. POSIX IPC fonksiyonunlarının isimlendirme tarzları da farklıdır. Örneğin mesaj kuyruklarını yaratmak için klasik Sistem 5 kuyruklarında msgget fonksiyonu kullanılırken POSIX mesaj kuyruklarında mq_open fonksiyonu kullanılmaktadır. Tabii isimlendirme ve genel kullanım POSIX IPC nesnelerinin arasında tutarlı bir biçimde oluşturulmuştur. POSIX IPC nesneleri, POSIX'in "real time extension" ekleriyle standartlara dahil edilmiştir. Bu fonksiyonlar bu nedenle libc kütüphanesinin içerisinde değil librt kütüphanesinin içerisinde bulunmaktadır. Bu nedenle POSIX IPC nesnelerini kullanan programları derlerken -lrt seçeneğinin kullanılması gerekir. Örneğin: $ gcc -o sample sample.c -lrt ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- POSIX mesaj kuyrukları şöyle kullanılmaktadır: 1) İ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ılmalıdı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 ise elemanı bir mesajın maksimum uzunluğunu belirtmektedir. mq_curmsgs elemanı da 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. Örneğin: mqd_t mqdes; struct mq_attr attr; attr.mq_maxmsg = 10; attr.mq_msgsize = 32; if ((mqdes = mq_open(MSGQUEUE_NAME, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, &attr)) == -1) exit_sys("mq_open"); 2) 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 dizinin 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, dördüncü parametre ise 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 blokeye yol açmaktadır. Ancak açış sırasında O_NONBLOCK bayrağı belirtilmişse mq_send kuyruk doluysa blokeye yol açmaz, 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. 3) 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. 4) Pekiyi POSIX mesaj kuyruklarında mesaj haberleşmesi nasıl sonlandırılacaktır? Burada da 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. 5) 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. 6) 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. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #define MSGQUEUE_NAME "/test_queue" void exit_sys(const char *msg); int main(void) { mqd_t mqdes; struct mq_attr attr; attr.mq_maxmsg = 10; attr.mq_msgsize = 32; if ((mqdes = mq_open(MSGQUEUE_NAME, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, &attr)) == -1) exit_sys("mq_open"); for (int i = 0; i < 100; ++i) { if (mq_send(mqdes, (const char *)&i, sizeof(int), 0) == -1) exit_sys("mq_send"); } mq_close(mqdes); if (mq_unlink(MSGQUEUE_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 MSGQUEUE_NAME "/test_queue" void exit_sys(const char *msg); int main(void) { mqd_t mqdes; char buf[32]; int val; if ((mqdes = mq_open(MSGQUEUE_NAME, O_RDONLY)) == -1) exit_sys("mq_open"); for (;;) { if (mq_receive(mqdes, buf, 32, NULL) == -1) exit_sys("mq_receive"); val = *(const int *)buf; printf("%d ", val); fflush(stdout); if (val == 99) break; } printf("\n"); mq_close(mqdes); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 44. Ders 08/04/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi mesajı alan taraf tutacağı tamponun büyüklüğünü nasıl anlayacaktır? Çünkü oluşturulması gereken tampon mesaj kuyruğu yaratılırken kullanılan struct mq_attr yapısının içerisinde belirtilmektedir. Anımsanacağı gibi mesaj kuyruğu yaratılırken mq_open fonksiyonunda mq_attr parametresi NULL geçilirse mesaj kuyruğundaki mesaj uzunlukları için default değer alınmaktadır. Her ne kadar Linux sistemlerinde şu anda bu default değer 8192 ise de başka sistemlerde ve Linux'ta ileride bunun böyle olacağının bir garantisi yoktur. Yukarıdaki örnekte olduğu gibi mesaj kuyruğu yaratılırken bu değer programcı tarafından belirleniyorsa zaten bu değer bilinmektedir. Ancak mq_open fonksiyonunda özellik parametresi NULL geçilebilir. Ya da mesajı okuyacak taraf bunu bilmeyebilir. Bu durumda uygulanacak şey mq_getattr fonksiyonu ile mesaj kuyruğunun özelliklerinin alınması ve tamponun dinamik bir biçimde orada belirtilen mq_msgsize değeri kadar oluşturulmasıdır. mq_getattr fonksiyonu mesaj kuyruğunun özellik bilgisini almak için kullanılır. Fonksiyonun prototipi şöyledir: #include int mq_getattr(mqd_t mqdes, struct mq_attr *attr); Fonksiyonun birinci parametresi mq_open fonksiyonu ile elde edilen mesaj kuyruğu betimleyicisidir. İkinci parametre ise mesaj kuyruğu özelliklerinin yerleştirileceği mq_attr yapısının adresini almaktadır. Fonksiyon başarı durumunda 0, başarısızlık durumunda -1 değerine geri dönmektedir. Mesaj kuyruğu yaratıldıktan sonra mesaj kuyruğunun özellikleri mq_setattr fonksiyonu ile değiştirilebilir. Fonksiyonun prototipi şöyledir: #include int mq_setattr(mqd_t mqdes, struct mq_attr *newattr, struct mq_attr *oldattr); Fonksiyonun birinci parametresi, mesaj kuyruğu betimleyicisini belirtir. İkinci parametre yeni özelliklerin bulunduğu struct mq_attr yapı nesnesinin adresini alır. Üçüncü parametre ise değiştirilmeden önceki mesaj kuyruğu özelliklerini elde etmek için kullanılan yapı nesnesinin adresini belirtir. Bu parametre NULL adres olarak geçilebilir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. mq_setattr fonksiyonu ile mq_attr yapısının yalnızca flags parametresi dikkate alınmaktadır. Dolayısıyla değiştirilebilecek tek özellik aslında O_NONBLOCK bayrağıdır. Yapının diğer elemanları, fonksiyon tarafından dikkate alınmamaktadır. (Mesaj kuyruğu yaratıldıktan sonra maksimum mesaj sayısının ya da mesaj uzunluklarının değiştirilmesi zaten genel olarak uygun değildir.) Aşağıdaki örnekte "prog1" programı stdin dosyasından mesajı ve mesajın öncelik değerini alarak mesaj kuyruğuna yazmaktadır. "prog2" programı da mesaj kuyruğundan bu bilgileri alarak stdout dosyasında bunları görüntülemektedir. Burada öncelikteki yüksek değerin gerçek yüksek öncelik anlamına geldiğine dikkat ediniz. (Halbuki klasik Sistem 5 mesaj kuyruklarında yüksek öncelik düşük değerler temsil edilmektedir.) Bu programları çalıştırırken öncelik testini yapabilmek için önce "prog1" programını çalıştırıp kuyruğa çeşitli önceliklerde mesajlar gönderiniz. Sonra "prog2" programını çalıştırıp durumu gözleyiniz. Örnekteki prog2 programı önce mq_getattr fonksiyonu ile mesaj kuyruğundaki maksimum mesaj uzunluğunu elde etmiş ve o uzunlukta dinamik bir alan tahsis etmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #define MSGQUEUE_NAME "/my_message_queue" void clear_stdin(void); void exit_sys(const char *msg); int main(void) { mqd_t mqdes; char buf[8192]; char *str; int prio; if ((mqdes = mq_open(MSGQUEUE_NAME, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, NULL)) == -1) exit_sys("mq_open"); for (;;) { printf("Message text:"); fflush(stdout); if (fgets(buf, 8192, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; printf("Priority:"); fflush(stdout); scanf("%d", &prio); clear_stdin(); if (mq_send(mqdes, buf, strlen(buf), prio) == -1) exit_sys("mq_send"); if (!strcmp(buf, "quit")) break; } mq_close(mqdes); if (mq_unlink(MSGQUEUE_NAME) == -1) exit_sys("mq_unlink"); return 0; } void clear_stdin(void) { int ch; while ((ch = getchar() != '\n') && ch != EOF) ; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #define MSGQUEUE_NAME "/my_message_queue" void exit_sys(const char *msg); int main(void) { mqd_t mqdes; char *buf; int val; struct mq_attr attr; int prio; ssize_t result; if ((mqdes = mq_open(MSGQUEUE_NAME, O_RDONLY)) == -1) exit_sys("mq_open"); if (mq_getattr(mqdes, &attr) == -1) exit_sys("mq_getattr"); if ((buf = malloc(attr.mq_msgsize + 1)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } for (;;) { if ((result = mq_receive(mqdes, buf, attr.mq_msgsize, &prio)) == -1) exit_sys("mq_receive"); buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("Message: %s, Priority: %d\n", buf, prio); } printf("\n"); free(buf); mq_close(mqdes); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bazı UNIX türevi sistemlerde POSIX mesaj kuyrukları özel bir dosya sistemi biçiminde mount edilebilmektedir. Örneğin Linux sistemlerinde mesaj kuyruklarına ilişkin dosya sistemi "/dev/mqueue" dizini üzerinde mount edilmiştir. Yani biz "/dev/mqueue" dizinine geçtiğimizde ls komutuyla o anda yaşamakta olan tüm POSIX mesaj kuyruklarını görüntüleyebiliriz. İstersek rm komutuyla onları silebiliriz. Örneğin: $ /dev/mqueue$ ls -l toplam 0 -rw-r--r-- 1 kaan kaan 80 Nis 8 11:25 my_message_queue Linux dağıtımları genellikle açılış sırasında bu dosya sistemini otomatik mount etmektedir. Ancak sistem yöneticisi isterse /dev/mqueue dizinini unmount edebilir. Örneğin: $ sudo unmount /dev/mqueue Ya da sistem yöneticisi isterse bu dosya sistemini başka bir yere de mount komutuyla mount edebilir. Örneğin: $ sudo mount -t mqueue somename posix-mqueue Burada "somename" mount noktalarını görüntülerken kullanılacak bir isimdir. Mesaj kuyrukları bu biçimde mount edilirken dizinin "sticky" biti set edilmektedir. Böylece bu dizinin birisi sahibi olsa da ancak oradaki dosyaları silebilmek için kişinin dosyanın sahibi olması ya da uygun önceliğe sahip olması gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi mesaj kuyrukları için iki önemli limit vardı: Bir mesaj kuyruğuna yazılabilecek maksimum mesajların sayısı (Linux'ta default 10 demiştik) ve mesaj kuyruğundaki bir mesajın maksimum uzunluğu (Linux'ta default 8192 demiştik). Pekiyi bunların default değerleri nereden gelmektedir? POSIX standartları bu konuda bir şey söylememektedir. Ancak Linux sistemlerinde bu değerler "/proc/sys/fs/mqueue" dizini içerisinde çeşitli dosyalar tarafından temsil edilmiştir. Buradaki dosyaların ve içerisindeki değerlerin anlamları şöyledir: msg_default: Bu değer, mq_open ile mesaj kuyruğu yaratılırken fonksiyonun özellik parametresi NULL geçildiğinde yaratılacak kuyruğa en fazla yerleştirilecek mesaj sayısını belirtmektedir. Bu dosyanın içini "cat" komutu ile yazdırırsak mevcut sistemlerde 10 değerini görürüz. msg_max: Bu dosya, mq_open ile özellik girilerek mesaj kuyruğu yaratıldığında mq_attr yapısının mq_maxmsg elemanına yerleştirilecek maksimum değeri belirtmektedir. Linux sistemlerinde de şimdilik bu dosyanın içerisinde 10 yazmaktadır. Yani biz mesaj kuyruğunu yaratırken 10'dan daha fazla mesajı tutabilecek biçimde yaratamayız. msgsize_default: Bu dosya, mq_open ile mesaj kuyruğu yaratılırken özellik parametresine NULL geçildiğinde kuyruğa yazılabilecek maksimum mesaj uzunluğunu belirtmektedir. Bu değerin zaten daha önce 8192 olduğunu belirtmiştik. O halde, bu dosyanın içeriğini yazdırırsak 8192 değerini görürüz. msgsize_max: Bu dosyada da mesaj kuyruğunu yaratırken özellik bilgisinde mq_attr yapısının mq_msgsize elemanına yerleştirilebilecek maksimum değer bulunmaktadır. Bu dosya yazdırılırsa 8192 değeri görülmektedir. Yani biz kuyruktaki mesajların uzunluğunu bu eğerin yukarısına çekemeyiz. queues_max: Bu değer, sistem genelinde yaratılabilecek mesaj kuyruklarının toplam sayısını belirtmektedir. Uygun önceliğe sahip prosesler yukarıdaki limitlerden etkilenmezler. Ayrıca, buradaki değerler proc dosya sisteminden bu dosyalara yazma yapılarak değiştirilebilmektedir. Örneğin: $ sudo sh -c "echo 20 > /proc/sys/fs/mqueue/msg_max" Buradaki değerlerin ayrı bir tavan limiti de vardır. Örneğin msg_max değerinin tavan limiti 3.5 ve sonrasındaki çekirdeklerde 65536'dır. Yine örneğin msgsize_max değerinin de tavan limiti 3.5 ve sonrasındaki çekirdeklerde 16,777,216 b içimindedir. Uygun önceliğe sahip prosesler /proc/sys/fs/mqueue içerisindeki limitlere takılmasalar da bu tavan limitlerine takılabilirler. Ayrıca belli bir kullanıcının maksimum kullanacağı mesaj kuyrukları için byte sayısı da bir limittir. Şu andaki çekirdeklerde bu 819200 byte'tır. Yani belli bir kullanıcı toplam mesaj kuyruklarının sayısı * bir mesajdaki maksimum byte sayısını bu değerin yukarısında set edemez. Bu konudaki detaylar için mq_overview(7) man sayfasına başvurabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- mq_send ve mq_receive fonksiyonlarının "zaman aşımlı (timeout)" versiyonları da vardır. Bu zaman aşımlı versiyonları kuyruk doluyken ya da boşken blokeyi belirlenen zaman çerçevesinde oluşturur. Eğer bloke daha uzun sürerse zaman aşımından dolayı bu fonksiyonlar başarısızlıkla geri dönerler ve errno değeri ETIMEDOUT ile set edilir. Fonksiyonların prototipleri şöyledir: #include #include int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio, const struct timespec *abs_timeout); ssize_t mq_timedreceive(mqd_t mqdes, char *restrict msg_ptr, size_t msg_len, unsigned int *msg_prio, const struct timespec *abs_timeout); Bu fonksiyonların mq_send ve mq_receive fonksiyonlarından tek farkı zaman aşımına ilişkin bir parametreye sahip olmasıdır. Zaman aşımı içerisinde bildirilmiş olan timespec yapısıyla ifade edilmektedir: struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; Bu yapının amacı 01/01/1970'ten geçen saniye sayısını nano saniye mertebesinde detaylandırmaktır. Ancak fonksiyonlardaki zaman aşımı değerleri "göreli" değil "mutlak"tır. Örneğin biz zaman aşımını 10 saniye tutacaksak buraya şimdiden 10 saniye sonraki timespec değerini girmeliyiz. Bunun için önce clock_gettime fonksiyonu ile şimdiki zamana ilişkin timespec değeri elde edilebilir. Sonra o değere belli bir değer toplanıp zaman aşımı göreli biçimde oluşturulabilir. clock_gettime fonksiyonu aynı zamanda standart bir C fonksiyonudur. Bu fonksiyondaki saat türünün CLOCK_REALTIME alınması uygun olur. clock_gettime fonksiyonunun prototipi şöyledir: #include int clock_gettime(clockid_t clock_id, struct timespec *tp); Fonksiyonun birinci parametresi saatin türünü, ikinci parametresi zaman bilgisinin yerleştirileceği yapı nesnesinin adresini almaktadır. Örneğin: if (clock_gettime(CLOCK_REALTIME, &ts) == -1) exit_sys("clock_gettime"); ts.tv_sec += 10; if (mq_timedreceive(mqdes, buf, 8192, NULL, &ts) == -1) if (errno == ETIMEDOUT) { /* timeout oluştu başka şeyler yap */ } Burada mq_timedreceive fonksiyonu ile 10 saniye kadar blokede beklenebilir. Ancak zaman aşımı dolduğunda bloke ortadan kalkacaktır. Fonksiyon, başarısızlıkla geri dönecek ve errno ETIMEDOUT özel değeri ile set edilecektir. Böylece programcı arka planda başka şeyler yapabilecektir. Bu zaman aşımlı işlemleri, blokesiz işlemlerle karıştırmayınız. Blokesiz işlemlerde bloke hiç oluşmamaktadır. Halbuki zaman aşımlı işlemlerde bloke oluşur ancak en fazla belirlenen zaman aşımı kadar bloke sürer. Zaman aşımlı işlemlerde, zaten zaman aşımı geçmişse ve kuyruk dolu ya da boşsa fonksiyon hemen başarısız olmaktadır. Fonksiyonlar zaman aşımına kuyruk doluysa ya da boşsa bakmaktadır. Eğer kuyruk, blokesiz modda açılmışsa zaman aşımlı fonksiyonların zaman aşımlı olmayanlardan davranış olarak bir farkı kalmaz. Aşağıdaki örnekte bir mesaj kuyruğu okuma amaçlı yaratılmış sonra mq_timedreceive fonksiyonu ile şimdiki zamandan 10 saniye kadar blokeli beklenmiştir. Bu programı çalıştırdığınızda 10 saniye sonra "timeout" yazısının basıldığını göreceksiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #define MSGQUEUE_NAME "/test_queue" void exit_sys(const char *msg); int main(void) { mqd_t mqdes; struct mq_attr attr; struct timespec ts; char buf[8192]; if ((mqdes = mq_open(MSGQUEUE_NAME, O_RDONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, NULL)) == -1) exit_sys("mq_open"); if (clock_gettime(CLOCK_REALTIME, &ts) == -1) exit_sys("clock_gettime"); ts.tv_sec += 10; if (mq_timedreceive(mqdes, buf, 8192, NULL, &ts) == -1) if (errno == ETIMEDOUT) printf("timedout\n"); else exit_sys("mq_timedreceive"); mq_close(mqdes); if (mq_unlink(MSGQUEUE_NAME) == -1) exit_sys("mq_unlink"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Klasik Sistem 5 mesaj kuyrukları ile POSIX mesaj kuyruklarını avantaj/dezavantaj bakımından şöyle karşılaştırabiliriz: - POSIX mesaj kuyrukları isimlendirme bakımından daha güzel bir arayüz sunmaktadır. - Klasik Sistem 5 mesaj kuyrukları daha taşınabilirdir. Ancak POSIX mesaj kuyrukları da artık taşınabilir hale gelmiştir. - POSIX mesaj kuyrukları dosyalar gibi kullanılmaktadır. Dolayısıyla "her şeyin dosya olduğu" tasarımı ile daha uyumludur. - Klasik Sistem 5 mesaj kuyruklarında belli bir önceliğe ilişkin mesajlar alınabilmektedir. POSIX mesaj kuyruklarında bu yapılamamaktadır. - POSIX mesaj kuyrukları silindiğinde onu kullanan prosesler kullanmaya devam ederler. Ancak klasik Sistem 5 mesaj kuyruklarında mesaj kuyruğu silindiğinde onu kullanan prosesler artık mesaj kuyruğunu kullanamaz hale gelirler. - POSIX mesaj kuyrukları dosya sistemi gibi mount edilebilmektedir. Dolayısıyla silme işlemi dosya siler gibi yapılabilmektedir. - Her iki mesaj kuyruğu da silinene kadar ya da sistem reboot edilene kadar yaşamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Diğer önemli proseslerarası haberleşme yöntemleriden biri de "paylaşılan bellek alanları (shared memory)" denilen yöntemdir. Paylaşılan bellek alanlarında işletim sistemi iki prosesin sayfa tablosundaki farklı sanal sayfa numaralarını aynı fiziksel sayfaya yönlendirir. Böylece iki proses, farklı sanal adreslerle aynı fiziksel sayfaya erişirler. Proseslerden biri oraya bir şey yazdığında diğeri onu hemen görür. Dolayısıyla yöntem çok hızlıdır. Ancak bu yöntem kendi içerisinde bir senkronizasyon içermemektedir. Dolayısıyla senkronizasyonun sağlanması için semaphore gibi bir senkronizasyon nesnesine de gereksinim duyulur. Zaten bu nedenle IPC nesnelerine semaphore'lar eklenmiştir. Paylaşılan bellek alanları da UNIX/Linux dünyasında tıpkı mesaj kuyruklarında olduğu gibi iki arayüzle kullanılabilmektedir: Klasik Sistem 5 paylaşılan bellek alanları ve POSIX paylaşılan bellek alanları. Biz de burada önce klasik Sistem 5 paylaşılan bellek alanlarını sonra da POSIX paylaşılan bellek alanlarını göreceğiz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 45. Ders 09/04/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Klasik Sistem 5 paylaşılan bellek alanları aşağıdaki adımlardan geçilerek oluşturulmaktadır: 1) Paylaşılan bellek alanı, shmget fonksiyonu ile anahtar verilip id elde edilecek biçimde yaratılır ya da olan açılır. Yine id değerleri sistem genelinde tektir. Yani bu id değeri, diğer prosesler tarafından biliniyorsa yetki durumu da uygunsa shmget fonksiyonu çağrılmadan doğrudan kullanılabilir. shmget fonksiyonunun prototipi şöyledir: #include int shmget(key_t key, size_t size, int shmflg); Fonksiyonun birinci parametresi yine paylaşılan bellek alanının anahtar değerini belirtir. Aynı anahtar için aynı id değerleri elde edilmektedir. Fonksiyonun ikinci parametresi yaratılacak paylaşılan bellek alanının büyüklüğünü belirtmektedir. Bu büyüklük normal olarak sayfa katlarına ilişkin bir değer olarak girilir. Ancak POSIX standartları bunu zorunlu hale getirmemiştir. Örneğin biz bu büyüklüğü 5000 olarak girdiğimizde işletim sistemi 4096'lık iki sayfayı eşler. Dolayısıyla genellikle burada girilen değer sayfa katlarına bakılarak yukarıya doğru yuvarlanmaktadır. (Yani biz bu değeri örneğin Linux sistemlerinde 5000 girsek de sanki sistem 2 * 4096 = 8192 girmişiz gibi durumu ele almaktadır.) Yukarıda da belirttiğimiz gibi buradaki büyüklüğün sayfa katlarına yukarıya doğru yuvarlanması POSIX standartlarında belirtilmemiştir. Ancak Linux dokümanlarında (Linux man sayfalarında) belirtilmiştir. Fonksiyonun son parametresi, IPC nesnesinin erişim haklarını ve yaratım seçeneklerini belirtmektedir. Tabii bu parametre eğer IPC nesnesi yaratılıyorsa (yani IPC_CREAT bayrağı kullanılmışsa) etkili olmaktadır. Eğer IPC nesnesi zaten yaratılmışsa bu parametre 0 geçilebilir. Bu parametrenin msgget get fonksiyonundaki ilgili parametreden bir farkı yoktur. Yine burada da erişim hakları S_IXXX sembolik sabitleriyle oluşturulabilir (ya da 2008 ve sonrasında doğrudan sayısal değerlerle oluşturulabilir). Erişim haklarına IPC_CREAT eklenebilir. Bu durum, IPC nesnesi yoksa onun yaratılacağı anlamına gelir. Yine IPC_CREAT|IPC_EXCL bayrakları birlikte kullanılırsa nesne zaten varsa fonksiyon başarısız olmaktadır. Yine tıpkı msgget fonksiyonunda olduğu gibi birinci parametreye IPC_PRIVATE değeri geçilebilir. Bu durumda nesne çakışmayan bir anahtarla yaratılır ve IPC nesnesinin id değeri elde edilir. Bu id değeri, diğer proseslere gönderilirse onlar bu IPC nesnesini doğrudan kullanabilirler. Fonksiyonun geri dönüş değeri, IPC nesnesinin id değerini vermektedir. Bu değer, sistem genelinde paylaşılan bellek alanları içerisinde tektir. Başka proseslere gönderilirse onlar tarafından da kullanılabilir. Fonksiyon başarısız olursa -1 değerine geri döner ve errno uygun biçimde set edilir. Linux'ta, Linux'a özgü bir biçimde erişim haklarında birkaç standart olmayan bayrak da kullanılabilmektedir. Bu bayrakları ilgili man sayfalarından inceleyebilirsiniz. Yine sistemdeki o anda yaratılmış olan paylaşılan bellek alanları "ipcs" komutuyla ya da "ipcs -m" komutuyla görüntülenebilir. Örneğin: $ ipcs -m ----- Paylaşımlı Bellek Bölütleri ----- anahtar shmid sahibi izinler bayt ekSayısı durum 0x00000000 98305 kaan 600 4194304 2 hedef 0x00000000 98321 kaan 600 524288 2 hedef 0x00000000 19 kaan 600 67108864 2 hedef 0x00000000 65556 kaan 600 524288 2 hedef 0x00000000 131099 kaan 606 10437756 2 hedef 0x00000000 131100 kaan 606 10437756 2 hedef 0x00012345 131101 kaan 644 4096 0 0x00000000 65570 kaan 600 4194304 2 hedef 0x00000000 51 kaan 600 4194304 2 hedef 0x00000000 59 kaan 600 524288 2 hedef Aslında daha önceden de belirttiğimiz gibi "ipcs" komutu bu bilgileri "/proc/sysvipc" dizinindeki üç dosyadan elde etmektedir. Klasik Sistem 5 paylaşılan bellek alanları için "/proc/sysvipc/shm" dosyası kullanılmaktadır. Örneğin: $ cat /proc/sysvipc/shm bu biçimde benzer bilgileri elde edebiliriz. Örneğin: int shmid; ... if ((shmid = shmget(SHM_KEY, 4096, 0)) == -1) exit_sys("shmget"); 2) Zaten yaratılmış bir klasik Sistem 5 paylaşılan bellek alanı nesnesini, prosesin sanal bellek alanında oluşturmak gerekir. Buna klasik Sistem 5 paylaşılan bellek alanları terminolojisinde "paylaşılan bellek alanının attach edilmesi" denilmektedir. Bu işlem sırasında işletim sistemi ilgili prosesin sayfa tablosunda bu paylaşılan bellek alanına erişmede kullanılacak sanal adresi oluşturmaktadır. Bu işlem shmat fonksiyonu ile yapılmaktadır. (İsimdeki "at" eki "attach" sözcüğünden kısaltmadır.) shmat fonksiyonunun prototipi şöyledir: #include void *shmat(int shmid, const void *shmaddr, int shmflg); Fonksiyonun birinci parametresi paylaşılan bellek alanının id'sini belirtmektedir. Fonksiyonun ikinci parametresi önerilen sanal adresi belirtmektedir. Programcı, işletim sistemine "şu sanal adres yoluyla paylaşılan bellek alanına erişmek istiyorum" diyebilmektedir. Ancak programcının önerdiği adres kullanılıyor olabilir ya da işletim sistemi tarafından kullanılamaz bir adres olabilir. Bu durumda fonksiyon başarısız olur. Bu parametre NULL adres geçilebilir. Bu durumda, işletim sistemi paylaşılan bellek alanına erişmek için kullanılacak sanal adresi kendisi tespit eder. Normal olarak bu adresin sayfa katlarında olması beklenmektedir. Ancak programcı bu adresi sayfa katları olarak vermezse ve fonksiyonun üçüncü parametresinde SHM_RND bayrağını kullanırsa bu durumda programcının verdiği adres sayfa katlarına aşağıya doğru yuvarlanmaktadır. Örneğin programcı ikinci parametreye 0x4A6C324 biçiminde bir adres girmiş olsun. Eğer fonksiyonun son parametresi SHM_RND olarak girilirse bu adres 4096'nın katlarına aşağıya doğru yuvarlanmaktadır. Yani bu durumda adres 0x4A6C000 biçimine dönüştürülecektir. Tabii programcının verdiği adres sayfa katlarında olsa bile işletim sistemi tarafından kabul edilmeyebilir. Çünkü sanal adres alanı içerisinde özel bölgeler, kullanılmayan alanlar bulunabilmektedir. Eğer ikinci parametreye bir adres girilirse ve bu adres sayfa katlarında değilse, üçüncü parametrede de SHM_RND girilmezse fonksiyon büyük olasılıkla başarısız olacaktır. POSIX standartlarında "sayfa katlarına aşağıya yuvarlama" biçiminde bir ibare bulunmamaktadır. Sayfa katları yerine, standartlarda SHMLBA sembolik sabiti kullanılmıştır. Yani standartlar anlatımı "SHMLBA katlarına aşağıya yuvarlama" biçiminde oluşturmuştur. SHMLBA sembolik sabiti içerisinde bildirilmiştir ve tipik olarak zaten sayfa uzunluğunu belirtir. Fonksiyonun son parametresi 0 geçilebilir ya da bazı bayraklar burada kullanılabilir. Yukarıda da belirttiğimiz gibi eğer ikinci parametrede bir adres girilmişse üçüncü parametrede SHM_RND bayrağı bu adresin sayfa katlarına aşağıya doğru yuvarlanacağını belirtmektedir. Üçüncü parametredeki SHM_RDONLY ilgili paylaşılan bellek alanına "read-only" erişim için kullanılmaktadır. Bu durumda, bu sayfaya yazma yapıldığında "page fault" oluşacak ve işletim sistemi prosesi sonlandıracaktır. Bu bayrak belirtilmezse erişim "read-write" yapılır. Proses, aynı paylaşılan bellek alanı nesnesini birden fazla shmat fonksiyonu ile birden fazla kez "attach" yapabilir. Ancak genellikle böyle bir duruma gereksinim duyulmamaktadır. Fonksiyon başarı durumunda paylaşılan bellek alanına erişmekte kullanılan sanal adrese, başarısızlık durumunda (void *)-1 adresine geri dönmektedir. errno değişkeni uygun biçimde set edilmektedir. Fonksiyonun NULL adrese geri dönmemesinin nedeni bazı sistemlerde 0 adresinin geçerli bir biçimde kullanılabiliyor olmasındandır. Tabii fonksiyon bize sayfa katlarına ilişkin bir adres vermektedir. (Yani 4096'lık sayfaların kullanıldığı sistemlerde verilen sanal adreslerin düşük anlamlı 3 hex digit'i 0 olacaktır.) Örneğin: int shmid; char *shmaddr; if ((shmid = shmget(SHM_KEY, 4096, 0)) == -1) exit_sys("shmget"); if ((shmaddr = (char *)shmat(shmid, NULL, 0)) == (void *)-1) exit_sys("shmat"); 3) Artık iki proses de muhtemelen farklı sanal adresler yoluyla aynı fiziksel sayfaya erişmektedir. Proseslerden biri o bölgeye bir şeyler yazarsa diğeri onu görür ve elde edebilir. Tabii burada bir senkronizasyonun sağlanması gerekmektedir. Daha önce görmüş olduğumuz borular ve mesaj kuyruklarında zaten bu organizasyon bu nesneler tarafından sağlanmaktadır. Bir tarafın paylaşılan bellek alanına bir şeyleri yazdığı diğerinin okuduğu bu tür senkronizasyon problemlerine "üretici tüketici problemi" (producer consumer problem)" denilmektedir. Bu problemin çözümü "thread'ler" konusunda ele alınacaktır. 4) Proses paylaşılan bellek alanının kullanımını bitirdikten sonra o alanı prosesin sanal bellek alanından çıkartmalıdır. Terminolojide paylaşılan bellek alanının prosesin bellek alanına iliştirilmesine "attach" denirken bunun tersine "detach" denilmektedir. (Yani shmat fonksiyonunu "malloc" gibi bir fonksiyona benzetirsek bunun bir de "free" gibi serbest bırakan bir karşılığının olması gerekir.) İşte shmat ile yapılan işlemi geri almak için shmdt fonksiyonu kullanılmaktadır (buradaki "dt" eki "detach" sözcüğünden kısaltılmıştır). Fonksiyonun prototipi şöyledir: #include int shmdt(const void *shmaddr); Fonksiyon parametre olarak shmat fonksiyonundan elde edilen sanal adresi alır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Her şey düzgün yapılmışsa fonksiyonun başarısının kontrol edilmesine de gerek yoktur. Eğer programcı bu shmdt işlemini yapmazsa proses sonlanırken bu işlem zaten yapılmaktadır. Örneğin: int shmid; char *shmaddr; ... if ((shmid = shmget(SHM_KEY, 4096, 0)) == -1) exit_sys("shmget"); if ((shmaddr = (char *)shmat(shmid, NULL, 0)) == (void *)-1) exit_sys("shmat"); ... if (shmdt(shmaddr) == -1) exit_sys("shmdt"); 5) Paylaşılan bellek alanlarının "detach" edilmesi ilgili nesnenin yok edileceği anlamına gelmemektedir. Programcı "detach" işlemi sonrasında yeniden attach işlemi yapabilir. Paylaşılan bellek alanları nesnesi, diğer IPC nesnelerinde olduğu gibi açıkça silinene kadar ya da reboot işlemine kadar yaşamaya devam etmektedir. 6) Klasik Sistem 5 paylaşılan bellek alanları nesnesi için işletim sistemi shmid_ds isimli bir yapı oluşturmaktadır. Bu yapıda paylaşılan bellek alanına ilişkin bazı önemli bilgiler tutulmaktadır. Bu bilgileri elde etmek için, set etmek için ya da IPC nesnesini yok etmek için shmctl isimli fonksiyon kullanılmaktadır. Bu fonksiyonun işlevi, klasik Sistem 5 mesaj kuyruklarındaki msgctl fonksiyonuna benzetilebilir. shmctl fonksiyonunun parametrik yapısı şöyledir: #include int shmctl(int shmid, int cmd, struct shmid_ds *buf); Fonksiyonun birinci parametresi paylaşılan bellek alanının id değerini, ikinci parametresi ise uygulanacak işlemi belirtmektedir. Fonksiyonun üçüncü parametresinde belirtilen yapı şöyledir: struct ipc_perm { uid_t uid; /* Effective UID of owner */ gid_t gid; /* Effective GID of owner */ uid_t cuid; /* Effective UID of creator */ gid_t cgid; /* Effective GID of creator */ unsigned short mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */ }; struct shmid_ds { struct ipc_perm shm_perm; /* Ownership and permissions */ size_t shm_segsz; /* Size of segment (bytes) */ time_t shm_atime; /* Last attach time */ time_t shm_dtime; /* Last detach time */ time_t shm_ctime; /* Creation time/time of last modification via shmctl() */ pid_t shm_cpid; /* PID of creator */ pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */ shmatt_t shm_nattch; /* No. of current attaches */ }; Daha önceden de ipc_perm yapısını görmüştük. Bu yapı içerisinde IPC nesnesinin o anki kullanıcı ve grup id'leri, nesneyi yaratan prosesin kullanıcı ve grup id'leri ve nesnenin erişim hakları bulunuyordu. shmid_ds yapısının diğer elemanları sırasıyla şu bilgileri barındırmaktadır: Paylaşılan bellek alanın büyüklüğü, son attach zamanı, son detach zamanı, son shmid_ds yapısındaki değişiklik zamanı, IPC nesnesini yaratan prosesin id'si, son attach ya da detach yapan prosesin id'si ve nihayet nesnenin kaç kere attach yapıldığı bilgisi. İkinci parametreye şunlardan biri girilebilir: IPC_STAT: Bu durumda paylaşılan bellek alanına ilişkin bilgiler fonksiyonun üçüncü parametresiyle belirtilen yapı nesnesine doldurulur. IPC_SET: Bu durumda shmid_ds yapısının aşağıdaki elemanları set edilmektedir: shm_perm.uid shm_perm.gid shm_perm.mode Tabii set işleminin yapılabilmesi için prosesin etkin kullanıcı id'sinin shm_perm.uid ya da sehm_perm.cuid değerine eşit olması ya da prosesin uygun önceliğe sahip olması gerekmektedir. IPC_RMID: Bu parametre değeri IPC nesnesini silmek için kullanılmaktadır. Benzer biçimde bu işlemin yapılabilmesi için de IPC_SET koşulunda belirtilen koşulların sağlanması gerekmektedir. Paylaşılan bellek alanları nesnesini bunu yaratan prosesin silmesi uygun olur. Klasik Sistem 5 mesaj kuyruklarında, bir proses mesaj kuyruğunu silerse mesaj kuyruğu hemen siliniyordu. Dolayısıyla onu kullanan bir proses ilgili işlemlerde başarısızlıkla karşılaşıyordu. Ancak paylaşılan bellek alanlarında durum böyle değildir. Bir klasik Sistem 5 paylaşılan bellek alanı, shmctl fonksiyonu ile bir proses tarafından silinse bile gerçek silinme onu kullanan tüm proseslerin bellek alanını "detach" yapmasıyla gerçekleşmektedir. IPC nesnesi, IPC_RMID parametresiyle silinecekse artık fonksiyonun son parametresi kullanılmaz. NULL adres geçilebilir. Paylaşılan bellek alanları komut satırında "ipcrm" komutuyla da silinebilmektedir. Komutta -M seçeneği anahtar ile silmek için -m seçeneği id ile silmek için kullanılmaktadır. Örneğin: $ ipcrm -m 131135 Burada 131135 id'sine sahip paylaşılan bellek alanı silinmektedir. Tabii bize ait olmayan paylaşılan bellek alanlarını silmek için sudo uygulamak gerekir. shmctl fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Aşağıdaki örnekte prog1 programı paylaşılan bellek alanına bir yazı yerleştirir. prog2 programı da bu yazıyı oradan alıp ekrana (stdout dosyasına) yazdırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #define SHM_KEY 0x12345 void exit_sys(const char *msg); int main(void) { int shmid; char *shmaddr; if ((shmid = shmget(SHM_KEY, 4096, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shmget"); if ((shmaddr = (char *)shmat(shmid, NULL, 0)) == (void *)-1) exit_sys("shmat"); printf("Shared memory virtual address: %p\n", shmaddr); strcpy(shmaddr, "this is a test"); printf("press ENTER to continue...\n"); getchar(); if (shmdt(shmaddr) == -1) exit_sys("shmdt"); if (shmctl(shmid, IPC_RMID, NULL) == -1) exit_sys("shmctl"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #define SHM_KEY 0x12345 void exit_sys(const char *msg); int main(void) { int shmid; char *shmaddr; if ((shmid = shmget(SHM_KEY, 4096, 0)) == -1) exit_sys("shmget"); if ((shmaddr = (char *)shmat(shmid, NULL, 0)) == (void *)-1) exit_sys("shmat"); printf("Shared memory virtual address: %p\n", shmaddr); printf("press ENTER to continue...\n"); getchar(); puts(shmaddr); if (shmdt(shmaddr) == -1) exit_sys("shmdt"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Her ne kadar POSIX standartlarında belirtilmiş olmasa da işletim sistemleri, paylaşılan bellek alanları için çeşitli sınır değerler oluşturmaktadır. Linux sistemlerinde paylaşılan bellek alanlarına ilişkin sınırlar şöyledir: SHMALL: Sistemdeki tüm paylaşılan bellek alanlarının kaplayabileceği toplam alanı sayfa sayısı cinsinden belirtmektedir. Bu değer Linux sistemlerinde /proc/sys/kernel/shmall dosyasından elde edilebilir ve değiştirilebilir. Buradaki default değer çok büyüktür yani adeta bir sınır olmadığını belirtmektedir. SHMMAX: Bu sınır belli bir prosesin shmget fonksiyonu ile oluşturabileceği maksimum paylaşılabilen bellek alanı büyüklüğüdür. Yani shmget fonksiyonunda belirtilecek büyüklüğün üst sınırını belirtmektedir. Bu sınır da proc dosya sisteminde /proc/sys/kernel/shmmax dosyası ile elde edilebilir ve değiştirilebilir. Bu değer byte cinsindendir. Şu andaki Linux sistemlerindeki default değer çok büyüktür. Adeta sınırsız gibi ele alınabilir. SHMMIN: Bu sınır shmget ile oluşturulabilecek minimum uzunluğu belirtmektedir. Şimdiki Linux sistemlerinde bu değer 1'dir. Ancak tabii biz shmget ile 1 byte alan oluşturmak istesek bile shmget bize en az bir sayfa tahsis etmektedir. Bunun için proc dosya sisteminde bir giriş bulunmamaktadır. SHMMNI: Bu sınır sistem genelinde yaratılabilecek maksimum paylaşılan bellek alanlarının sayısını belirtmektedir. Bu değer proc dosya sisteminde /proc/sys/kernel/shmmni dosyası ile elde edilebilir ve değiştirilebilir. Mevcut Linux sistemlerinde bu dosyada default olarak 4096 değeri bulunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Paylaşılan bellek alanları da "POSIX paylaşılan bellek alanları" denilen alternatif bir arayüze sahiptir. POSIX paylaşılan bellek alanları daha önce görmüş olduğumuz POSIX mesaj kuyruklarına benzer bir kullanım sunmaktadır. Yani burada farklı prosesler anahtarlarla değil yine kök dizindeki dosya isimleriyle anlaşma sağlarlar. POSIX arayüzü, tıpkı mesaj kuyruklarında olduğu gibi daha modern bir tasarıma sahiptir. Ayrıca POSIX arayüzü "bellek tabanlı dosyalar (memory mapped files)" denilen olguyla da birleştirilmiş durumdadır. POSIX arayüzü yine sanki nesne bir dosyaymış gibi davranmaktadır. Ancak mesaj kuyruklarında da belirttiğimiz gibi bir taşınabilirlik problemi burada da söz konusu olabilmektedir. Ancak pek çok UNIX türevi işletim sistemi artık bu arayüzü uzun süredir destekler duruma gelmiştir. POSIX paylaşılan bellek alanları librt kütüphanesi içerisinde bulunduğundan link işleminde -lrt seçeneğinin kullanılması gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 46. Ders 15/04/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- POSIX paylaşılan bellek alanları tipik olarak şu adımlardan geçilerek kullanılmaktadır: 1) 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. shm_open fonksiyonunun prototipi şöyledir: #include int shm_open(const char *name, int oflag, mode_t mode); Fonksiyonun birinci parametresi paylaşılan bellek alanı nesnesinin ismini belirtmektedir. Tıpkı POSIX mesaj kuyruklarında olduğu gibi bu ismin kök dizinde bir dosya ismi gibi verilmesi gerekmektedir. (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 hiç kullanılmamaktadır. Ancak shm_open fonksiyonunun bu üçüncü parametresi kullanılmayacak olsa bile girilmek zorundadır. Örneğin bu tür durumlarda bu parametre için 0 değerini girebilirsiniz. (open fonksiyonunun üçüncü parametresinin eğer gerek yoksa girilmeyebileceğini anımsayınız.) shm_open bize tıpkı bir disk dosyasında olduğu gibi bir dosya betimleyicisi vermektedir. (Halbuki mq_open fonksiyonunun bir dosya betimleyicisi vermesinin zorunlu olmadığını anımsayınız.) Fonksiyon başarısız olursa yine -1 değerine geri döner ve errno değişkeni uygun biçimde set edilir. Ö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"); 2) Paylaşılan bellek alanı yaratıldığında 0 uzunluktadır. Ona ftruncate fonksiyonu ile ona bir büyüklük vermek gerekir. truncate ve ftruncate fonksiyonlarının bir dosyayı büyütmek ya da küçültmek amacıyla kullanıldığını anımsayınız. Ö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. 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. 3) Artık paylaşılan bellek alanı nesnesinin belleğe "map" edilmesi gerekmektedir. (Klasik Sistem 5 paylaşılan bellek alanlarında "map etmek" yerine "attach etmek" terimi kullanılmaktaydı. Bu arayüzde ise "attach" yerine "mapping" sözcüğü tercih edilmiştir.) 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 (preferred) 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 normal 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 1 MB 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 şu sembolik sabitlerden 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 çalıştıkları işlemcide varmış gibi bu parametreyi oluşturmalıdır. Böylece kodun başka bir işlemcinin bulunduğu sistemde de düzgün çalışması sağlanacaktır. Tabii burada belirtilen koruma bayraklarının paylaşılan bellek alanı nesnesi oluşturulurken (yan shm_open fonksiyonundaki) belirtilen dosya bayraklarından daha geniş bir koruma içermemesi gerekir. Örneğin shm_open fonksiyonunda O_RDONLY bayrağı kullanılmışsa mapping işlemi PROT_READ|PROT_WRITE biçiminde yapılamaz. Ancak shm_open fonksiyonunda O_RDWR bayrağı belirtilmişse mmap fonksiyonundaki koruma bayrakları PROT_READ ya da PROT_WRITE ya da PROT_READ|PROT_WRITE seçilebilir. 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 bayrağı şunlara yol açmaktadır: - Okuma yapılınca paylaşılan bellek alanından okuma yapılmış olur. - 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 ve 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 bu durum "unspecified" olarak 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 private mapping yapan proses sayfaya yazma yaptığı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 proseslerarası haberleşme için 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 tam olarak bu adresten itibaren tahsisat yapılacak ve fonksiyon bu adresin aynısıyla geri dönecektir. (Tabii bu durumda programcının verdiği adres uygun olmayabilir. Bu durumda fonksiyon da başarısız olur.) 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 4 MB olsun. Biz bu nesnenin 1 MB'sinden itibaren 64K'lık kısmını map edebiliriz. Örneğin mmap fonksiyonunu şöyle çağırabiliriz: shmaddr = mmap(NULL, SHM_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fdshm, 0); Burada paylaşılan bellek alanı nesnesinin SHM_SIZE kadar alanı map edilmek istenmiştir. İlgili sayfalar PROT_READ|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 değişiklikler 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 flags parametresinde 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ın da 4 olması gerekir.) Ancak flags parametresinde 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) mmap fonksiyonunun başarı 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. 4) 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, ikinci parametresi ise unmap edilecek alanın uzunluğunu belirtmektedir. Fonksiyon birinci parametresinde belirtilen adresten itibaren ikinci parametresinde belirtilen miktardaki byte'ı içeren 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. Ancak proses sonlandığında tabii unmap işlemi otomatik olarak yapılmaktadır. 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. 5) 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. 6) Artık paylaşılan bellek alanı nesnesi shm_unlink fonksiyonu ile silinebilir. Anımsanacağı gibi bu silme yapılmazsa sistem reboot edilene kadar nesne hayatta kalmaya devam edecektir (kernel persistant). shm_unlink fonksiyonunun prototipi şöyledir: #include int shm_unlink(const char *name); Fonksiyon paylaşılan bellek alanı nesnesinin ismini alarak 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_NAME) == -1) exit_sys("shm_unlink"); Tıpkı POSIX mesaj kuyruklarında olduğu gibi bir proses paylaşılan bellek alanını shm_unlink fonksiyonu ile silse bile paylaşılan bellek alanını kullanan diğer prosesler unmap işlemi yapana kadar nesne gerçek anlamda silinmemektedir. Aşağıdaki örnekte "prog1" programı paylaşılan bellek alanına bir yazı yazmakta "prog2" programı da bu yazıyı alarak stdout dosyasına yazdırmaktadır. Tabii "prog1" programı sürekli paylaşılan bellek alanının başına eskisini ezecek biçimde yazıları yazar. Programı test ederken "prog1"de paylaşılan bellek alanına bir şeyler yazdıktan sonra "prog2"de ENTER tuşuna basarak o yazılanların alınmasını sağlamalısınız. Paylaşılan bellek alanları kendi içerisinde bir senkronizasyon içermemektedir. Örneğimizde "prog1" en sonunda paylaşılan bellek alanına "quit" yazdığında her iki program da sonlanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* prog1.c */ #include #include #include #include #include #include #include #define SHM_NAME "/sample_posix_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fdshm; char *shmaddr; char buf[4096]; char *str; if ((fdshm = shm_open(SHM_NAME, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, SHM_SIZE) == -1) exit_sys("ftruncate"); shmaddr = (char *)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, fdshm, 0); if (shmaddr == MAP_FAILED) exit_sys("mmap"); for (;;) { printf("Text:"); /* okuma doğrudan paylaşılan bellek alanına da yapılabilir */ if (fgets(buf, 4096, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; strcpy(shmaddr, buf); if (!strcmp(buf, "quit")) break; } if (munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(fdshm); if (shm_unlink(SHM_NAME) == -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 #include #define SHM_NAME "/sample_posix_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fdshm; char *shmaddr; if ((fdshm = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); shmaddr = (char *)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, fdshm, 0); if (shmaddr == MAP_FAILED) exit_sys("mmap"); for (;;) { printf("Press ENTER to read..."); getchar(); puts(shmaddr); if (!strcmp(shmaddr, "quit")) break; } if (munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(fdshm); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Konu ile ilgili tipik sorular ve yanıtları şunlardır: SORU: POSIX paylaşılan bellek alanı nesnesi demekle ne kastedilmektedir? YANIT: shm_open fonksiyonu sanki bir dosya yaratıyor gibi paylaşılan bellek alanı nesnesini yaratmaktadır. Aslında ortada bir dosya yoktur. Ancak gerçek dosyalar da map edilebildiğinden (ileride ele alınacak) tasarım sanki "paylaşılan bellek alanı nesnesi bir dosyaymış, o nesneye bir şeyler yazdığımızda o dosyaya bir şeyler yazıyormuşuz" gibi yapılmıştır. Tabii shm_open sırasında işletim sistemi sanal bellek alanı için swap dosyalarında yer de ayırabilmektedir. SORU: POSIX paylaşılan bellek alanı shm_open fonksiyonu ile yaratıldığında gerçekte diskte bir dosya yaratılmakta mıdır? YANIT: Hayır yapılmamaktadır. Yalnızca bize sanki paylaşılan bellek alanı nesneleri bir dosya gibi gösterilmektedir. İşletim sistemi arka planda paylaşılan bellek alanı nesneleri için swap dosyalarında yer ayırabilmektedir. SORU: POSIX paylaşılan bellek alanları arka planda nasıl işlem görmektedir? YANIT: Arka plan çalışma klasik Sistem 5 paylaşılan bellek alanlarında olduğu gibidir. Yani yine proseslerin sayfa tablolarında değişik sanal sayfa numaraları aynı fiziksel sayfalarla eşleştirilmektedir. SORU: ftruncate fonksiyonuna neden gereksinim duyulmaktadır? YANIT: Paylaşılan bellek alanı shm_open ile ilk kez yaratıldığında henüz alanın içi boştur. Ona bir uzunluk vermek gerekmektedir. ftruncate fonksiyonu ona bir uzunluk vermek için kullanılmaktadır. Tabii ftruncate fonksiyonu nesne yaratıldığı zaman bir kez çağrılmalıdır. Ancak aynı uzunlukla ftruncate işleminde nesne üzerinde bir değişiklik olmayacaktır. SORU: Private mapping yapılmasının ne anlamı olabilir? Çünkü private mapping'te nesneye yapılan yazma işlemleri nesneye yansıtılmamaktadır. YANIT: Private mapping'te nesneye yazma yapıldığında "copy on write" mekanizması devreye girer ve yazma yapılan sayfa paylaştırılan sayfadan ayrıştırılır. Copy on write mekanizması işletim sisteminin pek çok yerinde kullanılmaktadır. Yani private mapping programcılardan ziyade çekirdek tarafından kullanılmaktadır. SORU: Private mapping yapıldığında başka bir proses paylaşılan bellek alanına yazma yaptığında (tabii o proses de paylaşılan bellek alanını shared mapping ile açmış olsun) bu yazma işlemini private mapping yapan taraf görür mü? YANIT: Bu durum POSIX standartlarında "unspecified" bırakılmıştır. Linux çekirdeğinde bu yazma "private mapping yapan tarafta eğer sayfada "copy on write" yapılmadıysa" görülmektedir. SORU: mmap fonksiyonun birinci parametresi ve sonuncu parametresi sayfa katlarında olmak zorunda mıdır? YANIT: Eğer fonksiyonun flags parametresinde MAP_FIXED belirtilmemişse birinci parametre sayfa katlarında olmak zorunda değildir. Ancak MAP_FIXED belirtilmişse POSIX'in eski versiyonu sayfa katlarını zorunlu tutmaktaydı. Ancak güncel versiyonda bu zorunluluk gevşetilmiştir ve işletim sisteminin isteğine bağlı hale getirilmiştir. Linux çekirdeği MAP_FIXED durumunda birinci parametredeki adresin sayfa katlarında olmasını zorunlu tutmaktadır. Fonksiyonun son parametresindeki offset değeri eğer MAP_FIXED belirtilmişse POSIX standartlarına göre birinci parametrede belirtilen adresin sayfa katlarına bölümüne elde edilen kalanla aynı kalanı vermek zorundadır. Ancak POSIX standartları MAP_FIXED belirtilsin ya da belirtilmesin offset değerinin işletim sistemi tarafından sayfa katlarında olmasının zorunlu tutulabileceğini (may require) de belirtmiştir. Linux sistemlerinde MAP_FIXED belirtilse de belirtilmese de offset değeri sayfa katlarında olmak zorundadır. SORU: Paylaşılan bellek alanları shmget ya da shm_open fonksiyonuyla oluşturulduktan sonra neden onların attach edilmesi ya da map edilmesi gerekmektedir? YANIT: Klasik Sistem 5'teki shmget ve POSIX'teki shm_open fonksiyonları nesnenin kendisini oluşturur. Bu nesnenin proseste kullanılabilmesi için prosesin adres alanı içerisine attach ya da map edilmesi gerekmektedir. Yani nesnenin var olması ayrı bir durumdur onun proses tarafından kullanılır duruma getirilmesi ayrı bir durumdur. Kaldı ki aynı nesne aynı prosesin adres alanı içerisinde birden fazla kez de attach ya da map edilebilir. Örneğin tipik olarak işletim sistemi shm_open fonksiyonu ile bir paylaşılan bellek alanı oluşturulduğunda o alanı bir swap dosyası içerisinde diskte oluşturmaktadır. Sonra bu paylaşılan bellek alanı, prosesin sanal bellek alanına map edildiğinde işletim sistemi prosesin sayfa tablosunda ilgili girişleri ayırır ve paylaşılan bellek alanı kullanılmaya başlandığında onu RAM'e çeker. Başka bir proses de onu kullanmak isterse o prosesin sayfa tablosunda o girişleri aynı fiziksel sayfaya yönlendirir. SORU: Paylaşılan bellek alanı shm_open ile açıldıktan sonra bu betimleyiciyi ne zaman kapatmalıyız? YANIT: Aslında mapping işlemi yapıldıktan sonra bu betimleyiciyi hemen kapatabiliriz. Ancak bazen bu betimleyici ile başka işlemlerin yapılması da gerekebilmektedir. Örneğin fstat fonksiyonu ile bu betimleyiciyi kullanarak paylaşılan bellek alanının büyüklüğünü elde edebiliriz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 47. Ders 16/04/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bellek tabanlı dosyalar (memory mapped files) 90'lı yıllarla birlikte işletim sistemlerine sokulmuştur. 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. Microsoft ise ilk kez 32 bit Windows sistemlerinde (Windows NT (1993) ve sonra da Windows 95 (1995)) bellek tabanlı dosyaları işletim sisteminin çekirdeğine dahil etmiştir. 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. Örneğin 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 oldukça farklıdır. Birincisi, dosyanın tamamının belleğe okunması yine iki tamponun devreye girmesine yol açmaktadır. İkincisi ise 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 dosyayı bellek tabanlı (memory mapped) biçimde kullanmak için sırasıyla şu adımlardan geçilmektedir: 1) 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 belirtileceği 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. 2) 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 yazma 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ıç offset'ini 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. 3) 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"); 4) 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. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; char *maddr; struct stat finfo; 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 ((maddr = (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(maddr[i]); memcpy(maddr, "xxxxx", 6); /* dosya güncelleniyor */ if (munmap(maddr, finfo.st_size) == -1) exit_sys("munmap"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 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. - 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 büyütemeyiz. Ancak dosyalar dışarıdan büyütülebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 48. Ders 29/04/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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 mode'a 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" de denilmektedir. Bu tasarımdan dolayı zaten ilgili dosya üzerinde işlem yapan herkes aynı işletim sistemi içerisindeki tamponları kullanmaktadı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 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 "unifed 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. Sistem dosyanın o parçası için her zaman o tamponu kullandığından dolayı aslında bellek tabanlı dosyanın bellek alanına yazma yapıldığında Linux'ta o yazma adeta o anda dosyaya yapılmış gibi bir durum oluşmaktadır. Benzer biçimde başka bir proses dosyaya yazma yaptığında aslında o da aynı tampona (page cache) yazma yapmış olmaktadır. Ancak ne olursa olsun taşınabilir programların bu msync fonksiyonunu aşağıda belirteceğimiz biçimde çağırması gerekmektedir. Aşağıdaki örnekte "sample.c" programı bir dosyayı bellek tabanlı olarak açıp beklemiştir. "mample.c" isimli program ise aynı dosyayı open fonksiyonu ile açıp dosyanın başına write işlemi yapıp beklemiştir. Linux sistemlerinde hiç msync fonksiyonu çağrılmadan "mample.c" programının yazdığı şeyler "sample.c" programı tarafından görülecektir. Bu testi yaparken önce içi dolu olan bir dosya yaratınız. Bu dosyanın "test.txt" olduğunu varsayalım. Farklı terminallerden programları aşağıdaki gibi çalıştırınız: ./sample test.txt ./mample test.txt ---------------------------------------------------------------------------------------------------------------------------*/ /* sample.c */ #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; char *maddr; struct stat finfo; 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 ((maddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); printf("Press ENTER to continue...\n"); getchar(); printf("---------\n"); for (off_t i = 0; i < finfo.st_size; ++i) putchar(maddr[i]); printf("press ENTER to exit...\n"); getchar(); if (munmap(maddr, 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; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_WRONLY)) == -1) exit_sys("open"); if (write(fd, "zzzzz", 5) == -1 ) exit_sys("write"); printf("Press ENTER to exit...\n"); getchar(); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 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. Aşağıda msync fonksiyonunun kullanımına ilişkin bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; char *maddr; struct stat finfo; 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 ((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) exit_sys("msync"); printf("Press ENTER to continue...\n"); if (munmap(maddr, finfo.st_size) == -1) exit_sys("munmap"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi bellek tabanlı dosyanın MAP_PRIVATE ile private olarak map edilmesinin nasıl bir amacı olabilir? İşte daha önceden de belirtildiği gibi "private mapping" "copy on write" mekanizması için kullanılmaktadır. Örneğin iki porses aynı dosyayı private mapping yaptığında işletim sistemi bu iki prosesin dosyasını fiziksel bellekte aynı tamponu (page cache) belirtecek biçimde ayarlamaktadır. Ancak proseslerden biri dosyaya yazma yaptığında bu yazma dosyaya değil, tamponun o anda çıkartılan başka bir kopyasına yapılmaktadır. Yani "copy on write" şu anlama gelmektedir: "Okuma yapıldığı sürece aynı tamponu paylaş, yazma yapıldığında o sayfayı ayır". Bellek tabanlı dosyalarda private mapping özellikle işletim sistemi tarafından "çalıştırılabilir (executable)" dosyaların ve "dinamik kütüphanelerin" yüklenmesi sırasında kullanılmaktadır. Örneğin "sample" isimli çalıştırılabilir bir ELF dosyasının içeriği "kabaca" şöyledir: .text .data .bss diğer bölümler (sections) ELF formatı "bölümlerden (sections)" oluşmaktadır. exec fonksiyonları ELF formatını yüklerken her bölümü "sections" private mapping yaparak belleğe yüklemektedir. Dolayısıyla bir programı ikinci kez çalıştırdığımızda aslında mümkün olduğunca aynı fiziksel bellek kullanılmakta bir proses yazma yaptığında ilgili sayfa "copy on write" mekanizması yoluyla farklılaştırılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux sistemlerinde mmap fonksiyonun MAP_SHARED, MAP_PRIVATE ve MAP_FIXED bayraklarının dışında POSIX'te olmayan başka bayrakları da vardır. Bu bayraklardan en önemlisi MAP_ANONYMOUS bayrağıdır. Bu bayrakla mapping yapıldığında bu duruma Linux sistemlerinde "anonymous mapping" denilmektedir. Anonymous mapping, dosya üzerinde (yani bellek tabanlı dosya biçiminde) yapılamamaktadır. Dolayısıyla anonymous mapping yapılırken mmap fonksiyonunun fd ve offset parametreleri dikkate alınmamaktadır. Anonymous mapping işleminde kalıcı yer olarak (backing store) doğrudan işletim sisteminin "swap dosyaları" kullanılmaktadır. Böylece aslında anonymous mapping adeta bellek tahsisatı anlamına gelmektedir. Zaten glibc kütüphanesindeki malloc, calloc ve realloc fonksiyonları arka planda "anonymous mapping" işlemini yapmaktadır. Tipik olarak glibc kütüphanesinde "malloc" fonksiyonu önce geniş bir alanı mmap fonksiyonu ile anonymous mapping yaparak yaratmakta sonra bu alanı tahsisat için organize etmektedir. Yani malloc fonksiyonu güncel kütüphanede aslında önce geniş bir alanı anonymous mapping yöntemiyle tahsis etmekte sonra orayı organize etmektedir. Pekiyi biz anonymous mapping işlemini malloc işlemi yerine kullanabilir miyiz? Aslında kullanabiliriz. Ancak anonymous mapping sayfa katlarında tahsisat yapmaktadır. Oysa malloc fonksiyonu byte temelinde tahsisat yapmaktadır. Bu durumda anonymous mapping yerine malloc işlemi genel olarak çok daha uygundur. Fakat yine de büyük blokların tahsis edilmesi gibi durumlarda anonymous mapping daha doğrudan ve daha hızlı bir tahsisata olanak verebilmektedir. mmap fonksiyonunda MAP_ANONYMOUS kullanıldığında fonksiyonun fd parametresi dikkate alınmamaktadır. Ancak MAP_ANONYMOUS bayrağını destekleyen diğer bazı işletim sistemlerinde bu parametrenin -1 girilmesi gerekmektedir. Bu nedenle bu parametrenin -1 olması uygundur. Fonksiyonun offset parametresi de dikkate alınmamaktadır. Bu parametre 0 olarak girilebilir. MAP_ANONYMOUS bayrağı MAP_PRIVATE ya da MAP_SHARED ile birlikte kullanılmaktadır. En normal durum MAP_ANONYMOUS bayrağı ile MAP_PRIVATE bayrağının birlikte kullanılmasıdır. Pekiyi MAP_ANONYMOUS|MAP_PRIVATE ile MAP_ANONUMOUS|MAP_SHARED arasında ne farklılık vardır? İşte normal olarak anonymous mapping için ayrılan swap alanı başka prosesler tarafından kullanılamamaktadır. Ancak fork işlemi üst proses bir proses yarattığında tüm mapping alanları alt prosese aktarılmaktadır. O halde biz MAP_ANONYMOUS|MAP_SHARED uyguladığımızda fork yapmadıktan sonra bunun MAP_ANONYMOUS|MAP_SHARED işleminden hiçbir farkı olmayacaktır. Ancak fork yapıldığında MAP_ANONYMOUS|MAP_SHARED uygulamasında üst ve alt prosesler aynı anonymous alanı paylaşacaktır. Yani örneğin üst proses buraya bir şey yazdığında alt proses onu görecektir. Ancak MAP_ANONYMOUS|MAP_PRIVATE uygulamasında üst proseslerden biri alana bir şey yazdığında "copy on write" mekanizması devreye girecek ve yazılanı diğer proses görmeyecektir. malloc fonksiyonu da arka planda büyük alanı mmap ile tahsis ederken MAP_ANONYMOUS|MAP_PRIVATE uygulamaktadır. Aşağıdaki örnekte mmap ile anonymous mapping örneği verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { char *maddr; if ((maddr = (char *)mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0)) == MAP_FAILED) exit_sys("mmap"); strcpy(maddr, "ankara"); puts(maddr); if (munmap(maddr, 4096) == -1) exit_sys("munmap"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- POSIX standartlarına göre mmap fonksiyonunda bir tahsisat yapıldığında dosyaya ilişkin olmayan tüm alanlar otomatik olarak sıfırlanmaktadır. Linux sistemlerinde de durum böyledir. Default olarak anonymous mapping yapıldığında da tahsis edilen alan sıfırlanmaktadır. Ancak Linux'ta, POSIX'te olmayan MAP_UNINITIALIZED isimli bir bayrak da vardır. Eğer bu bayrak kullanılırsa tahsis edilen alanlar sıfırlanmaz. Bazen programcılar tahsisatı hızlandırmak için MAP_ANONYMOUS|MAP_PRIVATE|MAP_UNINITIALIZED biçiminde bayrak kullanabilmektedir. Daha önceden de "çalıştırılabilir dosyaların exec fonksiyonları tarafından bölüm bölüm (section section) belleğe mmap mekanizmasıyla map edildiğini belirtmiştik. İşte ilk değer verilmemiş global değişkenlerin bulunduğu ".bss" alanı da zaten mapping işlemi yapılırken sıfırlanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 49. Ders 30/04/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sanal bellek kullanan işletim sistemlerinde bazı sayfaların lock edilmesine olanak verilebilmektedir. Bir sayfa lock edildiğinde artık o sayfa fiziksel RAM'de yer açmak için işletim sistemi tarafından "swap out" yapılmamaktadır. Bir sayfanın belleğe lock edilmesinin iki avantajı olabilmektedir: 1) Bu sayede swap out yapılmayacağı için göreli bir hız kazancı sağlanır. Özellikle gerçek zamanlı uygulamalarda bu tür hız kazançları önemli olabilmektedir. 2) Bu sayede swap dosyalarından bilgi çalmaya çalışan kişiler engellenmiş olur. Tabii swap dosyalarından bilgi çalmak da aslında kolay bir şey değildir. Pekiyi bir proses istediği kadar sanal sayfayı lock edebilir mi? Şüphesiz eğer prosesler istedikleri kadar sanal sayfayı lock edebilselerdi swap işlemleri konusunda dolayısıyla sanal bellek kullanımı konusunda bir baskı oluşurdu. Bu nedenle işletim sistemlerinde uygun önceliğe sahip olmayan proseslerin lock edebileceği sayfa sayısında bir sınırlama yapılmaktadır. Linux'ta uygun önceliğe sahip olmayan proseslerin kilitleyebileceği sayfa sayısı RLIMIT_MEMLOCK isimli kaynak limitiyle belirlenmiştir. Bu kaynak limitinin hem hard hem de soft değeri mevcut Linux çekirdeklerinde 509853696 byte (124476 sayfa) kadardır. Ancak Linux sistemlerinde "uygun önceliğe sahip olan (appropriate privileges)" prosesler istedikleri kadar sayfayı kilitleyebilmektedir. Yani bu prosesler prosesin RLIMIT_MEMLOCK limitlerinden etkilenmemektedir. Sanal sayfaların belleğe lock edilmesi için birkaç fonksiyon kullanılabilmektedir. Bu amaçla kullanılan mlock isimli POSIX fonksiyonunun prototipi şöyledir: int mlock(const void *addr, size_t len); Fonksiyon birinci parametresiyle belirtilen adresten itibaren ikinci parametresiyle belirtilen uzunluktaki bellek alanını lock etmektedir. Buradaki adres + uzunluk içerisinde kalan tüm sayfalar lock edilmektedir. (Yani adres bir sayfanın başına hizalanmamışsa uzunluk da başka bir sayfanın ortalarına kadar gidiyorsa baştaki ve sonraki her iki sayfa lock işlemine dahil edilmektetedir.) POSIX standartlarında birinci parametresiyle belirtilen adresin ilgili sistem tarafından sayfa katlarında olmasının zorunlu tutulabileceği (may require) belirtilmiştir. Linux sistemlerinde birinci parametrede belirtilen adresin sayfa katlarında olması zorunluluğu yoktur. Uzunluk parametresi herhangi bir değerde olabilir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. errno uygun biçimde set edilmektedir. Bir adres sayfa katlarına şu şekilde align edilebilir: #include void *addr = (void *)((uintptr_t)buf & ~0xFFF); mlock fonksiyonu ile kilitlenmeye çalışılan sayfalar o anda RAM'de değilse mlock önce onları RAM'e alıp kilitlemektedir. Yani fonksiyon başarılı bir biçimde geri döndüğünde kesinlikle sayfalar o anda RAM'de bulunmaktadır. mlock ile kilitlenen sayfaların kilidini açmak için munlock POSIX fonksiyonu bulundurulmuştur. Fonksiyonun prototipi şöyledir: int munlock(const void *addr, size_t len); Fonksiyonun birinci parametresi unlock edilecek sayfalara ilişkin adresi, ikinci parametresi de uzunluğu belirtmektedir. Yine bu adres + uzunluk değerini içeren tüm sayfalar unlock edilmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner ve errno uygun biçimde set edilir. Tabii en kötü olasılıkla proses sonlandığında sayfalar zaten unlock edilip boşaltılmaktadır. mlock ve mulock fonksiyonlarında bir sayaç mekanizması yoktur. Yani bir alan birden fazla kez lock yapılsa bile tek bir unlock işlemi ile unlock yapılabilmektedir. Aşağıdaki örnekte mlock fonksiyonu ile bir global dizinin bulunduğu sanal sayfa kilitlenmiştir. Tabii bu örnekte aslında dizi 4096 byte olmasına karşın iki sayfa da kilitlenmiş olabilir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); char buf[4096]; int main(void) { if (mlock(buf, 4096) == -1) exit_sys("mlock"); printf("Ok\n"); if (munlock(buf, 4096) == -1) exit_sys("munlock"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bir prosesin bütün sayfalarını lock etmek için mlockall POSIX fonksiyonu kullanılabilmektedir: int mlockall(int flags); Buradaki flags parametresi aşağıdaki sembolik sabitlerin bit OR işlemine sokulmasıyla oluşturulur: MCL_CURRENT: Şu anda RAM'de olan tüm sayfaların kilitleneceği anlamına gelir. MCL_FUTURE: Bundan sonra RAM'e alınacak tüm sayfaların kilitleneceği anlamına gelir. munlockall fonksiyonu ise ters işlemi yapmaktadır: int munlockall(void); mlockall fonksiyonu yine prosesin RLIMIT_MEMLOCK kaynak limitinden etkilenmektedir. Bunun soft değeri mevcut çekirdeklerde oldukça yükseltilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); char buf[1000000]; int main(void) { if (mlockall(MCL_CURRENT|MCL_FUTURE) == -1) exit_sys("mlock"); printf("Ok\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Sayfa kilitleme işlemi Linux sistemlerinde mmap fonksiyonu ile mapping yapılırken mapping alanı için de MAP_LOCKED flags parametresi ile sağlanabilmektedir. (Bu bayrak POSIX standartlarında yoktur.) Yani biz bu sayede mapping yaptığımız alanları aynı zamanda lock edebilmekteyiz. Tabii burada da prosesin RLIMIT_MEMLOCK kaynak limiti bir kısıt oluşturabilmektedir. Ancak Linux sistemlerinde mmap fonksiyonundaki MAP_LOCKED bayrağı tüm map edilen sayfaları o anda RAM'e çekemeyebilmektedir. Bu durumda fonksiyon başarısız olmamaktadır. Yani başka bir deyişle bu davranış mlock kadar kesin değildir. Aynı davranış aslında Linux'a özgü biçimde klasik Sistem 5 paylaşılan bellek alanlarında da chmctl fonksiyonunda bazı küçük farklılıklarla bulunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread'ler 90'lı yıllarda işletim sistemlerine sokulmuştur. Microsoft'un thread'li ilk işletim sistemi Windows NT (1993) ve sonra da Windows 95 (1995) sistemleridir. Benzer biçimde UNIX/Linux dünyasına da thread'ler ilk kez 90'lı yıllarla sokulmuştur. Yani thread'ler daha önce denemeler yapılmış olsa da 90'lı yılların başlarında işletim sistemlerine sokulmuş durumdadır. Bugün pek çok programlama dili ve framework kendi içerisinde thread'leri barındıran kütüphanelere sahiptir. Hatta yeni dillerin bazıları artık thread'leri anahtar sözcüklerle dilin sentaksına dahil etmektedir. Örneğin C++ Programlama Dili'ne 2011 versiyonuyla (C++11) bir thread kütüphanesi eklenmiştir. Benzer biçimde .NET ve C#'ın ilk sürümlerinde (2002) bile framework bir thread kütüphanesine sahipti. Benzer biçimde Java Dilininde kütüphanesi içerisinde thread'lerle işlem yapan sınıflar bulunuyordu. Microsoft'un MFC framework'ü gibi, yaygın kullanılan Qt framework'ü gibi pek çok framework kendi içerisinde bir thread kütüphanesi barındırmaktadır. Tabii thread işlemleri bir dilin ya da bir framework'ün kontrolü altında olan işlemler değildir. Doğrudan işletim sisteminin kontrolü altında olan işlemlerdir. Dolayısıyla programlama dilleri ya da framework'ler işletim sistemlerinin sunduğu mekanizmayı kullanmaktadır. Tabii bu diller ve framework'ler thread kullanımını "platform bağımsız" hale de getirmektedir. Yani örneğin Windows'un ve Linux'un sağladığı thread mekanizması farklı olmasına karşın C++'ın standart thread kütüphanesi "platform bağımsız" bir kütüphanedir. Başka bir deyişle C++'ın standart thread kütüphanesi Windows sistemlerinde "Windows API Fonksiyonları" kullanılarak UNIX/Linux sistemlerinde "POSIX fonksiyonları" kullanılarak gerçekleştirilmiştir. C Programlama Dili'ne de 2011 sürümüyle (C11) birlikte "isteğe bağlı (optional)" mini bir thread kütüphanesi eklenmiştir. Ancak bu mini thread kütüphanesi Microsoft C derleyicisi tarafından ve gcc derleyicileri tarafından desteklenmiyordu. Ancak gcc derleyicilerinin son versiyonları bu thread kütüphanesini desteklemektedir. Biz kursumuzda (kursumuz bir sistem programlama kursu olduğu için) UNIX/Linux sistemlerindeki işletim sistemi çekirdeği ile ilgili olan aşağı seviyeli thread mekanizması üzerinde duracağız. Yukarıda da belirttiğimiz gibi programlama dili ya da framework ne olursa olsun UNIX/Linux sistemlerinde eninde sonunda işlemler burada anlatacak olduğumuz thread mekanizması yoluyla yapılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İşletim sistemlerinde bir prosesin bağımsız olarak çizelgelenen akışlarına "thread" denilmektedir. Thread sözcüğü İngilizce "iplik" anlamına gelmektedir. Akışlar ipliklere benzetilerek bu sözcük türetilmiştir. Proses kavramı çalışmakta olan programın tüm bilgisini içermektedir. Oysa thread'ler yalnızca akış belirtmektedir. Bir proses tek bir akışa sahip olmak zorunda değildir. Prosesler birden fazla akışa sahip olabilmektedir. İşte prosesin bu bağımsız akışlarına "thread" denilmektedir. İşletim sistemlerinin "çizelgeleyici (scheduler)" alt sistemleri prosesleri değil thread'leri çizelgelemektedir. Yani çizelgeleyici alt sistem bir thread'i çalıştırıp belli bir quanta süresi dolduğunda ona ara vermekte ve diğer bir thread'i CPU'ya atamaktadır. Ara verilen thread ile akışın CPU'ya verildiği thread aynı prosesin thread'leri olabileceği gibi farklı proseslerin thread'leri de olabilmektedir. Prosesler çalışmasına tek bir thread'le başlamaktadır. Yani fork işlemi ile aslında tek bir thread yaratılmaktadır. Bir proses birden fazla thread'e sahipken fork işlemi yapılsa bile yeni yaratılan alt proses tek bir thread ile yaratılmaktadır. O da fork işleminin yapıldığı thread'tir. Diğer thread'ler proses yaratıldıktan sonra programcı tarafından yaratılmaktadır. exec işlemi yapıldığında yine prosesin tüm thread'leri yok edilir. exec yapılan program tek bir thread'le çalışmaya başlar. Program çalışmaya başladığında var olan bu thread'e programın "ana thread'i (main thread)" denilmektedir. Örneğin biz "sample" programını çalıştırdığımızda çalışma tek bir thread ile başlatılmaktadır. Bu "sample" programının ana thread'idir. Thread'lerin olmadığı zamanlarda zaten proseslerin tek bir akışı vardı. Dolayısıyla o zamanlar yalnızca ana thread bulunmaktaydı. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi thread'lere neden gereksinim duyulmaktadır? Bunun birkaç bariz nedenini aşağıda maddeler halinde veriyoruz: 1) Thread'ler arka plan işlemlerin yapılması için iyi bir araç oluşturmaktadır. Bir işi yaparken aynı zamanda arka planda periyodik bir işlem yapmak istediğimizi düşünelim. Bu durumda biz bloke olursak artık o periyodik işlemi yapamayız. Örneğin: for (;;) { ch = getchar(); proc(ch); } Buradaki temsili kodda klavyeden bir karakter okunmuş ve o karakter işlenmiştir. Ancak arka planda ekrana bir saat de basılmaktadır. Oysa getchar gibi bir işlemde bloke oluşacağı için ekrandaki saat duracaktır. İşte eskiden bu tür işlemler oldukça zor biçimde ancak dolaylı olarak gerçekleştiriliyordu. Ancak thread'ler kullanılmaya başlandığında bu tür işlemleri gerçekleştirmek çok kolaylaştı. Thread'ler bağımsız çizelgelendiği için bir thread bloke olduğunda prosesin diğer thread'leri çalışmaya devam etmektedir. Örneğin: Ana thread: for (;;) { ch = getchar(); proc(ch); } Diğer thread: for (;;) { } 2) Thread'ler bir işi hızlandırmak için sıkça kullanılmaktadır. Bir işin tek bir akışa yaptırılmasıyla birden fazla akışa yaptırılması arasında önemli bir hız farkı olabilmektedir. Örneğin bir satranç programında thread'lerden biri mümkün hamleleri tespit ederken diğer bir thread bu hamleleri analiz ediyor olabilir. Tek bir CPU'nun bulunduğu durumda da thread'ler prosesin toplam CPU zamanının artırılmasını sağlayabilmektedir. (Örneğin tek bir işlemci bulunuyor olsun. Bizim dışımızda sistemde 10 thread çalışıyor olsun. Biz prosesimizde tek thread oluşturursak (default durum) 1 / 11 CPU zamanı elde ederiz. İki thread oluştursak 2 / 12 CPU zamanı elde ederiz. Üç thread oluştursak 3 / 13 CPU zamanı elde ederiz. Bir kesrin pay ve paydasına aynı sayı eklendiğinde kesrin büyüdüğüne dikkat ediniz.) 3) Bir programın çeşitli parçalarının çok işlemcili ya da çok çekirdekli makinelerde eş zamanlı biçimde çalıştırılmasına "paralel programlama" denilmektedir. Paralel programlama yapabilmek için programcının thread'leri kullanması ve thread'leri farklı işlemcilere ya da çekirdeklere ataması gerekmektedir. 4) Thread'ler bazı durumlarda mutlak zorunlu olmasa da tasarımı kolaylaştırmak için ve sistemi ölçeklenebilir (scalable) hale getirmek için kullanılabilmektedir. 5) Thread'ler bazı durumlarda zorunlu olarak da kullanılabilmektedir. Örneğin GUI programlama modelinde bir mesajın işlenmesi uzun süre aldığında bu durumda mecburen thread'ler kullanılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Programın akışına ilişkin birkaç terim bazen birbirleriyle karıştırılmaktadır. (Yine de bilgisayar bilimlerindeki terimlerin çoğu farklı konularda farklı anlamlarda kullanılabilmektedir.) Concurrent Computing: Bu terim genel bir şemsiye terimdir. Birden fazla akışın söz konusu olabildiği tüm durumlar için kullanılabilmektedir. Multithreading (ya da Multithreaded) Programming: Bir işin birden fazla thread ile gerçekleştirilmesine yönelik uygulamalar için bu terim kullanılmaktadır. Reentrancy: Bu terim de genellikle fonksiyon akışının içi içe geçebilmesini belirtmektedir. Örneğin bir fonksiyonun "reenterant" olması demek fonksiyon çalışırken yeniden aynı fonksiyonun çalıştırılması demektir. Reentrancy "özyineleme (recursion)" demek değildir. Özyinelemede tek bir akış vardır. Fakat örneğin iki thread'in aynı fonksiyona girmesi durumunda bir "reentrancy" durumu oluşur. Ya da örneğin bir mikro denetleyici sistemde akış bir fonksiyon üzerinde ilerlerken bir kesme oluştuğunda kesme kodu yeniden aynı fonksiyonu çağırırsa burada da bir "reentrancy" durumu söz konusu olur. UNIX/Linux sistemlerinde de hiç thread kullanmamış olsak da "sinyal (signal)" mekanizması bir "reentrancy" durumu oluşturabilmektedir. Parallel Programming: Aynı makinede bir prosesin çeşitli thread'lerinin farklı CPU ya da çekirdeklerde eşzamanlı çalıştırılma gayretine paralel programlama denilmektedir. Distributed Computing: Bir işin farklı bilgisayarlarda eş zamanlı bir biçimde ele alınmasına ilişkin bir terimdir. Paralel programlamaya benzemektedir. Ancak paralel programlama "aynı makinede" yürütülen bir faaliyetken "distributed computing" işlerin farklı makinelerde koordineli bir biçimde gerçekleştirilmesi anlamına gelmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 50. Ders 06/05/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread işlemleri işletim sistemlerinde aslında birtakım sistem fonksiyonlarıyla gerçekleştirilmektedir. Örneğin Linux sistemlerinde thread yaratan sys_clone isimli bir sistem fonksiyonu bulunmaktadır. Ancak daha önce de ifade ettiğimiz gibi sistem fonksiyonları taşınabilir değildir ve sistem fonksiyonları genellikle düşük seviyeli fonksiyonlardır. Yani bunların kullanılmaları zordur. Thread'ler UNIX/Linux sistemlerine ilk sokulduğunda farklı thread kütüphaneleri oluşturulmuştu. Programcılar bu kütüphanelerden birini kullanıyorlardı. Ancak 90'lı yılların ortalarında thread kütüphanesi "Realtime Extensions" başlığı altında POSIX standartlarına eklenmiştir. POSIX tarafından desteklenen bu thread kütüphanesine "POSIX Thread Kütüphanesi" ya da kısaca "pthread" kütüphanesi denilmektedir. POSIX thread kütüphanesinin içerisindeki bütün fonksiyonların isimleri pthread_xxx biçiminde "pthread_" öneki ile başlatılmış durumdadır. POSIX thread kütüphanesi "libpthread.so" ve "libpthread.a" isimli kütüphaneler biçiminde oluşturulmuştur. Bu kütüphaneleri kullanırken derleme sırasında "-lpthread" seçeneğinin bulundurulması gerekmektedir. Bu seçenek link aşamasında POSIX thread kütüphanesinin linker tarafından işleme sokulacağını belirtmektedir. Diğer programlama dillerindeki thread kütüphaneleri de UNIX/Linux sistemlerinde pthread kütüphanesi kullanılarak gerçekleştirilmiştir. Yani bu anlamda pthread kütüphanesi "taban (base)" bir kütüphanedir. (Örneğin C++11 ile birlikte C++'a bir thread kütüphanesi eklenmiştir. Bu kütüphanenin UNIX/Linux sistemlerindeki gerçekleştirimi pthread kütüphanesi kullanılarak yapılmıştır. Tabii C++'ın thread kütüphanesi C++'ın standart kütüphanesinin bir parçasıdır. Dolayısıyla "cross platform" özelliği vardır. Yani örneğin aynı kütüphane Windows sistemlerinde Windows API fonksiyonları kullanılarak gerçekleştirilmiş durumdadır.) Benzer biçimde C11 ile birlikte C'ye da isteğe bağlı (optional) minimalist bir thread kütüphanesi eklenmiştir. Mevcut C derleyicileri artık yavaş yavaş bu thread kütüphanesini de desteklemeye başlamıştır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread'ler aynı prosesin bağımsız çizelgelenen farklı akışlarıdır. Bir işi iki thread'e yaptırmakla iki prosese yaptırmak arasında şu önemli farklılıklar vardır: - Prosesin thread'leri aynı bellek alanını kullanmaktadır. Yani örneğin bir thread bir global değişkene bir şey yazsa diğer thread bunu görmektedir. Oysa proseslerin bellek alanları tamamen izole edilmiştir. Prosesler arasındaki haberleşme maliyeti yüksektir ve proseslerarası haberleşme kodu daha karmaşık hale getirmektedir. Halbuki thread'ler global değişkenler yoluyla ya da heap yoluyla çok daha az maliyetle haberleşebilmektedir. - Thread'lerin yaratılması ve yok edilmesi proseslere göre daha hızlıdır. Çünkü bir proses yaratılırken arka planda prosese özgü pek çok yaratım işlemleri de yapılmaktadır. - Thread'ler proseslere göre daha az sistem kaynağı harcama eğilimindedir. Yani bir prosesin yaratılması sırasında harcanan sistem kaynağı bir thread'in yaratılması sırasında harcanan sistem kaynağından daha yüksektir. Proses kavramı çalışmakta olan programın tüm bilgilerini belirtmektedir. Thread ise yalnızca bir akış belirtmektedir. Örneğin: - Thread'lerin gerçek ve etkin kullanıcı id'leri ve grup id'leri yoktur. Proseslerin vardır. Yani bir prosesin tüm thread'leri aynı kullancı ve grup id'sine sahiptir. - Thread'lerin çalışma dizinleri, çevre değişkenleri yoktur. Proseslerin vardır. Örneğin biz bir proseste chdir POSIX fonksiyonu ile çalışma dizinini (current working directory) değiştirdiğimizde tüm thread'ler bunu değiştirmiş oluruz. - Dosya betimleyici tablosu prosese özgüdür. Dolayısıyla tüm thread'ler aynı dosya betimleyici tablosunu kullanmaktadır. - Prosesin thread'lerinin proses id'leri aynıdır. Ancak UNIX/Linux dünyasında threadler arasında bir altlık-üstlük (parent-child) ilişkisi yoktur. Yani bir thread'in hangi thread tarafından yaratıldığının (istisna bazı durumlar dışında) bir önemi yoktur. Dolayısıyla UNIX/Linux dünyasında "üst thread (parent thread)" ve "alt thread (child thread)" biçiminde kavramlar yoktur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Proses çalışmaya tek bir thread ile başlamaktadır. Diğer thread'ler programcı tarafından yaratılmaktadır. Bir thread yaratılırken thread akışının başlatılacağı bir fonksiyon da belirtilmektedir. Nasıl programın ana thread'i yani akışı main fonksiyonundan başlıyorsa bizim yarattığımız thread'in akışı da bizim belirttiğimiz bir fonksiyondan başlatılmaktadır. Örneğin biz bir thread yaratmış olalım ve thread'imizin akışı da foo fonksiyonundan başlatılmış olsun: int main(void) { ... } ... void *foo(void *param) { ... } ... Burada bir akış main fonksiyonundan ilerlerken diğer akış bağımsız olarak foo fonksiyonundan itibaren ilerleyecektir. Bu iki akış aynı programın içerisindedir. Dolayısıyla bu akışlardan biri bir global değişkeni değiştirse diğeri de onu değişmiş görecektir. Şimdi bu durumu aşağıdaki durum ile kıyaslayınız: int main(void) { ... if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) foo(); ... } void foo(void) { ... } Biz burada benzer işlemi fork ile yapmaya çalıştık. Ancak fork yaptığımızda biz maliyetli bir biçimde yeni bir proses yarattık. Bu prosesin bilgileri üst prosesten alınmaktadır. Ayrıca yarattığımız alt proses ayrı bir bellek alanına sahiptir. Dolayısıyla örneğin aynı global değişkenleri kendi aralarında paylaşmamaktadır. Örneğin biz alt proseste dosya açsak üst proses bunu görmeyecektir. Halbuki thread'ler aynı prosesin akışlarıdır. Prosesin bir thread'i bir dosya açsa diğer thread'i onu açık olarak görmektedir. Proseslerle thread'ler akış bakımından benzemektedir. Her iki durumda da farklı akış oluşturulmaktadır. Thread'lere ilk zamanlar bu benzerlikten hareketle "lightweight (hafif siklet) proses" de denilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Her ne kadar prosesin thread'leri aynı bellek alanını kullanıyorsa da "stack" konusunda bu durum geçerli değildir. Thread'lerin stack'leri birbirinden ayrılmıştır. Başka bir deyişle bir thread yaratıldığında o thread için ayrı bir stack yaratılmaktadır. Yerel değişkenlerin ve parametre değişkenlerinin stack'te yaratıldığını anımsayınız. Thread'lerin stack'leri birbirinden ayrıldığı için farklı thread akışları aynı fonksiyon üzerinde ilerlese bile aynı yerel değişkenleri görmemektedir. Yerel değişkenlerin her thread için ayrı bir kopyası oluşturulmaktadır. Örneğin: int g_x; void *thread_proc1(void *param) { ... foo(); ... } void *thread_proc2(void *param) { ... foo(); ... } void foo(void) { int a = 10; static int b = 10; ... ++a; ... ++a; ... ++b; ... ++g_x; ... } Burada iki thread foo üzerinde ilerlerken aslında yerel değişken olan a'nın kendi kopyaları üzerinde işlem yapmaktadır. Yani bir thread buradaki yerel "a" değişkenini değiştirdiğinde diğer thread bu değişikliği görmez. Başka bir deyişle her thread'in kendine özgü farklı bir "a" değişkeni vardır. Bu durumun teknik açıklaması şöyledir: Yerel değişkenler stack'te yaratılmaktadır. Thread'lerin de stack'leri birbirinden ayrıldığı için bu "a" değişkeni hangi thread akışı o fonksiyonu çağırmışsa onun stack'inde yaratılmaktadır. Ancak prosesin bütün thread'leri aynı ".data" ve ".bss" ve "heap" alanını kullanmaktadır. Bu nedenle thread'lerden biri yukarıdaki örnekte "++g_x" global değişkenini artırdığında diğeri onu artırılmış olarak görecektir. Anımsanacağı gibi fonksiyonların static yerel değişkenleri stack'te yaratılmamaktadır. Static yerel değişkenlere eğer ilk değer verilmişse bunlar ".data" alanında, ilk değer verilmemişse ".bss" alanında yaratılmaktadır. Dolayısıyla örneğin bir thread static yerel yerel değişkeni değiştirirse diğer thread onu değişmiş olarak görmektedir. İşletim sistemlerinde yaratılan bir thread'in default stack büyüklüğü sistemden sisteme değişebilmektedir. Örneğin Linux sistemlerinde thread'ler default olarak 8 MB stack ile yaratılmaktadır. Ancak thread'ler yaratılırken programcı isterse stack büyüklüğünü kendisi de belirleyebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread'lerin errno değişkenleri birbirinden farklıdır. Yani her thread'in ayrı bir errno değişkeni vardır. Örneğin biz bir thread'te bir POSIX fonksiyonu çağırsak o POSIX fonksiyonu başarısız olsa o thread'in errno değişkeni set edilir. Diğer thread'lerin errno değişkenleri bundan etkilenmez. perror fonksiyonu da kendi thread'inin errno değişkenini kullanmaktadır. Yani biz perror fonksiyonu ile hata mesajını stderr dosyasına yazdırmak istersek perror o thread'in errno değişkeninin değerine başvurarak yazıyı oluşturacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- POSIX thread kütüphanesini incelemeden önce kütüphanedeki bazı ortak özelliklerin üzerinde durmak istiyoruz: - Yukarıda da belirttiğimiz gibi kütüphandeki bütün fonksiyonların isimleri "pthread_" öneki ile başlatılmıştır. - Kütüphanedeki fonksiyonların büyük bölümünün geri dönüş değeri int türdendir. Bu fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda bizzat errno değerinin kendisine geri dönmektedir. Bu fonksiyonlar errno değişkenini set etmemektedir. Başarısızlık durumunda errno değerinin kendisine geri dönmektedir. Bu durumda başarı kontrolünün yapılması ve hata mesajlarının yazdırılması şöyle yapılabilir: int result; ... if ((result = pthread_xxx(...)) != 0) { fprintf(stderr, "pthread_xxx: %s\n", strerror(result)); exit(EXIT_FAILURE); } Görüldüğü gibi burada perror fonksiyonu kullanılmamıştır. Çünkü perror fonksiyonu errno değerinin yazısını yazdırmaktadır. Halbuki pthread fonksiyonları errno değerini set etmemekte, bizzat errno değerinin kendisiyle geri dönmektedir. strerror fonksiyonunun bir errno numarası için hata yazısını verdiğini anımsayınız. Tabii biz yine bir sarma fonksiyon kullanabiliriz: void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Bu durumda biz thread fonksiyonlarını bu sarma fonksiyon yoluyla şöyle çağırabiliriz: int result; ... if ((result = pthread_xxx(...)) != 0) exit_sys_errno("pthread_xxx", result); errno ismi bazı sistemlerde bir değişken değil makro olduğu için errno isminde bir değişken tanımlamayınız. - Bütün POSIX thread fonksiyonlarının prototipleri dosyası içerisindedir. Dolayısıyla thread işlemleri yapacaksak bizim bu dosyayı include etmemiz gerekmektedir: #include - Thread kullanan programları derlerken link aşaması için "-lpthread" komut satırı argümanını kullanmayı unutmayınız. Örneğin: $ gcc -o sample sample.c -lpthread gcc ve clang derleyicilerinin ileri versiyonlarında başlık dosyalarındaki pragma direktifleri ile belli bir başlık dosyası include edildiğinde ilgili kütüphanenin link aşamasına otomatik biçimde sokulması sağlanabilmektedir. Ancak siz her zaman thread'li programları derlerken "-lpthread" seçeneği ile bağlayıcının bu kütüphaneye bakmasını sağlamalısınız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread'ler pthread_create isimli POSIX fonksiyonu ile yaratı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 değerinin yerleştirileceği nesnenin adresini almaktadır. Nasıl proseslerin id değerleri varsa thread'lerin de id değerleri vardır. Proseslerin id değerleri o anda sistem genelinde tektir (unique). Ancak thread'lerin id değerleri sistem genelinde tek olmak zorunda değildir. Fakat proses içerisinde tektir. Yani farklı proseslerin yarattığı thread'lerin id değerleri aynı olabilir, ancak aynı prosesin thread'lerinin id değerleri farklı olmak zorundadır. (Linux sistemlerinde thread'lerin id değerleri onlar için ayrılan task_struct yapısına ilişkin bir değer olduğu için sistem genelinde tektir. Ancak POSIX standartlarında bunun bir garantisi yoktur.) pthread_t türü ve dosyaları içerisinde typedef edilmiştir. POSIX standartlarına göre pthread_t türü "aritmetik bir tür (yani tamsayı türü ya da gerçek sayı türü)" olmak zorunda değildir. Yani bir yapı türünden de olabilir. Dolayısıyla pthread_t türünden iki nesneyi kendi aralarında karşılaştırmaya çalışmayınız. Linux'ta pthread_t türü "unsigned long int" olarak typedef edilmiştir. Fonksiyonun ikinci parametresi yaratılacak thread'in bazı özelliklerini belirten pthread_attr_t türünden bir nesnenin adresini almaktadır. Thread özellikleri konusu ileride ele alınacaktır. Ancak bu parametre NULL adres geçilirse bu durumda thread default özelliklerle yaratılmaktadır. Fonksiyonun üçüncü parametresi thread akışının başlatılacağı fonksiyonun adresini belirtmektedir. Thread fonksiyonlarının geri dönüş değerleri ve parametresi void * olmak zorundadır. Fonksiyonun son parametresi thread fonksiyonuna geçirilecek parametreyi belirtmektedir. Böylece aynı thread fonksiyonu farklı bilgilerle başlatılabilmektedir. Thread'e geçirilecek parametrenin void * türünden olduğuna dikkat ediniz. Çünkü adres bilgisi en genel parametrik bilgi durumundadır. Tabii programcı bu parametreye bir adres geçirmeyip bir değer de geçirmek isteyebilir. Bu durumda değeri void * türüne dönüştürmelidir. Fonksiyonun üçüncü parametresiyle belirtilen thread fonksiyonunun geri dönüş değerinin de void * türünden olduğuna dikkat ediniz. Thread'ler de sonlandığında tıpkı prosesler gibi bir exit kodu oluşturmaktadır. Ancak bu exit kodu int türden değil void * türündendir. Bu exit kodunun nasıl elde edileceği izleyen paragraflarda ele alınmaktadır. pthread_create fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Bir thread yaratıldığında hemen çalışmaya başlamaktadır. (Örneğin bazı thread kütüphanelerinde önce thread yaratılmakta, sonra "start" gibi bir fonksiyonla thread çalıştırılmaktadır. Halbuki POSIX kütüphanesinde thread'ler zaten yaratıldığında çalıştırılırlar.) Tabii pthread_create fonksiyonu ile thread yaratıldığında yaratan thread'in mi yoksa yeni yaratılan thread'in mi ilk olarak CPU'ya atanacağı işletim sisteminin çizelgeleme algoritmasına bağlı olarak değişebilmektedir. POSIX bu konuda herhangi bir garanti vermemektedir. Aşağıda thread yaratımına bir örnek verilmiştir. Bir thread'te sleep fonksiyonu kullanıldığında sleep fonksiyonu kendisini çağıran thread'i belirtilen saniye kadar bloke etmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #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); } 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); } /*-------------------------------------------------------------------------------------------------------------------------- Thread fonksiyonuna parametre geçebiliriz. Bunun için genellikle programcılar heap'te tahsisat yaparlar. Tahsis edilen bu alanlar thread fonksiyonunun sonunda free hale getirilebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); #define NTHREADS 10 int main(void) { pthread_t tid[NTHREADS]; int result; char *buf; for (int i = 0; i < NTHREADS; ++i) { if ((buf = (char *)malloc(1024)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } snprintf(buf, 1024, "Thread-%d", i); if ((result = pthread_create(&tid[i], NULL, thread_proc, buf)) != 0) exit_sys_errno("pthread_create", result); } for (int i = 0; i < 10; ++i) { printf("main thread: %d\n", i); sleep(1); } return 0; } void *thread_proc(void *param) { char *str = (char *)param; for (int i = 0; i < 10; ++i) { printf("%s: %d\n", str, i); sleep(1); } free(str); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Thread fonksiyonuna getirilecek parametre birden fazla ise tipik olarak programcı bu parametreleri bir yapı biçiminde oluşturur. İlgili yapı türünden bir dinammik bir nesne tahsis eder ve o nesnenin adresini thread fonksiyonuna geçirir. Bu dinamik alan thread fonksiyonu tarafından free hale getirilebilir. Aşağıda buna ilişkin bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); struct PARAM_INFO { char *name; int count; }; int main(void) { pthread_t tid; int result; struct PARAM_INFO *pi; if ((pi = (struct PARAM_INFO *)malloc(sizeof(struct PARAM_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } pi->name = "other thread"; pi->count = 10; if ((result = pthread_create(&tid, NULL, thread_proc, pi)) != 0) exit_sys_errno("pthread_create", result); for (int i = 0; i < 10; ++i) { printf("main thread: %d\n", i); sleep(1); } return 0; } void *thread_proc(void *param) { struct PARAM_INFO *pi = (struct PARAM_INFO *)param; for (int i = 0; i < pi->count; ++i) { printf("%s: %d\n", pi->name, i); sleep(1); } free(param); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s:%s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bir thread çeşitli biçimlerde sonlanabilmektedir: 1) Thread fonksiyonu bittiğinde thread'ler otomatik olarak sonlanırlar. Bu en çok karşılaşılan doğal sonlanma biçimidir. 2) Thread'ler pthread_exit fonksiyonuyla thread fonksiyonunun sonuna gelinmeden de sonlandırılabilmektedir. pthread_exit fonksiyonu "kendi thread'ini" sonlandırmaktadır. Başka bir thread'i sonlandırmamaktadır. Yani hangi thread akışı pthread_exit fonksiyonunu görürse o thread sonlandırılmaktadır. pthread_exit fonksiyonunun prototipi şöyledir: #include void pthread_exit(void *value_ptr); Nasıl exit fonksiyonu prosesi sonlandırırken bir exit kodunu alıyorsa pthread_exit fonksiyonu da thread'i sonlandırırken void * türünden bir exit kodu almaktadır. (main fonksiyonunun geri dönüş değeri int türdendir. exit fonksiyonunun parametresi de int türdendir. İşte thread fonksiyonunun geri dönüş değeri void * türünden olduğu için pthread_exit fonksiyonunun parametresi de void * türündendir.) 3) Bir programda exit standart C fonksiyonu ya da _exit POSIX fonksiyonu çağrılırsa proses sonlandırılır. Thread kavramı proses kavramının içerisindedir. Dolayısıyla proses sonlandığında prosesin tüm thread'leri de otomatik olarak sonlandırılmaktadır. Tabii exit ya da _exit fonksiyonu herhangi bir thread tarafından da çağrılabilir. Burada önemli bir nokta main fonksiyonu bittiğinde exit işleminin yapılmasıdır. Dolayısıyla main fonksiyonu biterse proses sonlanır; proses sonlanırsa da tüm thread'ler sonlanacaktır. Örneğin aşağıdaki gibi hataları POSIX thread kütüphanesini yeni kullananlar sıkça yapmaktadır: 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 bittiğinde proses sonlanacak ve bütün thread'ler de yok edilecektir */ return 0; } Burada thread yaratılmıştır ancak proses sonlandığı için yaratılan thread de hemen sonlanacaktır. Aslında prosesin ana thread'inin diğer thread'lerden bir farkı yoktur. Yani prosesin ana thread'i diğer thread'lerden önce sonlanabilir. Pekiyi o zaman proses nasıl sonlanacaktır? Çünkü akış main fonksiyonunu bitirmeyecektir. Dolayısıyla akış exit fonksiyonunu görmeyecektir. İşte bir prosesin son thread'i sonlandığında programın akışı exit ya da _exit fonksiyonunu görmese bile proses işletim sistemi tarafından sonlandırılmaktadır. 4) Prosesler sinyal yoluyla sonlandırılabilmektedir. Bu durumda prosese bir sinyal gönderilirse ve proses de bu sinyali ele almamışsa (handle etmemişse) bazı sinyaller prosesin sonlanmasına yol açabilmektedir. Dolayısıyla bu tür durumlarda da prosesin tüm thread'leri sinyal yüzünden sonlandırılacaktır. 5) Bir thread başka bir thread'i pthread_cancel fonksiyonu ile sonlandırabilmektedir. Fonksiyonun prototipi şöyledir: #include int pthread_cancel(pthread_t thread); Fonksiyon parametre olarak sonlandırılacak thread'in id değerini alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri döner. pthread_cancel fonksiyonu geri döndüğünde thread'in sonlanmış olması gerekmemektedir. Yani sonlanma işlemi asenkron biçimde yapılmaktadır. pthread_cancel fonksiyonu ile thread'leri sonlandırmanın bazı ayrıntıları vardır. Bu ayrıntılar izleyen paragraflarda ele alınmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte bir thread yaratılmış ve thread erken bir biçimde pthread_exit fonksiyonu ile sonlandırılmıştır. pthread_exit fonksiyonu hangi thread akışı tarafından çağrılırsa o thread sonlanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void *thread_proc(void *param); void foo(void); 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); } return 0; } void *thread_proc(void *param) { foo(); return NULL; } void foo(void) { for (int i = 0; i < 10; ++i) { if (i == 5) pthread_exit(NULL); printf("other thread: %d\n", i); sleep(1); } } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte proses exit fonksiyonuyla sonlandırılmıştır. Dolayısıyla prosesin tüm thread'leri de exit işlemiyle sonlandırılmış olmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #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) { if (i == 5) exit(EXIT_SUCCESS); printf("main thread: %d\n", i); sleep(1); } 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); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte main fonksiyonunda thread yaratılmış ancak hemen main fonksiyonu sonlanmıştır. main fonksiyonu sonlandığında exit fonksiyonu çağrılarak proses sonlandırılacağı için yaratılmış olan thread'ler de sonlandırılmış olacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #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 bittiğinde proses sonlanacak ve dolayısıyla prosesin tüm thread'leri 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); sleep(1); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte programın ana thread'i sonlandırılmıştır. Bu durumda prosesin son thread'i sonlandığında hiç exit ya da _exit fonksiyonu çağrılmasa bile proses otomatik olarak sonlandırılacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #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) { if (i == 5) pthread_exit(NULL); printf("main thread: %d\n", i); sleep(1); } return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("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); } /*-------------------------------------------------------------------------------------------------------------------------- 51. Ders 07/05/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde (aslında Windows sistemlerinde de böyle) thread'ler arasında altlık-üstlük (parent-child)" ilişkisi yoktur. Örneğin bir thread'in hangi thread tarafından yaratıldığının bir önemi yoktur. Bir thread'in ana thread tarafından yaratılması zorunlu değildir. Bir thread herhangi bir thread akışı tarafından yaratılabilir. Benzer biçimde örneğin thread'lerin exit kodları herhangi bir thread tarafından elde edilebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir thread'in exit kodu pthread_join fonksiyonu ile elde edilmektedir. pthread_join fonksiyonunun davranışı alt prosesin exit kodunu elde eden wait fonksiyonlarına benzemektedir. Ancak pthread_join fonksiyonu herhangi bir thread akışı tarafından çağrılabilmektedir. pthread_join fonksiyonu exit kodu alınacak thread henüz sonlanmamışsa onun sonlanmasını bekler. Eğer söz konusu thread sonlanmışsa herhangi bir bloke oluşmaz. Fonksiyonun prototipi şöyledir: #include int pthread_join(pthread_t thread, void **value_ptr); Fonksiyonun birinci parametresi exit kodu alınacak thread'in id değerini, ikinci parametresi exit kodunun yerleştirileceği void * türünden göstericinin adresini almaktadır. Bu parametreye NULL adres geçilebilir. Bu durumda thread'in exit kodu programcıya verilmez. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Aşağıdaki örnekte bir thread yaratılmış thread bir exit kodla geri döndürülmüştür. Sonra da bu thread'in sonlanması beklenmiş ve exit kod alınmıştır. Thread'lerin exit kodları birer adres biçiminde olsa da biz aşağıdaki örnekte olduğu gibi tamsayı değerleri sanki birer adresmiş gibi exit kod olarak oluşturabiliriz. ---------------------------------------------------------------------------------------------------------------------------*/ #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 *valptr; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); printf("waiting for the thread to finish...\n"); if ((result = pthread_join(tid, &valptr)) != 0) exit_sys_errno("pthread_join", result); printf("%d\n", (int)valptr); return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); sleep(1); } return (void *)100; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi bir thread'i yaratıp onu pthread_join fonksiyonu ile beklemezsek "zombie proses" benzeri bir durum oluşur mu? Gerçekten de tıpkı zombie proseslerde olduğu gibi işletim sistemleri sonlanan thread'e ilişkin bazı kaynakları "onların exit kodları her an programcı tarafından alınabilir" diye bekletmektedir. Bu durum da zombie proses gibi olumsuzluklara yol açabilmektedir. Gerçi zombie thread'ler zombie prosesler kadar sorunlara yol açmıyorsa da programcıların yarattıkları thread'i pthread_join fonksiyonu ile beklemesi gerekir. Ancak bazen programcılar çok sayıda thread yaratıp bunları bekleyecekleri noktaları oluşturamayabilirler. Bu durumda threadler'in "detached" modda yaratılmaları ya da yaratıldıktan sonra "detached" moda sokulmaları gerekir. "Detached" thread'ler sonlandığında kaynaklarını otomatik boşaltan dolayısıyla join yapılamayan thread'lerdir. Thread'lerin nasıl detached moda sokulacağı izleyen paragraflarda ele alınmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux çekirdeğinde (diğer sistemlerde böyle olmak zorunda değil) thread'ler için yine bir task_struct nesnesi tahsis edilmektedir. Yani Linux çekirdeği prosesin thread'lerini adeta sanki aynı bellek alanını kullanan porosesler gibi oluşturmaktadır. Tabii prosesin kendisi için de prosesin thread'leri için de aynı task_struct veri yapısının oluşturulması biraz verimsiz bir bellek kullanımına yol açmaktadır. Çünkü aslında thread'ler bu task_struct yapısının önemli bir kısmını zaten kullanmamaktadır. Ancak bu biçimdeki yaklaşım çekirdeğin kodlanmasını oldukça kolaylaştırmıştır. Bir task_struct yapısında onun bir thread'e ilişkin mi yoksa prosese ilişkin mi olduğu bilgisi tutulmaktadır. Benzer biçimde prosese ilişkin task_struct yapısının içerisinde o prosesin thread'lerine ilişkin task_struct nesneleri de bir biçimde tutulmaktadır. Zaten Linux çekirdeğinde aslında proses yaratmak için de thread yaratmak için de sys_clone isimli aynı sistem fonksiyonu kullanılabilmektedir. Proses yaratmakta kullanılan sys_fork isimli sistem fonksiyonu ve sys_clone sistem fonksiyonu aslında çekirdek içerisindeki do_fork isimli ortak bir fonksiyonu çağırmaktadır. Başka bir deyişle Linux çekirdeğinde aslında sys_fork sistem fonksiyonu sys_clone sistem fonksiyonunun özel bir biçimidir. Özetle Linux çekirdeği aslında thread'leri "aynı bellek alanını kullanan prosesler" gibi organize etmektedir. Zaten Linux'ta thread'ler için kullanılan "lightweight process" terimi bu tasarımdan dolayı uydurulmuştur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi bir thread prosesin herhangi bir thread'i tarafından sonlandırılmak istenebilir. Buna POSIX terminolojisinde "thread'in cancel edilmesi" denilmektedir. Bu işlem yukarıda da açıkladığımız üzere pthread_cancel POSIX fonksiyonuyla yapılmaktadır. #include int pthread_cancel(pthread_t thread); Tabii biz başka bir prosesin thread'ini pthread_cancel ile sonlandıramayız. Çünkü UNIX/Linux sistemlerinde thread'lerin id değerleri proses özgüdür. Yani o proses için anlamlıdır. Bir thread, pthread_cancel fonksiyonu ile sonlandırıldığında pthread_join fonksiyonundan thread'in exit kodu olarak PTHREAD_CANCELED özel değeri elde edilmektedir. Bu değer pek çok sistemde aşağıdaki gibi define edilmiştir: #define PTHREAD_CANCELED ((void *)-1) Aşağıdaki örnekte ana thread pthread_cancel fonksiyonu ile diğer thread'i sonlandırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #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); if (i == 5) if ((result = pthread_cancel(tid)) != 0) exit_sys_errno("pthread_cancel", result); } if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("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'in başka bir thread tarafından pthread_cancel POSIX fonksiyonu ile sonlandırılmasının bazı detayları vardır. Örneğin aslında default durumda sonlandırma ani bir biçimde yapılmamaktadır. Sonlandırma isteği thread'e gönderilmekte thread bazı POSIX fonksiyonlarının içerisine girerse sonlandırma o POSIX fonksiyonlarının içerisinde yapılmaktadır. Bu POSIX fonksiyonlarına "cancellation points" denilmektedir. Cancellation point belirten POSIX fonksiyonları standartlarda aşağıdaki bağlantıda listelenmiştir: https://pubs.opengroup.org/onlinepubs/000095399/functions/xsh_chap02_09.html#tag_02_09_05_02 Burada bizim gördüğümüz pek çok fonksiyonun zaten bir cancellation point belirttiğine dikkat ediniz. Örneğin open, read, write, close, sleep birer cancellation point belirtmektedir. Bir thread yukarıda belirtilen POSIX fonksiyonlarına girmezse pthread_cancel ile sonlandırma isteği thread'e gönderilse bile bu istek "askıda (pending durumda)" kalmaktadır. Dolayısıyla thread sonlandırılamayacaktır. Örneğin aşağıdaki gibi bir thread pthread_cancel ile sonlandırılamaz: void *thread_proc(void *param) { for (;;) { /* bir cancellation point yok */ } return NULL; } Aşağıdaki örnekte pthread_cancel kullanıldığı halde thread bir cancellation point içerisine girmediği için sonlanmayacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #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); if (i == 5) if ((result = pthread_cancel(tid)) != 0) exit_sys_errno("pthread_cancel", result); } printf("waiting for the thread to finish...\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { for (;;) { /* bir cancellation point yok */ } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi bir thread bu "cancellation" point fonksiyonlarına ihtiyaç duymadan bir döngü içerisinde işlemini yapıyorsa onu nasıl cancel edebiliriz? İşte bunun için yalnızca bu işlevi yerine getirecek pthread_testcancel isimli bir fonksiyon bulundurulmuştur: void pthread_testcancel(void); Fonksiyonun parametresi yoktur. Fonksiyon bir şey yapmamakta yalnızca cancellation point oluşturmaktadır. Aşağıda pthread_testcancel fonksiyonuna ilişkin bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #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); if (i == 5) if ((result = pthread_cancel(tid)) != 0) exit_sys_errno("pthread_cancel", result); } printf("waiting for the thread to finish...\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { for (;;) { pthread_testcancel(); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aslında bir thread'in pthread_cancel fonksiyonu karşısındaki davranışı değiştirilebilmektedir. Bunun için iki POSIX fonksiyonu kullanılmaktadır. pthread_setcanceltype isimli fonksiyon thread'in sonlandırma biçimini belirtir. Fonksiyonun prototipi şöyledir: #include int pthread_setcanceltype(int type, int *oldtype); Fonksiyonun birinci parametresi sonlandırma biçimini belirten aşağıdaki iki değerden biri olabilir: PTHREAD_CANCEL_DEFERRED PTHREAD_CANCEL_ASYNCHRONOUS Burada PTHREAD_CANCEL_DEFERRED yukarıda açıkladığımız "cancellation point fonksiyonlarına girildiğinde sonlandırma" anlamına gelmektedir. Thread yaratıldığında thread'in default sonlandırma biçimi böyledir. PTHREAD_CANCEL_ASYNCHRONOUS değeri ise thread'in o anda cancellation point olmadan doğrudan sonlandırılacağı anlamına gelmektedir. Fonksiyonun ikinci parametresi önceki sonlandırma biçiminin yerleştirileceği int nesnenin adresini almaktadır. Linux sistemlerinde bu parametre NULL adres geçilebilmektedir. Ancak POSIX standartlarında böyle bir durum belirtilmemiştir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Fonksiyonda bir thread id belirtilmediğine dikkat ediniz. Yani biz başka bir thread'in sonlanma biçimini değiştiremeyiz. Bu fonksiyonu hangi thread akışı çağırırsa o thread'in sonlanma biçimi değiştirilmektedir. Aşağıdaki örnekte thread'in sonlandırma biçimi PTHREAD_CANCEL_ASYNCHRONOUS biçiminde değiştirilmişir. Dolayısıyla artık thread'e pthread_cancel uygulandığında thread cancellation point'e girmeden o anda ani bir biçimde sonlandırılacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #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); if (i == 5) if ((result = pthread_cancel(tid)) != 0) exit_sys_errno("pthread_cancel", result); } printf("waiting for the thread to finish...\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { int oldtype; pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldtype); for (;;) { } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Thread'in pthread_cancel fonksiyonu bağlamındaki sonlandırma durumunun değiştirilmesi için ise pthread_setcancelstate fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_setcancelstate(int state, int *oldstate); Fonksiyonun birinci parametresi aşağıdaki iki değerden biri olabilir: PTHREAD_CANCEL_ENABLE PTHREAD_CANCEL_DISABLE PTHREAD_CANCEL_ENABLE değeri thread'in pthread_cancel fonksiyonu ile sonlandırılabileceğini belirtmektedir. PTHREAD_CANCEL_DISABLE değeri ise thread'in pthread_cancel fonksiyonu ile sonlandırılamayacağını belirtmektedir. Thread'ler yaratıldığında default olarak sonlandırılabilir yani PTHREAD_CANCEL_ENABLE durumdadır. Thread eğer PTHREAD_CANCEL_DISABLE duruma sokulursa bu durumda pthread_cancel uygulandığında thread sonlandırılmaz. Ancak istek "askıda (pending)" durumda kalır. Thread'in durumu eğer PTHREAD_CANCEL_ENABLE hale getirilirse askıda olan sonlandırma eylemi sonlandırma biçiminine göre işleme alınır. Fonksiyonun ikinci parametresi eski sonlandırma durumunun yerleştirileceği int nesnenin adresini almaktadır. Linux'ta bu değer NULL geçilebilirse de POSIX standartlarında böyle bir davranış tanımlanmamıştır. Aşağıdaki örnekte thread'in durumu PTHREAD_CANCEL_DISABLE yapılmıştır. Ana thread pthread_cancel uyguladığı halde thread sonlandırılmayacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #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); if (i == 5) if ((result = pthread_cancel(tid)) != 0) exit_sys_errno("pthread_cancel", result); } printf("waiting for the thread to finish...\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { int oldstat; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstat); 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); } /*-------------------------------------------------------------------------------------------------------------------------- Biz o anda hangi thread akışının ilerlemekte olduğunu anlamak isteyebiliriz. Bunun için pthread_self isimli POSIX fonksiyonu kullanılmaktadır. pthread_self fonksiyonu, bu fonksiyonu çağıran thread'in id değerini vermektedir. Fonksiyonun prototipi şöyledir: #include pthread_t pthread_self(void); Fonksiyon başarısız olamamaktadır. Programcı kendi thread'i ile ilgili birtakım işlemler yapmak istediğinde de bu fonksiyonu kullanabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 52. Ders 13/05/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar thread'lere özellik (attribute) bilgisi geçmedik. pthread_create fonksiyonunda bu özellik parametresine NULL adres verdik. Bu NULL adres thread'in default özelliklerle yaratılacağını belirtmekteydi. Şimdi thread özellikleri üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread özellikleri pthread_attr_t türü ile temsil edilmektedir. pthread_attr_t türü ve dosyalarında typedef edilmiştir. Tipik olarak bu pthread_attr_t türü bir yapı biçiminde typedef edilmektedir. Ancak POSIX standartları bu yapının elemanlarını açıklamamıştır. Yani bu yapının elemanları sistemden sisteme o sistemin özelliklerine göre değişebilmektedir. Thread'in özellik bilgileri bu yapının içerisine pthread_attr_setxxx fonksiyonlarıyla set edilmekte ve pthread_attr_getxxx fonksiyonlarıyla da get edilebilmektedir. (Bu durumu nesne yönelimli programlamadaki sınıflara benzetebilirsiniz. Nasıl sınıfın veri elemanları sınıfın "private" bölümünde tutulup onlara getter/setter fonksiyonlarla erişiliyorsa burada da aynı mantık izlenmiştir.) Bir thread yaratılırken ona özellik girmek için şu adımlardan geçilmelidir: 1) Önce pthread_attr_t türünden bir nesne yaratılır. 2) Yaratılan bu nesneye pthread_attr_init fonksiyonuyla ilk değer verilir. Fonksiyonun prototipi şöyledir: #include int pthread_attr_init(pthread_attr_t *attr); Fonksiyon pthread_attr_t türünden nesnenin adresini alarak nesneye ilk değer vermektedir. Fonksiyon başarı durumunda 0, başarısızlık durumunda errno değeri ile geri dönmektedir. Fonksiyon, Linux'ta her zaman başarılı olmaktadır. Ancak bazı sistemlerde bu pthread_attr_t yapısı içerisinde bazı göstericiler için alan tahsis ediliyor olabilir ve bu tahsisat başarısızlıkla sonuçlanabilir. Bu durumda pthread_attr_init fonksiyonu ENOMEM değeri ile geri döner. ENOMEM errno değeri malloc, calloc, realloc gibi fonksiyonlarda da kullanılan "not enough memory" durumunu anlatmaktadır. 3) pthread_attr_t nesnesine ilk değer verildikten sonra artık thread özellikleri pthread_attr_setxxx fonksiyonlarıyla set edilebilir, pthread_attr_getxxx fonksiyonlarıyla da get edilebilir. Bazı thread özellikleri bazı özel konulara ilişkindir. Dolayısıyla biz o özellikleri o konunun anlatıldığı yerde ele alacağız. Örneğin pthread_attr_setstacksize fonksiyonu ile biz özellik nesnesine yaratılacak thread'in stack uzunluğunu set edebiliriz. 4) Artık set işlemleri de yapıldıktan sonra pthread_create fonksiyonu ile thread bu pthread_attr_t türünden nesnenin adresi verilerek yaratılabilir. 5) Thread yaratıldıktan sonra pthread_attr_init fonksiyonuyla yapılan işlemler pthread_attr_destroy fonksiyonu ile geri alınabilir. Bu fonksiyon thread yaratıldıktan sonra hemen çağrılabilir. Yani pthread_create fonksiyonu bizim verdiğimiz pthread_attr_t nesnesinin adresini saklayıp kullanmak yerine onun içerisindekileri ilgili yere kopyalamaktadır. pthread_attr_destroy fonksiyonunun prototipi şöyledir: #include int pthread_attr_destroy(pthread_attr_t *attr); Her ne kadar fonksiyonun geri dönüş değeri benzer biçimde başarı-başarısızlık belirtiyorsa da fonksiyonun başarısız olması için bir neden yoktur. Linux sistemlerinde fonksiyon her zaman başarılı olmaktadır. O halde thread özelliklerini oluşturup thread'in bu özelliklerle yaratılması aşağıdaki gibi yapılabilmektedir: pthread_attr_t tattr; ... if ((result = pthread_attr_init(&tattr)) != 0) exit_sys_errno("pthread_attr_init", result); if ((result = pthread_attr_setxxx(&tattr, ...)) != 0) exit_sys_errno("pthread_attr_setxxx", result); if ((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_attr_destroy(&tattr)) != 0) exit_sys_errno("pthread_attr_destroy", result); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir thread'in kullanacağı stack büyüklüğü thread yaratılırken thread'in özellik bilgileriyle set edilebilmektedir. Bunun için pthread_attr_setstacksize fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); Fonksiyonun birinci parametresi thread özelliklerine ilişkin pthread_attr_t türünden nesnenin adresini almaktadır. İkinci parametresi ise stack uzunluğunu belirtmektedir. Eğer bu set işlemi yapılmazsa Linux sistemlerinde glibc kütüphanesi default stack uzunluğunu x86 32 bit sistemlerinde 2 MB, x86 64 bit sistemlerde 8 MB almaktadır. pthread_attr_init fonksiyonundan sonra biz bu özellik nesnesinden default stack uzunluğunu pthread_attr_getstacksize fonksiyonu ile elde edebiliriz: #include int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize); Fonksiyonun birinci parametresi özellik nesnesini, ikinci parametresi stack uzunluğunun yerleştirileceği size_t türünden nesnenin adresini belirtmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Aşağıdaki programda önce default stack uzunluğu yazdırılmış, sonra yeni yaratılan thread'in 64K stack kullanması sağlanmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #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; pthread_attr_t tattr; size_t size; if ((result = pthread_attr_init(&tattr)) != 0) exit_sys_errno("pthread_attr_init", result); if ((result = pthread_attr_getstacksize(&tattr, &size)) != 0) exit_sys_errno("pthread_attr_getstacksize", result); printf("Default stack size: %zd\n", size); if ((result = pthread_attr_setstacksize(&tattr, 65536)) != 0) exit_sys_errno("pthread_attr_getstacksize", result); if ((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_attr_destroy(&tattr)) != 0) exit_sys_errno("pthread_attr_destroy", result); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("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); } /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi bir thread sonlandığında onun exit kodu pthread_join fonksiyonu ile elde edilip thread zombie olmaktan kurtarılıyordu. pthread_join fonksiyonu thread sonlanana kadar akışı bloke ediyordu. İşte biz bir thread'i "detached" moda sokarsak artık thread sonlandığında otomatik olarak thread nesnesinin tuttuğu kaynaklar boşaltılır. Dolayısıyla zombie thread durumu ortadan kaldırılmış olur. Eğer biz çok sayıda thread yaratıp bunları pthread_join ile beklemek istemiyorsak thread'leri "detached" moda sokmalıyız. Thread'ler yaratıldığında default olarak detached durumda değildir. Bu default duruma "joinable" durum da denilmektedir. Detached durumda bir thread pthread_join fonksiyonu ile beklenemez. POSIX standartları bu durumu "tanımsız davranış (undefined behavior)" olarak nitelendirmiştir. Linux sistemlerinde detached bir thread pthread_join ile beklenmeye çalışılırsa pthread_join fonksiyonu EINVAL errno değeri ile başarısız olmaktadır. Bir thread'i detached moda sokmanın iki yolu vardır. Birincisi thread yaratılırken thread özellikleri ile bu sağlanabilir. Bunun için int pthread_attr_setdetachstate fonksiyonu kullanılmaktadır: #include int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); Fonksiyonun birinci parametresi özellik nesnesini almaktadır. İkinci parametre şunlardan biri olabilir: PTHREAD_CREATE_DETACHED PTHREAD_CREATE_JOINABLE pthread_attr_init fonksiyonu default olarak zaten bu detached durumunu PTHREAD_CREATE_JOINABLE olarak set etmektedir. Thread özellik parametresi NULL geçilerek yaratıldığında da default olarak "joinable" durumda olur. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Aşağıdaki örnekte thread özellik bilgisi ile detached moda sokulmuştur. ---------------------------------------------------------------------------------------------------------------------------*/ #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; pthread_attr_t tattr; if ((result = pthread_attr_init(&tattr)) != 0) exit_sys_errno("pthread_attr_init", result); if ((result = pthread_attr_setdetachstate(&tattr, PTHREAD_CREATE_DETACHED)) != 0) exit_sys_errno("pthread_attr_getstacksize", result); if ((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_attr_destroy(&tattr)) != 0) exit_sys_errno("pthread_attr_destroy", result); printf("press ENTER to exit...\n"); getchar(); 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); } /*-------------------------------------------------------------------------------------------------------------------------- Thread'i "detached" moda sokmanın diğer bir yolu thread yaratıldıktan sonra pthread_detach fonksiyonunu çağırmaktır. Fonksiyonun prototipi şöyledir: #include int pthread_detach(pthread_t thread); Fonksiyon detached duruma sokulacak thread'in id değerini parametre olarak almaktadır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Zaten detached durumda olan bir thread için bu çağrının yapılması POSIX standartlarına göre "tanımsız davranışa" yol açmaktadır. Linux sistemlerinde "joinable" olmayan thread'lerde bu fonksiyon başarısız olmakta ve EINVAL errno değeri ile geri dönmektedir. Thread fonksiyon çağrıldığında zaten sonlanmışsa thread'in kaynakları yine otomatik olarak yok edilmektedir. Aşağıdaki örnekte thread pthread_detach donksiyonu ile detached duruma sokulmuştur. ---------------------------------------------------------------------------------------------------------------------------*/ #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; pthread_attr_t tattr; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_detach(tid)) != 0) exit_sys_errno("pthread_detach", result); printf("press ENTER to exit...\n"); getchar(); 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); } /*-------------------------------------------------------------------------------------------------------------------------- Thread'ler konusunun önemli bir bölümünü "thread senkronizasyonu" oluşturmaktadır. Thread'lerin birlikte belli bir amacı ortak bir biçimde gerçekleştirirken uyum içinde çalışması gerekebilmektedir. Özellikle ortak kaynaklara erişen thread'lerin bu kaynakları bozmadan işleme sokması kritik önemdedir. Thread senkronizasyonunun önemini anlatmak için basit bir örnek verebiliriz. İki thread aynı global değişkeni aşağıdaki gibi artırıyor olsun: int g_count = 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; } Bu thread'lerin çalışması bittikten sonra bu global değişkenin 2000000 değerinde olması beklenir. Ancak gerçekte bu durum sağlanamayacaktır. Çünkü artırım tek bir C deyimi ile yapılmış olsa bile bunun için birkaç makine komutu gerekebilmektedir. Örneğin: MOV reg, g_count INC reg MOV g_count, reg Burada thread'lerden birinin aşağıdaki noktada quanta süresini bitirdiğini düşünelim. Bu durumda işletim sistemi tam o noktada thread'in çalışmasına ara verip CPU'ya başka bir thread'i atayacaktır: MOV reg, g_count ---> Bu noktada preemptive olarak çalışmaya ara verilmiş olsun INC reg MOV g_count, reg Burada görüldüğü gibi global değişkenin değeri CPU içerisinde bir yazmaca çekilmiştir. Ancak tam o sırada thread'ler arası geçiş (context switch) oluşmuştur. Bu sırada diğer bir thread CPU'ya atanıp çalıştırılmış olabilir. Böylece diğer thread global değişkeni artırmış olacaktır. Sonra yeniden çalışma bu thread'e döndüğünde, thread kalınan yerden çalışmaya devam edecekir: INC reg MOV g_count, reg Görüldüğü gibi çalışma devam ettiğinde global değişkenin o anki değeri kaybedilmiş olmaktadır. Thread'ler arası geçiş oluştuğunda işletim sistemi o andaki thread'e ilişkin CPU yazmaçlarının değerlerini de saklayıp geri yüklemektedir. Buradaki problem ise bir işlemin arada kesilmesi ile ilgilidir. Bu tür işlemlerde volatile anahtar sözcüğü bize bir fayda sağlamaz. Örneğin: volatile int g_count; volatile anahtar sözcüğü işlemin atomik yapılmasını garanti etmemektedir. volatile anahtar sözcüğü yalnızca "ilgili nesne kullanıldığında o nesnenin güncel değeri için belleğe başvurunun yapılacağı" anlamına gelmektedir. Yani burada nesne volatile yapılsa bile üretilen makine kodlarında bir farklılık oluşmayacaktır. Pekiyi yukarıdaki problemin kaynağı nedir? Problemin asıl kaynağı "thread'lerin ansızın belli bir makine komutunda preemptive biçimde quanta süresini doldurduklarında kesilebilmesi yani çalışmasına ara verilebilmesidir". Pekiyi bu problem nasıl çözülebilir? İşte bu problem iki biçimde çözülebilir: 1) Söz konusu işlemlerin atomik bir biçimde yapılmasıyla: İşlemlerin atomik yapılması demek o işlemler yapılırken hiç kesilme olmaması yani tek hamlede yapılması demektir. Genellikle böyle bir durumu sağlamak mümkün olmaz. Tabii yukarıdaki örnekte artırma işlemi tek bir makine komutu ile yapılıyor olsaydı işlem atomik olurdu. Dolayısıyla yukarıdaki örnekte bir sorun kalmazdı. Thread'ler arası geçiş makine komutları çalıştırılırken gerçekleşmemektedir. Bir makine komutunun çalışması bittikten sonra ancak diğeri henüz çalıştırılmadan gerçekleşebilmektedir. Yani makine komutları atomiktir. 2) Söz konusu işlemler sırasında kesilme (thread'ler arası geçiş) olsa da başka bir akışın bu işlemler bitene kadar bekletilmesiyle: Bu yöntem genellikle kullanılan yöntemdir. Bu yöntemde işlemler sırasında ters bir noktada kesilme (thread'ler arası geçiş) olabilir. Ancak geçilen eğer aynı ortak kaynağa erişiyorsa "thread, kesilen thread bu işlemi bitirene kadar" bekletilir. Yukarıdaki gibi durumlar threadler'le çalışmada çok sık karşımıza çıkmaktadır. Örneğin bir thread bir makineyi önce reset edip bir konuma soksun: - makine reset ediliyor - reset edilen makine belli bir konuma sokuluyor Bu konuma sokma işlemi mutlaka reset'ten sonra yapılmak zorunda olsun. Şimdi makineye birden fazla thread'ten eriştiğimizi düşünelim. Thread akış'larından biri aşağıdaki noktada kesilmiş olsun: - makine reset ediliyor ---> thread akışı bu noktada kesilmiş olsun - reset edilen makine belli bir konuma sokuluyor Bu thread kesilmişken başka bir thread bu makine üzerinde yukarıdaki iki işlemi kesilmeden yapmış olabilir. Bu durumda bu thread çalışmasına devam ettiğinde sanki makineyi reset sonrasında konumlandırıyor gibi bir durum oluşacaktır. Halbuki makinenin durumu değişmiştir. Thread senkronizasyonu genellikle "ortak kaynaklara" erişimde gerekmektedir. İki thread birbirinden bağımsız işlemler yapıyorsa yani bunlar hiçbir ortak kaynağa erişmiyorlarsa bunların senkronize edilmesi gerekliliği yoktur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Başından sonuna kadar tek bir akış tarafından çalıştırılması gereken kodlara "kritik kodlar (critical sections)" denilmektedir. Kritik kod bloklarına bir thread girdiğinde orada thread akışı kesilebilir. Yani thread'ler arası geçiş oluşabilir. Ancak başka bir thread önceki thread o bloktan çıkana kadar o bloğa girmemelidir. Örneğin: MOV reg, g_count INC reg MOV g_count, reg Burası bir kritik kod bloğudur. Çünkü bu kod bloğu başından sonuna kadar tek bir thread tarafından çalıştırılmalıdır. Bir thread bu kritik kod bloğunda kesilse bile önceki thread bu bloktan çıkana kadar, başka bir thread bu bloğa girmemelidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kritik kodlar manuel biçimde oluşturulamazlar. Örneğin aşağıdaki gibi bir çaba sonuçsuz kalacaktır: int g_flag = 0; ... while (g_flag == 1) ; g_flag = 1; ... ... KRİTİK KOD BLOĞU ... g_flag = 0; Burada programcı g_flag 1 olduğu SÜRECE "başka bir thread kritik kodda olduğu için" beklemiştir. g_flag 0'a çekildiğinde akış döngüden çıkmakta ve g_flag yeniden 1 yapılmaktadır. Bu kodun iki önemli problemi vardır. Birincisi kesilme döngüden çıkıldığında ancak henüz g_flag değişkenine 1 atanmadan gerçekleşmiş olabilir: while (g_flag == 1) ; ---> DİKKAT BU NOKTADA KESİLME OLABİLİR g_flag = 1; ... ... KRİTİK KOD BLOĞU ... g_flag = 0; Bu kodun ikinci problemi beklemenin "meşgul bir döngüde (busy loop)" yapılmasıdır. Yani burada bekleme bir döngü içerisinde CPU zamanı harcanarak yapılmaktadır. Halbuki beklemenin CPU zamanı harcanmadan thread bloke edilerek yapılması arzu edilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi kritik kod blokları nasıl oluşturulmaktadır? İşte kritik kodlar "işletim sisteminin de işe karıştığı" birtakım "senkronizasyon nesneleriyle" oluşturulmaktadır. Bu senkronizasyon nesneleri genel olarak kernel mode'a geçiş yapabilmektedir. Kernel mode'da atomik işlemler yapmak işlemcilerin sağladığı bazı özel makine komutlarıyla mümkün olabilmektedir. Artık pek çok modern masasütü ve mobil işlemci "user mode'da" bazı senkronizasyon problemleri için "atomik bir biçimde çalışan" özel komutlar barındırmaktadır. Bazı senkronizasyon nesneleri daha az kernel mode'a geçerek bu makine komutlarının yardımıyla daha etkin kilitleme yapabilmektedir. Linux sistemlerinde senkronizasyon nesneleri ismine "futex" denilen bir sistem fonksiyonu yoluyla gerçekleştirilmiştir. Yani Linux senkronizasyon amacıyla kullanılacak bir futex nesnesini bir sistem fonksiyonu olarak bulundurmuştur. Burada ele alacağımız senkronizasyon nesneleri Linux'ta bu "futex" denilen sistem fonksiyonu kullanılarak gerçekleştirilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kritik kod oluşturmak için yaygın kullanılan senkronizasyon nesnelerinden biri "mutex" denilen nesnedir. Mutex sözcüğü "mutual exclusion" sözcüklerinden kısaltma yapılarak uydurulmuştur. Mutex nesneleri pek çok farklı işletim sisteminde benzer biçimlerde bulunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 53. Ders 20/05/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde mutex nesneleri tipik olarak şu aşamalardan geçilerek kullanılmaktadır: 1) Mutex nesneleri pthread_mutex_t türü ile temsil edilmiştir. Bu tür ismi ve dosyaları içerisinde typedef edilmiş durumdadır. POSIX standartlarına göre pthread_mutex_t türü herhangi bir tür olarak typedef edilmiş olabilir. Örneğin Linux'ta bu tür bir yapı (birliğin içerisinde bir yapı) biçiminde typedef edilmiştir. Programcı pthread_mutex_t türünden global bir nesne tanımlamalıdır. Örneğin: pthread_mutex_t g_mutex; Bu nesneye ilk değer vermenin iki yolu vardır. İlk değer doğrudan PTHREAD_MUTEX_INITIALIZER isimli makro kullanılarak verilebilir. Bu makro tipik olarak küme parantezleri içerisinde yapıya ilk değer verme biçiminde açılmaktadır. Örneğin: pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; Mutex nesnesine ilk değer vermenin ikinci yolu pthread_mutex_init fonksiyonunu kullanmaktır. Fonksiyonun prototipi şöyledir: #include int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); Fonksiyonun birinci parametresi ilk değer verilecek mutex nesnesinin adresini almaktadır. İkinci parametresi mutex nesnesinin bazı özelliklerini belirten "özellik nesnesinin" adresini almaktadır. Mutex özellikleri daha sonra ele alınacaktır. Bu ikinci parametreye NULL adres geçilirse mutex default özelliklerle yaratılır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: if ((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); Mutex nesnesi PTHREAD_MUTEX_INITIALIZER makrosuyla yaratıldığında mutex nesnesinin özellik bilgisi de default değerlerle oluşturulmaktadır. Yani aşağıdaki iki ilk değer verme biçimi eşdeğerdir: pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_init(&g_mutex, NULL); Tabii bir mutex nesnesine yalnızca bir kez ilk değer vermeliyiz. POSIX standartlarına göre nesneye birden fazla kez ilk değer vermek tanımsız davranışa yol açmaktadır. Programcı mutex nesnesinin kullanımı bittikten sonra pthread_mutex_destroy fonksiyonu ile onu boşaltmalıdır. Aslında bazı sistemlerde bu pthread_mutex_destroy fonksiyonu boşaltım için bir şey yapmamaktadır. Yani bazı sistemlerde bu içi boş bir fonksiyondur. Ancak POSIX standartlarına göre mutex nesnesi yaratılırken birtakım tahsisatlar yapılmış olabilir ve bu tahsisatların geri alınması pthread_mutex_destroy fonksiyonuyla sağlanmaktadır. Yine bazı sistemlerde mutex nesnesine PTHREAD_MUTEX_INITIALIZER makrosuyla ilk değer verilmişse onun boşaltımının yapılması gerekmeyebilmektedir. Ancak POSIX standartlarına göre nesneye makro ile ilk değer verilmiş olsa bile nesnenin boşaltımı yine yapılmalıdır. #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. Linux'ta bu fonksiyon bir şey yapmamaktadır. Ancak POSIX standartlarına göre bu fonksiyonun çağrılması gerekir. Çünkü yukarıda da belirttiğimiz gibi pthread_mutex_init fonksiyonu içerisinde dinamik tahsisatlar yapılmış olabilir ve bu tahsisatların geri alınması gerekebilir. Tabii aslında bu fonksiyonun başarısız olması da genel olarak mümkün değildir. Örneğin: if ((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destory", result); Burada bir noktayı yeniden vurgulamak istiyoruz: Eğer mutex nesnesine PTHREAD_MUTEX_INITIALIZER makrosuyla ilk değer verilmişse mutex nesnesinin destroy edilmesi pek çok sistemde gerekmemektedir. Ancak POSIX standartlarında bu durum belirtilmemiştir, bu da mutex nesnesi makroyla initialize edilse bile destroy işlemini yapılması gerektiği anlamına gelmektedir. Ancak pek çok programcı bu tür durumlarda destroy işlemini yapmamaktadır. Pekiyi mademki PTHREAD_MUTEX_INITIALIZER makrosu daha pratik bir kullanım sunmaktadır o halde neden pthread_mutex_init fonksiyonuna gereksinim duyulmuştur? İşte bazen mutex nesneleri dinamik bir biçimde de tahsis edilebilmektedir. Örneğin bir yapı nesnesini senkronize etmek için bir mutex kullanılacaksa, mutex o yapının içerisinde yapı elemanı olarak bildirilebilir. Bu durumda ona statik biçimde makroyla ilk değer vermek mümkün olmaz. Çünkü C'de bir yapı nesnesine küme parantezleri ile atama yapamayız. (C99 ile birlikte ismine "compound literals" denilen bir sentaks ile bu durum mümkün hale getirilmiştir.) Örneğin (kontroller ihmal edilmiştir): struct INFO { int a; int b; int c; pthread_mutex_t mutex; }; ... struct INFO *info; info = (struct INFO *)malloc(sizeof(struct INFO)); pthread_mutex_init(&info->mutex, NULL); 2) Mutex nesneleriyle kritik kod aşağıdaki gibi oluşturulmaktadır: if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ... ... ... if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); Görüldüğü gibi kritik kodun başında pthread_mutex_lock fonksiyonu sonunda da pthread_mutex_unlock fonksiyonu çağrılmıştır. Mutex nesneleri, işletim sistemlerinde "thread temelinde sahipliği (ownership) alınan" nesnelerdir. Bir thread, pthread_mutex_lock fonksiyonuna girdiğinde mutex nesnesinin sahipliğinin başka bir thread tarafından alınıp alınmadığı fonksiyonda kontrol edilmektedir. Eğer mutex nesnesinin sahipliği başka bir thread tarafından alınmışsa yani mutex nesnesi zaten "kilitlenmişse (locked edilmişse)" bu durumda pthread_mutex_lock fonksiyonu thread'i bloke eder, ta ki mutex'in sahipliğini alan thread pthread_mutex_unlock ile mutex'in sahipliğini bırakana kadar. Eğer mutex'in sahipliği herhangi bir thread tarafından alınmamışsa yani mutex kilitli değilse, bu durumda pthread_mutex_lock fonksiyonu mutex'in sahipliğini alır (yani onu kilitler) ve akış kritik koda girer. Mutex kilitliyken diğer thread akışları pthread_mutex_lock fonksiyonunda bloke edilerek bekletilecektir. Mutex nesnesinin kilidi onu kilitleyen thread tarafından açıldığında pthread_mutex_lock fonksiyonunda bloke olmuş olan thread'lerden biri mutex'in sahipliğini alarak (yani onu kilitleyerek) kritik koda girecektir. Böylece kritik kod içerisinde başından sonuna kadar tek bir thread akışı bulunuyor olacaktır. Ancak pthread_mutex_lock fonksiyonunda birden fazla thread'in bloke olması durumunda mutex'in kilidi açıldığı zaman hangi thread'in mutex'in sahipliğini alacağı konusunda işletim sistemleri bir garanti vermemektedir. Yani burada bir FIFO sisteminin uygulanmasının bir garantisi yoktur. Ancak işletim sistemleri çeşitli koşullar sağlanıyorsa mümkün olduğunca adil bir durum oluşturmaya çalışırlar. pthread_mutex_lock ve pthread_mutex_unlock fonksiyonlarının prototipleri şöyledir: #include int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); Fonksiyonlar parametre olarak mutex nesnesinin adresini alırlar. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönerler. Tabii kritik kodun tek bir yerde olması gerekmemektedir. Önemli olan aynı mutex nesnesinin kullanılıyor olmasıdır. Örneğin thread'lerden biri aşağıdaki gibi bir kritik kodla bir fonksiyonda karşılaşmış olabilir: if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ... ... ... if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); Başka bir thread de başka bir fonksiyonda aşağıdaki gibi bir kritik kodla karşılaşmış olabilir: if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ... ... ... if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); Burada kullanılan mutex nesneleri aynı nesnedir. Dolayısıyla iki thread de aynı mutex nesnesini kilitlemeye çalışmaktadır. O halde thread'lerden biri o fonksiyonda kritik koda girerse, diğer bir thread başka bir fonksiyonda diğer kritik koda giremez. Normal olarak programcının her paylaşılan kaynak için ayrı bir mutex nesnesini bulundurması gerekir. Örneğin global bir bağlı liste söz konusu olsun. Bu bağlı liste üzerinde eleman ekleme, eleman silme ve arama işlemlerini yapan üç fonksiyon bulunuyor olsun: add_item(...) { ... } remove_item(...) { ... } search_item(...) { ... } Burada bir thread eleman eklerken diğer thread'lerin bu iş bitene kadar eleman silmeye çalışmaması ya da bağlı listede arama yapmaması gerekir. Ya da bir eleman silerken diğer thread'lerin diğer işleri yapmaması gerekir. O halde bu üç fonksiyonda aynı mutex kullanılarak kritik kod oluşturulmalıdır. Örneğin (sembolik kodlar kullanıyoruz): pthread_mutex_t g_llmutex = PTHREAD_MUTEX_INITIALIZER; add_item(...) { pthread_mutex_lock(&g_llmutex); pthread_mutex_unlock(&g_llmutex); } remove_item(...) { pthread_mutex_lock(&g_llmutex); pthread_mutex_unlock(&g_llmutex); } search_item(...) { pthread_mutex_lock(&g_llmutex); pthread_mutex_unlock(&g_llmutex); } Mutex nesnelerinin thread temelinde sahipliği söz konusudur. Yani mutex nesnesinin sahipliği pthread_mutex_lock fonksiyonu ile alındığında ancak alan thread nesnenin sahipliğini pthread_mutex_unlock fonksiyonuyla bırakabilir. Özetle biz başka bir thread'in kilitlediği mutex nesnesinin kilidini açamayız. POSIX standartlarına göre sahipliği alınmamış olan bir mutex nesnesini unlock etmeye çalışmak mutex özelliklerine bağlı olarak "tanımsız davranışa" yol açabileceği gibi pthread_mutex_unlock fonksiyonunun başarısız olmasına da yol açabilmektedir. Normal mutex'ler (yani default özellikle yaratılmış mutex'ler) için POSIX standartları sahipliğini almayan thread'in mutex nesnesinin kilidini açmaya çalışmasının "tanımsız davranışa" yol açacağını belirtmektedir. 3) Mutex nesneleri için bazı yardımcı fonksiyonlar da bulunmaktadır. Örneğin; biz bir mutex nesnesi kilitliyken bloke olmayı istemeyip "madem nesne kilitli o zaman ben de başka şeyler yapayım" diyebiliriz. Bu işlem pthread_mutex_trylock fonksiyonuyla sağlanabilmektedir. Fonksiyonun prototipi şöyledir: #include int pthread_mutex_trylock(pthread_mutex_t *mutex); Fonksiyon parametre olarak mutex nesnesinin adresini almaktadır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Eğer mutex nesnesi zaten başka bir thread tarafından kilitlenmişse fonksiyon bloke olmaz ve EBUSY errno değeri ile geri döner. Örneğin: result = pthread_mutex_trylock(&g_mutex); if (result != 0) if (result == EBUSY) { } else exit_sys_errno("pthread_mutex_trylock", result); Bu örnekte pthread_mutex_trylock fonksiyonu başarısız olduğunda errno değeri EBUSY ise başka birtakım işlemler yapılmaktadır. pthread_mutex_timedlock isimli fonksiyon, pthread_mutex_lock fonksiyonunun "zaman aşımlı (timeout)" biçimidir. Fonksiyonun prototipi şöyledir: #include int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abstime); Fonksiyonun birinci parametresi mutex nesnesinin adresini, ikinci parametresi de mutlak zamana ilişkin timespec yapı nesnesinin adresini almaktadır. Burada zaman aşımı mutlak zamana ilişkindir. Yani biz örneğin 5 saniyelik bir zaman aşımı vereceksek önce 01/01/1970'ten geçen saniye sayısını elde edip onun üzerine 5 saniye katarak bu fonksiyona vermeliyiz. Örneğin: struct timespec ts; ... if (clock_gettime(CLOCK_REALTIME, &ts) == -1) exit_sys("clock_gettime"); ts.tv_sec += 5; result = pthread_mutex_timedlock(&g_mutex, &ts); if (result != 0) { if (result == ETIMEDOUT) { } else exit_sys_errno("pthread_timedlock", result); } ... Bazı algoritmalarda "kilitlenme (deadlock)" problemlerinin çözümü zor olabilmektedir. Bu tür durumlarda son çare olarak bu fonksiyon kullanılabilmektedir. Bazen makul bir süre aşıldığında arka plan belirli işlemlerin yapılması da gerekebilmektedir. Bu tür seyrek durumlarda da pthread_mutex_timedlock fonksiyonu kullanılabilmektedir. Ancak bu fonksiyona toplamda çok seyrek gereksinim duyulmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte iki thread kritik kod içerisinde aynı global değişkeni 1000000 kez artırmıştır. Bu durumda global değişken olması gerektiği değerde (2000000) olacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #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 = 0; 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); printf("%d\n", g_count); if ((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); return 0; } void *thread_proc1(void *param) { int result; for (int i = 0; i < 1000000; ++i) { if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++g_count; if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", 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_unlock", result); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte iki thread de bir makineyi 1'den 5'e kadar konuma sokmaktadır. Ancak bu işlemlerin iç içe geçmesi istenmemektedir. Örnekte rastgele beklemeler de uygulanmıştır. Burada thread'lerin sırasıyla kritik koda girmesinin zorunlu olmadığına dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void do_something(const char *name); void exit_sys_errno(const char *msg, int eno); pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; int main(void) { pthread_t tid1, tid2; int result; if ((result = pthread_create(&tid1, NULL, thread_proc1, "thread1")) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, "thread2")) != 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 *thread_proc1(void *param) { const char *name = (const char *)param; for (int i = 0; i < 10; ++i) do_something(name); return NULL; } void *thread_proc2(void *param) { const char *name = (const char *)param; for (int i = 0; i < 10; ++i) do_something(name); return NULL; } void do_something(const char *name) { int result; if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); 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); printf("-----------------------\n"); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 54. Ders 21/05/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir veri yapısı thread'ler arasında ortak bir biçimde kullanılıyorsa ve bu thread'ler veri yapısı üzerinde değişikler yapabiliyorsa veri yapısının bozulmaması için senkronize edilmesi gerekir. Her veri yapısı için tipik olarak o veri yapısını senkronize etmekte kullanılan bir mutex nesnesinin veri yapısı içerisinde bulundurulması yoluna gidilmektedir. Örneğin thread'ler arasında ortak kullanılabilecek bir "bağlı liste (linked list)" oluşturmak isteyelim. Bu bağlı listeye iki thread de eleman eklemeye çalışırsa bağlı liste bozulabilecektir. Bu nedenle bağlı listeyi senkronize etmek için bir mutex bulundurulmalı ve bu mutex nesnesi ile senkronizasyon işlemi yapılmalıdır. Aşağıda böyle bir örnek verilmiştir. Bu örnekte create_llist fonksiyonu "tek bağlı (single linked)" bağlı liste yaratmaktadır. Fonksiyon bağlı liste bilgilerinin bulunduğu yapı nesnesinin adresine geri dönmektedir: typedef struct tagNODE { int val; struct tagNODE *next; } NODE; typedef struct tagLLIST { NODE *head; NODE *tail; size_t count; pthread_mutex_t mutex; } LLIST; LLIST *create_llist(void); add_item_tail ve add_item_head fonksiyonları bağlı listenin sonuna ve başına mutex kontrolü ile eleman eklemektedir. NODE *add_item_tail(LLIST *llist, int val); NODE *add_item_head(LLIST *llist, int val); Fonksiyonlar başarı durumunda eklenen düğümün adresine, başarısızlık durumunda NULL adrese geri dönmektedir. Fonksiyonlar başarısızlık durumunda errno değerini de set etmektedir. Bağlı listeyi dolaşan bir walk__llist fonksiyonu da bulunmaktadır: int walk_llist(LLIST *llist); Fonksiyon başarı durumunda 0, başarısızlık durumunda -1 değerine geri dönmekte ve errno uygun biçimde set edilmektedir. Nihayet bağlı liste destroy_llist fonksiyonu ile silinmektedir: void destroy_llist(LLIST *llist); ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include typedef struct tagNODE { int val; struct tagNODE *next; } NODE; typedef struct tagLLIST { NODE *head; NODE *tail; size_t count; pthread_mutex_t mutex; } LLIST; LLIST *create_llist(void); void destroy_llist(LLIST *llist); NODE *add_item_tail(LLIST *llist, int val); NODE *add_item_head(LLIST *llist, int val); int walk_llist(LLIST *llist); void *thread_proc1(void *param); void *thread_proc2(void *param); void *thread_proc3(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); static inline size_t get_count(LLIST *llist) { return llist->count; } LLIST *create_llist(void) { LLIST *llist; int result; if ((llist = (LLIST *)malloc(sizeof(LLIST))) == NULL) return NULL; llist->head = NULL; llist->tail = NULL; llist->count = 0; if ((result = pthread_mutex_init(&llist->mutex, NULL)) != 0) { errno = result; free(llist); return NULL; } return llist; } NODE *add_item_tail(LLIST *llist, int val) { NODE *new_node; int result; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if ((result = pthread_mutex_lock(&llist->mutex)) != 0) { free(new_node); goto FAILED; } if (llist->head == NULL) llist->head = new_node; else llist->tail->next = new_node; llist->tail = new_node; ++llist->count; if ((result = pthread_mutex_unlock(&llist->mutex)) != 0) goto FAILED; return new_node; FAILED: errno = result; return NULL; } NODE *add_item_head(LLIST *llist, int val) { NODE *new_node; int result; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if ((result = pthread_mutex_lock(&llist->mutex)) != 0) { free(new_node); goto FAILED; } if (llist->head == NULL) llist->tail = new_node; new_node->next = llist->head; llist->head = new_node; ++llist->count; if ((result = pthread_mutex_unlock(&llist->mutex)) != 0) goto FAILED; return new_node; FAILED: errno = result; return NULL; } int walk_llist(LLIST *llist) { NODE *node; int result; if ((result = pthread_mutex_lock(&llist->mutex)) != 0) { errno = result; return -1; } node = llist->head; while (node != NULL) { printf("%d ", node->val); fflush(stdout); node = node->next; } if ((result = pthread_mutex_unlock(&llist->mutex)) != 0) { errno = result; return -1; } printf("\n"); return 0; } void destroy_llist(LLIST *llist) { NODE *node, *temp_node; node = llist->head; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } free(llist); } int main(void) { pthread_t tid1, tid2, tid3; int result; LLIST *llist; if ((llist = create_llist()) == NULL) exit_sys("create_list"); if ((result = pthread_create(&tid1, NULL, thread_proc1, llist)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, llist)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid3, NULL, thread_proc3, llist)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); walk_llist(llist); printf("%zd\n", get_count(llist)); destroy_llist(llist); return 0; } void *thread_proc1(void *param) { LLIST *llist = (LLIST *)param; for (int i = 0; i < 1000000; ++i) if (add_item_tail(llist, i) == NULL) exit_sys("add_item_tail"); return NULL; } void *thread_proc2(void *param) { LLIST *llist = (LLIST *)param; for (int i = 0; i < 1000000; ++i) if (add_item_tail(llist, i) == NULL) exit_sys("add_item_tail"); return NULL; } void *thread_proc3(void *param) { LLIST *llist = (LLIST *)param; for (int i = 0; i < 1000000; ++i) if (add_item_head(llist, i) == NULL) exit_sys("add_item_head"); 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); } /*-------------------------------------------------------------------------------------------------------------------------- Mutex nesnesine pthread_mutex_init fonksiyonu ile ilk değer verilirken mutex özellikleri de bu fonksiyonun ikinci parametresinde belirtilebilir. Eğer mutex nesnesine PTHREAD_MUTEX_INITIALIZER makrosuyla ilk değer verilmişse bu durumda mutex default özelliklerle yaratılmaktadır. Daha önceden de belirttiğimiz gibi aşağıdaki iki ilk değer verme tamamen eşdeğerdir: pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_init(&g_mutex, NULL); Mutex özelliklerinin set edilmesi tamamen thread özelliklerinin set edilmesinde olduğu gibi yapılmaktadır. Yani işlemler şu adımlardan geçilerek gerçekleştirilir: 1) Önce pthread_mutexattr_t türünden bir özellik nesnesi tanımlanır. 2) Bu özellik nesnesine pthread_mutexattr_init fonksiyonu ile ilk değerleri verilir. Bu nesneye ilk değer vermek için bir makro yoktur. Fonksiyonun prototipi şöyledir: #include int pthread_mutexattr_init(pthread_mutexattr_t *attr); Fonksiyon parametre olarak mutex özellik nesnesinin adresini almaktadır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri döner. 3) Bundan sonra artık pthread_mutexattr_setxxx fonksiyonlarıyla mutex özellikleri özellik nesnesinin içerisine yerleştirilir. 4) Artık özellik nesnesi oluşturulmuştur. Mutex nesnesi, bu özellik nesnesi verilerek pthread_mutex_init fonksiyonu ile oluşturulabilir. 5) Mutex nesnesi yaratıldıktan sonra artık özellik nesnesine gerek kalmamaktadır. Özellik nesnesi pthread_mutexattr_destroy fonksiyonu ile yok edilebilir. Fonksiyonun prototipi şöyledir: #include int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); Fonksiyon yine parametre olarak mutex özellik nesnesini alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri döner. Özetle işlemler aşağıdaki kod parçasında belirtilen sırada yapılmaktadır: pthread_mutexattr_t mattr; ... if ((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_init", result); /* pthread_mutexattr_setxxx fonksiyonlarıyla set işlemi */ 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); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir thread, bir mutex nesnesinin sahipliğini pthread_mutex_lock fonksiyonu ile aldıktan sonra yeniden aynı fonksiyonu çağırarak aynı mutex nesnesinin sahipliğini almaya çalışırsa ne olur? Muhtemel üç durum söz konusudur: 1) Thread kendi kendini kilitler ve "deadlock" oluşur. 2) Thread mutex'in sahipliğini ikinci kez alır ve bir sorun oluşmaz. Böyle mutex'lere "recursive mutex" denilmektedir. 3) Sahipliği alınmış mutex nesnesine yeniden pthread_mutex_lock uygulandığında fonksiyon başarısız olur. İşte bu muhtemel senaryoların her biri mutex nesnesinin türü denilen özelliği değiştirilerek sağlanabilmektedir. Mutex nesnesinin türünü değiştirmek için pthread_mutexattr_settype fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); Fonksiyonun birinci parametresi mutex özellik nesnesini, ikinci parametresi mutex'in türünü almaktadır. Tür şunlardan biri olabilir: PTHREAD_MUTEX_NORMAL: Mutex'in sahipliği ikinci kez alınmak istenirse kilitlenme "deadlock" oluşur. PTHREAD_MUTEX_ERRORCHECK: Şu durumlarda fonksiyonlar başarısız olmaktadır: - Mutex nesnenin sahipliği ikinci kez alınmaya çalışıldığında - Mutex nesnesinin sahipliğini almadan pthread_mutex_unlock işlemi uygulandığında PTHREAD_MUTEX_RECURSIVE: Mutex'in sahipliğini alan thread yeniden pthread_mutex_lock fonksiyonu ile sahipliği alabilir. Ancak bu durumda fonksiyon sahipliğini aldığı miktarda onu pthread_mutex_unlock uygulamalıdır. Recursive mutex'ler için mutex nesnesinin içerisinde bir sayaç bulundurulmaktadır. (Yani her pthread_mutex_lock fonksiyonundan geçildiğinde mutex'in sayacı 1 artırılır, her pthread_mutex_unlock fonksiyonundan geçildiğinde sayaç 1 eksiltilir. Sayaç 0'a düşünce mutex'in kilidi açılır.) PTHREAD_MUTEX_DEFAULT: Bu default durumdur. POSIX standartlarına göre bu default durum yukarıdaki üç durumdan biri olabilir. Linux sistemlerinde default durum PTHREAD_MUTEX_NORMAL ile aynıdır. Eğer mutex nesnesinin türü set edilmediyse mutex nesnesi default olarak PTHREAD_MUTEX_DEFAULT durumda olur. Yukarıda da belirtildiği gibi Linux sistemlerinde bu değer zaten PTHREAD_MUTEX_NORMAL ile aynıdır. Yani default durumda Linux sistemlerinde thread yeniden mutex nesnesinin sahipliğini almaya çalışırsa "deadlock" oluşmaktadır. Mutex nesnesinin türü de benzer biçimde pthread_mutexattr_gettype fonksiyonu ile alınabilmektedir: #include int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type); Fonksiyon mutex'in tür bilgisini ikinci parametresiyle belirtilen nesneye yerleştirir. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri döner. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte iki thread bir mutex'in sahipliğini almaya çalışmaktadır. Mutex'in sahipliğini alan thread "foo" fonksiyonunu çağırdığında mutex'in sahipliğini ikinci kez almaya çalışmaktadır. Mutex nesneleri Linux'ta default durumda PTHREAD_MUTEX_NORMAL türünde olduğu için "deadlock" oluşacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void foo(const char *name); 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 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, "thread1")) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, "thread2")) != 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 foo(const char *name) { int result; printf("%s: entering pthread_mutex_lock\n", name); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("%s mutex locked...\n", name); printf("%s: entering pthread_mutex_unlock\n", name); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%s: mutex unlocked...\n", name); } void *thread_proc1(void *param) { char *name = (char *)param; int result; printf("%s: entering pthread_mutex_lock\n", name); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("%s mutex locked...\n", name); foo(name); printf("%s: entering pthread_mutex_unlock\n", name); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%s: mutex unlocked...\n", name); return NULL; } void *thread_proc2(void *param) { char *name = (char *)param; int result; printf("%s: entering pthread_mutex_lock\n", name); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("%s mutex locked...\n", name); foo(name); printf("%s: entering pthread_mutex_unlock\n", name); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%s: mutex unlocked...\n", name); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de yukarıdaki örneği mutex türünü PTHREAD_MUTEX_RECURSIVE yaparak yeniden düzenleyelim. Burada artık mutex nesnesi "recursive" duruma sokulduğu için aynı thread mutex nesnesinin sahipliğini almaya çalıştığında bloke oluşmayacaktır. Ekrana çıkan yazıları dikkatlice inceleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void foo(const char *name); 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 main(void) { pthread_t tid1, tid2; int result; 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); if ((result = pthread_create(&tid1, NULL, thread_proc1, "thread1")) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, "thread2")) != 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 foo(const char *name) { int result; printf("%s: entering pthread_mutex_lock\n", name); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("%s mutex locked...\n", name); printf("%s: entering pthread_mutex_unlock\n", name); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%s: mutex unlocked...\n", name); } void *thread_proc1(void *param) { char *name = (char *)param; int result; printf("%s: entering pthread_mutex_lock\n", name); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("%s mutex locked...\n", name); foo(name); printf("%s: entering pthread_mutex_unlock\n", name); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%s: mutex unlocked...\n", name); return NULL; } void *thread_proc2(void *param) { char *name = (char *)param; int result; printf("%s: entering pthread_mutex_lock\n", name); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("%s mutex locked...\n", name); foo(name); printf("%s: entering pthread_mutex_unlock\n", name); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%s: mutex unlocked...\n", name); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi bir thread bir mutex nesnesinin sahipliğini aldıktan sonra sonlanırsa ne olur? (Sahipliği bir thread tarafından alınmış ancak o thread'in sonlanmış olması durumunda bu tür mutex'lere "abandoned mutex"ler de denilmektedir.) Bu tür durumlarda iki olası durum söz konusudur: 1) Thread sonlanmış olsa da mutex'in sahipliği sonlanan thread'te kalır. Dolayısıyla başka bir thread mutex'i kilitlemek isterse ya da zaten pthread_mutex_lock ile blokede bekliyorsa "deadlock" oluşur. 2) Sahipsiz kalmış mutex bir daha kilitlenemez. Eğer bir thread, bu mutex'i kilitlemeye çalışırsa ya da daha önce kilitlemeye çalışıp bloke olmuşsa pthread_mutex_lock başarısız olur. İşte bu iki durumun hangisinin uygulanacağı mutex özellikleri ile belirlenebilmektedir. Bunun için pthread_mutexattr_setrobust fonksiyonu kullanılmaktadır: int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust); Fonksiyonun birinci parametresi yine mutex özellik nesnesinin adresini almaktadır. İkinci parametre şu değerlerden biri olabilir: PTHREAD_MUTEX_STALLED: Bu durumda mutex kilitli kalır ve başka bir thread mutex'i kilitlemeye çalışırsa "deadlock" oluşur. PTHREAD_MUTEX_ROBUST: Bu durumda başka bir thread "abandoned mutex"i kilitlemeye çalışırsa pthread_mutex_lock başarısız olur ve fonksiyon EOWNERDEAD errno değeri ile geri döner. Ancak POSIX standartlarına göre bu kilitlemeyi ilk yapmaya çalışan thread'de bu hata elde edilir. Diğer thread'lerin yeniden bu mutex'i kilitlemeye çalışması durumunda yeniden EOWNERDEAD hatasının elde edilip edilmeyeceği işletim sistemini yazanların isteğine bırakılmıştır. Sahibi sonlanmış bir mutex kilitlenmeye çalışılırken EOWNERDEAD errno değeri ile fonksiyon geri döndüğünde thread'in yeniden kullanılabilir hale getirilebilmesi için pthread_mutex_consistent fonksiyonun çağrılması gerekmektedir. Fonksiyonun prototipi şöyledir: #include int pthread_mutex_consistent(pthread_mutex_t *mutex); Fonksiyon parametre olarak mutex nesnesinin adresini alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri döner. Mutex "consistent" duruma sokulduğunda aynı zamanda kilitlenmiş de olur. Örneğin: result = pthread_mutex_lock(&g_mutex); if (result == EOWNERDEAD) { if ((result = pthread_mutex_consistent(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_consistent", result); /* mutex locked durumda * } else exit_sys_errno("pthread_mutex_lock", result); Mutex nesneleri default özellikle yaratıldığında robust durumu default olarak PTHREAD_MUTEX_STALLED biçimdedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte mutex nesnesi default özelliklerle yaratılmıştır. Burada thread_proc1, mutex nesnesinin sahipliğini alarak sonlandırılmıştır. thread_proc2 fonksiyonunun pthread_mutex_lock işleminde "deadlock" oluşacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #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 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, "thread1")) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, "thread2")) != 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 *thread_proc1(void *param) { char *name = (char *)param; int result; printf("%s: entering pthread_mutex_lock\n", name); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("%s: thread terminates\n", name); return NULL; } void *thread_proc2(void *param) { char *name = (char *)param; int result; sleep(1); printf("%s: entering pthread_mutex_lock\n", name); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("%s mutex locked...\n", name); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%s mutex unlocked...\n", name); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte mutex "robust" duruma sokularak aynı işlemler yapılmıştır. Bu durumda thread_proc2 artık mutex nesnesini kilitlemeye çalıştığında fonksiyon EOWNERDEAD errno değeri ile başarısız olacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #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 main(void) { pthread_t tid1, tid2; int result; pthread_mutexattr_t mattr; if ((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_init", result); if ((result = pthread_mutexattr_setrobust(&mattr, PTHREAD_MUTEX_ROBUST)) != 0) exit_sys_errno("pthread_mutexattr_setrobust", 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, "thread1")) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, "thread2")) != 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 *thread_proc1(void *param) { char *name = (char *)param; int result; printf("%s: entering pthread_mutex_lock\n", name); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("%s: thread terminates\n", name); return NULL; } void *thread_proc2(void *param) { char *name = (char *)param; int result; sleep(1); printf("%s: entering pthread_mutex_lock\n", name); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("%s mutex locked...\n", name); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%s mutex unlocked...\n", name); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Mutex nesneleri bazı programlama dillerinde bir deyim olarak bile bulundurulmuştur. Örneğin C#'ta lock deyimi ve Java'da syncronize deyimi mutex kontrolü altında işlem yapmak için dile eklenmiştir. Örneğin C#'taki lock deyiminin genel biçimi şöyledir: lock (obj) Burada farklı thread'ler aynı nesneyi kullanırlarsa aynı kilit üzerinde işlem yapıyor olurlar. Aslında .NET dokümanlarında lock deyiminin Monitor nesnesi kullandığı belirtilmiştir. "Monitor nesnesi" de bir çeşit mutex nesnesi gibidir. Aynı deyim Java'da "synchronized" ismiyle bulunmaktadır. Oradaki deyimin genel biçimi de C#'taki gibidir: synchronized (obj) Aşağıda C#'ta lock deyiminin kullanılmasına bir örnek verilmiştir: ---------------------------------------------------------------------------------------------------------------------------*/ using System; using System.Threading; namespace CSD { class App { private static int m_count; private static object m_obj = new object(); public static void Main(string[] args) { Thread thread1 = new Thread(new ParameterizedThreadStart(ThreadProc1)); Thread thread2 = new Thread(new ParameterizedThreadStart(ThreadProc2)); thread1.Start(null); thread2.Start(null); thread1.Join(); thread2.Join(); Console.WriteLine(m_count); } public static void ThreadProc1(object o) { for (int i = 0; i < 1000000; i++) lock (m_obj) { ++m_count; } } public static void ThreadProc2(object o) { for (int i = 0; i < 1000000; i++) lock (m_obj) { ++m_count; } } } } /*-------------------------------------------------------------------------------------------------------------------------- 55. Ders 27/05/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar mutex nesnelerini aynı prosesin thread'lerini senkronize etmek amacıyla kullandık. Mutex nesneleri farklı proseslerin thread'lerini senkronize etmek için de kullanılabilmektedir. Aynı prosesin thread'leri, mutex nesnesi ile senkronize edilirken mutex nesnesi global bir değişken olarak tanımlanıyordu. Her iki thread de aynı mutex nesnesini kullanabiliyordu. Pekiyi mutex nesneleri farklı prosesin thread'leri arasında senkronizasyon amacıyla kullanılmak istendiğinde iki proses de aynı mutex nesnesini nasıl görecektir? Eğer Windows sistemlerinde olduğu gibi mutex nesnelerinin UNIX/Linux sistemlerinde isimleri olsaydı bu kullanım kolaylıkla sağlanabilirdi. Ancak UNIX/Linux sistemlerinde mutex nesnelerinin isimleri yoktur. O halde tek yol mutex nesnesini paylaşılan bir bellek alanında yaratmaktır. Ancak POSIX standartlarına göre mutex nesnelerinin paylaşılan bellek alanlarında yaratılması proseslerarası kullanım için yeterli değildir. Aynı zamanda mutex nesnesinin proseslerarası paylaşılabilirliğini set etmek gerekir. Bu da mutex özellikleriyle yapılmaktadır. pthread_mutexattr_setpshared fonksiyonu ile mutex paylaşımlı moda sokulabilmektedir: #include int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); Fonksiyonun birinci parametresi mutex özellik nesnesinin adresini almaktadır. İkinci parametresi ise nesnenin proseslerarası paylaşımını belirtmektedir. Bu parametre PTHREAD_PROCESS_SHARED geçilirse nesne proseslerarasında paylaşılmakta, PTHREAD_PROCESS_PRIVATE geçilirse nesne proseslerarasında paylaşılmamaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Mutex özellik nesnesinin paylaşılabilirlik özelliği pthread_mutexattr_getpshared fonksiyonu ile elde edilebilir: #include int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared); Fonksiyonun birinci parametresi mutex özellik nesnesinin adresini, ikinci parametresi paylaşılabilirlik durum bilgisinin yerleştirileceği int türden nesnesinin adresini almaktadır. Tabii fonksiyon, bu nesneye PTHREAD_PROCESS_SHARED ya da PTHREAD_PROCESS_PRIVATE değerlerinden birini yerleştirmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Mutex özellikleri belirtilmezse ya da pthread_mutexattr_setpshared fonksiyonu ile set edilmezse default durumda nesnenin paylaşım özelliği PTHREAD_PROCESS_PRIVATE biçimindedir. Görüldüğü gibi mutex nesnelerinin proseslararasındaki paylaşımı biraz zahmetlidir. Bu nedenle proseslerarasındaki senkronizasyonlar için daha çok semaphore nesneleri tercih edilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte iki proses ortak bir paylaşılan bellek alanını kullanmaktadır. Paylaşılan bellek alanının başında aşağıdaki gibi bir yapı nesnesi bulunmaktadır: typedef struct tagSHARED_INFO { int count; /* other members... */ pthread_mutex_t mutex; } SHARED_INFO; Örneğimizde iki proses de count değerini asenkron biçimde mutex koruması eşliğinde birer milyar kez artırmaktadır. Bu örnekte paylaşılan bellek alanı "prog1.c" programı tarafından yaratılmış ve oradaki mutex nesnesine bu program tarafından ilk değer verilmiştir. Dolayısıyla bizim önce "prog1" programını çalıştırmamız gerekmektedir. Mutex nesnesinin ve count nesnesinin paylaşılan bellek alanında bulunduğuna dikkat ediniz. Bu paylaşılan bellek alanı "prog1.c" programı tarafından shm_unlik fonksiyonu ile yok edilmiştir. Tabii anımsanacağı gibi shm_unlink fonksiyonu başka bir proses paylaşılan bellek alanını kullanıyorsa o proses paylaşılan bellek alanı nesnesini bırakana kadar zaten gerçek bir silme yapılmamaktadır. Pekiyi buradaki mutex nesnesi ne zaman destroy edilmelidir? Normal olarak bir nesneyi hangi proses yaratmışsa onun yok etmesi uygun olur. Örneğimizde de mutex nesnesini "prog1.c" programı yarattığına göre onun destroy edilmesi de aynı program tarafından yapılmalıdır. Ancak mutex nesnesini destroy ederken diğer proseslerin artık bunu kullanmadığına emin olmak gerekir. Programların tasarımına göre buna emin olabildiğimiz noktalar söz konusu olabilir. Bu durumda biz de mutex nesnesimizi o noktalarda destroy edebiliriz. Ya da destroy işlemi için paylaşılan bellek alanında ayrı bir sayaç da tutulabilir. Mutex'i destroy edecek program da bu sayaca bakabilir. Tabii bu sayacın kontrol edilmesi de blokeli bir biçimde yapılabilir. Bunun için "durum değişkenleri (condition variable)" kullanılabilir. Biz aşağıdaki programda her iki prosesin de işini bitirdikten sonra beklemesini sağladık. Böylece mutex'i yine iki program da bittikten sonra "prog1.c" programı destroy etmektedir. Programları aşağıdaki gibi derleyip farklı terminallerden çalıştırabilirsiniz: $ gcc -o prog1 prog1.c -lrt -lpthread $ gcc -o prog2 prog2.c -lrt -lpthread ---------------------------------------------------------------------------------------------------------------------------*/ /* sharing.h */ #ifndef SHARING_H_ #define SHARING_H_ #include #define SHM_NAME "/shared_memory_for_mutex" typedef struct tagSHARED_INFO { int count; /* other members... */ pthread_mutex_t mutex; } SHARED_INFO; #endif /* prog1.c */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int fdshm; SHARED_INFO *shminfo; pthread_mutexattr_t mattr; int result; if ((fdshm = shm_open(SHM_NAME, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, sizeof(SHARED_INFO)) == -1) exit_sys("ftruncate"); shminfo = (SHARED_INFO *)mmap(NULL, sizeof(SHARED_INFO), PROT_WRITE, MAP_SHARED, fdshm, 0); if (shminfo == MAP_FAILED) exit_sys("mmap"); 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(&shminfo->mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if ((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", result); shminfo->count = 0; for (int i = 0; i < 1000000000; ++i) { if ((result = pthread_mutex_lock(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++shminfo->count; if ((result = pthread_mutex_unlock(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } printf("Press ENTER to exit...\n"); getchar(); printf("%d\n", shminfo->count); if (munmap(shminfo, sizeof(SHARED_INFO)) == -1) exit_sys("munmap"); close(fdshm); if (shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int fdshm; SHARED_INFO *shminfo; pthread_mutexattr_t mattr; int result; if ((fdshm = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); shminfo = (SHARED_INFO *)mmap(NULL, sizeof(SHARED_INFO), PROT_WRITE, MAP_SHARED, fdshm, 0); if (shminfo == MAP_FAILED) exit_sys("mmap"); for (int i = 0; i < 1000000000; ++i) { if ((result = pthread_mutex_lock(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++shminfo->count; if ((result = pthread_mutex_unlock(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } printf("Press ENTER to exit...\n"); getchar(); printf("%d\n", shminfo->count); if (munmap(shminfo, sizeof(SHARED_INFO)) == -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); } /*-------------------------------------------------------------------------------------------------------------------------- Mutex nesneleri, UNIX/Linux sistemlerinde nispeten hızlı senkronizasyon nesneleridir. Linux sistemlerinde senkronizasyon nesnelerinin hepsi zaten "futex (fast user space mutex)" denilen sistem fonksiyonu çağrılarak gerçekleştirilmektedir. mutex fonksiyonları duruma göre hiç kernel mode'a geçmeden işlemini user mode'da yapabilmektedir. Şöyle ki: Biz mutex nesnesini pthread_mutex_lock fonksiyonu ile kilitlemek isteyelim. Mutex nesnesi kendi içerisinde bayrak değişkenleri tutarak kilitleme işlemini atomik bir biçimde yapmak isteyecektir. Atomikliği sağlamanın bir yolu "kernel mode'a geçerek CLI gibi makine komutuyla kesme mekanizmasını kapatıp bayrak değişkenlerini set etmek" olabilir. Ancak kernel mode'a geçmenin maliyeti yüksektir. İşte daha önceden de belirttiğimiz gibi yeni modern işlemcilere "compare and set" gibi atomik makine komutları eklenmiştir. Bu makine komutları sayesinde bu tür flag değişkenleri hiç kernel mode'a geçmeden user mode'da atomik bir biçimde set edilebilmektedir. Ancak pthread_mutex_lock gibi bir fonksiyonun hiç kernel mode'a geçmeden işlem yapma olanağı da yoktur. Eğer mutex nesnesi kilitli değilse onun kilitlenmesi user mode'da "compare and set" komutlarıyla yapılabilmektedir. Ancak ya mutex nesnesi zaten kilitliyse? İşte bu durumda mecburen fonksiyon kernel mode'a geçerek thread'i bloke edecektir. Tabii aslında mutex kilitliyken bu fonksiyonlar hemen kernel mode'a geçip bloke oluşturmazlar. Çünkü pek çok senkronizasyon nesnesi çok kısa bir süre için kilitlenip açılmaktadır. Bu nedenle pthread_mutex_lock, mutex'in kilitli olduğunu gördüğünde birden fazla işlemci ya da çekirdek varsa başka bir işlemci ya da çekirdekteki mutex'i kilitleyen thread'in kısa süre içerisinde kilidi bırakacağını umarak meşgul bir döngüde sürekli bayrak değişkenine bakıp bir süre beklemektedir. Bu tür meşgul döngülere senkronizasyon dünyasında "spin lock" denilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yaygın seknronizasyon nesnelerinden bir diğeri de "koşul değişkenleri (condition variables)" denilen nesnelerdir. Bu nesneler UNIX/Linux sistemlerinde uzunca bir süredir bulunmaktadır. Windows sistemlerine de belli bir süreden sonra sokulmuştur. Koşul değişken nesneleri tek başlarına kullanılmaz mutex nesneleriyle beraber kullanılmaktadır. Koşul değişkenlerinin temel kullanım amacı "belli bir koşul sağlanana kadar" thread'in blokede bekletilmesidir. Bu koşul programcı tarafından oluşturulur. Örneğin programcı global bir g_count değişkeni 0'dan büyük olana kadar thread'i bloke edip bekletmek isteyebilir. Ya da örneğin programcı bir g_flag değişkeni 1 olana kadar thread'ini blokede bekletmek isteyebilir. Maalesef koşul değişkenleri anlaşılması en zor senkronizasyon nesnelerinden biridir. Bu nesnelerin düzgün kullanılabilmesi için belli bir yöntemin izlenmesi gerekmektedir. Programcılar koşul değişkenlerinin kullanımına ilişkin kalıpları anlamakta zorluk çekebilmektedir. Aynı zamanda programcıların kafası bu senkronizasyon nesnelerinin neden gerektiği konusunda da kafası karışabilmektedir. Biz önce koşul değişkenlerinin tipik olarak nasıl kullanılması gerektiği üzerinde duracağız. Sonra konunun ayrıntılarına gireceğiz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Koşul değişkenleri tipik olarak şu adımlardan geçilerek kullanılmaktadır: 1) Programcı önce global düzeyde pthread_cond_t türünden bir koşul değişken nesnesi tanımlar. Koşul değişkenleri pthread_cond_t türü ile temsil edilmiştir. Bu tür ve içerisinde typedef edilmiştir. pthread_cond_t türü sistemlerde tipik olarak bir yapı biçiminde typedef edilmektedir. Örneğin: pthread_cond_t g_cond; 2) Koşul değişkenlerine tıpkı mutex nesnelerinde olduğu gibi statik ya da dinamik olarak ilk değer verilebilmektedir. Biz koşul değişkenlerine statik düzeyde PTHREAD_COND_INITIALIZER makrosuyla ilk değer verebiliriz. Örneğin: pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER; İlkdeğer verme işlemi dinamik olarak pthread_cond_init fonksiyonuyla da yapılabilmektedir: #include int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); Fonksiyonun birinci parametresi koşul değişken nesnesinin adresini, ikinci parametresi onun özellik bilgilerinin bulunduğu özellik nesnesinin adresini almaktadır. Koşul değişkenlerinin de özellik bilgisi vardır. Koşul değişkenlerinin özellikleri üzerinde daha ileride duracağız. İkinci parametre NULL geçilirse koşul değişkenleri default özelliklerle yaratılmış olur. Aşağıdaki iki yaratım işlevsel olarak eşdeğer etkiyi sağlamaktadır: pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER; pthread_cond_init_(&g_cond, NULL); 3) Koşul değişkenleri denilen senkronizasyon nesneleri tek başlarına kullanılmaz. Bir mutex eşliğinde kullanılmaktadır. Dolayısıyla bizim koşul değişkeninin yanı sıra onunla birlikte kullanacağımız bir mutex nesnesine de ihtiyacımız vardır. Örneğin: pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER; 4) Programcının bazı değişkenlerle ilişkili bir koşul oluşturması gerekmektedir. Örneğin koşul "g_flag değişkeninin 1 olması" biçiminde olabilir. Koşul "thread'in bloke olmaması için gerekli olan durumu" belirtmektedir. Bizim koşulumuz g_flag değişkeninin 1 olması ise bu durum "eğer g_flag değişkeni 1 değilse blokede beklemek istediğimiz" anlamına gelmektedir. Yani programcının oluşturduğu koşul sağlanmadığı sürece programcı thread'ini blokede bekletmek istemektedir. 5) Koşul sağlanmadığı sürece blokede beklemek için, koşul sağlanıyorsa bloke oluşturmadan akışın devam etmesi için tipik kalıp aşağıdaki gibidir (kontroller yapılmamıştır): pthread_mutex_lock(&g_mutex); while () pthread_cond_wait(&cond, &g_mutex); /* kritik kod işlemleri */ pthread_mutex_unlock(&g_mutex); Bu kalıpta koşul değişkenini bekleyen asıl fonksiyon pthread_cond_wait isimli fonksiyondur. Fonksiyonun prototipi şöyledir: #include int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); Fonksiyonun birinci parametresi koşul değişken nesnesinin 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. 6) Durum değişkenlerinde blokede bekleme işlemi pthread_cond_wait tarafından sağlanmaktadır. Bu fonksiyon doğrudan thread'i bloke etmektedir. Koşul değişkenini bekleyen bir thread'in uyandırılması iki fonksiyonla yapılabilir: pthread_cond_signal ve pthread_cond_broadcast. Fonksiyonların prototipleri şöyledir: #include int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond); Her iki fonksiyon da koşul değişkeni nesnesinin adresini almaktadır. Fonksiyonlar yine başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Bir thread, pthread_cond_wait fonksiyonunda blokede beklerken o thread'in blokesini kaldırmak için başka bir thread'in pthread_cond_signal ya da pthread_cond_broadcast fonksiyonlarını çağırması gerekmektedir. pthread_cond_signal fonksiyonunun asıl amacı koşul değişkeninde blokede bekleyen herhangi tek bir thread'i uyandırmaktır. Ancak işletim sistemlerinde kernel tasarımından dolayı bu mümkün olamayabilmektedir. Her ne kadar pthread_cond_signal aslında tek bir thread'i uyandırmak için düşünülmüşse de işletim sisteminin tasarımından dolayı birden fazla thread'i de uyandırabilmektedir. Bu nedenle POSIX standartlarında pthread_cond_signal fonksiyonunun "koşul değişkeninde bekleyen en az bir thread'i uyandıracağı" söylenmiştir. Ancak pthread_cond_broadcast fonksiyonu, kesinlikle o koşul değişkeni ile blokede bekleyen tüm thread'leri uyandırmaktadır. Genellikle algoritmalarda "koşul değişkenini bekleyen tek bir thread'in uyandırılması" istenir. Bu nedenle pthread_cond_broadcast yerine çoğu kez pthread_cond_signal fonksiyonu kullanılır. pthread_cond_wait fonksiyonu koşul değişkeni nesnesinin yanı sıra bir de mutex nesnesini de bizden istemektedir. Fonksiyon atomik bir biçimde (yani tek bir işlem gibi) uykuya dalarken (yani bloke olurken) aynı zamanda bu mutex nesnesinin sahipliğini bırakmaktadır. Yani mutex nesnesinin sahipliği yukarıdaki kalıpta aslında pthread_cond_wait tarafından bırakılmaktadır. Örneğin: pthread_mutex_lock(&g_mutex); /* mutex'in sahipliği alındı */ while () pthread_cond_wait(&cond, &g_mutex); /* mutex'in sahipliği bırakılıyor */ /* kritik kod işlemleri */ pthread_mutex_unlock(&g_mutex); /* mutex'in sahipliği bırakılıyor */ Burada mutex nesnesinin sahipliği alınmış daha sonra koşul sağlanmıyorsa pthread_cond_wait fonksiyonuna girilmiştir. İşte bu fonksiyon thread'i uykuya yatırırken aynı zamanda mutex'in sahipliğini de bırakmaktadır. Pekiyi koşul değişkeni pthread_cond_signal ya da pthread_cond_broadcast fonksiyonuyla uyandırıldığında ne olacaktır? İşte pthread_cond_wait fonksiyonu ile koşul değişkeninin blokesi çözüldüğünde fonksiyon buradaki mutex'in sahipliğini de yeniden alarak fonksiyondan çıkmaya çalışır. Koşul değişkeni nesnesi ile mutex nesnesi farklı nesnelerdir. Diğer bir thread, pthread_cond_signal ya da pthread_cond_broadcast uyguladığında koşul değişkeninin blokesinden uyanılmaktadır. Ancak pthread_cond_wait fonksiyonu, geri dönmeden önce mutex nesnesinin de sahipliğini almaya çalışır. pthread_cond_wait fonksiyonunun "sözde kodu (pseudo code)" şöyle düşünülebilir: int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) { 1) Atomik biçimde mutex'in sahipliğini bırak (yani kilidini aç) ve uykuya dal. 2) Şimdi uyandın. Çünkü başka bir thread, pthread_cond_signal ya da pthread_cond_broadcast fonksiyonlarını çağırdı. 3) Şimdi mutex sahipliğini pthread_mutex_lock ile almaya çalış. return } Burada önemli bir nokta şudur: pthread_cond_wait fonksiyonu koşul değişkeninden uyandığında mutex nesnesinin sahipliğini de almaya çalışmaktadır. İşte bu noktada, mutex nesnesi kilitliyse pthread_cond_wait geri dönmeyecektir. Ta ki mutex nesnesinin sahipliği bırakılana kadar. Tabii bu durumda her ne kadar pthread_cond_wait geri dönmüyorsa da onun geri dönmesi için artık pthread_cond_signal ya da pthread_cond_broadcast çağrılarına gerek yoktur. Yalnızca mutex kilidinin açılması gerekmektedir. Böylece yukarıdaki kalıpta mutex'in kilidi iki kez açılmamaktadır. Zaten pthread_cond_wait fonksiyonundan çıkıldığında mutex yeniden kilitlendiği için kilitli olan mutex'in kilidi kritik kodun sonunda açılmaktadır. Pekiyi biz yukarıdaki kalıpta mutex'in sahipliğini almadan pthread_mutex_unlock fonksiyonunu çağırabilir miyiz? pthread_mutex_wait fonksiyonu, mutex'in sahipliğini bırakacağına göre sahipliğin alınmış olması gerekmektedir. Anımsanacağı gibi sahipliği alınmayan mutex'lerin unlock edilmesi duruma göre "undefined behavior" ya da işlemin başarısız olmasına yol açmaktadır. Yukarıdaki kalıpta diğer önemli bir nokta, pthread_cond_wait fonksiyonundan çıkıldığında koşul değişkenin bir döngü içerisinde kontrol edilmesidir. Örneğin, biz g_flag değişkenin 1 olması koşulunu uygulayalım. Bu durumda buradaki döngü şöyle olmalıdır: pthread_mutex_lock(&g_mutex); /* mutex'in sahipliği alındı */ while (g_flag != 1) pthread_cond_wait(&cond, &g_mutex); /* mutex'in sahipliği bırakılıyor */ /* kritik kod işlemleri */ pthread_mutex_unlock(&g_mutex); /* mutex'in sahipliği bırakılıyor */ En normal durum pthread_cond_signal ya da pthread_cond_broadcast işlemini yapan ve bizi koşul değişkenindeki uykudan uyandıran thread'in bu işlemi uygulamadan önce koşulu olumlu hale getirmesidir (yani koşulu sağlanır hale getirmesidir). Örneğin: pthread_mutex_lock(&g_mutex); g_flag = 1; pthread_cond_signal(&g_cond); pthread_mutex_unlock(&g_mutex); Burada koşulun sağlanma işleminin kritik kod içerisinde yapıldığını görüyorsunuz. Normal olarak koşul değişkeninin koşulun sağlanması biçiminde ayarlanması işleminin ve pthread_cond_signal ya da pthread_cond_broadcast işleminin de aynı mutex'i kullanarak kritik kod içerisinde yapılmasıdır. Çünkü programda başka bir thread bu değişkene aynı anda erişirse yine sorun ortaya çıkabilir. Tabii böyle bir durum yoksa koşul kritik kod içerisinde set edilmeye de bilir. Özetle diğer bir thread koşul değişkenindeki blokeyi pthread_cond_signal ya da pthread_cond_broadcast ile kaldırmadan önce koşul değişkenini koşul sağlanacak biçimde set etmesi gerekir. Aksi takdirde koşul sağlanmadığı için pthread_cond_wait fonksiyonundan uyanan thread döngü içerisinde yeniden uykuya dalacaktır. 7) Kullanım bittikten sonra koşul değişkeni pthread_cond_destroy fonksiyonu ile boşaltılmalıdır. Fonksiyonun prototipi şöyledir: #include int pthread_cond_destroy(pthread_cond_t *cond); Fonksiyon koşul değişken nesnesinin adresini alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri döner. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 56. Ders 03/06/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki kalıbı uygulayan bir örnek aşağıda verilmiştir. Bu örnekte Thread2 g_flag == 1 koşulu sağlanana kadar koşul değişkeninde blokede beklemektedir. Thread1 ise g_flag == 1 koşulunu sağlayıp pthread_cond_signal fonksiyonu ile Thread2'yi koşul değişkenindeki blokeden kurtarmaktadır. Programın çıktısı aşağıdaki gibi olacaktır: Thread2 locked mutex Thread2 is waiting at the condition variable... Press ENTER to continue... Thread1 continues... Thread2 unlocked mutex Thread2 continues... ---------------------------------------------------------------------------------------------------------------------------*/ #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); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if ((result = pthread_cond_destroy(&g_cond)) != 0) exit_sys_errno("pthread_cond_destroy", result); return 0; } void *thread_proc1(void *param) { int 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_signal(&g_cond)) != 0) exit_sys_errno("pthread_cond_signal", result); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("Thread1 continues...\n"); return NULL; } void *thread_proc2(void *param) { int result; if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("Thread2 locked mutex\n"); while (g_flag != 1) { printf("Thread2 is waiting at the condition variable...\n"); 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("Thread2 unlocked mutex\n"); printf("Thread2 continues...\n"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Koşul değişkenlerinin kullanımına ilişkin bazı ayrıntılar vardır. Şimdi o ayrıntılar üzerinde duralım: Yukarıda verdiğimiz kalıpta koşul değişkenini pthread_cond_wait ile beklerken bir döngü kullandık. Bu döngünün amacı nedir? while () pthread_cond_wait(&g_cond); Yukarıda da belirttiğimiz gibi bir koşul değişkeni için pthread_cond_signal işlemi yapıldığında o koşul değişkeninde bekleyen tek bir thread değil birden fazla thread uyandırılabilmektedir. Her ne kadar pthread_cond_signal tek bir thread'i koşul değişkeninde uyandırmak istese de bu işletim sistemi tasarımından kaynaklanan nedenlerle mümkün olmayabilmektedir. pthread_cond_signal istemeden birden fazla thread'i uyandırdığında bu thread'ler mutex'in sahipliğini almaya çalışırlar. Bunlardan yalnızca biri mutex'in sahipliğini almayı başarır. Diğer thread'ler koşul değişkeninden uyanmıştır. Ancak bunlardan biri mutex nesnesinin sahipliğini aldığı için mutex'te blokede beklerler. Aşağıdaki sözde kodu inceleyeniz: int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) { 1) Atomik biçimde mutex'in sahipliğini bırak (yani kilidini aç) ve uykuya dal. 2) Şimdi uyandın. Çünkü başka bir thread, pthread_cond_signal ya da pthread_cond_broadcast fonksiyonlarını çağırdı. 3) Şimdi mutex sahipliğini pthread_mutex_lock ile almaya çalış. return } İşte bu durumda pthread_cond_signal fonksiyonunu uygulayan taraf koşul değişkeninden tek bir thread'in uyandırılıp kritik koda girmesini isterken yanlışlıkla birden fazla thread uyandırılmış olacaktır. O halde eğer programcı bunu istiyorsa uyanan thread yeniden koşul değişkenini koşul sağlanmaz hale getirebilir ve bu döngü sayesinde diğer thread'ler yeniden uykuya dalabilir. Örneğin: pthread_mutex_lock(&g_mutex); /* mutex'in sahipliği alındı */ while (g_flag != 1) pthread_cond_wait(&cond, &g_mutex); /* mutex'in sahipliği bırakılıyor */ /* kritik kod işlemleri */ g_flag = 0; /* Dikkat! koşul yeniden sağlanmaz hale getiriliyor */ pthread_mutex_unlock(&g_mutex); /* mutex'in sahipliği bırakılıyor */ Tabii burada "koşul yeniden sağlanmaz hale getirildiğine göre diğer uykuya dalan thread'lerin akıbeti" merak edilebilmektedir. İşte thread'ler yeniden koşul sağlandığında teker teker uyandırılacaktır. Yani koşul değişkeninde bekleyen thread'ler de aslında aynı amacı gerçekleştirmek için beklemektedir. Amaç onları teker teker kritik kod içerisinde oradan çıkarmaktır. Döngü oluşturulmasının ikinci nedeni "spurious wakeup" denilen durumdur. Bazı sistemlerde hiç pthread_cond_signal ya da pthread_cond_broadcast yapılmasa bile thread'ler işletim sisteminin tasarımından kaynaklanan nedenlerle koşul değişkeninden uyandırılabilmektedir. Bu durumda koşul sağlanmadığına göre yanlışlıkla uyanan ("spurious" Türkçe "yapay, sahte, yanlış" anlamlarına gelmektedir) thread'lerin bu döngü sayesinde yeniden uyutulması gerekmektedir. pthread_cond_signal işlemi kaydedilen bir işlem değildir. Yani biz pthread_cond_signal yaptığımızda eğer koşul değişkeninde bekleyen hiçbir thread yoksa bu işlem boşa çıkmış olur. Başka bir deyişle biz pthread_cond_signal ya da pthread_cond_broadcast yaptıktan sonra bir thread pthread_cond_wait fonksiyonuna girerse bloke olur. pthread_cond_signal ya da pthread_cond_broadcast işlemi sadece o anda koşul değişkeninde bekleyen thread'ler için bir uyandırma yapmaktadır. Şimdi de pthread_cond_signal fonksiyonunun mutex kilidi içerisinde uygulanması üzerinde duralım. Aşağıdaki kodu inceleyiniz: pthread_mutex_lock(&g_mutex); g_flag = 1; pthread_cond_signal(&g_cond); ... pthread_mutex_unlock(&g_mutex); Burada biz pthread_cond_signal işleminde koşul değişkeninde bekleyen thread'i uyandırmış olmaktayız. Ancak pthread_cond_wait koşul değişkeninden uyanmakla birlikte mutex'in sahipliğini almak için bloke olur. Tabii biz mutex'in sahipliğini bırakınca o thread mutex'in sahipliğini alıp pthread_cond_wait fonksiyonundan çıkacaktır. Aslında pthread_cond_signal ya da pthread_cond_broadcast fonksiyonlarını mutex'in sahipliğini bıraktıktan sonra da uygulayabiliriz: Örneğin: pthread_mutex_lock(&g_mutex); g_flag = 1; pthread_mutex_unlock(&g_mutex); pthread_cond_signal(&g_cond); Ancak burada duruma göre aşağıdaki gibi bir senkronizasyon sorunu ortaya çıkabilir: Bekleyen Thread Diğer Thread --------------- ------------ pthread_mutex_lock(&g_mutex); g_flag = 1; pthread_mutex_unlock(&g_mutex); pthread_mutex_lock(&g_mutex); while (g_flag != 1) phread_cond_signal(&g_cond); pthread_cond_wait(&g_cond, &g_mutex) pthread_mutex_unlock(&g_mutex); Burada diğer thread'in uyguladığı pthread_cond_signal boşa düşebilecektir. Koşul değişkeninde bekleme döngüsünün oluşturulmasının bir nedeni de şudur: Birden fazla thread koşulu sağlayıp pthread_cond_signal uygulamış olsun. Örneğin: Threadlerden Biri ------------------ pthread_mutex_lock(&g_mutex); pthread_cond_signal(&g_cond); pthread_mutex_unlock(&g_mutex); Diğer Bir Thread ---------------- pthread_mutex_lock(&g_mutex); pthread_cond_signal(&g_cond); pthread_mutex_unlock(&g_mutex); Burada iki ayrı thread bir üretici-tüketici probleminde değer üretip tüketiciyi uyandırmak istesin. Burada koşul değişkeninde bekleyen birden fazla tüketici thread koşul değişkeninden uyandırılabilecektir. Ancak bunların yalnızca bir tanesi mutex'in sahipliğini alacaktır. İşte mutex'in sahipliğini alan thread kritik koda girip tüm tüketimi yapıp kritik koddan çıktığında eğer bir döngü olmazsa artık diğer thread kritik koda girecektir. Üretici-tüketici problemini izleyen paragraflarda ele alacağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Üretici-Tüketici Problemi (Producer-Consumer Problem) gerçek hayatta en fazla karşılaşılan senkronizasyon problemlerinden biridir. Problemin çeşitli varyasyonları vardır. Üretici-Tükettici problemi aynı prosesin thread'leri arasında uygulanabileceği gibi farklı proseslerin thread'leri arasında da uygulanabilmektedir. Üretici-Tüketici probleminde en az bir üretici thread ve en az bir tüketici thread vardır. Biz bir üretici bir de tüketici thread'in olduğunu varsayalım. Üretici thread belli bir işlemden sonra bir değer üretir. Ancak o değerin işlenmesini tüketici thread'e havale eder. Bunun için ortak bir paylaşılan alan oluşturulur. Üretici thread elde ettiği değeri paylaşılan alana yerleştirir. Tüketici thread de onu oradan alarak kullanır. Yani üretici thread'in görevi değer elde edip onu bir paylaşılan bellek alanına yazmaktır. Tüketici thread'in görevi de onu paylaşılan bellek alanından alıp işlemektir. Tabii eğer problem aynı prosesin thread'leri arasında uygulanacaksa bu durumda paylaşılan alan global bir nesne olabilir. Ancak problem farklı proseslerin thread'leri arasında uygulanacaksa bu durumda paylaşılan alan gerçekten "paylaşılan bellek alanı (shared memory)" olarak oluşturulmalıdır. Problemdeki ana unsur üretici thread'in ve tüketici thread'in asenkron çalışması nedeniyle koordine edilmesi gerekliliğidir. Eğer üretici thread tüketici thread'ten daha hızlı davranırsa daha tüketici thread önceki değeri paylaşılan bellek alanından almadan üretici thread yeni bir değeri oraya yerleştirerek önceki değeri ezebilir. Benzer biçimde eğer tüketici thread, üretici thread'ten daha hızlı davranırsa bu durumda önceki değeri yeniden alıp işlemeye çalışabilir. O halde bu problemde "tüketici thread'in önceki değeri almadan üretici thread'in yeni bir değeri paylaşılan alana yerleştirmemesi" gerekir. Benzer biçimde tüketici thread de "üretici thread yeni bir değeri paylaşılan alana yerleştirmeden aynı değeri ikinci kez" almamalıdır. Yani iki thread'in birbirlerini beklemesi gerekmektedir. Bu problemde neden tek bir thread'in hem değeri elde edip hem de onu işlemek yerine iki ayrı thread'in bu işi yapmaya çalıştığını merak edebilirsiniz. Bunun amacı hız kazancı sağlamaktır. Bu sayede üretici thread üretim yaparken tüketici thread de tüketim yapabilmektedir. Halbuki bu işlemler seri bir biçimde yapılırsa üretim ve tüketim faaliyetlerinin eş zamanlı yapılması mümkün olmaz. Tabii birden fazla thread de seri olarak üretim ve tüketim faaliyetlerinde bulunabilir. Ancak çoğu kez üretim faaliyetinin de koordineli bir biçimde yapılması gerekmektedir. Yani bu çözüm de gerekli hızlanmayı sağlamayabilmektedir. Tek bir işlemcinin ya da çekirdeğin olduğu durumda üretici-tüketici problemi bir hızlanma sağlayabilir mi? Burada da yine prosesin toplam CPU zamanı birden fazla thread'le fazlalaştırılabilmektedir. Yani üretici thread, üretim işlemi bittiğinde kesildiğinde hemen tüketici thread daha hızlı devreye girebilmektedir. Ancak şüphesiz çok işlemcili ya da çekirdekli sistemlerde performansın daha fazla artması beklenir. Üretici-tüketici probleminin değişik biçimleri vardır. Örneğin ortadaki paylaşılan bellek alanı tek bir nesneyi tutacak biçimde değil birden fazla nesneyi tutacak biçimde bir "FIFO kuyruk sistemi olarak" oluşturulabilir. Paylaşılan bir kuyruk sistemi olursa üretici ve tüketicinin birbirlerini bekleme olasılığı azaltılmış olur. Artık üretici thread kuyruk tam dolmuşken, tüketici thread de kuyruk tam boşken bloke olacaktır. Üretici-tüketici probleminde, üretici ve tüketici thread'ler birden fazla da olabilmektedir. Yani çok üretici ve çok tüketici paralel biçimde çalıştırılabilmektedir. Böylece tek işlemcili ya da çekirdekli sistemlerde bile performans artırılabilmektedir. Üretici tüketici probleminin gerçek hayat uygulamalarına çok yerde kaşılaşılmaktadır. Örneğin client-server sistemlerde client'lar üretici durumdadır, server'lar da tüketici durumdadır. Burada birden fazla client thread üretim yapmakta ve birden fazla server thread de tüketim yapmaktadır. Örneğin bir satranç prgramında üretici thread "geçerli hamleleri" elde ederken tüketici thread bu geçerli hamleleri analiz ediyor olabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte üretici-tüketici problemi simüle edilmeye çalışılmıştır. Üretici thread 0'dan 99'a kadar sayıları rastgele beklemelerle bir global değişkene yerleştirmekte tüketici thread'de rastgele beklemelerle o değeri bu global değişkenden alıp ekrana yazdırmaktadır. Buradaki usleep fonksiyonu "mikrosaniye" mertebesinde bekleme yapmaktadır. srand ve rand yerine rand_r isimli onların "thread-safe" versiyonu kullanılmıştır. Buradaki örnek bir çalıştırmadan elde edilen çıktı şöyledir: 0 1 1 3 3 5 7 9 11 13 14 14 16 18 20 20 22 22 23 24 27 28 29 31 32 33 34 34 36 37 37 38 38 39 41 43 43 43 44 45 45 48 49 49 51 51 51 52 54 54 54 55 57 60 60 60 61 61 61 61 62 62 64 66 66 68 69 70 72 73 75 76 76 79 81 81 81 82 83 84 85 85 87 87 90 92 93 94 95 98 98 98 99 Görüldüğü gibi tüketici thread bazı değerleri kaçırmış bazı değerleri de birden fazla kez almıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void *thread_producer(void *param); void *thread_consumer(void *param); void exit_sys_errno(const char *msg, int eno); int g_shared; int main(void) { pthread_t tid1, tid2; int result; if ((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_producer(void *param) { int val; unsigned seed; seed = time(NULL) + 123; val = 0; for (;;) { usleep(rand_r(&seed) % 300000); g_shared = val; if (val == 99) break; ++val; } return NULL; } void *thread_consumer(void *param) { int val; unsigned seed; seed = time(NULL) + 456; for (;;) { val = g_shared; usleep(rand_r(&seed) % 300000); printf("%d ", val); fflush(stdout); if (val == 99) break; } printf("\n"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Üretici-tüketici problemi tipik olarak semaphore'lar ve koşul değişkenleri kullanılarak çözülmektedir. Semaphore'lar daha basit bir kullanıma sahiptir. Biz burada üretici-tüketici probleminin koşul değişkenleriyle çözümü üzerinde duracağız. Semaphore'lar izleyen paragraflarda ele alınacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Üretici-tüketici probleminin koşul değişkenleriyle çözümünde, üretici ve tüketici için iki ayrı koşul değişkeni oluşturulur. Koşul bir flag değişkeni ile ifade edilebilir. Çözümdeki temel fikir üreticinin-tüketiciyi beklemekten kurtarması, tüketicinin de üreticiyi beklemekten kurtarması biçimindedir. Adeta bir tahterevalli gibi işlemler yürütülmektedir. Problemin çözümünde kullanılan ortak değişkenler şöyledir: pthread mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t g_cond_producer = PTHREAD_COND_INITIALIZER; pthread_cond_t g_cond_consumer = PTHREAD_COND_INITIALIZER; int g_flag = 0; Üretici thread'in sembolik kodu şöyledir: for (;;) { pthread_mutex_lock(&g_mutex); while (g_flag == 1) pthread_cond_wait(&g_producer, &g_mutex); g_flag = 1; pthread_cond_signal(&g_cond_consumer); pthread_mutex_unlock(&g_mutex); } Üretici thread'in sembolik kodu şöyledir: for (;;) { pthread_mutex_lock(&g_mutex); while (g_flag == 0) pthread_cond_wait(&g_consumer, &g_mutex); g_flag = 0; pthread_cond_signal(&g_cond_producer); pthread_mutex_unlock(&g_mutex); } Burada kodu dikkatlice inceleyiniz. g_flag değişkeninin başlangıçtaki değeri 0'dır. Bu durumda başlangıçta tüketici thread bekleyecek ancak üretici thread beklemeden değeri üretip paylaşılan alana yerleştirecektir. Tüketici çok yavaş çalışsa bile üretici thread değeri paylaşılan bellek alanına yerleştirdikten sonra artık g_flag değişkenini 1 yaptığı için pthread_cond_wait fonksiyonunda bekleyecektir. Bu sırada tüketici thread paylaşılan alandan bilgiyi alıp g_flag değişkenini 0 yaptıktan sonra üreticiyi blokeden kurtaracaktır. Görüldüğü gibi üretici-tüketiciyi, tüketici de üreticiyi beklemektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 57. Ders 04/06/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda üretici-tüketici probleminin koşul değişkenleriyle çözümüne bir örnek verilmiştir. Burada ortadaki paylaşılan alan yine tek bir int nesneden oluşmaktadır. Bu tür durumlarda kritik kodun mümkün olduğu kadar kısa tutulması iyi bir tekniktir. Yani biz kodda yalnızca gerekli kısımları kritik kod içerisine almalıyız. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void *thread_producer(void *param); void *thread_consumer(void *param); void exit_sys_errno(const char *msg, int eno); pthread_mutex_t g_mutex; pthread_cond_t g_cond_producer; pthread_cond_t g_cond_consumer; int g_flag = 0; int g_shared; 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_cond_init(&g_cond_producer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if ((result = pthread_cond_init(&g_cond_consumer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if ((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_cond_destroy(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if ((result = pthread_cond_destroy(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if ((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); return 0; } void *thread_producer(void *param) { int val; unsigned seed; int result; seed = time(NULL) + 123; val = 0; for (;;) { usleep(rand_r(&seed) % 300000); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while (g_flag == 1) if ((result = pthread_cond_wait(&g_cond_producer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); g_shared = val; g_flag = 1; if ((result = pthread_cond_signal(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_signal", result); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if (val == 99) break; ++val; } return NULL; } void *thread_consumer(void *param) { int val; unsigned seed; int result; seed = time(NULL) + 456; for (;;) { if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while (g_flag == 0) if ((result = pthread_cond_wait(&g_cond_consumer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); val = g_shared; g_flag = 0; if ((result = pthread_cond_signal(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); usleep(rand_r(&seed) % 300000); printf("%d ", val); fflush(stdout); if (val == 99) break; } printf("\n"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Üretici-tüketici probleminde paylaşılan alan bir kuyruk sistemi olursa üreticinin tüketiciyi, tüketicinin de üreticiyi bekleme olasılığı düşürülmüş olur. Çünkü bu durumda üretici yalnızca kuyruk tam doluyken tüketici de kuyruk tam boşken bekleyecektir. Böyle bir kuyruk gerçekleştirimi çeşitli biçimlerde yapılabilir. En çok kullanılan kuyruk gerçekleştirimlerinden biri "döngüsel kuyruk sistemi (circular queue)" de denilen gerçekleştirimdir. Bu gerçekleştirimde bir dizi oluşturulur. head ve tail olmak üzere iki indeks ya da gösterici kuyruğun başını ve sonunu tutar. Kuyruğa bilgi yerleştiririlirken tail göstericisinin gösterdiği yere yerleştirme yapılır ve tail göstericisi bir artırılır. Kuyruktan eleman alınırken eleman head göstericisinin gösterdiği yerden alınır ve head göstericisi bir artırılır. Tabii head ve tail göstericileri dizinin sonuna geldiğinde yeniden dizinin başına çekilir (zaten "döngüsel" terimi bu nedenle kullanılmaktadır). Kuyrukta o anda kaç elemanının bulunduğu ayrı bir sayaçla tutulabilir. Burada önemli noktalardan biri head ve tail göstericilerinin aynı yeri göstermesi durumunda kuyruğun tam boş ya da tam dolu olabileceğidir. Bunun tespiti sayaç değişkenine bakılarak yapılabilir. Üretici-tüketici probleminin kuyruklu çözümünde üreticinin bekleme koşulu şöyle oluşturulabilir (kontroller yapılmamıştır): while (g_count == QUEUE_SIZE) pthread_cond_wait(&g_cond_producer, &g_mutex); Bu koşul üreticinin yalnızca kuyruktaki eleman sayısı kuyruk uzunluğuna eşit olduğunda bloke olacağı anlamına gelmektedir. Yani kuyruk tam doluysa üretici bloke olacaktır. Tüketicinin bekleme koşulu da şöyle oluşturulabilir: while (g_count == 0) pthread_cond_wait(&g_cond_consumer, &g_mutex); Burada da tüketici yalnızca kuyruk tam boş ise bloke olacaktır. Aşağıda üretici-tüketici probleminin kuyruklu versiyonuna bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #define QUEUE_SIZE 10 void *thread_producer(void *param); void *thread_consumer(void *param); void exit_sys_errno(const char *msg, int eno); pthread_mutex_t g_mutex; pthread_cond_t g_cond_producer; pthread_cond_t g_cond_consumer; int g_queue[QUEUE_SIZE]; int g_head = 0; int g_tail = 0; int g_count = 0; 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_cond_init(&g_cond_producer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if ((result = pthread_cond_init(&g_cond_consumer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if ((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_cond_destroy(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if ((result = pthread_cond_destroy(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if ((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); return 0; } void *thread_producer(void *param) { int val; unsigned seed; int result; seed = time(NULL) + 123; val = 0; for (;;) { usleep(rand_r(&seed) % 300000); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while (g_count == QUEUE_SIZE) if ((result = pthread_cond_wait(&g_cond_producer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); g_queue[g_tail++] = val; g_tail %= QUEUE_SIZE; ++g_count; if ((result = pthread_cond_signal(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_signal", result); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if (val == 99) break; ++val; } return NULL; } void *thread_consumer(void *param) { int val; unsigned seed; int result; seed = time(NULL) + 456; for (;;) { if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while (g_count == 0) if ((result = pthread_cond_wait(&g_cond_consumer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); val = g_queue[g_head++]; g_head %= QUEUE_SIZE; --g_count; if ((result = pthread_cond_signal(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); usleep(rand_r(&seed) % 300000); printf("%d ", val); fflush(stdout); if (val == 99) break; } printf("\n"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Koşul değişkenlerinin zaman aşımlı bekleme yapan pthread_cond_timedwait isimli bir biçimi de vardır. Bu fonksiyon belli bir zaman aşımı dolduğunda koşul değişkenini otomatik olarak açmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); Burada yine timespec yapısı ile belirtilen zaman aşımı değeri göreli değil mutlaktır. Fonksiyon yine atomik bir biçimde mutex'in sahipliğini bırakır ve çıkışta yine mutex'in sahipliğini almaya çalışır. Ancak koşul değişkeninde bekleme en kötü olasılıkla zaman aşımı dolduğunda sonlanmaktadır. Fonksiyon başarı durumunda 0, başarısızlık durumunda errno değerine geri döner. Yine fonksiyon eğer zaman aşımı dolayısıyla sonlanmışsa ETIMEDOUT değeri ile geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Koşul değişkenleri de özellik (attribute) parametresine sahiptir. Ancak koşul değişkenlerinin set edilebilecek yalnızca iki özelliği vardır. Koşul değişkenlerine özellik iliştirmek diğer nesnelerde olduğu gibi yapılmaktadır. Programcı önce pthread_condattr_t türünden bir nesne tanımlar. Sonra bu nesneye pthread_condattr_init fonksiyonuyla ilk değer verir. Sonra pthread_condattr_setxxx fonksiyonlarıyla özellikleri nesne içerisine set eder. Oluşturduğu bu özellik nesnesini de pthread_cond_init fonksiyonunda kullanır. Yine özellik nesnesinin pthread_cond_init fonksiyonundan sonra korunmasına gerek yoktur. Özellik nesnesi, pthread_condattr_destroy fonksiyonu ile boşaltılabilir. Fonksiyonların prototipileri şöyledir: #include int pthread_condattr_destroy(pthread_condattr_t *attr); int pthread_condattr_init(pthread_condattr_t *attr); Koşul değişkenlerine iki özellik set edip bunları alabiliriz. Birincisi, koşul değişkeninin proseslerarası kullanımını sağlayan özelliktir. Bu özellik pthread_condattr_setpshared fonksiyonu ile set edilmektedir. Fonksiyonun prototipi şöyledir: #include int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared); int pthread_condattr_getpshared(const pthread_condattr_t *attr, int * pshared); Buradaki pshared parametresi PTHREAD_PROCESS_SHARED ya da PTHREAD_PROCESS_PRIVATE değerinde olabilir. pthread_condattr_getpshared fonksiyonu, bu değeri özellik nesnesinden alıp parametresiyle belirtilen nesneye yerleştirmektedir. Böyle bir set işlemi yapılmazsa default durum PTHREAD_PROCESS_PRIVATE biçimindedir. Biz pthread_cond_timedwait fonksiyonunda zaman aşımında kullanılacak saatin cinsini de belirleyebiliriz. Bu işlemler için pthread_condattr_setclock ve pthread_condattr_getclock fonksiyonları kullanılmaktadır: #include int pthread_condattr_getclock(const pthread_condattr_t *attr, clockid_t *clock_id); int pthread_condattr_setclock(pthread_condattr_t *attr, clockid_t clock_id); Fonksiyon, kullanılacak clock nesnesinin id'sini parametre olarak almaktadır. Default olarak "system clock" kullanılmaktadır. Tabii koşul değişkenlerinin proseslerarası kullanımı için yine onların paylaşılan bellek alanında oluşturulması gerekmektedir. Tabii bu durumda mutex nesnesinin de paylaşılan bellek alanında oluşturulması gerekecektir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda üretici-tüketici probleminin proseslerarasında oluşturulmasına ilişkin bir örnek verilmiştir. Bu örnekte tüm koşul değişkenleri, mutex nesnesi ve kuyruk bilgileri paylaşılan bellek alanında oluşturulmuştur. Önce "producer" programı çalıştırılmalıdır. Tüm bu nesneleri producer yaratıp kendisi silmektedir. Derleme işlemlerini şöyle yapabilirsiniz: $ gcc -Wall -o producer producer.c -lpthread -lrt $ gcc -Wall -o consumer consumer.c -lpthread -lrt ---------------------------------------------------------------------------------------------------------------------------*/ /* sharing.h */ #ifndef SHARING_H_ #define SHARING_H_ #include #define SHM_NAME "/producer-consumer" #define QUEUE_SIZE 10 typedef struct tagSHARED_INFO { pthread_cond_t cond_producer; pthread_cond_t cond_consumer; pthread_mutex_t mutex; int head; int tail; int queue[QUEUE_SIZE]; int count; } SHARED_INFO; #endif /* producer.c */ #include #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int fdshm; SHARED_INFO *shminfo; pthread_mutexattr_t mattr; pthread_condattr_t cattr; int result; int val; srand(time(NULL)); if ((fdshm = shm_open(SHM_NAME, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, sizeof(SHARED_INFO)) == -1) exit_sys("ftruncate"); shminfo = (SHARED_INFO *)mmap(NULL, sizeof(SHARED_INFO), PROT_WRITE, MAP_SHARED, fdshm, 0); if (shminfo == MAP_FAILED) exit_sys("mmap"); 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(&shminfo->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_condattr_init(&cattr)) != 0) exit_sys_errno("pthread_condattr_init", result); if ((result = pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED)) != 0) exit_sys_errno("pthread_condattrattr_setpshared", result); if ((result = pthread_cond_init(&shminfo->cond_producer, &cattr)) != 0) exit_sys_errno("pthread_cond_init", result); if ((result = pthread_cond_init(&shminfo->cond_consumer, &cattr)) != 0) exit_sys_errno("pthread_cond_init", result); if ((result = pthread_condattr_destroy(&cattr)) != 0) exit_sys_errno("pthread_condattr_destroy", result); shminfo->count = 0; shminfo->head = 0; shminfo->tail = 0; val = 0; for (;;) { usleep(rand() % 300000); if ((result = pthread_mutex_lock(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while (shminfo->count == QUEUE_SIZE) if ((result = pthread_cond_wait(&shminfo->cond_producer, &shminfo->mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); shminfo->queue[shminfo->tail++] = val; shminfo->tail %= QUEUE_SIZE; ++shminfo->count; if ((result = pthread_mutex_unlock(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if ((result = pthread_cond_signal(&shminfo->cond_consumer)) != 0) exit_sys_errno("pthread_cond_signal", result); if (val == 99) break; ++val; } if ((result = pthread_mutex_lock(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while (shminfo->count != 0 ) if ((result = pthread_cond_wait(&shminfo->cond_producer, &shminfo->mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); if ((result = pthread_mutex_unlock(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if ((result = pthread_cond_destroy(&shminfo->cond_consumer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if ((result = pthread_cond_destroy(&shminfo->cond_producer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if ((result = pthread_mutex_destroy(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if (munmap(shminfo, sizeof(SHARED_INFO)) == -1) exit_sys("munmap"); close(fdshm); if (shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* consumer.c */ #include #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int fdshm; SHARED_INFO *shminfo; int result; int val; srand(time(NULL)); if ((fdshm = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); shminfo = (SHARED_INFO *)mmap(NULL, sizeof(SHARED_INFO), PROT_WRITE, MAP_SHARED, fdshm, 0); if (shminfo == MAP_FAILED) exit_sys("mmap"); for (;;) { if ((result = pthread_mutex_lock(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while (shminfo->count == 0) if ((result = pthread_cond_wait(&shminfo->cond_consumer, &shminfo->mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); val = shminfo->queue[shminfo->head++]; shminfo->head %= QUEUE_SIZE; --shminfo->count; printf("%d ", val); fflush(stdout); if ((result = pthread_mutex_unlock(&shminfo->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if ((result = pthread_cond_signal(&shminfo->cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); usleep(rand() % 300000); if (val == 99) break; } printf("\n"); if ((result = pthread_cond_signal(&shminfo->cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); if (munmap(shminfo, sizeof(SHARED_INFO)) == -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); } /*-------------------------------------------------------------------------------------------------------------------------- Semaphore'lar (semaphores) en çok kullanılan senkronizasyon nesnelerindendir. Pek çok işletim sisteminde semaphore nesneleri benzer işlevselliklerle bulunmaktadır. UNIX/Linux sistemlerinde semaphore'lar IPC konusuyla ilişkilendirilmiştir. Bu nedenle tıpkı paylaşılan bellek alanlarında ve mesaj kuyruklarında olduğu gibi semaphore'lar için de iki ayrı arayüz fonksiyon grubu bulunmaktadır. Ancak Sistem 5 semaphore'ları maalesef oldukça karışık ve kötü bir arayüzle tasarlanmıştır. POSIX semaphore'larında bu tasarım düzeltilmiştir. Biz kursumuzda IPC nesnelerini, önce Sistem 5 sonra POSIX olacak biçimde açıklamıştık. Ancak burada bunun tersini yapacağız. Yani önce POSIX semaphore'larını açıklayıp sonra Sistem 5 semaphore'ları üzerinde duracağız. Uygulamada özel bir gerekçe yoksa POSIX semaphore'ları tercih edilmelidir. Semaphore'lar "sayaçlı" senkronizasyon nesneleridir. Semaphore sözcüğü "trafikteki dur-geç lambalarından" gelmektedir. (Bu sözcüğü "anafor" sözcüğü ile karıştırmayınız.) Semaphore'lar bir kritik koda en fazla n tane akışın girmesini sağlamak için düşünülmüştür. Örneğin biz bir kritik koda en fazla 3 thread'in girmesini ancak daha fazla thread'in girmemesini isteyebiliriz. Bu durumda üç thread kritik koda girdikten sonra diğer thread'ler kritik koda giremeyecek ve blokede bekleyecektir. Kritik koda girmiş olan bir thread kritik koddan çıktığında, bekleyen bir thread kritik koda girebilecektir. Bu örnekte önemli olan üçten daha fazla thread'in aynı anda kritik koda girmemesinin bloke yoluyla sağlanmasıdır. Kritik koda birden fazla thread'in girmesi kişilere anlamsız gelebilmektedir. Çünkü iki thread bile ortak kaynağı bozabilir. O halde kritik koda n tane thread'in girmesinin ne anlamı olabilir? İşte bunun en önemli kullanım gerekçesi "kaynak paylaşımının" sağlanmasıdır. Örneğin elimizde üç tane makine olsun. Ancak 10 tane thread bu makineleri kullanmak istesin. Bizim bu üç makineyi yalnızca üç thread'e tahsis etmemiz gerekir. Makineyi kullanmak isteyen diğer thread'ler bu makinelerden biri boşaltılana kadar CPU zamanı harcamadan blokede bekletilmelidir. İşte burada tipik bir semaphore kullanımı söz konusudur. Örneğin: ... ... ... ... ... Kritik koda en fazla kaç akışın girebileceği "semaphore sayacı" ile ilgilidir. Eğer semaphore sayacı 1'de tutulursa kritik koda en fazla bir akış girebilir. Bu tür semaphore'lara "binary semaphore" denilmektedir. Binary semaphore'lar adeta mutex nesneleri gibi bir etkiye sahiptir. Yani bu anlamda binary semaphore'lar bir mutex alternatifi olarak da kullanılabilirler. Ancak mutex nesnelerinin thread temelinde sahipliği vardır. Yani mutex'in sahipliğini hangi thread almışsa onun bırakması gerekir. Halbuki semaphore nesnelerinde böyle bir zorunluluk yoktur. Bu nedenden dolayı bazı kesimler tarafından semaphore nesneleri hataya daha açık bir nesneler olarak değerlendirilebimektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 58. Ders 10/06/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- POSIX semaphore nesneleri isimli ve isimsiz olarak yaratılabilmektedir. İsimli semaphore nesneleri proseslerarası kullanım için daha uygundur. İsimsiz semaphore nesneleri -her ne kadar proseslerarasında da kullanılabiliyorsa da- özellikle prosesin thread'leri arasındaki senkronizasyonda tercih edilmektedir. İsimsiz POSIX semaphore nesnelerinin kullanımı şöyledir: 1) Semaphore nesneleri sem_t türü ile temsil edilmiştir. POSIX standartlarına göre sem_t herhangi bir tür olarak typedef edilebilmektedir. UNIX türevi sistemlerde tipik olarak sem_t bir yapı biçiminde typedef edilmektedir. Programcı bu türden global bir nesne tanımlar ve ona sem_init fonksiyonuyla ilk değer verir. sem_init fonksiyonunun prototipi şöyledir: #include int sem_init(sem_t *sem, int pshared, unsigned value); Fonksiyonun birinci parametresi sem_t türünden nesnesinin adresini alır. İkinci parametre semaphore nesnesinin prosesler arasında paylaşılıp paylaşılmayacağını belirtir. Burada 0 değeri nesnenin prosesler arasında paylaşılmayacağını, sıfır dışı değer ise nesnenin prosesler arasında paylaşılacağını belirtmektedir. Üçüncü parametre başlangıçtaki semaphore sayacının değerini belirtir. Yani bu değer kritik koda en fazlası kaç akışın girebileceğini belirtmektedir. 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. (Thread fonksiyonlarının bizzat başarısızlık durumunda errno değeriyle geri döndüğünü anımsayınız. Halbuki POSIX semaphore fonksiyonları doğrudan errno değişkenini set etmektedir.) Örneğin: sem_t g_sem; ... if (sem_init(&g_sem, 0, 3) == -1) exit_sys("sem_init"); 2) Kritik kod aşağıdaki gibi oluşturulabilir (kontroller yapılmamıştır): sem_wait(&g_sem); ... ... ... sem_post(&g_sem); sem_wait fonksiyonu semaphore sayacına bakar. Semaphore sayacı 0'dan büyükse bloke oluşturmaz. Böylece thread kritik koda girer. Ancak sem_wait fonksiyonu semaphore sayacı 0'dan büyükse atomik bir biçimde semaphore sayacını 1 eksiltmektedir. Örneğin başlangıçtaki semaphore sayacı 3 olsun. Bu durumda thread'lerden biri sem_wait fonksiyonundan geçtiğinde semaphore sayacı 2 olur. Diğer bir thread de geçtiğinde semaphore sayacı 1 olacaktır. Nihayet bir thread daha sem_wait fonksiyonundan geçtiğinde semaphore sayacı 0 olur. Artık kritik kodda 3 tane thread vardır. Başka thread'ler sem_wait fonksiyonuna geldiğinde semaphore sayacı 0 olduğu için bloke olurlar ve kritik koda giremezler. İşte sem_post fonksiyonu da semaphore sayacını 1 artırmaktadır. Böylece kritik koddan çıkıldığında semaphore sayacı 1 artırılmış olur. sem_wait fonksiyonunda bekleyen thread'lerden biri artık semaphore sayacı 0'dan büyük olduğu için kritik koda girer. Görüldüğü gibi kritik kodda belli bir anda en fazla 3 thread bulunabilmektedir. Yukarıda da belirttiğimiz gibi aslında semaphore'lar belli sayıda kaynağın thread'lere paylaştırılması için kullanılmaktadır. Örneğin elimizde 3 tane makine olabilir. Biz bu üç makineyi 10 thread'in kullanmasını isteyebiliriz. Ancak makine atamadığımız thread'lerin CPU zamanı harcamadan bloke durumda bekletilmesi gerekmektedir. O halde biz her kritik koda giren thread'e bir makine atarız. Tıpkı mutex nesnelerinde olduğu gibi sem_wait fonksiyonunda birden fazla thread'in beklemesi durumunda bu thread'lerin hangisinin kritik koda gireceği konusunda bir garanti verilmemektedir. İşletim sistemleri belirli koşullarda adil bir sistem uygulamaya çalışsa da bunun bir garantisini vermemektedir. Böylece üç makineyi, 10 thread'in etkin bir biçimde kullanmasını sağlarız. sem_wait ve sem_post fonksiyonlarının prototipleri şöyledir: #include int sem_wait(sem_t *sem); int sem_post(sem_t *sem); Fonksiyonlar semaphore nesnesinin adresini parametre olarak alırlar. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönerler ve errno değişkeni uygun biçimde set edilir. sem_wait fonksiyonunun ayrıca bir de sem_timedwait isimli zaman aşımlı bir biçimi de vardır. Bu zaman aşımlı biçim eğer semaphore'da bloke olunmuşsa zaman aşımı dolduğunda blokeyi çözmektedir. sem_timedwait fonksiyonunun prototipi şöyledir: #include int sem_timedwait(sem_t *sem, const struct timespec *abstime); Ancak buradaki zaman aşımı yine göreli değil mutlak zamanı belirtmektedir. Fonksiyon eğer zaman aşımından dolayı başarısız olursa -1 değerine geri döner ve errno değişkeni ETIMEDOUT değeri ile set edilmektedir. 3) Semaphore kullanımı bittikten sonra semaphore nesnesi sem_destroy fonksiyonu ile boşaltılmalıdır. Fonksiyonun prototipi şöyledir: #include int sem_destroy(sem_t *sem); Fonksiyon semaphore nesnesinin adresini parametre olarak alır. Başarı durumunda 0, başarısızlık durumunda -1 değerine geri döner ve errno değişkeni uygun biçimde set edilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda basit bir binary semaphore örneği verilmiştir. Bu örnekte yine iki thread tıpkı mutex örneğinde olduğu gibi bir semaphore eşliğinde global bir değişkeni artırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #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 = 0; 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_wait"); } 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_wait"); } 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 mademki binary semaphore'lar mutex'e çok benzemektedir. Biz hangisini tercih etmeliyiz? İşte mutex'in sahipliği thread temelinde alındığı için mutex genel olarak binary semaphore'lara göre daha güvenlidir. Ayrıca genel olarak işletim sistemlerinde mutex işlemleri, semaphore işlemlerine göre daha hızlı olma eğilimindedir. Yani eğer bir senkronizasyon işlemini mutex kullanarak da binary semaphore kullanarak da yapabiliyorsak mutex'i tercih etmeliyiz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte belli sayıda kaynak (örneğimizde 3) belli sayıda thread tarafından (örneğimizde 10) paylaşılmaktadır. Örnekte semaphore kontrolü ile kritik kod oluşturulmuş ve kritik koda her giren thread'e bir kaynak atanmıştır. Bu örnekten amaç böyle bir durumun simülasyonunun yapılmasıdır. Bu örnekte kaynağı elde edemeyen thread'ler blokede bekletilmektedir. Bir thread'in kaynak kullanımı bittiğinde artık kritik koda yeni bir thread girmekte ve o kaynak o thread'e atanmaktadır. Örneğimizde bir semaphore nesnesinin yanı sıra bir de mutex nesnesi kullanılmıştır. Bu mutex nesnesi kaynak ataması sırasında oluşabilecek senkronizasyon problemini (race condition) engellemektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #define NTHREADS 10 #define NRESOURCES 3 typedef struct tagRESOURCES { int useflags[NRESOURCES]; sem_t sem; pthread_mutex_t mutex; } RESOURCES; typedef struct tagTHREAD_INFO { pthread_t tid; char name[32]; unsigned seed; } THREAD_INFO; void assign_resource(THREAD_INFO *ti); void do_with_resource(THREAD_INFO *ti, int nresource); void *thread_proc(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); RESOURCES g_resources; int main(void) { int result; THREAD_INFO *threads_info[NTHREADS]; srand(time(NULL)); for (int i = 0; i < NRESOURCES; ++i) g_resources.useflags[i] = 0; if ((sem_init(&g_resources.sem, 0, NRESOURCES)) == -1) exit_sys("sem_init"); if ((result = pthread_mutex_init(&g_resources.mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); for (int i = 0; i < NTHREADS; ++i) { if ((threads_info[i] = (THREAD_INFO *)malloc(sizeof(THREAD_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } snprintf(threads_info[i]->name, 32, "Thread-%d", i + 1); threads_info[i]->seed = rand(); if ((result = pthread_create(&threads_info[i]->tid, NULL, thread_proc, threads_info[i])) != 0) exit_sys_errno("pthread_create", result); } for (int i = 0; i < NTHREADS; ++i) { if ((result = pthread_join(threads_info[i]->tid, NULL)) != 0) exit_sys_errno("pthread_join", result); free(threads_info[i]); } if ((result = pthread_mutex_destroy(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if (sem_destroy(&g_resources.sem) == -1) exit_sys("sem_destroy"); return 0; } void assign_resource(THREAD_INFO *ti) { int result; int i; if (sem_wait(&g_resources.sem) == -1) exit_sys("sem_wait"); if ((result = pthread_mutex_lock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); for (i = 0; i < NRESOURCES; ++i) if (!g_resources.useflags[i]) { g_resources.useflags[i] = 1; break; } if ((result = pthread_mutex_unlock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%s thread acquired resource \"%d\"\n", ti->name, i + 1); do_with_resource(ti, i + 1); if ((result = pthread_mutex_lock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); g_resources.useflags[i] = 0; if ((result = pthread_mutex_unlock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%s thread released resource \"%d\"\n", ti->name, i + 1); if (sem_post(&g_resources.sem) == -1) exit_sys("sem_wait"); usleep(rand_r(&ti->seed) % 10000); } void do_with_resource(THREAD_INFO *ti, int nresource) { printf("%s doing something with resource \"%d\"\n", ti->name, nresource); usleep(rand_r(&ti->seed) % 500000); } void *thread_proc(void *param) { THREAD_INFO *ti = (THREAD_INFO *)param; for (int i = 0; i < 10; ++i) assign_resource(ti); return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Biz daha önce üretici-tüketici problemini koşul değişkenleri ile çözmüştük. Aynı problemi semaphore nesneleriyle da daha kolay bir biçimde çözebiliriz. Yine tahterevalli sistemi burada geçerlidir. Semaphore sayaçlarının başka bir thread tarafından sem_post fonksiyonu ile artırılabildiğini anımsayınız. Yani sem_post uygulayabilmek için sem_wait yapmış olmak gerekmemektedir. Tipik çözüm şöyledir: Yine üretici ve tüketici için iki semaphore alınır. Semaphore sayaçları paylaşılan alanın uzunluğuna ayarlanır. Biz ortadaki paylaşılan alanın 1 elemanlık olduğunu varsayalım (kontroller yapılmamıştır): sem_t g_sem_producer; sem_t g_sem_consumer; ... sem_init(&g_sem_producer, 0, 1); sem_init(&g_sem_consumer, 0, 0); Başlangıçta üretici semaphore'un sayacının 1 olduğuna, tüketici semaphore'un sayacının 0 olduğuna dikkat ediniz. Böylelikle işin başında tüketici bekleyecek ancak üretici beklemeyecektir. ÜRETİCİ THREAD for (;;) { sem_wait(&g_sem_producer); sem_post(&g_sem_consumer); } TÜKETİCİ THREAD for (;;) { sem_wait(&g_sem_consumer); sem_post(&g_sem_producer); } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 59. Ders 11/06/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda üretici-tüketici probleminin kuyruksuz versiyonu semaphore nesneleri ile çözülmüştür. Burada üretici semaphore'unun başlangıçta 1'e tüketici semaphore'unun da 0'a kurulduğuna dikkat ediniz. Üretici tüketiciyi, tüketici de üreticiyi tahterevalli misali blokeden kurtarmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include 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_shared; int main(void) { pthread_t tid1, tid2; int result; 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_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_shared = val; 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_shared; 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); } /*-------------------------------------------------------------------------------------------------------------------------- Üretici tüketici probleminin kuyruklu versiyonu da tamamen benzer biçimde semaphore nesneleriyle çözülebilmektedir. Ancak bu durumda üretici semaphore'unun başlangıçta kuyruk uzunluğuna kurulması gerekmektedir. Böylece tüketici hiç çalışmadığında, üretici kuyruğu doldurur ve bekler. Benzer biçimde üretici çalışmadığı durumda tüketici kuyruktaki tüm elemanları alır ve bekler. Örneğin: sem_t g_sem_producer; sem_t g_sem_consumer; ... sem_init(&g_sem_producer, 0, QUEUE_SIZE); sem_init(&g_sem_consumer, 0, 0); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda üretici-tüketici probleminin kuyruklu biçimi için bir veri yapısı oluşturulmuştur. Bu veri yapısı önce init_squeue fonksiyonu ile yaratılır. Sonra put_squeue ve get_squeue fonksiyonları ile bu kuyruk veri yapısına eleman eklenip alınır. Kullanım bittikten sonra kuyruk veri yapısı destroy_squeue fonksiyonu ile boşaltılmalıdır. Programın testi için derlemeyi şöyle yapabilirsiniz: $ gcc -o sample sample.c syncqueue.c -lpthread ---------------------------------------------------------------------------------------------------------------------------*/ /* syncqueue.h */ #ifndef SYNCQUEUE_H_ #define SYNCQUEUE_H_ #include #include /* Type Declarations */ typedef int DATATYPE; typedef struct tagSYNC_QUEUE { DATATYPE *queue; size_t size; size_t head; size_t tail; sem_t sem_producer; sem_t sem_consumer; } SYNC_QUEUE; /* Function Prototypes */ SYNC_QUEUE *init_squeue(size_t size); int put_squeue(SYNC_QUEUE *sd, DATATYPE val); int get_squeue(SYNC_QUEUE *sd, DATATYPE *val); int destroy_squeue(SYNC_QUEUE *sq); #endif /* syncqueue.c */ #include #include #include "syncqueue.h" /* Function Definitions */ SYNC_QUEUE *init_squeue(size_t size) { SYNC_QUEUE *sq; if ((sq = (SYNC_QUEUE *)malloc(sizeof(SYNC_QUEUE))) == NULL) goto FAILED1; if ((sq->queue = (DATATYPE *)malloc(sizeof(DATATYPE) * size)) == NULL) goto FAILED2; sq->size = size; sq->head = sq->tail = 0; if (sem_init(&sq->sem_producer, 0, size) == -1) goto FAILED3; if (sem_init(&sq->sem_consumer, 0, 0) == -1) goto FAILED3; return sq; FAILED3: free(sq->queue); FAILED2: free(sq); FAILED1: return NULL; } int put_squeue(SYNC_QUEUE *sq, DATATYPE val) { if (sem_wait(&sq->sem_producer) == -1) return -1; sq->queue[sq->tail++] = val; sq->tail %= sq->size; if (sem_post(&sq->sem_consumer) == -1) return -1; return 0; } int get_squeue(SYNC_QUEUE *sq, DATATYPE *val) { if (sem_wait(&sq->sem_consumer) == -1) return -1; *val = sq->queue[sq->head++]; sq->head %= sq->size; if (sem_post(&sq->sem_producer) == -1) return -1; return 0; } int destroy_squeue(SYNC_QUEUE *sq) { if (sem_destroy(&sq->sem_producer) == -1) return -1; if (sem_destroy(&sq->sem_consumer) == -1) return -1; free(sq->queue); free(sq); return 0; } /* sample.c */ #include #include #include #include #include #include #include "syncqueue.h" 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); int main(void) { pthread_t tid1, tid2; int result; SYNC_QUEUE *sd; if ((sd = init_squeue(10)) == NULL) exit_sys("init_squeue"); if ((result = pthread_create(&tid1, NULL, thread_producer, sd)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_consumer, sd)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if (destroy_squeue(sd) == -1) exit_sys("destroy_squeue"); return 0; } void *thread_producer(void *param) { SYNC_QUEUE *sd = (SYNC_QUEUE *)param; int val; unsigned seed; seed = time(NULL) + 123; val = 0; for (;;) { usleep(rand_r(&seed) % 300000); if (put_squeue(sd, val) == -1) exit_sys("put_squeue"); if (val == 99) break; ++val; } return NULL; } void *thread_consumer(void *param) { SYNC_QUEUE *sd = (SYNC_QUEUE *)param; int val; unsigned seed; seed = time(NULL) + 456; for (;;) { if (get_squeue(sd, &val) == -1) exit_sys("get_squeue"); 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); } /*-------------------------------------------------------------------------------------------------------------------------- İsimsiz semaphore nesneleri de yine istenirse sem_init fonksiyonunda pshared parametresi sıfır dışı yapılarak ve paylaşılan bellek alanında oluşturularak prosesler arasında kullanılabilir. Ancak prosesler arasında kullanım için isimli semaphore nesneleri genellikle daha uygundur. Aşağıda iki proses arasında isimsiz semaphore nesneleri ile üretici tüketici problemi uygulanmıştır. Burada önce "producer" programını çalıştırınız. Derleme işlemlerini aşağıdaki gibi yapabilirsiniz: $ gcc -o producer producer.c $ gcc -o consumer consumer.c ---------------------------------------------------------------------------------------------------------------------------*/ /* sharing.h */ #ifndef SHARING_H_ #define SHARING_H_ #include #include #define SHM_NAME "/producer-consumer" #define QUEUE_SIZE 10 typedef struct tagSHARED_INFO { sem_t sem_producer; sem_t sem_consumer; int head; int tail; int queue[QUEUE_SIZE]; } SHARED_INFO; #endif /* producer.c */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int fdshm; SHARED_INFO *shminfo; int val; srand(time(NULL)); if ((fdshm = shm_open(SHM_NAME, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, sizeof(SHARED_INFO)) == -1) exit_sys("ftruncate"); shminfo = (SHARED_INFO *)mmap(NULL, sizeof(SHARED_INFO), PROT_WRITE, MAP_SHARED, fdshm, 0); if (shminfo == MAP_FAILED) exit_sys("mmap"); if ((sem_init(&shminfo->sem_producer, 1, QUEUE_SIZE)) == -1) exit_sys("sem_init"); if ((sem_init(&shminfo->sem_consumer, 1, 0)) == -1) exit_sys("sem_init"); shminfo->head = 0; shminfo->tail = 0; val = 0; for (;;) { usleep(rand() % 300000); if (sem_wait(&shminfo->sem_producer) == -1) exit_sys("sem_wait"); shminfo->queue[shminfo->tail++] = val; shminfo->tail %= QUEUE_SIZE; if (sem_post(&shminfo->sem_consumer) == -1) exit_sys("sem_post"); if (val == 99) break; ++val; } if (sem_destroy(&shminfo->sem_consumer) == -1) exit_sys("sem_destroy"); if (sem_destroy(&shminfo->sem_producer) == -1) exit_sys("sem_destroy"); if (munmap(shminfo, sizeof(SHARED_INFO)) == -1) exit_sys("munmap"); close(fdshm); if (shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* consumer.c */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int fdshm; SHARED_INFO *shminfo; int val; srand(time(NULL)); if ((fdshm = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); shminfo = (SHARED_INFO *)mmap(NULL, sizeof(SHARED_INFO), PROT_WRITE, MAP_SHARED, fdshm, 0); if (shminfo == MAP_FAILED) exit_sys("mmap"); for (;;) { if (sem_wait(&shminfo->sem_consumer) == -1) exit_sys("sem_wait"); val = shminfo->queue[shminfo->head++]; shminfo->head %= QUEUE_SIZE; if (sem_post(&shminfo->sem_producer) == -1) exit_sys("sem_post"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if (val == 99) break; } printf("\n"); if (munmap(shminfo, sizeof(SHARED_INFO)) == -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); } /* consumer.c */ /*-------------------------------------------------------------------------------------------------------------------------- İsimli POSIX semaphore nesneleri daha önce görmüş olduğumuz POSIX paylaşılan bellek alanı ve POSIX mesaj kuyruklarına benzer biçimde kullanılmaktadır. Kullanım adımları şöyledir: 1) İsimli POSIX semaphore nesneleri sem_open fonksiyonu ile yaratılır ya da zaten var olan nesne açılır. Fonksiyonun prototipi şöyledir: #include sem_t *sem_open(const char *name, int oflag, ...); Fonksiyonun birinci parametresi prosesler arasında kullanım için gereken ismi belirtmektedir. Bu isim yine diğer POSIX IPC nesnelerinde olduğu gibi kök dizinde bir dosya ismi gibi verilmelidir. İkinci parametre açış modunu belirtir. Burada eğer O_CREAT kullanılırsa nesne yoksa yaratılır, varsa olan açılır. O_CREAT ile O_EXCL birlikte kullanılabilir. Bu durumda nesne zaten varsa fonksiyon başarısız olmaktadır. Burada açış modunda O_RDONLY, O_WRONLY ya da O_RDWR gibi bayraklar kullanılmaz. Başka bir deyişle isimli semaphore nesnelerinde okuma yapmak ya da onlara yazma yapmak biçiminde işlemler tanımlı değildir. Eğer zaten var olan bir semaphore nesnesi açılacaksa bu ikinci parametre 0 geçilebilir. Eğer semaphore nesnesinin yaratılması söz konusu ise sem_open fonksiyonuna iki argüman daha girilmelidir. Bu durumda üçüncü parametre nesnenin erişim haklarını, dördüncü parametre ise semaphore sayacının başlangıçtaki değerini belirtmektedir. Yani bu durumda adeta fonksiyonun prototipi aşağıdaki gibi olmaktadır: sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); Semaphore nesneleri için "okuma ve yazma" eylemlerinin tanımsız olduğunu belirtmiştik. Bu nedenle POSIX standartları da nesnenin erişim haklarının hangi durumda erişime izin vereceği yönünde bir açıklama yapmamıştır. Linux sistemlerinde isimli semaphore nesnesi yaratılırken bir prosesin bu nesneye erişmesi isteniyorsa, erişim haklarında hem read hem de write bulunuyor olmalıdır. Yani erişim işlemi sanki "read/write" düzeyde (O_RDWR) yapılan bir işlem gibi kontrole girmektedir. Ancak buradaki erişim hakkı diğer dosya yaratan fonksiyonlarda olduğu gibi prosesin umask değerinden etkilenmektedir. Fonksiyon başarı durumunda kendi yarattığı semaphore nesnesinin adresine, başarısızlık durumunda SEM_FAILED özel değerine geri döner. errno değişkeni uygun biçimde set edilmektedir. Örneğin: sem_t *sem; ... if ((sem = sem_open("/my-test-semaphore", O_CREAT, S_IRUSR|S_IWUSR, 1)) == SEM_FAILED) exit_sys("sem_open"); 2) Artık kritik kod yine isimsiz semaphore nesnelerinde olduğu gibi sem_wait ve sem_post fonksiyonlarıyla oluşturulabilir. Örneğin (kontroller uygulanmamıştır): sem_wait(sem); ... ... ... sem_post(sem); 3) Semaphore nesnesinin kullanımı bittikten sonra nesne sem_close fonksiyonu ile (sem_destroy fonksiyonu ile değil) boşaltılmalıdır. Örneğin: sem_close(sem); Fonksiyonun prototipi şöyledir: #include int sem_close(sem_t *sem); Fonksiyon, semaphore nesnesinin adresini alır ve nesneyi kapatır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. errno değişkeni uygun biçimde set edilmektedir. 4) Nihayet isimli semaphore nesnesi sem_unlink fonksiyonu ile yok edilebilir. Eğer nesne yok edilmezse "kernel persistant" bir biçimde sistem reboot edilene kadar kalmaya devam eder. sem_unlik fonksiyonunun prototipi şöyledir: #include int sem_unlink(const char *name); Fonksiyon, 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. errno değişkeni uygun biçimde set edilmektedir. Linux'ta isimli POSIX semaphore nesneleri de tıpkı POSIX paylaşılan bellek alanı nesnelerinde olduğu gibi /dev/shm dizininde görüntülenmektedir. Ancak bu dizinde bu nesnelerin isimlerinin başında "sem." öneki bulunmaktadır. Programcı isterse bu nesneleri komut satırında rm komutuyla silebilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda prosesler arasında üretici-tüketici problemi isimli semaphore nesneleri ile çözülmüştür. Derleme işlemleri aşağıdaki gibi yapılabilir: $ gcc -o producer producer.c $ gcc -o consumer consumer.c Test işleminde önce "producer" programının çalıştırılması gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /* shared.h */ #ifndef SHARED_H_ #define SHARED_H_ #include #define SHM_PATH "/sample_shared_memory" #define SEM_PATH_PRODUCER "/sample_producer_semaphore" #define SEM_PATH_CONSUMER "/sample_consumer_semaphore" #define QUEUE_SIZE 10 struct QUEUE { int qarray[QUEUE_SIZE]; size_t head; size_t tail; }; #endif /* producer.c */ #include #include #include #include #include #include #include #include #include #include #include "shared.h" void exit_sys(const char *msg); int main(void) { int fdshm; struct QUEUE *queue; sem_t *sem_producer; sem_t *sem_consumer; int val; srand(time(NULL)); 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 ((queue = (struct QUEUE *)mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) exit_sys("mmap"); memset(queue, 0, sizeof(struct QUEUE)); if ((sem_producer = sem_open(SEM_PATH_PRODUCER, O_CREAT, S_IRUSR|S_IWUSR, QUEUE_SIZE)) == SEM_FAILED) exit_sys("sem_open"); if ((sem_consumer = sem_open(SEM_PATH_CONSUMER, O_CREAT, S_IRUSR|S_IWUSR, 0)) == SEM_FAILED) exit_sys("sem_open"); val = 0; for (;;) { usleep(rand() % 300000); if (sem_wait(sem_producer) == -1) exit_sys("sem_wait"); queue->qarray[queue->tail] = val; queue->tail = (queue->tail + 1) % QUEUE_SIZE; if (sem_post(sem_consumer) == -1) exit_sys("sem_post"); if (val == 99) break; ++val; } sem_destroy(sem_consumer); sem_destroy(sem_producer); if (sem_unlink(SEM_PATH_CONSUMER) == -1 && errno != ENOENT) exit_sys("sem_unlink"); if (sem_unlink(SEM_PATH_PRODUCER) == -1 && errno != ENOENT) exit_sys("sem_unlink"); if (munmap(queue, 4096) == -1) exit_sys("munmap"); close(fdshm); if (shm_unlink(SHM_PATH) == -1 && errno != ENOENT) exit_sys("shm_unlink"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* consumer.c */ #include #include #include #include #include #include #include #include #include #include #include "shared.h" void exit_sys(const char *msg); int main(void) { int fdshm; struct QUEUE *queue; sem_t *sem_producer; sem_t *sem_consumer; int val; srand(time(NULL)); 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 ((queue = (struct QUEUE *)mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) exit_sys("mmap"); if ((sem_producer = sem_open(SEM_PATH_PRODUCER, O_CREAT, S_IRUSR|S_IWUSR, QUEUE_SIZE)) == SEM_FAILED) exit_sys("sem_open"); if ((sem_consumer = sem_open(SEM_PATH_CONSUMER, O_CREAT, S_IRUSR|S_IWUSR, 0)) == SEM_FAILED) exit_sys("sem_open"); for (;;) { if (sem_wait(sem_consumer) == -1) exit_sys("sem_wait"); val = queue->qarray[queue->head]; queue->head = (queue->head + 1) % QUEUE_SIZE; if (sem_post(sem_producer) == -1) exit_sys("sem_post"); usleep(rand() % 300000); printf("%d ", val); fflush(stdout); if (val == 99) break; } putchar('\n'); sem_destroy(sem_consumer); sem_destroy(sem_producer); if (sem_unlink(SEM_PATH_CONSUMER) == -1 && errno != ENOENT) exit_sys("sem_unlink"); if (sem_unlink(SEM_PATH_PRODUCER) == -1 && errno != ENOENT) exit_sys("sem_unlink"); if (munmap(queue, 4096) == -1) exit_sys("munmap"); close(fdshm); if (shm_unlink(SHM_PATH) == -1 && errno != ENOENT) exit_sys("shm_unlink"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 60. Ders 17/06/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi POSIX IPC nesneleri 90'lı yılların ortalarında POSIX standartlarına sokuldu. Daha önceleri klasik Sistem 5 IPC nesneleri kullanıyordu. İşte klasik IPC nesnelerinin içerisinde semaphore nesneleri de bulunmaktadır. Ancak maalesef Sistem 5 semaphore arayüzü oldukça karışıktır. Bu nedenle artık yeni programların eğer özel bir taşınabilirlik sorunu yoksa POSIX semaphore nesnelerini kullanması uygun olur. Sistem 5 semaphore nesneleri, aynı prosesin thread'leri arasında değil farklı prosesler arasında kullanım için düşünülmüştür. (Zaten bunların tasarlandığı yıllarda henüz thread'ler uygulamaya girmemişti.) Halbuki isimsiz POSIX semaphore nesnelerini aynı prosesin thread'leri arasında kullanabilmekteyiz. Sistem 5 semaphore nesnelerinin isimlendirme biçimleri ve temel parametrik yapıları diğer Sistem 5 IPC nesnelerine oldukça benzemektedir. Sistem 5 semaphore arayüzünü karışık yapan unsurlardan biri tek hamlede birden fazla semaphore üzerinde (buna semaphore set de denilmektedir) işlem yapılmasıdır. Ayrıca sayaç mekanizması da biraz karışık ve çok işlevli tasarlanmıştır. Biz kursumuzda Sistem 5 semaphore nesneleri için çok ayrıntıya girmeyeceğiz. Yukarıda da belirttiğimiz gibi karmaşık tasarımlarından dolayı bunlar gittikçe POSIX semaphore nesneleri lehine daha az kullanılır hale gelmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sistem 5 semaphore nesnelerinin tipik kullanımı şöyledir: 1) Semaphore nesnesi semget fonksiyonu ile (diğer fonksiyonların shmget ve msgget biçiminde isimlendirildiğini anımsayınız) yaratılır ya da zaten var olan açılır. Sistem 5 IPC nesnelerinde isim yerine numara (key) kullanıldığını anımsayınız. semget fonksiyonunun prototipi şöyledir: #include int semget(key_t key, int nsems, int semflg); Fonksiyonun birinci parametresi prosesler arasında kullanım için gereken anahtar değerdir. Anımsanacağı gibi Sistem 5 IPC nesneleri birer anahtar verilerek yaratılıp açılmakta ve bu işlemden bir id değeri elde edilmektedir. Bu id değerinin sistem genelinde tek (unique) olduğunu anımsayınız. Yine anımsayacağınız gibi bu anahtarı isim gibi kullanabilmek için ftok fonksiyonundan faydalanılabiliyordu. Tabii fonksiyonun birinci parametresi diğer Sistem 5 IPC nesnelerinde olduğu gibi IPC_PRIVATE biçiminde girilebilir. Bu durumda sistem olmayan bir anahtar kullanarak bize bir id vermektedir. Fonksiyonun ikinci parametresi semaphore kümesindeki semaphore sayısını belirtmektedir. Yani bu parametre toplam kaç tane semaphore'un yaratılacağını belirtmektedir. Fonksiyonun üçüncü parametresi yaratılacak IPC nesnesinin erişim haklarını almaktadır. Bu parametreye ayrıca IPC_CREAT ve IPC_EXCL bayrakları da eklenebilir. Yine IPC_CREAT bayrağı nesne yoksa onu yaratmak için kullanılmaktadır. IOC_EXCL tek başına kullanılamaz. Ancak IPC_CREAT|IPC_EXCL biçiminde kullanılabilir. Bu durumda nesne zaten varsa semget fonksiyonu başarısız olmaktadır. semget fonksiyonu başarı durumunda IPC nesnesinin id değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Örneğin: if ((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if ((semid = semget(key, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); Biz bu notlarda "Sistem 5 semaphore nesnesi" demekle aslında bir semaphore kümesini kastetmiş olacağız. 2) Sistem 5 semaphore nesnesi yaratıldıktan sonra artık bu semaphore nesnesi içerisindeki semaphore'ların ilk değerlerinin verilmesi gerekir. Çünkü Sistem 5 semaphore nesneleri aslında bir semaphore kümesi içermektedir. Yani biz semget ile aslında bir semaphore kümesi yaratmış olmaktayız. Yaratılan semaphore kümesindeki her semaphore'un ilk semaphore "0" olmak üzere bir indeks numarası vardır. Semaphore kümesindeki semaphore'lar üzerinde işlem yapmak için semctl isimli fonksiyon kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int semctl(int semid, int semnum, int cmd, ...); Fonksiyonun birinci parametresi semaphore nesnesinin id değerini belirtmektedir. İkinci parametresi semaphore kümesindeki üzerinde işlem yapılacak olan semaphore'un indeks numarasını belirtir. Fonksiyonun üçüncü parametresi semaphore kümesindeki semaphore'lara uygulanacak işlemleri belirtmektedir. Bu işlemlere göre fonksiyona bir dördüncü parametre gerekebilmektedir. Eğer fonksiyon çağrılırken bir dördüncü argüman girilecekse bu argüman aşağıdaki gibi bir birlik türünden olmak zorundadır: union semun { int val; struct semid_ds *buf; unsigned short *array; } arg; Birlik elemanlarının çakışık yerleştirildiğini anımsayınız. Yani aslında fonksiyonun bu dördüncü parametresi, üçüncü parametredeki işleme göre int, struct semid_ds türünden bir adres ya da unsigned short türden bir adres olabilmektedir. Ancak karışıklığı engellemek için bu türlerin hepsi bir birlik (union) içerisinde toplanmıştır. Maalesef bu birlik bildirimi herhangi bir başlık dosyasında bulunmamaktadır. Dolayısıyla bu bildirimi programcının kendisinin yapması gerekmektedir. semctl fonksiyonu başarısızlık durumunda -1 değerine geri dönmektedir ve errno uygun biçimde set edilmektedir. semctl fonksiyonunun üçüncü parametresi semaphore kümesindeki semaphore üzerinde hangi işlemin yapılacağını belirtmektedir. Bu işlem şunlardan biri olabilir (IPC_RMID komutu daha sonra açıklanacaktır): GETVAL: İkinci parametre ile belirtilen semaphore'un semaphore sayaç değerinin elde edilmesi için kullanılır. Bu durumda semctl fonksiyonu semaphore kümesindeki ikinci parametrede belirtilen semaphore'un sayaç değerine geri döner. Bu işlemin yapılabilmesi için prosesin semaphore nesnesine "read" hakkının bulunuyor olması gerekir. Örneğin (kontroller yapılmamıştır): result = semctl(semid, 1, GETVAL); Burada biz semaphore kümesi içerisindeki 1 numaralı semaphore'un sayaç değerini elde etmiş olduk. SETVAL: Bu durumda ikinci parametreyle belirtilen semaphore'a ilişkin semaphore sayacı fonksiyona girilecek olan dördüncü argümandaki birliğin val elemanı ile set edilmektedir. Bunun için prosesin semaphore nesnesine "write" hakkına sahip olması gerekmektedir. Örneğin (kontroller yapılmamıştır): union semun { int val; struct semid_ds *buf; unsigned short *array; } arg; ... arg.val = 5; semctl(semid, 1, SETVAL, arg); Burada biz 1 numaralı semaphore'un semaphore sayacını 5 olarak set etmiş olduk. GETALL: Bu komutla semaphore kümesindeki belli bir semaphore'un değil tüm semaphore'ların semaphore sayaç değerleri elde edilmektedir. Bu durumda fonksiyona girilecek dördüncü argüman olan birliğin array elemanı semaphore sayaçlarının yerleştirileceği unsigned short türünden dizinin başlangıç adresini göstermelidir. Yani programcı önce unsigned short türden semaphore kümesindeki semaphore sayısı kadar bir dizi açmalı ve bu dizinin adresini birliğin array elemanına yerleştirmeli ve bu birliği de semctl fonksiyonunun son elemanına girmelidir. Örneğin semaphore kümemizde iki semaphore bulunuyor olsun (kontroller yapılmamıştır): union semun { int val; struct semid_ds *buf; unsigned short *array; } arg; unsigned short semvals[2]; ... arg.array = semvals; semctl(semid, 0, GETALL, arg); Bu arada artık semaphore kümesindeki tüm semaphore'ların semaphore sayaçları semvals dizisine yerleştirilecektir. Tabii GETALL komutunda artık fonksiyonun ikinci parametresindeki semaphore numarası dikkate alınmamaktadır. Bu işlemin yapılabilmesi için yine prosesin semaphore nesnesine "read" hakkının olması gerekmektedir. SETALL: Bu komut semaphore kümesindeki tüm semaphore'ların semaphore sayaçlarını set etmek için kullanılmaktadır. Yine bunun için fonksiyonun dördüncü parametresine yukarıdaki birlik türünden bir nesne geçirilir. Birliğin array elemanı semaphore sayaçlarının değerinin bulunduğu unsigned short türünden dizinin başlangıç adresini göstermelidir. Tabii bu dizinin yine semaphore kümesindeki semaphore sayısı kadar uzunlukta olması gerekir. Örneğin semaphore kümesinde iki semaphore bulunuyor olsun (kontroller yapılmamıştır): union semun { int val; struct semid_ds *buf; unsigned short *array; } arg; unsigned short semvals[2] = {0, 10}; ... arg.array = semvals; semctl(semid, 0, SETALL, arg); Burada semaphore kümesindeki ilk semaphore'un sayacı "0" olarak ikinci semaphore'un sayacı "10" olarak set edilmiştir. Tabii yine bu durumda semctl fonksiyonunun ikinci parametresi fonksiyon tarafından kullanılmamaktadır. Bu işlemin yapılabilmesi için yine prosesin semaphore nesnesine "write" hakkının olması gerekmektedir. GETPID: Bu komutta fonksiyonun dördüncü parametresine gereksinim yoktur. semctl fonksiyonu bize ikinci parametreyle belirtilen semaphore üzerinde en son hangi prosesin semop uyguladığı bilgisini verir. Bu komuta çok seyrek gereksinim duyulmaktadır. Bu komut için de prosesin semaphore nesnesine "read" hakkının olması gerekmektedir. GETNCNT ve GETZCNT: Bu komutlar da sırasıyla semaphore sayacının artırılmasını bekleyen ve "0" olmasını bekleyen proseslerin sayısını elde etmek için kullanılmaktadır. Bu komutlar için de prosesin semaphore nesnesine "read" hakkının olması gerekmektedir. Semaphore nesneleri yine "kernel persistant" biçimdedir. Yani sistem reboot edilene kadar ya da semaphore nesnesi silinene kadar kalıcı olmaktadır. 3) Kritik kod semop fonksiyonu ile oluşturulmaktadır. Maalesef semop fonksiyonunun kullanımı POSIX semophore nesnelerinden biraz daha karmaşıktır. Fonksiyonun prototipi şöyledir: #include int semop(int semid, struct sembuf *sops, size_t nsops); Fonksiyonun birinci parametresi semaphore nesnesinin id değerini belirtmektedir. İkinci parametre sembuf isimli bir yapı nesnesinin adresini almaktadır. Bu yapı içerisinde bildirilmiştir. Bu nesnenin içi fonksiyonu çağıran kişi tarafından doldurulur. sembuf yapısı şöyle bildirilmiştir: struct sembuf { unsigned short sem_num; /* semaphore number */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */ }; Yapının sem_num elemanı semaphore kümesindeki işlem yapılacak semaphore'u belirtmektedir. İkinci parametre semaphore sayacı üzerinde yapılacak işlemi belirtmektedir. Bu parametreye aşağıdaki üç durumdan birine ilişkin bir değer girilebilir: a) Eğer yapının sem_op elemanının değeri 0'dan büyük bir değerse bu değer semaphore sayacına toplanır. Örneğin o anda semaphore'un sayacının 1 olduğunu düşünelim. Biz sem_op değerine 1 yerleştirirsek artık semaphore sayaç değeri 2 olacaktır. b) Eğer yapının sem_op elemanında negatif bir değer varsa bu durumda semop fonksiyonu o andaki semaphore sayacının değerine bakar. Eğer o anda semaphore sayacından burada belirtilen değer çıkartıldığında (yani negatif değerin mutlak değeri çıkartıldığında) semaphore sayacı 0'ın altına düşecek gibi bir durum oluşursa thread'i bloke eder. Ancak bu durumda henüz çıkartma işlemini yapmaz. Ancak semaphore sayacının değerinden bu negatif değerin mutlak değeri çıkartıldığında sonuç 0 ya da 0'dan büyük olacaksa fonksiyon bu çıkartmayı yapar ve blokeye yol açmadan geri döner. Örneğin: - Semaphore sayacının değeri 5 olsun. Biz de sem_op değerine -4 girmiş olalım. Bu durumda 5 - 4 = 1 olduğu için bir bloke oluşmaz. semop başarıyla geri döner ve artık semaphore sayacının değeri 1 olur. - Semaphore sayacının değeri 5 olsun biz de sem_op değerine -5 girmiş olalım. Bu durumda 5 - 5 = 0 olduğu için bloke oluşmaz. semop başarıyla geri döner ve artık semaphore sayacının değeri 0 olur. - Semaphore sayacının değeri 5 olsun. Biz de sem_op değerine -10 girmiş olalım. Şimdi 5 - 10 = -5'tir. Yani semaphore sayacı negatif bir değere düşecek gibi olmuştur. İşte fonksiyon bu durumda asla semaphore sayacını negatif bir değere düşürmeyeceği için thread'i bloke eder. Artık bu blokeden kurtulmanın yolu semaphore sayacını 10 ya da 10'un üzerine çekmektir. Şimdi başka bir prosesin sem_op değerine 5 girerek semaphore sayacını 10'a çektiğini düşünelim. Artık bu proses 10 - 10 = 0 olacağı için blokeden çıkabilecek ve semaphore sayacı 0 olarak set edilecektir. Buradaki önemli nokta şudur: Aslında semop hiçbir zaman semaphore sayacını 0'ın altına düşürmemektedir. Eğer semaphore sayacı 0'ın altına düşecek gibi bir durum oluşursa zaten thread'i blokede bekletmektedir. c) Eğer yapının sem_op elemanında 0 değeri varsa bu özel ve başka bir durum anlamına gelmektedir. Bu durumda semaphore sayacı 0'a düşürülene kadar ilgili proses blokede bekletilir. Blokeden çıkmanın yolu semaphore sayacını 0'ın yukarısına çekmektedir. Örneğin semaphore'un semaphore sayacı 1 olsun. 10 tane proses sem_op değerini 0'a çekerek blokede bekleyebilir. Sonra semaphore sayacı 1 eksiltildiğinde (yani 0'a çekildiğinde) bu 10 proses de blokeden kurtulacaktır. Bu haliyle Sistem 5 semaphore'ları birden fazla prosesi blokede bekletip uyandırmak için kullanılabilmektedir. Ancak bu kullanım çok seyrektir. Yani sem_op değeri 0 ise bu durum "semaphore sayacı 0 olmadığı sürece blokede bekle" anlamına gelmektedir. sembuf yapısının sem_flg elemanı IPC_NOWAIT, SEM_UNDO bayraklarının birini ya da her ikisini içerebilir. Tabii bu bayraklardan herhangi birisi girilmeyecekse bu elemana 0 değeri girilmelidir. IPC_NOWAIT blokesiz işlem yapmak için kullanılmaktadır. Eğer bu bayrak belirtilirse bloke oluşturabilecek durumlarda bloke oluşmaz ve semop fonksiyonu -1 değeri ile geri döner ve errno değişkeni EAGAIN değeri ile set edilir. SEM_UNDO bayrağı, proses sonlandığında "semaphore adjustment" değerini (semadj) işleme sokarak ters işlem yapmaktadır. semop fonksiyonun üçüncü parametresi, ikinci parametredeki sembuf dizisinin eleman sayısını belirtmektedir. Yani aslında ikinci parametreye tek bir sembuf nesnesinin adresi değil, bir sembuf dizisinin adresi geçirilebilmektedir. Bu durumda semop fonksiyonu birden fazla semaphore üzerinde işlem yapar. Fakat böyle bir kullanım genellikle çok seyrektir. Örneğin bizim semaphore nesnemizin (kümemizin) içerisinde iki semaphore olsun. Biz tek bir semop çağrısıyla bir semaphore'un sayacını 1 artırırken diğerini 1 eksiltmek isteyebiliriz. Tabii bu kullanım seyrektir. Dolayısıyla genellikle fonksiyonun son parametresi 1 geçilir. Eğer semop fonksiyonunda birden fazla semaphore için işlem yapılacaksa bu işlemler atomik bir biçimde yapılmaktadır. Yani örneğin biz semop fonksiyonu ile iki semaphore'un sayacını 1 eksiltmek isteyelim. Semaphore'lardan birinin sayacı 1, diğerinin sayacı 0 olsun. Şimdi biz sayacı 1 olan semaphore'un sayacını 1 eksiltebiliriz. Ancak sayacı 0 olan semaphore'un sayacını 1 eksiltemeyiz. O halde biz bloke oluruz. Ancak bloke olurken sayacı 1 olan semaphore'un sayacı eksiltilmemektedir. Tüm işlemler eğer yapılacaksa tek hamlede tek bir işlem gibi yapılmaktadır. semop fonksiyonu 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. Pekiyi Sistem 5 semaphore'ları ile kritik kod nasıl oluşturulmaktadır? İşte bunun tipik senaryosu şöyledir: Başlangıçta semaphore sayacı semctl fonksiyonu ile ayarlanır. Sonra kritik kod aşağıdaki gibi oluşturulur (kontroller yapılmamıştır): struct sembuf sbuf; ... sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flags = 0; semop(semid, &sbuf, 1); ... ... ... sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flags = 0; semop(semid, &sbuf, 1); 4) Sistem 5 semaphore nesneleri de diğer Sistem 5 IPC nesneleri gibi "kernel persistant" biçimdedir. Yani silinene kadar ya da reboot işlemine kadar kalıcıdır. Semaphore nesnelerini silmek için yine semctl fonksiyonunda IPC_RMID kullanmak gerekir. Bu durumda semctl fonksiyonunun semaphore numarasını alan ikinci parametresi dikkate alınmamaktadır. Örneğin: if (semctl(semid, 0, IPC_RMID) == -1) exit_sys("semctl"); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 61. Ders 18/06/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Tıpkı Sistem 5 paylaşılan bellek alanı ve mesaj kuyruklarında olduğu gibi Sistem 5 semaphore nesnelerinde de yine semctl fonksiyonunda IPC_GET ve IPC_SET komut kodlarıyla semaphore nesnelerinin bazı değerleri get ve set edilebilmektedir. Sistem 5 semaphore nesneleri için işletim sistemi semid_ds isimli bir yapı nesnesi oluşturmaktadır. Bu yapı nesnesi şöyle bildirilmiştir: struct semid_ds { struct ipc_perm sem_perm; /* Ownership and permissions */ time_t sem_otime; /* Last semop time */ time_t sem_ctime; /* Creation time/time of last modification via semctl() */ unsigned long sem_nsems; /* No. of semaphores in set */ }; Bu yapının ilk elemanı yine IPC nesnesinin erişim haklarını belirtir. İkinci ve üçüncü elemanları semaphore nesnesi üzerinde yapılan son işlemlerin zamanları hakkında bilgi verir. Son elemanı ise semaphore kümesindeki semaphore sayısını belirtmektedir. semctl ile ilk değer verilmemiş semaphore'lar için yapının sem_ctime ve sem_nsems elemanları çöp değerlerdedir. Standartlar henüz semop fonksiyonu uygulanmadan yapının sem_otime elemanının 0 olacağını garanti etmektedir. Bu garanti sayesinde iki proses aynı semaphore'a ilişkin initialize işlemi yapmak istediğinde oluşan sorun çözülebilmektedir. İzleyen paragraflarda bu durum yeniden açıklanacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sistem 5 semaphore nesneleri proseslerarası kullanım için düşünülmüştür. Özellikle prosesler arasındaki üretici-tüketici problemi gibi tipik senkronizasyon problemleri eskiden bu nesnelerle çözülüyordu. Tabii bu nesneler aslında ağırlıklı olarak Sistem 5 paylaşılan bellek alanlarıyla birlikte kullanılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sistem 5 semaphore nesnelerinin kullanımı POSIX semaphore nesnelerine göre daha zahmetlidir. Ancak istersek sarma fonksiyonlar yazarak Sistem 5 semaphore nesnelerini POSIX semaphore nesneleri gibi de kullanabiliriz. Böylece bu karmaşıklığı ortadan kaldırabiliriz. Aşağıda 6 tane sarma fonksiyon yazılmıştır: int sem_create(int key, int mode); int sem_open(int key); int sem_init(int semid, int val); int sem_wait(int semid); int sem_post(int semid); int sem_destroy(int semid); Bu fonksiyonlar semaphore nesnesindeki tek bir semaphore üzerinde çalışmaktadır. sem_create fonksiyonu anahtar ve erişim haklarını alarak semaphore nesnesini yaratır. sem_open fonksiyonu yaratılmış olanı açar. sem_init fonksiyonu semaphore sayacına değerini atar. sem_wait ve sem_post fonksiyonları kritik kod oluşturmakta kullanılır. Nihayet sem_destroy fonksiyonu da semaphore nesnesini yok eder. ---------------------------------------------------------------------------------------------------------------------------*/ int sem_create(int key, int mode) { return semget(key, 1, IPC_CREAT|mode); } int sem_open(int key) { return semget(key, 1, 0); } int sem_init(int semid, int val) { union semun { int val; struct semid_ds *buf; unsigned short *array; struct seminfo *__buf; } su; su.val = val; return semctl(semid, 0, SETVAL, su); } int sem_wait(int semid) { struct sembuf sb; sb.sem_num = 0; sb.sem_op = -1; sb.sem_flg = 0; return semop(semid, &sb, 1); } int sem_post(int semid) { struct sembuf sb; sb.sem_num = 0; sb.sem_op = 1; sb.sem_flg = 0; return semop(semid, &sb, 1); } int sem_destroy(int semid) { return semctl(semid, 0, IPC_RMID); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda Sistem 5 paylaşılan bellek alanları ve Sistem 5 semaphore nesneleri ile kuyruklu üretici-tüketici problemi örneği verilmiştir. Bu örnekte paylaşılan alan aşağıdaki yapıyla temsil edilmiştir: typedef struct tagSHARED_INFO { int head; int tail; int queue[QUEUE_SIZE]; int semid; /* two semaphore, 0 is producer, 1 is consumer */ } SHARED_INFO; Burada Sistem 5 semaphore'unun id değeri de bu yapının içerisinde bulundurulmuştur. Örneğimizde paylaşılan bellek alanını ve semaphore nesnesini üretici program (producer.c) yaratmaktadır. Tüketici program (consumer.c) yalnızca yaratılmış olan nesneleri kullanmaktadır. Tabii bu nedenle önce üretici programın çalıştırılması gerekmektedir. IPC nesnelerini üretici program yarattığı için bunları yine üretici program silmektedir. Anımsanacağı gibi Sistem 5 paylaşılan bellek alanı shmctl fonksiyonununda IPC_RMID ile silinse bile onu kullanan prosesler shmdt ile alanı serbest bırakmadan silme işlemi gerçek anlamda yapılmıyordu. Ancak örnekte şöyle bir problem vardır: Tüketici program, üretici program bu nesneleri yarattıktan sonra çalıştırılmalıdır. Benzer biçimde üretici program, tüketici program işini bitirdikten sonra semaphore nesnesini silmelidir. Bunun için biz programın bitişine henüz silme işlemi yapılmadan bir bekleme yerleştirdik. Eğer tüketici programın da önce çalıştırılabilmesi isteniyorsa bu problemli durum başka bir senkronizasyon nesnesi ile (örneğimizde bir Sistem 5 semaphore'u olabilir) çözülebilir. Programda semaphore nesnesinin IPC_PRIVATE ile yaratıldığına dikkat ediniz. Bu durumda programcının çakışmayan bir anahtar belirlemesine gerek kalmamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* sharing.h */ #ifndef SHARED_H_ #define SHARED_H_ #define SHM_KEY 0x12345678 #define QUEUE_SIZE 10 typedef struct tagSHARED_INFO { int head; int tail; int queue[QUEUE_SIZE]; int semid; /* two semaphore, 0 is producer, 1 is consumer */ } SHARED_INFO; #endif /* producer.c */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char *msg); int main(void) { int shmid; SHARED_INFO *shminfo; struct sembuf sbuf; unsigned short semvals[] = {QUEUE_SIZE, 0}; int val; srand(time(NULL)); if ((shmid = shmget(SHM_KEY, sizeof(SHARED_INFO), IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shmget"); if ((shminfo = (SHARED_INFO *)shmat(shmid, NULL, 0)) == (void *)-1) exit_sys("shmat"); shminfo->head = 0; shminfo->tail = 0; if ((shminfo->semid = semget(IPC_PRIVATE, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); if (semctl(shminfo->semid, 0, SETALL, semvals) == -1) exit_sys("semctl"); val = 0; for (;;) { usleep(rand() % 300000); sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if (semop(shminfo->semid, &sbuf, 1) == -1) exit_sys("semop"); shminfo->queue[shminfo->tail++] = val; shminfo->tail %= QUEUE_SIZE; sbuf.sem_num = 1; sbuf.sem_op = 1; sbuf.sem_flg = 0; if (semop(shminfo->semid, &sbuf, 1) == -1) exit_sys("semop"); if (val == 99) break; ++val; } printf("Press ENTR to exit...\n"); getchar(); if (semctl(shminfo->semid, 0, IPC_RMID) == -1) exit_sys("semctl"); if (shmdt(shminfo) == -1) exit_sys("shmdt"); if (shmctl(shmid, IPC_RMID, 0) == -1) exit_sys("shmctl"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* consumer.c */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char *msg); int main(void) { int shmid; SHARED_INFO *shminfo; struct sembuf sbuf; unsigned short semvals[] = {QUEUE_SIZE, 0}; int val; srand(time(NULL)); if ((shmid = shmget(SHM_KEY, 0, 0)) == -1) exit_sys("shmget"); if ((shminfo = (SHARED_INFO *)shmat(shmid, NULL, 0)) == (void *)-1) exit_sys("shmat"); val = 0; for (;;) { sbuf.sem_num = 1; sbuf.sem_op = -1; sbuf.sem_flg = 0; if (semop(shminfo->semid, &sbuf, 1) == -1) exit_sys("semop"); val = shminfo->queue[shminfo->head++]; shminfo->head %= QUEUE_SIZE; sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if (semop(shminfo->semid, &sbuf, 1) == -1) exit_sys("semop"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if (val == 99) break; } printf("\n"); if (shmdt(shminfo) == -1) exit_sys("shmdt"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki örnekte önce üretici de tüketici de çalışsa, çalışmanın sorunsuz yürütülebilmesi nasıl sağlanabilir? İlk akla gelen yöntem başka bir semaphore'un bu iş için kullanılmasıdır. Bu iş için bir semaphore yaratılır. Ancak hangi programın önce çalıştırılacağı bilinmediği için her iki program da bu semaphore'u yaratmaya çalışır. Tabii yalnızca bunlardan biri semaphore'u yaratıp diğeri yaratılmış olanı açacaktır. Buradaki semaphore sayacının başlangıç değeri 0'da tutulur. Böylece tüketici bu semaphore'u bekler. Üretici, IPC nesnelerini yarattığında semaphore sayacını 1 artırarak tüketiciyi blokeden kurtarır. Ancak üreticinin de henüz IPC nesnelerini silmeden tüketicinin işini bitirdiğinden emin olması gerekir. Bunun için yine aynı semaphore kullanılabilir. Ancak burada başka bir sorun ortaya çıkmaktadır. Bu semaphore'un sayacına ilk değer olan 0'ı kim verecektir. Eğer üretici önce çalışırsa IPC nesnelerini yaratıp semaphore sayacını artırdıktan sonra tüketici çalışınca, tüketici de semaphore sayacına 0 değerini verirse "kilitlenme (deadlock)" oluşur. Bu problem aslında yaygın bir problemdir. İşte standartlar semid_ds yapısının sem_otime elemanının henüz semop işlem yapılmadıysa 0 olmasını garanti etmektedir. O zaman tüketici program, semctl ile semid_ds bilgilerini elde eder. Sonra yapının bu elemanına bakar. Eğer bu elemanda 0 görürse, demek ki daha üretici işlemine başlamamıştır. O halde tüketici devam etmek için semaphore sayacını initialize edebilir. Eğer bu elemanda 0 değeri yoksa zaten üretici semaphore sayacını initialize etmiştir. Tüketicinin bunu yapmasına gerek yoktur. ---------------------------------------------------------------------------------------------------------------------------*/ /* sharing.h */ #ifndef SHARED_H_ #define SHARED_H_ #define SHM_KEY 0x12345678 #define SEM_KEY 0x12345678 #define QUEUE_SIZE 10 typedef struct tagSHARED_INFO { int head; int tail; int queue[QUEUE_SIZE]; int semid; /* two semaphore, 0 is producer, 1 is consumer */ } SHARED_INFO; union semun { int val; struct semid_ds *buf; unsigned short *array; }; #endif /* producer.c */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char *msg); int main(void) { int semid; int shmid; SHARED_INFO *shminfo; struct sembuf sbuf; unsigned short semvals[] = {QUEUE_SIZE, 0}; int val; union semun arg; if ((semid = semget(SEM_KEY, 1, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); arg.val = 0; if (semctl(semid, 0, SETVAL, arg) == -1) exit_sys("semctl"); srand(time(NULL)); if ((shmid = shmget(SHM_KEY, sizeof(SHARED_INFO), IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shmget"); if ((shminfo = (SHARED_INFO *)shmat(shmid, NULL, 0)) == (void *)-1) exit_sys("shmat"); shminfo->head = 0; shminfo->tail = 0; if ((shminfo->semid = semget(IPC_PRIVATE, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); arg.array = semvals; if (semctl(shminfo->semid, 0, SETALL, arg) == -1) exit_sys("semctl"); sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if (semop(semid, &sbuf, 1) == -1) exit_sys("semop"); val = 0; for (;;) { usleep(rand() % 300000); sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if (semop(shminfo->semid, &sbuf, 1) == -1) exit_sys("semop"); shminfo->queue[shminfo->tail++] = val; shminfo->tail %= QUEUE_SIZE; sbuf.sem_num = 1; sbuf.sem_op = 1; sbuf.sem_flg = 0; if (semop(shminfo->semid, &sbuf, 1) == -1) exit_sys("semop"); if (val == 99) break; ++val; } sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if (semop(semid, &sbuf, 1) == -1) exit_sys("semop"); if (semctl(shminfo->semid, 0, IPC_RMID) == -1) exit_sys("semctl"); if (shmdt(shminfo) == -1) exit_sys("shmdt"); if (shmctl(shmid, IPC_RMID, 0) == -1) exit_sys("shmctl"); if (semctl(semid, 0, IPC_RMID) == -1) exit_sys("semctl"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* consumer.c */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char *msg); int main(void) { int semid; int shmid; SHARED_INFO *shminfo; struct sembuf sbuf; unsigned short semvals[] = {QUEUE_SIZE, 0}; int val; union semun arg; struct semid_ds semds; srand(time(NULL)); if ((semid = semget(SEM_KEY, 1, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); if (semctl(semid, 0, IPC_STAT, &semds) == -1) exit_sys("semctl"); if (semds.sem_otime == 0) { arg.val = 0; if (semctl(semid, 0, SETVAL, arg) == -1) exit_sys("semct"); } sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if (semop(semid, &sbuf, 1) == -1) exit_sys("semop"); if ((shmid = shmget(SHM_KEY, 0, 0)) == -1) exit_sys("shmget"); if ((shminfo = (SHARED_INFO *)shmat(shmid, NULL, 0)) == (void *)-1) exit_sys("shmat"); val = 0; for (;;) { sbuf.sem_num = 1; sbuf.sem_op = -1; sbuf.sem_flg = 0; if (semop(shminfo->semid, &sbuf, 1) == -1) exit_sys("semop"); val = shminfo->queue[shminfo->head++]; shminfo->head %= QUEUE_SIZE; sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if (semop(shminfo->semid, &sbuf, 1) == -1) exit_sys("semop"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if (val == 99) break; } printf("\n"); sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if (semop(semid, &sbuf, 1) == -1) exit_sys("semop"); if (shmdt(shminfo) == -1) exit_sys("shmdt"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Sistem 5 semaphore nesneleri proseslerarası kullanım için tasarlanmıştır. Zaten bu nesnelerin tasarlandığı zamanlarda henüz thread'ler kullanılmıyordu. Her ne kadar bu nesneler aynı prosesin thread'leri arasında da kullanılabilirse de böyle bir kullanım verimsiz ve gereksizdir. Thread'ler arasında semaphore kullanmak istiyorsanız isimsiz POSIX semaphore'larını tercih etmelisiniz. POSIX senkronizasyon nesneleri UNIX türevi sistemlere thread eklenirken eklenmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 62. Ders 01/07/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Barrier nesneleri nispeten az kullanılan senkronizasyon nesnelerindendir. Windows sistemlerinde bu senkronizasyon nesnesinin tam bir karşılığı yoktur. Barrier nesnelerinin çalışması şöyle bir örnekle açıklanabilir: Önümüzde bir bariyer olsun. Biz bu bariyeri kuvvet uygulayarak kırmaya çalışalım. Ama bizim kuvvetimiz bu bariyeri kırmaya yetmesin. Sonra başka birisi bize yardım etmek istesin. Bu kez bu bariyeri iki kişi kuvvet uygulayarak kırmaya çalışalım. Yine iki kişinin kuvveti de bu bariyeri kırmaya yetmiyor olsun. Sonra bir üçüncü kişi de bize yardıma gelmiş olsun. Şimdi üçümüz kuvvet uygulayarak bu bariyeri kırmaya çalıştığımızda bunu başardığımızı düşünelim. Artık bariyer kırıldığına göre üçümüz de yollarımıza devam edebiliriz. Yukarıdaki örnekte olduğu gibi bariyer nesneleri ancak n tane thread belli bir noktaya geldiğinde açılmaktadır. Örneğin buradaki açılma koşulunun 3 thread olduğunu düşünelim. Birinci thread bariyere geldiğinde bloke olur. İkinci thread de bariyere geldiğinde bloke olur. Ancak üçüncü thread de bariyere geldiğinde artık bariyer açılır ve diğer iki thread'in de blokesi çözülerek üç thread yollarına devam eder. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Barrier parçalardan oluşan bir işin parçalarının çeşitli thread'lere yaptırıldığı durumlarda bu thread'lerin işlerini bitirdiği zaman nihai işlemin yapılması gerektiği durumlarda kullanılmaktadır. Örneğin büyük bir dizinin sort edilmek istendiğini düşünelim. Biz bu diziyi 10 parçaya ayıralım ve 10 farklı thread'le bu parçaları sıraya dizmek isteyelim. Her thread kendi parçasını sıraya dizdikten sonra artık bunların birleştirilmesi gerekmektedir. Ancak birleştirme işlemi tüm thread'lerin kendi parçalarını sıraya dizdikten sonra yapılmalıdır. (Sıralı dizilerin birleştirilmesi işlemine "merge" denilmektedir.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Barrier nesneleri aşağıdaki adımlardan geçilerek kullanılmaktadır: 1) Önce barrier nesnesini temsil eden pthread_barrier_t türünden global bir nesne tanımlanır. pthread_barrier_t türü ve dosyaları içerisinde typedef edilmiş durumdadır. POSIX standartlarına göre pthread_barrier_t türü herhangi bir tür olarak typedef edilmiş olabilir. Linux sistemlerinde bu tür bir yapı biçiminde typedef edilmiştir. Örneğin: pthread_barrier_t g_barrier; 2) Tanımlanan bu global barrier nesnesine pthread_barrier_init fonksiyonu ile ilk değer verilmesi gerekmektedir. Fonksiyonun prototipi şöyledir: #include int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned count); Fonksiyonun birinci parametresi barrier nesnesinin adresini almaktadır. İkinci parametre, barrier nesnesinin özellik bilgilerinin bulunduğu pthread_barrierattr_t türünden nesnenin adresini almaktadır. Bu parametre NULL adres geçilebilir. Bu durumda barrier nesnesi default özelliklerle yaratılmaktadır. Fonksiyonun üçüncü parametresi barrier'in kırılması için o noktaya gelecek thread sayısını belirtmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: if ((result = pthread_barrier_init(&g_barrier, NULL, 3)) != 0) exit_sys_errno("pthread_barrier_init", result); Barrier nesnelerine statik düzeyde makroyla ilk değer verilememektedir. 3) N tane thread'in bir noktada bekletilmesi için pthread_barrier_wait fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_barrier_wait(pthread_barrier_t *barrier); Fonksiyon barrier nesnesinin adresini alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Ancak fonksiyonun geri dönüş değeri hakkında önemli bir ayrıntı daha vardır. Birtakım thread'ler bir işin parçalarını yaptıktan sonra geri kalan birleştirme işleminin yalnızca bir thread tarafından yapılmasını sağlamak için pthread_barrier_wait fonksiyonu yalnızca tek bir thread'te PTHREAD_BARRIER_SERIAL_THREAD özel değeriyle geri dönmektedir. Yani bekleme başarılıysa pthread_barrier_wait fonksiyonundan thread'ler 0 değeri ile geri dönerler. Ancak bunlardan yalnızca biri PTHREAD_BARRIER_SERIAL_THREAD özel değeriyle geri dönmektedir. PTHREAD_BARRIER_SERIAL_THREAD özel değeri 0'dan farklı bir değerdir. Dolayısıyla fonksiyon bu değerle geri dönmüşse başarısız kabul edilmemelidir. O halde fonksiyonun başarı kontrolü aşağıdaki gibi yapılabilir: if ((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); Tabii thread'lerden birinde bu birleştirme işlemi yapılacaksa ayrıca yine fonksiyonun geri dönüş değerinin kontrol edilmesi gerekir. Örneğin: if ((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if (result == PTHREAD_BARRIER_SERIAL_THREAD) { /* birleştirme işlemi */ } Tabii bu kontrol aşağıdaki gibi de yapılabilir: if ((result = pthread_barrier_wait(&g_barrier)) != 0) if (result == PTHREAD_BARRIER_SERIAL_THREAD) { /* birleştirme işlemi */ } else exit_sys_errno("pthread_barrier_wait", result); Barrier nesnesi açıldıktan sonra yeniden otomatik olarak pthread_barrier_init fonksiyonu çağrılmış gibi ilk durumuna gelmektedir. POSIX standartları hangi thread'in PTHREAD_BARRIER_SERIAL_THREAD değeriyle geri döneceği konusunda bir garanti vermemektedir. 4) Kullanım bittikten sonra barrier nesnesi pthread_barrier_destroy fonksiyonu ile boşaltılmalıdır. Fonksiyonun prototipi şöyledir: #incude int pthread_barrier_destroy(pthread_barrier_t *barrier); Fonksiyon barrier nesnesinin adresini alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri döner. Örneğin: if ((result = pthread_barrier_destroy(&g_barrier)) != 0) exit_sys_errno("pthread_barrier_destroy", result); 5) Barrier nesneleri için tek bir özellik bilgisi vardır. O da nesnenin prosesler arasında kullanılıp kullanılmayacağını belirtmektedir. Default durumda (yani pthread_barrier_init fonksiyonunun ikinci parametresi NULL geçildiğinde) nesne prosesler arasında kullanılamamaktadır. Özellik nesnesinin kullanımı diğer nesnelerde olduğu gibidir. Önce pthread_barrierattr_t türünden bir nesne yaratılır. Sonra bu nesneye pthread_barrierattr_init fonksiyonu ile ilk değer verilir. Sonra nesnenin özelliğinin set ve get edilmesi için pthread_barrierattr_setpshrared ve pthread_barrierattr_getpshrared fonksiyonları kullanılır. Burada proseslerarası paylaşım PTHREAD_PROCESS_SHARED değeri ile belirtilmektedir. Tabii en sonunda bu özellik nesnesi pthread_barrierattr_destroy fonksiyonu ile yok edilmelidir. Özellik ile ilgili fonksiyonların prototipleri şöyledir: #include int pthread_barrierattr_init(pthread_barrierattr_t *attr); int pthread_barrierattr_getpshared(const pthread_barrierattr_t *attr, int *pshared); int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr,int pshared); int pthread_barrierattr_destroy(pthread_barrierattr_t *attr); Fonksiyonların hepsi başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: pthread_barrierattr_t battr; ... if ((result = pthread_barrierattr_init(&battr)) != 0) exit_sys_errno("pthread_barrierattr_init", result); if ((result = pthread_barrierattr_setpshared(&battr, PTHREAD_PROCESS_SHARED)) != 0) exit_sys_errno("pthread_barrierattr_setpshared", result); if ((result = pthread_barrier_init(&g_barrier, &battr, 3)) != 0) exit_sys_errno("pthread_barrier_init", result); if ((result = pthread_barrierattr_destroy(&battr)) != 0) exit_sys_errno("pthread_barrierattr_destroy", result); Tabii barrier nesnesini prosesler arasında kullanabilmek için nesneyi yine paylaşılan bellek alanında oluşturmak gerekir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte 3 thread pthread_barrier_wait ile aynı barrier nesnesinde bekletilmiştir. 3 thread'de rastgele zamanlarda bu noktaya erişmektedir. İçlerinden yalnızca biri PTHREAD_BARRIER_SERIAL_THREAD değeriyle geri dönecektir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void *thread_proc3(void *param); void exit_sys_errno(const char *msg, int eno); pthread_barrier_t g_barrier; int main(void) { pthread_t tid1, tid2, tid3; int result; if ((result = pthread_barrier_init(&g_barrier, NULL, 3)) != 0) exit_sys_errno("pthread_barrier_init", result); if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid3, NULL, thread_proc3, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_barrier_destroy(&g_barrier)) != 0) exit_sys_errno("pthread_barrier_destroy", result); return 0; } void *thread_proc1(void *param) { unsigned seed; int stime; int result; printf("thread1 starts...\n"); seed = time(NULL) + 123; stime = rand_r(&seed) % 10; printf("thread1 running %d second(s)\n", stime); sleep(stime); if ((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if (result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("thread1 returns with PTHREAD_BARRIER_SERIAL_THREAD\n"); } printf("thread1 terminates...\n"); return NULL; } void *thread_proc2(void *param) { unsigned seed; int stime; int result; printf("thread2 starts...\n"); seed = time(NULL) + 456; stime = rand_r(&seed) % 10; printf("thread2 running %d second(s)\n", stime); sleep(stime); if ((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if (result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("thread2 returns with PTHREAD_BARRIER_SERIAL_THREAD\n"); } printf("thread2 terminates...\n"); return NULL; } void *thread_proc3(void *param) { unsigned seed; int stime; int result; printf("thread3 starts...\n"); seed = time(NULL) + 789; stime = rand_r(&seed) % 10; printf("thread3 running %d second(s)\n", stime); sleep(stime); if ((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if (result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("thread3 returns with PTHREAD_BARRIER_SERIAL_THREAD\n"); } printf("thread3 terminates...\n"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda bir barrier kullanım örneği verilmiştir. 50000000 (elli milyon) tane rastgele değerlerden oluşan int türden bir dizi 10 parçaya bölünmüş ve her parça bir thread tarafından qsort fonksiyonu ile sort edilmiştir. Sonra da bu kendi aralarında sıralı olan bu 10 dizi birleştirilmiştir. Sonra aynı dizi tek bir thread tarafından sıraya dizilmiştir. 3 çekirdekli bir Linux sisteminde (kullandığımız sanal makine) thread'li biçim 6 saniye civarında thread'siz biçim 12 saniye civarında zaman almaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #define SIZE 50000000 #define NTHREADS 10 int comp(const void *pv1, const void *pv2); void *thread_proc(void *param); void merge(void); int check(const int *nums); void exit_sys_thread(const char *msg, int err); pthread_barrier_t g_barrier; int g_nums[SIZE]; int g_snums[SIZE]; int main(void) { int result; int i; pthread_t tids[NTHREADS]; clock_t start, stop; double telapsed; srand(time(NULL)); for (i = 0; i < SIZE; ++i) g_nums[i] = rand(); start = clock(); if ((result = pthread_barrier_init(&g_barrier, NULL, NTHREADS)) != 0) exit_sys_thread("pthread_barrier_init", result); for (i = 0; i < NTHREADS; ++i) if ((result = pthread_create(&tids[i], NULL, thread_proc, (void *)i)) != 0) exit_sys_thread("pthread_create", result); for (i = 0; i < NTHREADS; ++i) if ((result = pthread_join(tids[i], NULL)) != 0) exit_sys_thread("pthread_join", result); pthread_barrier_destroy(&g_barrier); stop = clock(); telapsed = (double)(stop - start) / CLOCKS_PER_SEC; printf("Total second with threaded sort: %f\n", telapsed); printf(check(g_snums) ? "Sorted\n" : "Not Sorted\n"); start = clock(); qsort(g_nums, SIZE, sizeof(int), comp); stop = clock(); telapsed = (double)(stop - start) / CLOCKS_PER_SEC; printf("Total second with threaded sort: %f\n", telapsed); printf(check(g_nums) ? "Sorted\n" : "Not Sorted\n"); return 0; } void exit_sys_thread(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } void *thread_proc(void *param) { int part = (int)param; int result; qsort(g_snums + part * (SIZE / NTHREADS), SIZE / NTHREADS, sizeof(int), comp); if ((result = pthread_barrier_wait(&g_barrier)) && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_thread("pthread_barrier_wait", result); if (result == PTHREAD_BARRIER_SERIAL_THREAD) merge(); return NULL; } int comp(const void *pv1, const void *pv2) { const int *pi1 = (const int *)pv1; const int *pi2 = (const int *)pv2; return *pi1 - *pi2; } void merge(void) { int indexes[NTHREADS]; int min, min_index; int i, k; int partsize; partsize = SIZE / NTHREADS; for (i = 0; i < NTHREADS; ++i) indexes[i] = i * partsize; for (i = 0; i < SIZE; ++i) { min = indexes[0]; min_index = 0; for (k = 1; k < NTHREADS; ++k) if (indexes[k] < (k + 1) * partsize && g_nums[indexes[k]] < min) { min = g_nums[indexes[k]]; min_index = k; } g_snums[i] = min; ++indexes[min_index]; } } int check(const int *nums) { int i; for (i = 0; i < SIZE - 1; ++i) if (nums[i] > nums[i + 1]) return 0; return 1; } /*-------------------------------------------------------------------------------------------------------------------------- Senkronizasyon dünyasında karşımıza çıkan diğer bir senkronizasyon nesnesi de "spinlock" denilen nesnedir. Spinlock nesneleri tıpkı mutex'ler ya da binary semaphore'lar gibi bir kritik kodun başından sonuna kadar yalnızca tek bir thread tarafından çalıştırılmasını sağlamak için kullanılmaktadır. Bu nesnelerin mutex ve binary semaphore nesnelerinden farkı eğer nesne kilitliyse beklemenin bloke yoluyla değil, meşgul bir döngü yoluyla blokesiz yapılmasıdır. Yani spinlock nesnelerinin içerisinde bir döngü vardır. Bu döngünün içerisinde sürekli "kilit açılmış mı" diye kontrol yapılmaktadır. Şüphesiz bu biçimdeki çalışma eğer nesne kilitliyse gereksiz CPU zamanının harcanmasına yol açacaktır. Ancak bazı durumlarda spinlock'lar hızlandırma sağlayabilmektedir. Çünkü senkronizasyon nesnelerinin kilitli olması durumunda thread'in uykuya yatırılması ve uyandırılması göreli olarak önemli bir zaman kaybı da oluşturmaktadır. Spinlock nesneleri yalnızca bazı özel koşullar sağlanıyorsa kullanılmalıdır. Örneğin tek işlemcili ya da tek çekirdekli sistemlerde spinlock kullanımının genellikle zararından başka bir faydası yoktur. Çünkü tek işlemcili ya da tek çekirdekli sistemlerde, bir thread nesneyi kilitlemiş ve thread'ler arası geçiş oluşmuşsa artık o nesnenin kilidinin açılması ancak o thread'in yeniden CPU'ya atanmasıyla gerçekleşeceği için diğer bir thread spinlock içerisinde meşgul bir döngüde CPU zamanı harcayarak bekleyecektir. Halbuki çok işlemcili ya da çekirdekli sistemlerde nesneyi kilitleyen thread bir işlemcide ya da çekirdekte, spinlock'ta bekleyen thread ise başka bir işlemcide ya da çekirdekte çalışıyor olabilir. Bu durumda spinlock çok fazla dönmeden nesneyi kilitleyebilir. Tabii işletim sistemleri, çok işlemcili ya da çekirdekli sistem söz konusu olsa bile bu thread'leri aynı işlemci ya da çekirdeğin kuyruğuna atayabilmektedir. Öte yandan bazı aşağı seviyeli uygulamalarda sistemde tek bir işlemci ya da çekirdek olsa bile bir donanım kesmesi ile kilidin açılmasını mümkün hale getiren bir mekanizma da oluşturulabilmektedir. Mademki işletim sistemleri spinlock'ı kullanan thread'leri aynı işlemci ya da çekirdeğin kuyruğuna atayabilmektedir bu durumda spinlock'ı kilitleyen thread'lerin daha yüksek öncelikli olması, bu tür durumlardaki gereksiz beklemeleri bir ölçüde engelleyebilmektedir. Özetle spinlock kullanılırken dikkat edilmesi gerekir. Uygunsuz yerde spinlock kullanımı performansı tam tersine olumsuz etkileyebilmektedir. Ancak bu nesnenin gerektiği yerde kullanılması da performans üzerinde olumlu etkiler sağlayabilmektedir. Spinlock kilidini almak için iki thread'in farklı işlemci ya da çekirdeklerde çalıştığı durumda bunlardan birinin spinlock kilidini aldığında kısa süre içerisinde bırakması en uygun durumdur. Çünkü, diğer işlemci ya da çekirdekteki thread meşgul bir döngüde bekliyor olabilir. Spinlock ile oluşturulan kritik kodun kısa ve çabuk geçilen bir kod olması anlamlıdır. Daha önceden de belirttiğimiz gibi aynı prosesin thread'leri arasındaki senkronizasyon için ilk akla gelmesi gereken senkronizasyon nesneleri mutex nesneleridir. Spinlock nesneleri bazı özel durumlarda performansı artırmak için dikkatlice kullanılmalıdır. Modern mutex, semaphore gibi nesnelerin gerçekleştiriminde de aslında kısa spin işlemleri yapılmaktadır. Örneğin pthread_mutex_lock hemen kilide bakıp kilit kapalıysa bloke olmamaktadır. Kısa bir süre spin içerisinde kilidin açılmasını bekleyip sonra bloke olmaktadır. Yani bu nesnelerin gerçekleştiriminde de kısa spin işlemleri yapılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Spinlock nesneleri şu adımlardan geçilerek kullanılmaktadır: 1) Spinlock nesneleri pthread_spinlock_t türüyle temsil edilmiştir. Öncelikle bu türden global bir nesnenin tanımlanması gerekir. Örneğin: pthread_spinlock_t g_spinlock; pthread_spinlock_t türü yine ve dosyaları içerisinde herhangi bir tür olarak typedef edilebilmektedir. Linux sistemlerinde tipik olarak bu tür bir yapı belirtmektedir. 2) Spinlock nesnesine pthread_spin_init fonksiyonu ile ilk değer verilir. Fonksiyonun prototipi şöyledir: #include int pthread_spin_init(pthread_spinlock_t *lock, int pshared); Fonksiyonun birinci parametresi spinlock nesnesinin adresini alır. İkinci parametre, nesnenin prosesler arasında kullanılıp kullanılmayacağını belirtmektedir. Bu parametre 0 ya da PTHREAD_PROCESS_SHARED biçiminde geçilir. Nesnenin ayrıca bir özellik (attribute) parametresinin olmadığına dikkat ediniz. Spinlock nesnesine ilk değer vermek için bir makro bulunmamaktadır. Örneğin: if ((result = pthread_spin_init(&g_spinlock, 0)) != 0) exit_sys_errno("pthread_spin_init", result); 3) Kritik kod pthread_spin_lock ve pthread_spin_unlock fonksiyonlarıyla oluşturulmaktadır. Fonksiyonların prototipleri şöyledir: #include int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_unlock(pthread_spinlock_t *lock); Fonksiyonlar, spinlock nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri döner. Kritik kod şöyle oluşturulmaktadır (kontroller yapılmamıştır): pthread_spin_lock(&g_spinlock); ... ... ... pthread_spin_unlock(&g_spinlock); Thread'in akışı pthread_spin_lock fonksiyonuna girdiğinde meşgul döngü içerisinde kilide bakılıp beklenmektedir. pthread_spin_unlock fonksiyonu da kilidi açmaktadır. Yine buradaki lock fonksiyonunun try'lı biçimi de vardır: #include int pthread_spin_trylock(pthread_spinlock_t *lock); Fonksiyon kilit açık mı diye bakar. Eğer kilit açık değilse spin yapmadan EBUSY errno değeri ile geri dönmektedir. 4) Nihayet işlem bittikten sonra spinlock nesnesi pthread_spin_destoy fonksiyonu ile boşaltılmalıdır. Fonksiyonun prototipi şöyledir: #include int pthread_spin_destroy(pthread_spinlock_t *lock); Fonksiyon, spinlock nesnesinin adresini parametre olarak alır ve nesneyi boşaltır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: if ((result = pthread_spin_destroy(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_destroy", result); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 63. Ders 02/07/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte iki thread global bir sayacı spinlock koruması ile artırmaktadır. Buradaki işlem CPU yoğun bir işlemdir. Bu tür CPU yoğun işlemlerde eğer makinenizde birden fazla işlemci ya da çekirdek varsa spinlock daha iyi bir sonuç verebilmektedir. Tabii eğer işletim sistemi, söz konusu bu thread'leri aynı işlemci ya da çekirdeklere atarsa tam tersine spinlock daha kötü bir sonuç da verebilir. Bu örnekte programın çalışma süresini time programıyla ölçebilirsiniz. Aynı örneği mutex nesneleriyle yapıp sonuçlarını karşılaştırabilirsiniz. Örneğin 3 çekirdeği kullanan sanal makinede aşağıdaki sonuçlar elde edilmiştir: $ time ./spin 200000000 real 0m6,031s user 0m11,695s sys 0m0,024s $ time ./mutex 200000000 real 0m11,915s user 0m16,650s sys 0m7,002s ---------------------------------------------------------------------------------------------------------------------------*/ /* spin.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); pthread_spinlock_t g_spinlock; int g_count; int main(void) { pthread_t tid1, tid2; int result; if ((result = pthread_spin_init(&g_spinlock, 0)) != 0) exit_sys_errno("pthread_spin_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_spin_destroy(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_destroy", result); printf("%d\n", g_count); return 0; } void *thread_proc1(void *param) { int result; for (int i = 0; i < MAX_COUNT; ++i) { if ((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if ((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void *thread_proc2(void *param) { int result; for (int i = 0; i < MAX_COUNT; ++i) { if ((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if ((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* mutex.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); pthread_mutex_t g_mutex; int g_count = 0; 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); printf("%d\n", g_count); if ((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); return 0; } void *thread_proc1(void *param) { int result; for (int i = 0; i < MAX_COUNT; ++i) { if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++g_count; if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } return NULL; } void *thread_proc2(void *param) { int result; for (int i = 0; i < MAX_COUNT; ++i) { if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++g_count; if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Diğer sık karşılaşılan bir senkronizasyon nesnesi de "reader-writer lock" denilen nesnedir. Bu nesne, bir veri yapısından birden fazla thread'in okuma yapmasına izin veren ancak bir thread yazma yapıyorsa diğer thread'lerin okuma ya da yazma yapmasını engelleyen bir senktronizasyon nesnedir. Bu senkronizasyon nesnesinin kullanım amacı şöyle bir örnekle açıklanabilir: Elimizde global bir bağlı liste olsun. Bu bağlı listeye eleman ekleyen bir grup thread, bu bağlı listeden eleman silen bir grup thread ve bu bağlı listede arama yapan bir grup thread söz konusu olsun. Bir thread bağlı listeye eleman eklerken ya da bağlı listeden eleman silerken, diğer thread'lerin bu işlemin bitmesini beklemesi gerekir. Benzer biçimde bir thread bağlı listede arama yaparken diğer thread'lerin bu arama bitene kadar eleman eklemesilme işlemlerine başlamaması gerekir. Ancak buradaki kritik nokta birden fazla thread'in bağlı liste üzerinde arama yapabilmesinin bir sıkıntıya yol açmayacağıdır. Yani birden fazla thread'in bağlı liste üzerinde arama yapması mümkün hale getirilmelidir. Aslında bu örnek bağlı listeye eleman ekleme ya da bağlı listeden eleman silme işlemi bir "write" işlemi olarak ele alınabilir. Benzer biçimde bağlı listede eleman arama işlemi de bir "read" işlemi olarak ele alınabilir. O halde bizim sağlamamız gereken durum şöyle özetlenebilir: - Bir thread kaynak üzerinde write işlemi yaparken diğer thread'ler bu işlem bitene kadar aynı kaynak üzerinde read ya da write işlemi yapmamalıdır. - Bir thread kaynak üzerinde read işlemi yaparken diğer thread'ler bu işlem bitene kadar aynı kaynak üzerinde write işlemi yapmamalıdır. - Bir thread kaynak üzerinde read işlemi yaparken diğer thread'ler kaynak üzerinde read işlemi yapabilmelidir. Bu koşulları şöyle de özetleyebiliriz: write - write (izin verilmemeli) write - read (izin verilmemeli) read - write (izin verilmemeli) read - read (izin verilmeli) Burada açıkladığımız durumu daha önce gördüğümüz senkronizasyon nesneleriyle sağlamanın basit bir yolu yoktur. İşte bunu sağlamak için "reader-writer lock" denilen özel bir senkronizasyon nesnesi kullanılmaktadır. Yukarıdaki problemin bir mutex nesnesiyle neden çözülemeyeceğini de açıklamak istiyoruz. Biz mutex kullanırken okuma ve yazma işlemleri sırasında mecburen mutex'in sahipliğini almak zorunda kalacağız. Bu da birden fazla read durumunu engelleyecektir. Örneğin: pthread_mutex_lock(&g_mutex); ... ... ... pthread_mutex_unlock(&g_mutex); Görüldüğü gibi bir thread kaynak üzerinde read işlemi yapmaya çalıştığı zaman mutex'i kilitlediği için başka bir thread de read işlemi yapamayacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Reader-Writer lock nesneleri şu adımlardan geçilerek kullanılmaktadır: 1) Programcı pthread_rwlock_t türünden global bir nesne tanımlar. Bu tür yine ve dosyalarında herhangi bir türden olabilecek biçimde typedef edilmektedir. Linux sistemlerinde pthread_rwlock_t türü bir yapı belirtmektedir. Örneğin: pthread_rwlock_t g_rwlock; 2) pthread_rwlock_t nesnesine, PTHREAD_RWLOCK_INITIALIZER makrosuyla ya da pthread_rwlock_init fonksiyonuyla ilk değer verilebilir. pthread_rwlock_init fonksiyonunun prototipi şöyledir: #include int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); Fonksiyonun birinci parametresi reader-writer lock nesnesinin adresini, ikinci parametresi ise onun özellik bilgisini almaktadır. Özellik parametresine NULL adres geçilebilmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Bu durumda nesneye ilk değer verme işlemi aşağıdaki iki biçimden biri ile yapılabilir: pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER; ... if ((result = pthread_rwlock_init(&g_rwlock, NULL)) != 0) exit_sys_errno("pthread_rwlock_init", result); 3) Okuma amaçlı kritik kod oluşturmak için pthread_rwlock_rdlock fonksiyonu, yazma amaçlı kritik kod oluşturmak için pthread_rwlock_wrlock fonksiyonu kullanılmaktadır. Nesne nasıl kilitlenmiş olursa olsun kilidi açmak için pthread_rwlock_unlock fonksiyonu kullanılmaktadır. Bu durumda okuma amaçlı kritik kod aşağıdaki gibi oluşturulmalıdır (kontroller yapılmamıştır): pthread_rwlock_rdlock(&g_rwlock); ... ... ... pthread_rwlock_unlock(&g_rwlock); Yazma amaçlı kritik kod da şöyle oluşturulmalıdır (kontroller yapılmamıştır): pthread_rwlock_wrlock(&g_rwlock); ... ... ... pthread_rwlock_unlock(&g_rwlock); Buradaki 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ın hepsi reader-writer lock nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri döner. 4) Nesnenin kullanımı bittikten sonra nesne pthread_rwlock_destroy fonksiyonu ile boşaltılabilir. Fonksiyonun prototipi şöyledir: #include int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); Fonksiyon reader-writer lock nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri döner. 5) Reader-writer lock nesnelerinin de bir özellik bilgisi vardır. Özellik oluşturma daha önce görmüş olduğumuz nesnelerdekine benzer biçimde yapılmaktadır. Önce pthread_rwlockattr_t türünden bir özellik nesnesi tanımlanır. Sonra bu özellik nesnesine pthread_rwlockattr_init fonksiyonu ile ilk değer verilir. Sonra da özellik nesnesine pthread_rwlockattr_setxxx fonksiyonlarıyla özellikler iliştirilir. Sonra da bu özellik nesnesi kullanılarak reader-writer lock nesnesi pthread_rwlock_init fonksiyonuyla yaratılır. Aslında şu anda nesnenin tek bir özelliği vardır. O da onun prosesler arasında paylaşılabilirliğini belirtmektedir. Nesne default durumda (yani PTHREAD_RWLOCK_INITIALIZER ile yatarıldığında ya da pthread_rwlock_init fonksiyonunda özellik parametresi NULL geçildiğinde) prosesler arasında paylaşılamamaktadır. Prosesler arası paylaşım için pthread_rwlock_setpshared fonksiyonu kullanılmaktadır. Bu bilginin alınması için de pthread_rwlock_getpshared fonksiyonu bulunmaktadır. Tabii özellik nesnesinin kullanımı bittikten sonra (yani pthread_rwlock_init fonksiyonu çağrıldıktan sonra) onun pthread_rwlock_destroy fonksiyonu ile boşaltılması gerekmektedir. Buradaki fonksiyonların prototipleri aşağıda verilmiştir #include int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr); int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared); int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte dört ayrı thread yaratılmıştır. İki thread okuma amaçlı, iki thread de yazma amaçlı kritik koda girmektedir. Bu örnek bir simülasyon niteliğindedir. Örnekte rastgele beklemeler yapılmıştır. Örnekten görülmesi gereken şey iç içe read işleminin yapılabildiği ancak diğer işlemlerin iç içe yapılamadığıdır. Programın çalıştırılmasında aşağıdakine benzer bir çıktı oluşacaktır: thread3 ENTERS to critical section for WRITING... thread3 EXITS from critical section... thread2 ENTERS to critical section for READING... thread1 ENTERS to critical section for READING... thread1 EXITS from critical section... thread2 EXITS from critical section... thread4 ENTERS to critical section for WRITING... thread4 EXITS from critical section... thread1 ENTERS to critical section for READING... thread1 EXITS from critical section... thread3 ENTERS to critical section for WRITING... thread3 EXITS from critical section... thread2 ENTERS to critical section for READING... thread1 ENTERS to critical section for READING... thread2 EXITS from critical section... thread1 EXITS from critical section... ... ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void exit_sys_errno(const char *msg, int err); void *thread_proc1(void *param); void *thread_proc2(void *param); void *thread_proc3(void *param); void *thread_proc4(void *param); pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER; int main(void) { int result; pthread_t tid1, tid2, tid3, tid4; if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid3, NULL, thread_proc3, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid4, NULL, thread_proc4, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid4, NULL)) != 0) exit_sys_errno("pthread_join", result); pthread_rwlock_destroy(&g_rwlock); return 0; } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } void *thread_proc1(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 12345; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_rdlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); printf("thread1 ENTERS to critical section for READING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread1 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_unlock", result); } return NULL; } void *thread_proc2(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 23456; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_rdlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); printf("thread2 ENTERS to critical section for READING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread2 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_unlock", result); } return NULL; } void *thread_proc3(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 35678; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_wrlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_wrlock", result); printf("thread3 ENTERS to critical section for WRITING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread3 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_unlock", result); } return NULL; } void *thread_proc4(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 356123; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_wrlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_wrlock", result); printf("thread4 ENTERS to critical section for WRITING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread4 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_unlock", result); } return NULL; } /*-------------------------------------------------------------------------------------------------------------------------- İşlemcileri, CISC (Complex Intruction Set Computing) ve RISC (Reduced Instruction Set Computing) olmak üzere iki sınıfa ayırabiliriz. Tabii bu iki sınıf aslında bir spektrumdur. Yani işlemciler bir tarafı CISC olan diğer tarafı RISC olan bu spektrumda herhangi bir yerde olabilirler. Intel x86 işlemcileri CISC işlemlerine örnek oluştururken, ARM işlemcileri RISC işlemlerine bir örnek oluşturmaktadır. İki işlemci ailesi arasında temel farklılıklar şunlardır: - CISC işlemlerinde çok sayıda makine komutu vardır. Bu komutların bazıları karmaşık işlemler yapmaktadır. Ancak RISC işlemcilerinde az sayıda makine komutu daha etkin çalışacak biçimde tasarlanmıştır. - CISC işlemcilerinde makine komutları değişik uzunlukta olabilmektedir. Ancak CISC işlemcilerinde tüm makine komutları aynı uzunluktadır. - CISC işlemcilerinde az sayıda genel amaçlı CPU yazmacı vardır. Ancak RISC işlemcilerinde çok sayıda genel amaçlı CPU yazmacı bulunmaktadır. - CISC işlemcilerinde komutlar değişik çalışma sürelerine sahiptir. Ancak RISC işlemcilerinde genellikle aynı çalışma süresine sahiptir. - RISC işlemcilerinde pipeline işlemleri CISC işlemcilerine göre daha etkin yapılabilmektedir. - CISC işlemcilerinde doğrudan bellek üzerinde işlem yapan makine komutları bulunmaktadır. RISC işlemcilerinde ise bellek üzerinde doğrudan işlemler yapılmaz. Her zaman bellekteki nesneler önce CPU yazmaçlarına çekilir. Bu yüzden RISC işlemcilerine "load/store" işlemcileri denilmektedir. - CISC işlemcilerinde genel olarak makine komutlarının iki operand'ı bulunur. İşlem sonrasında, işleme giren bir yazmacın değeri bozulmaktadır. Ancak RISC işlemcilerinde makine komutlarının genel olarak üç operand'ı bulunmaktadır. İşlem sonucu operand'ları bozmamaktadır. - CISC işlemcileri daha fazla güç harcama eğilimdedir. Ancak RISC işlemcileri daha az güç harcama eğilimindedir. Bugün artık RISC tasarımının daha iyi bir tasarım olduğu kabul edilmektedir. Ancak Intel x86 serisi gibi bazı işlemciler çok yaygın kullanıldığı için halen CISC mimarisini devam ettirmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bugün çok işlemcili ve çok çekirdekli sistemlerde işlemci-bellek bağlantısında iki mimari kullanılmaktadır: - SMP (Symmetric Multiprocessor) Mimarisi - NUMA (Non Unified Memory Access) Mimarisi SMP mimarisi pek çok işlemci tarafından default olarak kullanılan mimaridir. Bu mimaride işlemciler ya da çekirdekler aynı RAM'e erişmektedir. Dolayısıyla "bus" çakışmasını ortadan kaldırmak için bir işlemci ya da çekirdek RAM'e erişirken diğerleri onun işini bitirmesini beklemektedir. Dolayısıyla bu mimaride aslında işlemci ya da çekirdek sayısı arttıkça bus çakışmaları da artar ve performans düşmeye başlar. Tabii bu mimaride, her işlemcinin ve çekirdeğin ayrı bir cache sistemi de bulunmaktadır. Bu işlemciler ya da çekirdekler önce bu cache sistemine başvurmakta bilgi orada yoksa RAM'e başvurmaktadırlar. Tabii cache tutarlılığı (cache consistency) da donanımsal olarak sağlanmaktadır. NUMA mimarisinde her işlemcinin ya da her çekirdeğin RAM'de bağımsız olarak erişebileceği ayrı bir bank'ı vardır. Her işlemci ya da çekirdek kendi bank'ına hızlı erişir ancak diğer işlemcilerin ya da çekirdeklerin bank'larına yavaş erişir. Bu nedenle işlemcilerin ve çekirdeklerin belleğe erişim süreleri erişitikleri yere bağlı olarak (non unified) değişebilmektedir. Bugün NUMA mimarisini kullanan işlemciler ve board'lar oldukça azdır. Örneğin Intel Xeon, AMD EPYC, AMD Opteron gibi işlemciler NUMA mimarisini kullanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread'ler dünyasında "atomiklik (atomicity)" bir işlemin kesilmeden tek parça halinde yapılmasına denilmektedir. Atomik işlemler tek bir parça halinde thread'ler arası geçiş oluşmadan yapılan işlemlerdir. Pekiyi makine komutları atomik midir? Genel olarak makine komutlarının atomik olduğunu söyleyebiliriz. Yani bir makine komutu çalıştırılırken kesilme olmaz. Bir makine komutu çalıştırılırken çalıştırmanın ortasında thread'ler arası geçiş oluşamaz. Çünkü thread'ler arası geçiş donanım kesmeleriyle sağlanmaktadır. Donanım kesmeleri de ancak makine komutlarının arasında etkili olabilmektedir. Pekiyi iki işlemci ya da çekirdek aynı global değişkeni aynı anda değiştirmek isteseler ne olur? Burada normalde hangisi bu işlemi geç yaparsa o değişkende o değer gözükecektir. Peki aynı anda bu işlemi yaparlarsa ne olacaktır? Her ne kadar işlemciler ya da çekirdekler RAM'e erişirken diğerleri onu bekliyorsa da (yani erişim aslında aynı anda gerçeklemiyorsa da) yine de özel bazı durumlarda değişkende bozulmalar olabilmektedir. Şöyle ki, aşağıdaki gibi bir INTEL makine komutunu düşünelim: INC g_count Bu komut çalışırken her ne kadar thread'ler arası geçiş oluşmayacak olsa da maalesef duruma INTEL işlemcisi ya da çekirdeği bu işlem sırasında işlemin başından sonuna kadar BUS'ı tutup işlemi atomik yapmamaktadır. Bu tarz işlemlerde INTEL işlemcileri bus'ı tutup bırakmakta yeniden tutup bırakmakta yani birden fazla kez kesikli bir biçimde bus'ı tutup bırakabilmektedir. İşte bu işlemler sırasında başka bir işlemci ya da çekirdek bus'ın kontrolünü alıp oraya bir şeyler yazmak isteyebilir. Bu durumda o nesnede bozuk bir değer oluşabilir. Tabii bunun olasılığı çok düşüktür. Ancak böyle bir olasılık söz konusu olmaktadır. Şimdi INTEL mimarisinde, iki ayrı çekirdeğin aynı global değişkene tek bir makine komutuyla atama yaptığını düşünelim: Birinci Çekirdek MOV g_val, 100 İkinci Çekirdek MOV g_val, 200 Burada g_val içerisinde 100 ya da 200 olması normal karşılanacak bir durumdur. Ancak bozuk bir değerin bulunması istenemeyen bir durumdur. İşte INTEL işlemcilerinde buradaki g_val belleğe düzgün hizalanmamışsa böyle riskli bir durum oluşabilmektedir. Tabii işlemciler genellikle bu biçimdeki erişimlerde makine komutlarının sonuna kadar bus'ın tutulması için de olanak sağlamaktadır. Örneğin INTEL işlemcilerinde komutun başına LOCK prefix'i sayesinde işlemci baştan sona kadar bus'ı tutabilmektedir: LOCK INC g_val Tabii buradaki LOCK prefix'i komutu yavaşlatmaktadır. Bu durumda derleyiciler böyle bir prefix'i default durumda kullanmazlar. Biz daha önce iki thread'in aynı global değişkeni artırmasına yönelik bir örnek yapmıştık: for (int i = 0; i < 1000000; ++i) ++g_count; Elimizdeki C derleyicilerinin çoğu bu artırım işlemini tek bir makine komutuyla değil üç ayrı makine komutuyla yapmaktadır. Çünkü aslında bu üç ayrı makine komutu doğrudan belleği artıran makine komutundan daha hızlı çalışmaktadır: MOV reg, g_count INC reg MOV g_count, reg Tabii artık artırma işlemi atomik olmadığı için thread'ler arası geçiş değişkende bozulma yaratacaktır. Biz bu bozulmayı engellemek için kritik kod bloğu oluşturmuştuk. Pekiyi derleyicimiz yukarıdaki artırma için yavaş olmasına karşın tek bir makine komutu üretseydi bu durumda bozulma oluşur muydu? INC g_count İşte koşullara bağlı olarak olasılık düşük olsa bile bu durumda bozulma olasılığı yine vardır. Tabii derleyicimiz tek bir makine komutu ile bu işlemi yapıp komutun başına da LOCK gibi bir prefix getirseydi bu durumda bir sorun oluşmazdı: LOCK INC g_count Burada önemli nokta şudur: Aslında biz tek bir makine komutu ile yapılacak bazı işlemlerin atomik bir biçimde yapılmasını sağlayabiliriz. Ancak derleyicimiz bunu bizim için sağlayamamaktadır. Bazı C derleyicilerinde C ile yazarken arada sembolik makine kodlarını kullanabilmekteyiz. Bu kullanım derleyiciye özgüdür ve bu kullanımın ismine "inline assembly" denilmektedir. Ancak "inline assembly" yazmak zahmetlidir va makine dili bilgisi gerektirmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 64. Ders 08/07/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bazı C derleyicilerinde "built-in" ya da "intrinsic" fonksiyon denilen bir kavram vardır. Derleyiciler tarafından prototipe gereksinim duyulmadan tanınan ve ne yaptığı bilinen özel fonksiyonlara "built-in" ya da "intrinsic" fonksiyonlar denilmektedir. Tabii bu kavramlar C standartlarında yoktur. Eklenti (extension) biçiminde C derleyicilerinde bulunmaktadır. Bazı built-in ya da intrinsic fonksiyonlar makro gibi derleyici tarafından özel makine komutları ile açılmaktadır. Bazı built-in ya da intrinsic fonksiyonlar ise makro gibi açılmamalarına karşın derleyici bunların ne yaptığını bildiği için o işlemlerde optimizasyon uygulayabilmektedir. Örneğin: for (int i = 0; i < strlen(s); ++i) { ... } Normal olarak derleyici strlen fonksiyonunun ne yaptığını bilmediği için burada optimizasyon uygulayıp çağrıyı döngü dışına alamaz. Ancak ilgili derleyicide strlen fonksiyonu aynı zamanda built-in ya da intrinsic bir fonksiyonsa derleyici burada bu optimizasyonu yapabilir. Her derleyicinin built-in ya da intrinsic fonksiyon listesi diğerinden farklı olabilmektedir. gcc derleyicilerinin built-in fonksiyonlarına aşağıdaki bağlantıdan erişilebilir: https://gcc.gnu.org/onlinedocs/gcc/x86-Built-in-Functions.html gcc'de bazı built-in fonksiyonlar atomik işlemler için bulundurulmuştur. 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 global bir değişken iki thread tarafından atomic bir biçimde artırılmıştır. Biz daha önce aynı örneği mutex ve spinlock senkronizasyon nesneleri için de vermiştik. Aşağıda aynı örneğin mutex ve spinlock versiyonlarını da yeniden veriyoruz. Örneğin yapıldığı 2 çekirdekli sanal makinede elde edilen sonuçlar şöyledir: Atomic versiyon: 1.86 saniye Mutex versiyonu: 4.30 saniye Spinlock versiyonu: 2.83 saniye Görüldüğü gibi en hızlı yöntem artırma işleminin atomic yapılmasıdır. ---------------------------------------------------------------------------------------------------------------------------*/ /* 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); } /* mutex.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); pthread_mutex_t g_mutex; int g_count = 0; 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); printf("%d\n", g_count); if ((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); return 0; } void *thread_proc1(void *param) { int result; for (int i = 0; i < MAX_COUNT; ++i) { if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++g_count; if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } return NULL; } void *thread_proc2(void *param) { int result; for (int i = 0; i < MAX_COUNT; ++i) { if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++g_count; if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* spinlock.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); pthread_spinlock_t g_spinlock; int g_count; int main(void) { pthread_t tid1, tid2; int result; if ((result = pthread_spin_init(&g_spinlock, 0)) != 0) exit_sys_errno("pthread_spin_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_spin_destroy(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_destroy", result); printf("%d\n", g_count); return 0; } void *thread_proc1(void *param) { int result; for (int i = 0; i < MAX_COUNT; ++i) { if ((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if ((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void *thread_proc2(void *param) { int result; for (int i = 0; i < MAX_COUNT; ++i) { if ((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if ((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki anlatımdan da anlaşılacağı gibi atomic built-in ya da intrinsic fonksiyonlar derleyiciler arasında taşınabilir değildir. Örneğin gcc'deki __atomic_xxx_ fonksiyonları yerine Microsoft sistemlerinde InterlockedXXX fonksiyonları kullanılmaktadır. İşte bu tür atomic işlemlerde taşınabilirliği sağlamak için zamanla programlama dillerininin sentaks ve semantiğine atomic işlemler eklenmiştir. C11 ile birlikte C'ye thread kavramı sokulunca atomic işlemler de dile "isteğe bağlı olarak (optional)" dahil edilmiştir. C11'de _Atomic anahtar sözcüğü "bir tür niteleyicisi (type qualifier)" olarak eklenmiştir. (Yani sentaks bakımından _Atomic niteleyicisi const ve volatile gibi bir tür niteleyicisi grubundadır.) Örneğin: _Atomic int g_count; Burada g_count nesnesi atomic olarak tanımlanmıştır. Ayrıca C11'de _Atomic anahtar sözcüğü parantezli biçimiyle bir "tür belirleyicisi (type specifier)" olarak da kullanılabilmektedir. Örneğin: _Atomic(int) g_count; Bu bakımdan "tür niteleyicisi (type qualifier)" ve "tür belirleyicisi (type specifier)" kullanımları arasında göstericiler söz konusu olduğunda farklılıklar oluşabilmektedir. Ancak genellikle programcılar tür niteleyicisi kullanımını tercih etmektedir. Ayrıca C11 ile birlikte başlık dosyası da standartlara eklenmiştir. Bu dosya içerisinde çeşitli atomic fonksiyonların prototipleri bulunmaktadır. Aynı zamanda _Atomic tür niteleyicisi ile oluşturulmuş bazı typedef isimleri de bulunmaktadır. Bu typedef isimlerinin oluşturulma biçimleri aşağıdaki gibidir: typedef atomic_bool _Atomic _Bool; typedef atomic_char _Atomic char; typedef atomic_schar _Atomic signed char; typedef atomic_uchar _Atomic unsigned char; typedef atomic_short _Atomic short; typedef atomic_ushort _Atomic unsigned short; typedef atomic_int _Atomic int; typedef atomic_uint _Atomic unsigned int; typedef atomic_long _Atomic long; typedef atomic_ulong _Atomic unsigned long; typedef atomic_llong _Atomic long long; typedef atomic_ullong _Atomic unsigned long long; ... Bu durumda örneğin: _Atomic int g_count; tanımlaması ile aşağıdaki tanımlama eşdeğerdir: atomic_int g_count; atomic nesnelerin =, ++, --, +=, *=, ... gibi operatörlerle kullanılmasında derleyici işlemleri tıpkı __atomic_xxx fonksiyonlarında olduğu gibi atomic bir biçimde yapmaktadır. Dolayısıyla örneğin atomic işlemler için gcc'nin built-in __atomic_xxx fonksiyonları yerine doğrudan C11 ile gelen _Atomic niteleyicisi tercih edilebilir. C11 ile birlikte gelen _Atomic tür niteleyicisi ve tür belirleyicisinin ve dosyasının bazı ayrıntıları vardır. Biz burada bu ayrıntılara girmeyeceğiz. Bunlar için uygun dokümanlara başvurabilirsiniz. C11'deki atomic konusu standartlara "isteğe bağlı (optional)" bir öğe olarak eklenmiştir. Yani her C derleyicisi bunu desteklemek zorunda değildir. (Örneğin bir gömülü sistemde atomic işlemlerin bir anlamı olmayabilir ya da atomic'lik ilgili işlemcide bulunmayabilir.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Daha önce yapmış olduğumuz global nesneyi artırma örneğini bu kez aşağıda C11'deki _Atomic tür niteleyicisi ile yeniden yapıyoruz. Burada programın çalışma zamanı __atomic_xxx built-in fonksiyonunu kullandığımız örnekle çok benzerdir. ---------------------------------------------------------------------------------------------------------------------------*/ /* 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); _Atomic 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) ++g_count; return NULL; } void *thread_proc2(void *param) { for (int i = 0; i < MAX_COUNT; ++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); } /*-------------------------------------------------------------------------------------------------------------------------- Atomic işlemler için C++11'e atomic isimli sınıf şablonu eklenmiştir. Bu sınıf şablonunda tür parametresi atomic nesnenin türünü belirtmektedir. Örneğin: #include ... atomic g_count; Yukarıdaki örneğin C++'ın atomic sınıfı ile gerçekleştirimi aşağıda verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* atomic.cpp */ #include #include #include #include #include using namespace std; #define MAX_COUNT 100000000 void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); atomic 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", (int)g_count); return 0; } void *thread_proc1(void *param) { for (int i = 0; i < MAX_COUNT; ++i) ++g_count; return NULL; } void *thread_proc2(void *param) { for (int i = 0; i < MAX_COUNT; ++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); } /*-------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi birden fazla thread'e sahip bir proseste fork işlemi yapıldığında yaratılan alt proses her zaman tek bir thread'e sahip olur. O thread de fork işleminin yapıldığı thread'tir. Örneğin biz programamızda 10 tane thread yaratmış olalım. Bu thread'lerden birinde fork yaptığımızı düşünelim. Üst prosesin bütün bellek alanı alt prosese kopyalanacaktır. Ancak alt proseste yalnızca tek bir thread akışı bulunacaktır. Bu akış da fork işleminin yapıldığı thread akışı olacaktır. Yani alt proses her zaman çalışmasına tek bir thread ile başlamaktadır. Tabii daha önceden de belirttiğimiz gibi bir prosesin son thread'i de sonlandığında prosesin çalışması işletim sistemi tarafından sonlandırılır. Bu durumda örneğin üst proseste fork yapan thread, main thread değilse programın akışı exit fonksiyonunu hiç görmeyebilir. Ancak bu thread bittiğinde zaten proses otomatik sonlandırılmaktadır. Thread'li bir programda fork işlemi yapıldığında alt proseste yalnızca fork işlemini yapan thread'in yaratılmış olacağını belirtmiştik. Tabii fork işlemi sırasında proseslerin tüm bellek alanları kopyalandığına göre pthread_t türü ile temsil edilen thread id değerleri de kopyalanmaktadır. Yani fork işlemi sonrasında üst prosesin fork işlemini yapan thread'in id'si ile alt prosesin bu thread'e ilişkin id'leri aynı olacaktır. Thread id'lerin prosese özgü olduğunu anımsayınız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte üst proses iki thread yaratmıştır. Üst proses bu iki thread'i yarattıktan sonra ana thread'te fork işlemi yapmıştır. Bu durumu alt proses yalnızca ana thread akışına sahip olacaktır. Örnekte alt prosesteki ana thread'te pthread_exit fonksiyonu ile bu ana thread'te sonlandırılmıştır. Bu durumda alt proses otomatik olarak sonlandırılacaktır. Dolayısıyla biz yalnızca üst prosesin thread akışlarının stdout dosyasına yazdıklarını göreceğiz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { int result; pthread_t tid1, tid2; pid_t pid; if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) printf("parent process\n"); else { printf("child process\n"); pthread_exit(NULL); } 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 (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void *thread_proc1(void *param) { int i; for (i = 0; i < 20; ++i) { printf("thread-1: %d\n", i); sleep(1); } return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 20; ++i) { printf("thread-2: %d\n", i); sleep(1); } return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte üst proses iki ayrı thread yaratmıştır. Ancak fork işlemi bu thread'lerden birinde yapılmıştır. Alt proseste yalnızca fork işleminin yapıldığı thread'in çalıştığına dikkat ediniz. Tabii yine alt proses akışı aslında exit fonksiyonu hiç görmemektedir. Ancak bir prosesin son thread'i sonlandığında zaten proses işletim sistemi tarafından sonlandırılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { int result; pthread_t tid1, tid2; if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc1(void *param) { int i; pid_t pid; for (i = 0; i < 20; ++i) { printf("thread-1: %d\n", i); if (i == 10) { if ((pid = fork()) == -1) exit_sys("fork"); } sleep(1); } if (pid != 0 && waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 20; ++i) { printf("thread-2: %d\n", i); sleep(1); } return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bazı çok thread'li uygulamalarda bir thread'in fork işlemi yapması durumunda üst ve alt proseslerde otomatik olarak bazı işlemlerin yapılması gerekebilmektedir. Ancak böylesi durumlara oldukça seyrek gereksinim duyulmaktadır. İşte bir thread fork yaptığında üst ve alt proseslerde bazı fonksiyonların otomatik çağrılmasını sağlamak için pthread_atfork isimli bir POSIX fonksiyonu bulundurulmuştur. Fonksiyonun prototipi şöyledir: #include int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)); pthread_atfork fonksiyonu üç fonksiyonu parametre olarak almaktadır. Birinci fonksiyon üst proses tarafından henüz alt proses yaratılmadan çağrılmaktadır. İkinci fonksiyon üst proses tarafından alt proses yaratıldıktan sonra çağrılır. Üçüncü fonksiyon ise alt proses tarafından alt proses yaratıldığında çağrılmaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. pthread_atfork fonksiyonu birden fazla kez çağrılabilir. Bu durumda prepare, parent ve child fonksiyonları ters sırada çağrılır. Aşağıda pthread_atfork fonksiyonunun kullanımına ilişkin bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void prepare(void); void parent(void); void child(void); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { int result; pthread_t tid1, tid2; if ((result = pthread_atfork(prepare, parent, child)) != 0) exit_sys_errno("pthread_atfork", result); if ((result = pthread_atfork(prepare, parent, child)) != 0) exit_sys_errno("pthread_atfork", result); if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc1(void *param) { int i; pid_t pid; for (i = 0; i < 20; ++i) { printf("thread-1: %d\n", i); if (i == 10) { if ((pid = fork()) == -1) exit_sys("fork"); } sleep(1); } if (pid != 0 && waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 20; ++i) { printf("thread-2: %d\n", i); sleep(1); } return NULL; } void prepare(void) { printf("prepare handler...\n"); } void parent(void) { printf("parent handler...\n"); } void child(void) { printf("child handler...\n"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Thread'li uygulamalarda fork yaparken senkronizasyon nesnelerine dikkat edilmelidir. Çünkü fork işlemi sırasında üst prosesteki başka bir thread o anda mutex gibi bir nesneyi kilitlemiş olabilir. Bu durumda üst prosesin başka bir thread'i fork yaptığında alt proseste bu mutex nesnesi kilitli gibi olacaktır. Alt proseste bu mutex kullanılmak istendiğinde zaten kilitli olduğu için deadlock oluşabilecektir. Çünkü artık alt proseste onu kilitlemiş olan thread bulunmadığı için kilit açılmayacaktır. Örneğin üst prosesteki bir thread aşağıdaki gibi bir kritik kod oluşturmuş olsun: thread_proc1() { ... pthread_mutex_lock(&g_mutex); ... ... ... pthread_mutex_unlock(&g_mutex); ... } Üst prosesin diğer bir thread'i bu thread kritik kodun içerisinde fork yapmış olabilir: thread_proc2() { ... if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { /* child process */ do_something(); } ... } Şimdi artık alt proseste bu mutex nesnesi kilitli durumda olacaktır. Çünkü fork işlemi sırasında üst prosesin bütün bellek alanı dolayısıyla mutex'in kilitli olduğuna ilişkin bilgiler de alt prosese aktarılmış olacaktır. Artık bu mutex kilidinin açılma olasılığı yoktur. Dolayısıyla örnekte do_something fonksiyonun içerisinde de kritik kod varsa orada deadlock oluşacaktır. İşte bu tür durumlarda pthread_atfork fonksiyonu yararlı bir işlev görebilir. Şöyle ki; fork işleminden önce prepare fonksiyonunda mutex nesnesinin kilidi açılabilir. Bu durumda mutex alt prosese kilidi açık bir biçimde aktarılacaktır. Sonra üst proses, parent fonksiyonunda yine onu kilitleyebilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında thread'li programlarda exec amacı olmadan fork işlemi yapmak genellikle kötü bir tekniktir. Çünkü zaten thread'ler proseslerin aynı bellek alanı içerisinde çalışan biçimleri gibidir. Yani aslında thread'li uygulamalarda exec amacıyla olmayan fork gereksinimi de genellikle olmamaktadır. Ancak bazı durumlarda yine de thread'li uygulamalarda fork işleminin yapılması gerekebilmektedir. Programcı, thread'li uygulamalarda mutlak gerekmiyorsa, exec amacıyla olmayan fork işlemi yapmaktan kaçınmalıdır. Thread'li programlarda, programcı exec amacıyla fork yapmış olabilir. Thread'li uygulamalarda exec için fork yapma işlemine nispeten daha fazla gereksinim duyulabilmektedir. Thread'li programlarda alt prosesin fork işleminden sonra exec uygulaması genel olarak probleme yol açmamaktadır. Çünkü exec işlemi ile zaten prosesin bellek alanı tamamen yok edilmektedir. Pekiyi thread'li bir program hiç fork yapmadan exec yaparsa ne olur? İşte thread'li bir program exec işlemi uygularsa prosesin exec yapan thread'i dışındaki tüm thread'leri sonlandırılır. Dolayısıyla exec işlemi sonrasında yine exec yapılan program tek thread'le çalışmaya başlar. Özetle thread'li programlarda fork-exec işlemleri için şunlar söylenebilir: 1) Thread'li programlarda exec amacıyla fork yapmakta sakınca yoktur. Ancak exec amacı olmadan fork yapmak genel olarak kötü bir tekniktir. 2) Thread'li uygulamalarda fork işlemi sonrasında tek bir thread alt proseste çalışır. Bu thread'te fork işlemini yapan thread'tir. 3) Thread'li programlarda fork olmadan exec uygulanırsa exec yapılan program yine tek thread'le çalışmaya başlar. Yani proseste o ana kadar yaratılmış olan bütün thread'ler sonlandırılır. Yalnızca exec işlemini yapan thread exec edilen programı çalıştırır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 65. Ders 09/07/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İşletim sistemlerinde proseslerin ve thread'lerin CPU'ya atanıp zaman paylaşımlı olarak çalıştırılmasına proses ya da thread çizelgelemesi denilmektedir. Eskiden thread'ler yokken çizelgelenen şeyler proseslerdi. Ancak daha sonra işletim sistemine thread'ler sokulduğunda artık thread temelinde çizelgeleme yöntemi uygulanmaya başlandı. Bugün Windows, Linux ve macOS gibi sistemler thread temelinde çizelgeleme uygulamaktadır. Yani bu işletim sistemleri thread'leri zaman paylaşımlı biçimde CPU'ya atayıp çalıştırmaktadır. İşletim sistemlerinin thread'leri (eskiden prosesleri) çizelgelemekte kullandığı algoritmalara "çizelgeleme algoritmaları (scheduling algorithms)" denilmektedir. Tarihsel süreç boyunca işletim sistemlerinde çeşitli çizelgeleme algoritmaları denenmiştir. Bu çizelgeleme algoritmaları bazı somut hedefleri gerçekleştirmeye çalışmaktadır. Örneğin "birim zamanda yapılan iş miktarı (throughput)" önemli bir performans ölçütüdür. Thread'ler arası geçiş (context switch) belli bir zaman aldığı için bu zamanın thread'lere ayrılan zamana göre makul olması istenir. İnteraktivite diğer önemli bir ölçüttür. İlgili thread uzun süre çalışmadan bekletilirse bu durum interaktiviteyi olumsuz etkiler ve gerçek zamanlı olayların izlenmesini zorlaştırır. Bir işin toplamda ne kadar sürede bitirileceği (turnaround time) diğer bir ölçüttür. Günümüzde en yaygın kullanılan çizelgeleme algoritması "döngüsel çizelgeleme (round robin scheduling)" denilen algoritmadır. Bu algoritmada kabaca thread'ler ismine "çalışma kuyruğu (run queue)" denilen bir kuyruk sisteminde tutulur. Sonra bu kuyruktan alınarak CPU'ya atanır. Belli bir quanta süresi kadar çalıştırılır. Sonra CPU'dan preemptive biçimde (zorla) koparılır. Kuyruktaki sırada thread CPU'ya atanır. Döngüsel çizelgeleme adil bir çizelgeleme yöntemidir. Bir thread CPU'ya atanmışken bloke olduğunda "çalışma kuyruğundan (run queue)" çıkartılır ve o olaya ilişkin bir "bekleme kuyruğuna (wait queue)" yerleştirilir. Blokede bekleyen thread'lerin ilgili olay gerçekleştiğinde yeniden çalışma kuyruğuna yerleştirilmesi genellikle "kesme (interrupt)" mekanizmasıyla sağlanmaktadır. Örneğin bir thread'in sleep fonksiyonu ile 10 saniye beklemek istediğini düşünelim. Bu durumda, bu sleep fonksiyonun içerisinde işletim sistemi thread'i çalışma kuyruğundan çıkartıp sleep için oluşturulan bekleme kuyruğuna yerleştirir. Pekiyi 10 saniyenin geçtiğini işletim sistemi nasıl anlayacaktır? İşte aslında modern işletim sistemlerinde periyodik "timer kesmeleri" oluşturulmaktadır. Örneğin Linux sistemlerinde aslında her bir milisaniyede bir timer kesmesi oluşup işletim sisteminin bir kodu devreye girmektedir. (Linux sistemlerinde buna "jiffy" de denilmektedir.) Bu kod sleep kuyruklarına da bakmakta ve süresi dolan thread'leri yeniden çalışma kuyruğuna yerleştirmektedir. Örneğin bir thread bir soketten okuma yapmak istesin ancak sokete henüz bilgi gelmemiş olsun. İşletim sistemi thread'i bloke ederek çalışma kuyruğundan çıkartır ve ilgili bekleme kuyruğuna yerleştirir. Network kartına bir paket geldiğinde bir kesme de oluşmaktadır. Bu kesme sayesinde network kartına gelen paket işletim sistemi tarafından alınır. Böylece bilginin gelmesini bekleyen thread de uyandırılarak bekleme kuyruğundan yeniden çalışma kuyruğuna yerleştirilir. O halde belli bir zamanda çalışma kuyruğunda belli miktarda thread bulunurken bekleme kuyruklarında da belli miktarda thread bloke durumda bulunmaktadır. Döngüsel çizelgelemenin çeşitli varyasyonları vardır. Örneğin Windows sistemlerinde "öncelik sınıfı temelinde döngüsel çizelgeleme" yöntemi kullanılmaktadır. Linux sistemlerinde çizelgeleme algoritması temel olarak "döngüsel çizelgeleme" esasına dayanmaktadır. Ancak bu algoritma Linux tarihi boyunca üç kez değiştirilmiştir. Linux'un ilk versiyonları uzunca bir süre özel bir isim verilmeyen bir döngüsel çizelgeleme algoritması kullanıyordu. Sonra çekirdeğin 2.6 versiyonlarıyla (2003) birlikte "O(1) çizelgelemesi" denilen yeni bir algoritma kullanılmaya başlandı. Daha sonra çekirdeğin 2.6.23 versiyonu ile birlikte (2007) şu anda kullanılmakta olan "CFS (Completely Fair Scheduling)" algoritmasına geçilmiştir. Tabii izleyen paragraflarda da ele alınacağı gibi UNIX türevi sistemlerde proseslerin "çizelgeleme politikaları (process scheduling policy)" vardır. Buradaki algoritmalar SCHED_NORMAL ya da SCHED_OTHER denilen politikalar için özellikle etkili olmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde her prosesin bir "çizelgeleme politikası (scheduling policy)" vardır. POSIX standartlarına göre proseslerin çizelgeleme politikaları şunlardan biri olabilir: SCHED_FIFO SCHED_RR SCHED_OTHER SCHED_SPORADIC SCHED_SPORADIC çizelgelemesi isteğe bağlı tutulmuştur ve Linux sistemlerinde desteklenmemektedir. POSIX standartlarında SCHED_FIFO ve SCHED_RR çizelgelemelerine "gerçek zamanlı (realtime) çizelgeleme politikaları" denilmektedir. Prosesin çizelgeleme politikası fork işlemi sırasında üst prosesten alınmaktadır. Linux'ta default çizelgeleme politikası SCHED_OTHER biçimindedir. Linux dünyasında bu çizelgeleme politikasına SCHED_NORMAL da denilmektedir. (Ancak SCHED_NORMAL POSIX standartlarında bulunmamaktadır.) Linux'ta ayrıca POSIX standartlarında olmayan SCHED_BATCH ve SCHED_IDLE çizelgeleme politikaları da vardır. POSIX standartlarında prosesin default çizelgeleme politikasının ne olacağı konusunda bir belirlemede bulunulmamıştır. Ancak sistemlerin hemen hepsinde default çizelgeleme politikası SCHED_OTHER biçimindedir. POSIX standartlarına göre bir prosesin çizelgeleme politikası onun bütün thread'lerinin çizelgeleme politikasını belirlemektedir. Yani örneğin POSIX standartlarına göre biz prosesin çizelgeleme politikasını değiştirirsek onun mevcut ve yaratılacak olan bütün thread'lerinin çizelgeleme politikasını değiştirmiş oluruz. Ancak Linux sistemleri bu bağlamda POSIX standartlarına uymamaktadır. Linux sistemlerinde bir prosesin çizelgeleme politikası değiştirildiğinde yalnızca onun ana thread'inin çizelgeleme politikası değiştirilmiş olmaktadır. POSIX standartlarında SCHED_FIFO ve SCHED_RR çizelgeleme algoritmaları açıkça tanımlanmıştır. Ancak SCHED_OTHER çizelgeleme politikası işletim sistemlerini yazanların isteğine bırakılmıştır. Yukarıda da belirttiğimiz gibi UNIX türevi sistemlerde genellikle proseslerin default çizelgeleme politikaları SCHED_OTHER biçimindedir. Bu SCHED_OTHER çizelgeleme politikası da işletim sistemlerini yazanların isteğine bırakılmıştır. (Ayrıca POSIX standartları SCHED_OTHER politikasının SCHED_FIFO ve SCHED_RR ile aynı olabileceğini de belirtmiştir.) O halde mademki UNIX türevi sistemlerde genellikle proseslerin default çizelgeleme politikaları SCHED_OTHER biçimindedir ve bu SCHED_OTHER da sistemden sisteme değişebilmektedir, o halde aslında UNIX türevi istemlerde çizelgeleme algoritması sistemden sisteme değişebilir niteliktedir. Zaten yukarıda da bahsettiğimiz gibi Linux'ta arihsel olarak kullanılan üç çizelgeleme algoritması aslında SCHED_OTHER çizelgeleme politikasına ilişkindir. Çünkü SCHED_FIFO ve SCHED_RR çizelgelemeleri zaten POSIX standartlarında açıkça tanımlanmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Tipik olarak UNIX türevi işletim sistemlerinde SCHED_OTHER çizelgeleme politikası (ya da algoritması) kabaca şöyle yürütülmektedir: 1) İşletim sistemi "çalışma kuyruğundaki (run queue)" her thread için bir quanta sayaç değeri tutar. (Linux işletim sisteminde thread'ler de task_struct yapısı ile temsil edildiği için bu quanta sayaç değeri task_struct içerisindedir.) Örneğin bu sayaç değerinin başlangıçta 200 olduğunu varsayalım. (Mevcut Linux sistemlerinde genellikle bir quanta sayaç değeri 1 milisaniyedir.) 2) Bir thread CPU'ya atandığında, her timer kesmesi oluştuğunda onun quanta sayacı bir eksiltilir. Günümüzdeki Linux sistemlerinde genellikle timer kesmesi 1 milisaniyeye ayarlandığı için her bir milisaniye geçtiğinde bu quanta sayaç değeri 1 eksiltilmektedir. 3) İşletim sistemi timer kesmesinde (jiffy) thread'in quanta sayaç değeri 0'a düştüğünde thread'ler arası geçiş yoluyla thread'in çalışmasına ara verir ve çalışma kuyruğundaki diğer thread'i CPU'ya atar. 4) Genellikle işletim sistemleri CPU'ya atanacak thread'i belirlemek için çalışma kuyruğundaki tüm thread'lerin quanta sayaç değerlerine bakıp en yüksek quanta sayaç değerine sahip thread'i CPU'ya atamaktadır. Örneğin çalışma kuyruğunda 5 thread olsun bunların sayaç değerleri de aşağıdaki gibi olsun: T1: 120 T2: 89 T3: 0 T4: 18 T5: 0 Şimdi varsayalım ki T3 thread'inin quanta sayacı 0'a düşmüş olsun ve thread'ler arası geçiş yapılsın. Burada T1 thread'i CPU'ya atanacaktır. 5) Pekiyi çalışma kuyruğunda quanta sayacı 0'a düşmüş olan thread'lerin yeni sayaçları ne zaman doldurulmaktadır? İşte genellikle işletim sistemleri çalışma kuyruğundaki tüm thread'lerin quanta sayaçları 0'a düştüğünde hepsini birden doldurmaktadır. Yani çalışma kuyruğundaki bir thread'in quanta sayacı 0'a düştüğünde onun quanta sayacı hemen doldurulmamakta, çalışma kuyruğundaki tüm thread'lerin quanta sayaçları 0'a düştüğünde hepsi birden doldurulmaktadır. Bu durumda quanta sayacı 0'a düşen thread, çalışma kuyruğundaki diğer thread'lerin hepsinin quanta sayacı 0'a düşene kadar CPU'ya atanmamaktadır. 6) İşletim sistemi yalnızca çalışma kuyruğundaki thread'leri çizelgelemektedir. Bir thread çalışırken bloke olduğunda çalışma kuyruğundan çıkartılıp bekleme kuyruklarına alınır. Artık işletim sistemi o thread'i CPU'ya atamaya çalışmaz. Şimdi bir thread'in quanta sayacı 100'e geldiğinde bloke olduğunu düşünelim. İşletim sistemi de çalışma kuyruğundaki en yüksek sayaçlı thread'i CPU'ya atamış olsun ve bu biçimde çalışma kuyruğundaki tüm thread'lerin sayaçları 0'a düşmüş olsun. Pekiyi doldurma işleminde, bekleme kuyruklarındaki thread'lere de doldurma yapılacak mıdır? İşletim sistemleri genellikle onlara da doldurma yapar. Ancak bir üst sınır da belirlemektedir. Yani bizim bekleme kuyruğundaki thread'imizin quanta sayacı 100 olsun. Şimdi çalışma kuyruğundaki her thread'e doldurma yapılacak olsun. Bu quanta sayacı 100 olan thread'e de doldurma yapılacaktır. Yani bu thread uyandığında daha fazla CPU zamanı kullanacaktır. Böylesi bir sistemin daha adil olduğu düşünülmektedir. 7) Pekiyi çalışma kuyruğundaki tüm thread'lerin quanta sayaçları 0'a düşmüş olsun. İşletim sistemi de onların quanta sayaçlarını yeniden dolduracak olsun. Hepsini aynı değerle mi dolduracaktır? İşte SCHED_OTHER çizelgeleme politikasına sahip olan thread'ler için bu doldurma değeri onların "nice" değeri ya da "dinamik öncelikleri" denilen bir değerle orantılı bir biçimde yapılmaktadır. POSIX standartları bu nice değerinin bu konuda etkili olacağını söylemiş olsa da hiçbir ayrıntı vermemiştir. nice değerinin etkisi SCHED_OTHER politikasına özgüdür ve sistemden sisteme değişebilir. Örneğin Linux'ta CFS algoritmasında nice değerinin etkisi önceki klasik çizelgeleme ve O(1) çizelgelemesine göre biraz daha artırılmıştır. Biz yukarıda tipik olarak bir UNIX türevi sistemin SCHED_OTHER politikasında nasıl döngüsel çizelgeleme yaptığını kabaca açıkladık. Ancak işletim sistemlerinin çizelgeleyici alt sistemlerinin pek çok ayrıntıları vardır. Linux'un CFS algoritmasının ayrıntılarını ilgili dokümanlardan inceleyebilirsiniz. Biz yukarıda sanki tek bir CPU ya da çekirdek varmış gibi durumu ele aldık. Günümüz bilgisayarlarında genellikle birden fazla CPU ya da çekirdek bulunmaktadır. İşletim sistemlerinin çoğu her CPU ya da çekirdek için ayrı bir çalışma kuyruğu (run queue) oluşturmaktadır. Tabii bir CPU ya da çekirdeğin çalışma kuyruğundaki thread'ler azalırsa, diğerlerinin çalışma kuyruğundaki thread'ler o CPU ya da çekirdeğin kuyruğuna aktarılabilmektedir. Tabii bu konuda sistemler arasında ve algoritmalar arasında farklılıklar söz konusu olmaktadır. Örneğin Linux'un O(1) çizelgeleme algoritmasında tek bir çalışma kuyruğu oluşturulmuştu. Hangi CPU ya da çekirdekteki thread quantasını doldurduğunda bu tek kuyruğa bakılıp oradan seçme yapılıyordu. (Örneğin mağazada üç kasa olabilir. Ancak tek kuyruk olabilir. Bu durumda her kasadaki işlem bittiğinde aynı kuyruktan yeni kişi o kasaya alınmaktadır.) Ancak Linux'ta CFS algoritmasıyla her CPU ya da çekirdek için ayrı çalışma kuyrukları oluşturulmuş ve kuyruktan kuyruğa gerektiğinde transfer imkanı sağlanmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi SCHED_OTHER çizelgeleme politikasına sahip olan proseslerin (yani onun bütün thread'lerinin) "dinamik önceliği" ya da "nice" değeri denilen onların diğerlerine göre CPU kullanım miktarını ayarlamakta etkili olan değerleri vardır. İşletim sistemleri tipik olarak çalışma kuyruğundaki tüm thread'lerin quanta sayaçları 0'a düştüğünde, onların yeni sayaç değerlerini bu dinamik önceliğe ya da nice değerine göre doldurmaktadır. Ancak bu dinamik öncelik ya da nice değerinin tam olarak bu quanta sayaçları üzerinde nasıl bir etkiye sahip olacağı hakkında POSIX standartlarında bir şey söylenmemiştir. Örneğin Linux'ta SCHED_OTHER prosesler için CFS algoritmasına geçildiğinde bu nice değerinin etkisi her bir nice değeri için %10 civarında olmaktadır. Linux sistemlerinde, SCHED_OTHER proseslerin nice değerinin sınır değerleri [0, 39] arasındadır. POSIX standartları bunun orta noktasına NZERO değeri demektedir. POSIX standartlarına göre SCHED_OTHER proseslerin dinamik önceliğinin [0, 2*NZERO-1] aralığında olduğu belirtilmiştir. Yani Linux sistemleri için bu NZERO değeri 20'dir. Dinamik öncelik ya da nice için yüksek değer düşük öncelik, düşük değer ise yüksek öncelik belirtmektedir. (Yani 0 değeri en yüksek önceliği, 39 değeri en düşük önceliği belirtmektedir.) Yukarıda belirttiğimiz gibi Linux sistemlerinde SCHED_OTHER proseslerin nice değerleri için orta nokta (NZERO) 20'dir. Bu 20 değeri ortalama 200 milisaniyelik bir quanta süresine karşı gelmektedir. Bu değer yükseltildikçe quanta süresi düşmekte, düşürüldükçe quanta süresi yükseltilmektedir. Ancak bugün kullanılan CFS algoritmasının bazı ayrıntıları vardır. Bu ayrıntılar "Linux Kernel" kursumuzda ele alınmaktadır. İşletim sistemi tarafından çizelgelenen öğelerin prosesler olmadığına thread'ler olduğuna dikkat ediniz. POSIX standartları "prosesin dinamik önceliği ya da nice değeri" demekle onun bütün thread'lerinin dinamik önceliğini kastetmektedir. Yani dinamik öncelik proses temelinde değiştirilse de aslında Linux işletim sistemi bu dinamik öncelikleri thread temelinde ele almaktadır. Ayrıca spesifik bir thread'in dinamik önceliği de değiştirilebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir SCHED_OTHER politikasına sahip prosesin dinamik önceliği yani nice değeri setpriority fonksiyonuyla set edilebilir ve getpriority fonksiyonu ile alınabilir. Bunun için nice ve renice isimli iki fonksiyon da bulunmaktadır. setprioriy ve getpriority fonksiyonlarının prototipleri şöyledir: #include int getpriority(int which, id_t who); int setpriority(int which, id_t who, int value); Buradaki which parametresi şunlardan biri olabilir: PRIO_PROCESS PRIO_PGRP PRIO_USER PRIO_PROCESS yalnızca tek bir prosesin dinamik önceliğinin alınacağını ya da değiştirileceğini, PRIO_PGRP bir proses grubundaki tüm proseslerin dinamik önceliklerinin alınacağını ya da değiştirileceğini ve PRIO_USER belli bir etkin user id'ye ilişkin tüm proseslerin dinamik önceliklerinin alınacağını ya da değiştirileceğini belirtmektedir. Fonksiyonun who isimli ikinci parametresi, which parametresine göre ilgili prosesin proses id'si, proses grubunun proses grup id'si ve etkin kullanıcı id'si olabilmektedir. (Zaten bu nedenle id_t isimli bir tür uydurulmuştur. Bu tür pid_t ve uid_t türlerinin her ikisini de ifade edebilecek bir türdür.) Fonksiyonun bu ikinci parametresi 0 girilirse duruma göre çağrıyı yapan proses, çağrıyı yapan prosese ilişkin proses grup id ve çağrıyı yapana prosese ilişkin etkin kullanıcı id'si anlamına gelmektedir.) setpriority fonksiyonunun value parametresi set edilecek dinamik öncelik ya da nice değerini belirtmektedir. Burada value değeri NZERO değerine (yani Linux'ta 20) göreli biçimde girilmelidir. Yani örneğin bu value değeri 10 girilirse nice değeri aslında 20 + 10 = 30 anlamına gelir. Ya da örneğin bu value değeri -10 biçiminde girilirse 20 - 10 = 10 anlamına gelir. Bu duruma Linux sistemlerinde bu value değeri [-20, 19] aralığında girilmelidir. Ancak POSIX standartlarına göre bu value değerine, bu aralığın dışında bir değer verilirse bu aralıktaki minimum ya da maksimum değer anlaşılmaktadır. (Yani örneğin biz value değerine -50 verirsek bu değer Linux'ta geçersizdir. Ancak fonksiyon başarısız olmaz. Bu durumda sanki bu değer -20 girilmiş gibi etki gösterir. Yani nice değeri 0 olur.) setpriority fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. getpriority fonksiyonu ise başarı durumunda NZERO (20) değerine göreli olan nice değerine, başarısızlık durumunda -1 değerine geri dönmektedir. getpriority fonksiyonunun geri dönüş değeri -1 ise bu durum başarısızlık ya da gerçekten göreli -1 anlamına gelebileceği için bunun kontrolü ayrıca yapılmalıdır. Bu kontrol için baştan errno değeri 0'a set edilir. Sonra fonksiyon -1 değerine geri dönmüşse errno değerine bakılır. Örneğin: errno = 0; if ((result = getpriority(PRIO_PROCESS, pid)) == -1 && errno != 0) exit_sys("getpriority"); Biz getpriority fonksiyonu ile herhangi bir prosesin, proses grubunun ya da etkin kullanıcı id'sine ilişkin proseslerin nice değerini elde edebiliriz. Bu konuda bir erişim kontrolü uygulanmamaktadır. Ancak biz getpriority fonksiyonu ile bir proses grubunun ya da belli bir etkin kullanıcı id'sine ilişkin proseslerin nice değerlerini elde ederken aslında birden fazla proses söz konusu olduğu için getpriority fonksiyonu en düşük nice değerini (yani en öncelikli değeri) bize vermektedir. Biz setpriority fonksiyonu ile istediğimiz bir prosesin nice değerini değiştiremeyiz. POSIX standartlarına göre ancak uygun önceliğe sahip (örneğin root) prosesler nice değerini düşürerek daha fazla CPU zamanının elde edilmesini sağlayabilirler. Ancak normal prosesler eğer fonksiyonu çağıran prosesin gerçek ya da etkin kullanıcı id'si hedef prosesin etkin kullanıcı id'si ile aynı ise nice değerini yükselterek prosesin daha az CPU kullanmasını sağlayabilmektedir. Özetle kontrol mekanizması şöyledir: - Biz herhangi bir prosesin nice değerini yükseltip onun daha az CPU kullanmasını sağlayabilmemiz için o hedef prosesin etkin kullanıcı id'sinin bizim gerçek ya da etkin kullanıcı id'miz ile aynı olması gerekir. Başka bir deyişle biz yalnızca kendi proseslerimizin nice değerini düşürebiliriz. - Herhangi bir prosesin (kendimiz dahil) nice değerini düşürmek için (yani daha fazla CPU zamanı kullanmasını sağlamak için) bizim uygun önceliğe (tipik olarak root ya da Linux için CAP_SYS_NICE yeterliliğine) sahip olmamız gerekir. Linux 2.6.12 çekirdeği ile birlikte prosesin nice değerinin değiştirilmesini prosesin RLIMIT_NICE kaynak limiti ile ilişkilendirmiştir. Uygun önceliğe sahip olmayan prosesler ancak 0 ile 39 arasındaki nice değerini en fazla 20 - rlim_cur değeri olarak düşürebilir. rlim_cur değerinin default değeri ise 0 biçimindedir. Bu da uygun önceliğe sahip proseslerin normal olarak kendi nice değerlerini sayısal olarak düşüremeyeceği anlamına gelmektedir. Prosesin kaynak limitleri konusu ileride ele alınacaktır. Daha önceden de belirttiğimiz gibi POSIX standartlarına göre dinamik öncelik ya da nice değeri prosese özgüdür. Yani biz bir prosesin nice değerini set ettiğimizde onun bütün thread'lerinin nice değerini set etmiş oluruz. Ancak maalesef Linux sistemleri bu bakımdan POSIX standartlarına uymamaktadır. Linux'ta biz prosesin nice değerini set ettiğimizde bu işlemden yalnızca o prosesin ana thread'i etkilenmektedir. Aynı durum prosesin nice değerini alırken de söz konusu olmaktadır. Linux'ta her ne kadar nice değeri prosese değil thread'e özgü ise de Linux çekirdeği bir thread başka bir thread'i yarattığında yaratan thread'in nice değerini yaratılan thread'e geçirmektedir. Dolayısıyla her ne kadar thread'ler arasında altlık-üstlük durumu yoksa da bu istisnai durumlardan biridir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 66. Ders 16/07/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte çalışmakta olan programın nice değeri göreli bir biçimde elde edilmiş ve yazdırılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int result; errno = 0; if ((result = getpriority(PRIO_PROCESS, 0)) == -1 && errno != 0) exit_sys("getpriority"); printf("%d\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte komut satırından alınan proses id'ye ilişkin (argv[1]) prosesin nice değeri yine komut satırından alınan göreli nice değeri (argv[2]) olarak değiştirilmektedir. Örneğin: $ ./sample 0 -1 Burada biz kendi prosesimizin değerini düşürerek önceliğini yükseltmeye çalıştık. Bu işlem başarısızlıkla sonuçlanacaktır. Bunu yapabilmemiz için programı root önceliği ile çalıştırmamız gerekir. Örneğin: $ sudo ./sample 0 -1 Tabii biz kendi prosesimizin nice değerini yükselterek önceliğini düşürebiliriz. Örneğin: $ sudo ./sample 0 1 Bu işlem başarıyla sonuçlanacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { pid_t pid; int prio; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } pid = (pid_t)atol(argv[1]); prio = atoi(argv[2]); if (setpriority(PRIO_PROCESS, pid, prio) == -1) exit_sys("setpriority"); printf("success...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi biz terminalden proseslerin nice değerlerini nasıl görebiliriz? Bunun için ilk akla gelen "ps" komutunu kullanmaktır. ps komutu aslında ileride göreceğimiz gibi "proc" dosya sisteminden bilgileri almaktadır. ps komutunun kullanımına ilişkin pek çok ayrıntı vardır. Biz burada komutun bazı kullanımlarını açıklayacağız. - Eğer ps komtu hiç seçeneksiz çalıştırılırsa bulunulan terminale ve kullanıcıya ilişkin prosesler kısa biçimde listelenir. Örneğin: $ ps PID TTY TIME CMD 5471 pts/2 00:00:00 bash 5561 pts/2 00:00:00 ps - ps komutunda -l seçeneği daha fazla bilgi vermektedir. Bu bilgiler arasında nice değeri de vardır. Örneğin: $ ps -l F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 S 1000 5471 2108 0 80 0 - 3505 do_wai pts/2 00:00:00 bash 4 R 1000 5564 5471 0 80 0 - 3857 - pts/2 00:00:00 ps Ancak default durumda yine komut ilgili terminalde çalıştırılan prosesler hakkında bilgileri vermektedir. - ps default olarak o anki kullanıcının o anki terminaldeki proseslerini görüntüler. Eğer komutta -t ya da --tty belirtilirse spesifik bir terminalde çalışan prosesler görüntülenebilmektedir. Örneğin: $ ps -l -t pts/1 F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 1 S 0 5456 5455 0 80 0 - 4718 - pts/1 00:00:00 sudo 4 S 0 5457 5456 0 79 -1 - 693 - pts/1 00:00:00 sample - ps komutundaki -u seçeneği belli bir etkin kullanıcı id'sine ilişkin tüm prosesleri görüntülemektedir. Örneğin: $ ps -u USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND kaan 2133 0.0 0.1 14152 5624 pts/0 Ss 07:50 0:00 bash kaan 5471 0.0 0.1 14020 5316 pts/2 Ss 11:31 0:00 bash kaan 5609 0.0 0.0 15428 1580 pts/2 R+ 11:49 0:00 ps -u - Aslında ps komutundaki sütunlar istenildiği gibi ayarlanabilmektedir. Bunun için -o seçeneği kullanılmaktadır. Sütun isimleri ps komutunun man sayfasında "OUTPUT FORMAT CONTROL" başlığında tek tek belirtilmiştir. Örneğin: $ ps -t pts/1 -o pid,ni,cmd - ps komutunda prosesin thread'leri de görülmek isteniyorsa bu durumda -T seçeneği kullanılmalıdır. Bu seçenek girildiğinde aynı zamanda thread'in "task struct id'leri" de SPID sütunu ile görüntülenmektedir. Örneğin: $ ps -l -T -t pts/1 F S UID PID SPID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 1 S 0 5681 5681 5680 0 80 0 - 4717 - pts/1 00:00:00 sudo 4 S 0 5682 5682 5681 0 79 -1 - 19126 - pts/1 00:00:00 sample 1 S 0 5682 5683 5681 0 80 0 - 19126 - pts/1 00:00:00 sample Linux sistemlerinde proseslerin kullandığı kaynakları sort edilmiş bir biçimde görüntülemek için "top" isimli bir komut da bulunmaktadır. Bu komut da bilgileri aslında "proc" dosya sisteminden almaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi aslında POSIX standartlarında prosesin nice değeri değiştirildiğinde onun bütün thread'lerinin nice değerinin değiştirilmesi gerekmektedir. Ancak Linux buna uymamaktadır. Aşağıdaki örnekte biz "sample" programında bir thread yarattık. Sonra da prosesin nice değerini değiştirdik. Prosesin nice değerini -1 yaptığımızda bundan yalnızca prosesin ana thread'inin etkilendiğini göreceksiniz. Programı bir terminalde aşağıdaki gibi çalıştırabilirsiniz: $ sudo ./sample 0 -1 Diğer bir terminalden prosesin thread'lerine aşağıdaki gibi bakabilirsiniz: $ ps -t pts/1 -T -l F S UID PID SPID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 1 S 0 6191 6191 6190 0 80 0 - 4706 - pts/1 00:00:00 sudo 4 S 0 6192 6192 6191 0 79 -1 - 2742 - pts/1 00:00:00 sample 1 S 0 6192 6193 6191 0 80 0 - 2742 - pts/1 00:00:00 sample Görüldüğü gibi yalnızca prosesin ana thread'inin nice değeri değişmiştir. Tabii yukarıda da belirttiğimiz gibi biz bu örnekte setpriority fonksiyonunu thread yaratılmadan önce çağırmış olsaydık Linux'ta yalnızca üst thread'in nice değeri değiştirilecekti ancak thread yaratılırken bu nice değeri yaratılan thread'e de geçirilecekti. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include void *thread_proc(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(int argc, char *argv[]) { pid_t pid; int prio; pthread_t tid; int result; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } pid = (pid_t)atol(argv[1]); prio = atoi(argv[2]); if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if (setpriority(PRIO_PROCESS, pid, prio) == -1) exit_sys("setpriority"); printf("success...\n"); printf("press Ctrl+C to exit...\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { printf("thread is running...\n"); pause(); 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); } /*-------------------------------------------------------------------------------------------------------------------------- SCHED_OTHER çizelgeleme politikasına sahip olan proseslerin nice değerlerini değiştirmenin diğer bir yolu da nice isimli POSIX fonksiyonunu kullanmaktadır. Fonksiyonun prototipi şöyledir: #include int nice(int incr); Fonksiyon her zaman kendi prosesinin nice değerini değiştirmektedir. Bu anlamda setpriority fonksiyonunun düşük bir alt kümesi gibi çalışmaktadır. Ancak nice fonksiyonu her zaman prosesin o anki nice değerine göreli olarak nice değerini set etmektedir. Örneğin prosesin o anki nice değeri -2 olsun. Biz nice fonksiyonunu -2 değeri ile çağırırsak artık prosesin nice değeri -4 olacaktır. Halbuki setpriorty fonksiyonu prosesin o anki nice değerine göreli işlem yapmamaktadır. POSIX standartlarına göre nice fonksiyonu o anda prosesin tüm thread'lerinin nice değerini değiştirmektedir. Ancak Linux sistemleri yalnızca fonksiyonu çağıran thread'in nice değerini değiştirmektedir. Yukarıda da belirttiğimiz gibi nice fonksiyonu ile değiştirilen bu değer, thread başka bir thread yarattığında o thread'e geçirilmektedir. nice fonksiyonu başarı durumunda değiştirilmiş olan yeni nice değerine geri döner. nice fonksiyonu başarısızlık durumunda -1 değerine geri döner. Ancak programcı yine bu -1 değerinin başarıdan dolayı mı başarısızlıktan dolayı mı oluştuğunu errno değişkeni yoluyla belirlemelidir. Örneğin: errno = 0; if (nice(-1) == -1 && errno != 0) exit_sys("nice"); Aşağıdaki örnekte prosesin nice değeri, nice fonksiyonuyla iki kez üst üste değiştirilmiştir. Programı aşağıdaki gibi çalıştırabilirsiniz: $ sudo ./sample Diğer bir terminalden prosesin durumuna baktığınızda aşağıdakine benzer bir rapor elde edeceksiniz: $ ps -t pts/1 -T -l F S UID PID SPID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 1 S 0 6516 6516 6515 0 80 0 - 4708 - pts/1 00:00:00 sudo 4 S 0 6517 6517 6516 0 76 -4 - 2742 - pts/1 00:00:00 sample 1 S 0 6517 6518 6516 0 76 -4 - 2742 - pts/1 00:00:00 sample Bu örnekte önce nice fonksiyonunun çağrıldığına sonra thread'in yaratıldığına dikkat ediniz. Dolayısıyla yaratan thread'in nice değeri yaratılan thread'e aktarılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void *thread_proc(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; if ((result = nice(-2)) == -1) exit_sys("nice"); printf("%d\n", result); if ((result = nice(-2)) == -1) exit_sys("nice"); printf("%d\n", result); if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); printf("success...\n"); printf("press Ctrl+C to exit...\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { printf("thread is running...\n"); pause(); 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); } /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde nice isimli bir kabuk komutu da vardır. Bu komut belirtilen programı belirtilen nice değeri ile çalıştırmaktadır. nice komutunun genel biçimi şöyledir: nice - Örneğin: $ nice -1 ./sample Burada sample programı 1 nice değeri ile çalıştırılmaktadır. Burada "-" karakteri seçenek için kullanılan "-" karakteridir. Örneğin aşağıdaki programı nice komutuyla şöyle çalıştıralım: $ nice -1 ./sample Burada başka bir terminale geçip duruma bakalım: $ ps -t pts/0 -T -l F S UID PID SPID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 S 1000 2133 2133 2108 0 80 0 - 3538 do_wai pts/0 00:00:00 bash 0 S 1000 6735 6735 2133 0 81 1 - 2742 futex_ pts/0 00:00:00 sample 1 S 1000 6735 6736 2133 0 81 1 - 2742 do_sys pts/0 00:00:00 sample Prosesin iki thread'inin de nice değerinin değiştiğini görüyorsunuz. Çünkü nice programı önce fork yapıp prosesin nice değerini değiştirip exec yapmaktadır. Linux sistemlerinde de yaratan thread'in nice değerinin yaratılan thread'e aktarıldığını anımsayınız. Şimdi de nice değerini düşürüp aynı denemeyi yapalım: $ sudo nice --1 ./sample Diğer terminalden duruma bakalım: $ ps -t pts/1 -T -l F S UID PID SPID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 1 S 0 6777 6777 6776 0 80 0 - 4707 - pts/1 00:00:00 sudo 4 S 0 6778 6778 6777 0 79 -1 - 2742 - pts/1 00:00:00 sample 1 S 0 6778 6779 6777 0 79 -1 - 2742 - pts/1 00:00:00 sample ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void *thread_proc(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); printf("success...\n"); printf("press Ctrl+C to exit...\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { printf("thread is running...\n"); pause(); 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); } /*-------------------------------------------------------------------------------------------------------------------------- nice komutu (yani programı) bir nice değeri ile bir programı çalıştırmaktadır. Ancak ayrıca bir de renice isimli komut vardır. Bu renice programı zaten bizim yukarıda setpriority örneğinde yazdığımız program gibi çalışmaktadır. Yani belli bir prosesin nice değerini değiştirmektedir. Bunun için programın zaten çalışıyor olması gerekmektedir. Komutun basit kullanımı şöyledir: renice - -p Örneğin: renice -1 -p 6827 Tabii burada Linux sistemlerinde yalnızca prosesin ana thread'inin nice değeri değişecektir. nice ve renice komutları aslında POSIX standartlarında belirtilmiş olan standart komutlardandır. Ancak POSIX standartlarında renice fonksiyonu prosesin tüm thread'lerinin nice değerinin değişmesine yol açmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi mademki Linux sistemlerinde setpriority ve nice fonksiyonları yalnızca prosesin ana thread'leri üzerinde etkili olmaktadır. Pekiyi bu durumda Linux sistemlerinde belli bir thread'in nice değeri nasıl değiştirilir? İşte Linux'ta prosesler de olduğu gibi tüm thread'ler için de birer task_struct yapısı bulundurulmuştur. Aslında Linux sistemlerinde pid değeri, task_struct yapısına özgü bir değerdir ve her thread'in de bir pid değeri vardır. Bu pid değerine, thread'ler söz konusu olduğunda pid değeri yerine tid değeri de denilebilmektedir. Linux çekirdeği ana thread'in task_struct yapısı içerisinde thread_group isimli bir bağlı liste yoluyla o ana thread'e (yani prosese) ilişkin tüm task_struct yapılarını tutmaktadır. Böylece bir çekirdeğe bir prosesin pid değerini verdiğimizde, çekirdek o pid değerinden hareketle prosesin ana thread'inin task_struct yapısını bulmakta ve o yapının thread_group bağlı listesinden hareketle o prosesin tüm thread'lerinin task_struct yapısına erişebilmektedir. task_struct yapısı içerisinde tgid isimli eleman o prosesin ana thread'ine ilişkin task_struct yapısının pid değerini vermektedir. Biz prosesin hangi thread'inde getpid fonksiyonunu çağırırsak çağıralım aslında prosesin ana thread'ine ilişkin task_struct yapısının pid değerini elde etmiş oluruz. Zaten POSIX standartlarında thread'lerin pid değerleri yoktur. Yalnızca proseslerin pid değerleri vardır. Yukarıda da belirttiğimiz gibi Linux'ta thread'lerin task_struct yapısından hareketle elde edilen bu id değerine pid yerine daha çok tid denilmektedir. Özetle Linux çekirdeğinde durum şöyledir: - Her thread'in ayrı bir task_struct yapısı vardır. Bu durumda aslında her thread'in ayrı bir pid değeri var gibidir. Thread'ler için bu pid değerine tid de denilmektedir. - Ana thread'e ilişkin task_struct yapısının pid değeri aynı zamanda prosesin pid değeri olarak kullanılmaktadır. Biz hangi thread'te getpid fonksiyonunu çağırırsak çağıralım hep prosesin pid değerini elde ederiz. - Prosesin ana thread'inin task_struct yapısı içerisinde prosesin bütün thread'lerinin task_struct adresleri tutulmaktadır. Yani çekirdek bir proses id'den hareketle prosesin tüm thread'lerine ilişkin task_struct yapılarına erişebilmektedir. Pekiyi mademki Linux sistemlerinde her thread'in ayrı bir pid değeri (tid değeri) varmış gibi bir durum oluşmaktadır. Thread'lerin bu tid değerlerini nasıl elde edebiliriz? İşte bunun için Linux gettid isimli bir sistem fonksiyonu ve onu çağıran bir kütüphane fonksiyonu bulundurulmuştur. Fonksiyonun prototipi şöyledir: #define _GNU_SOURCE #include pid_t gettid(void); Fonksiyon başarı durumunda thread'in pid değerine (tid değerine) geri döner. Fonksiyon başarısız olamaz. gettid fonksiyonunun bir POSIX fonksiyonu olmadığına, Linux'a özgü bir fonksiyon olduğuna dikkat ediniz. Fonksiyonu kullanabilmek için kaynak kodun tepesinde ( include işleminin yukarısına) _GNU_SOURCE makrosunu define etmelisiniz. Tabii bunun yerine gcc ve clang derleyicilerinde -D seçeneğini de kullabilirsiniz. Örneğin: $ gcc -D _GNU_SOURCE -o sample sample.c ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 67. Ders 22/07/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte prosesin pid değeri, ana thread'in pid değeri ve yaratılmış olan thread'in pid değeri yazdırılmıştır. Program çalıştırıldığında aşağıdaki gibi bir çıktı elde edilmelidir: Process pid: 3026 Main thread pid: 3026 Thread pid (tid): 3027 Programdan çıkmak için Ctrl+C tuşlarına basınız. Başka bir terminalden durumu ps komutuyla aşağıdaki gibi gözleyebilirsiniz: ps -t pts/0 -T -l Burada programı çalıştırdığınız terminalin hangi terminal olduğunu tty komutu ile belirleyebilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #define _GNU_SOURCE #include #include #include #include #include #include #include void *thread_proc(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; pid_t pid; pid = getpid(); printf("Process pid: %jd\n", (intmax_t)pid); pid = gettid(); printf("Main thread pid (tid): %jd\n", (intmax_t)pid); if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { pid_t pid; pid = gettid(); printf("Thread pid (tid): %jd\n", (intmax_t)pid); pause(); 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); } /*-------------------------------------------------------------------------------------------------------------------------- O halde Linux'ta belli bir thread'in nice değerini elde etmek ve değiştirmek için getpriority ve setpriority fonksiyonları thread'e ilişkin pid değeri ile (tid değeri ile) çağrılabilir. nice fonksiyonu POSIX standartlarında prosesin tüm thread'leri üzerinde etkili olmaktadır. Ancak yukarıda da belirttiğimiz gibi Linux sistemlerinde yalnızca çağıran thread üzerinde etkili olmaktadır. Yani biz herhangi bir thread akışında nice fonksiyonunu çağırırsak zaten Linux'ta yalnızca o thread'in nice değerini değiştirmiş oluruz. Aşağıdaki örnekte bir thread yaratılmış ve o thread'in nice değeri setpriority fonksiyonu ile set edilip, getpriority fonksiyonu ile get edilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #define _GNU_SOURCE #include #include #include #include #include #include #include #include void *thread_proc(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { pid_t pid; int prio; pid = gettid(); if (setpriority(PRIO_PROCESS, pid, 10) == -1) exit_sys("setpriority"); errno = 0; if ((prio = getpriority(PRIO_PROCESS, pid)) == -1 && errno != 0) exit_sys("getpriority"); printf("Thread priority: %d\n", prio); return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Konunun başında da belirttiğimiz gibi UNIX/Linux sistemlerinde SCHED_FIFO ve SCHED_RR isimli iki "realtime" çizelgeleme politikası vardır. Her ne kadar bu politikalara "realtime" çizelgeleme politikaları deniliyorsa da aslında gerçek anlamda bir "realtime" uygulama bu politikalarla sağlanamamaktadır. Buradaki "realtime" olsa olsa soft bir realtime uygulamaya izin verebilmektedir. POSIX standartlarında bir proses SCHED_FIFO ve SCHED_RR çizelgeleme politikalarına sokulursa bundan o prosesin tüm thread'leri etkilenmektedir. Ancak POSIX standartları belli bir thread'in çizelgeleme politikasının SCHED_FIFO ya da SCHED_RR yapılabilmesine de olanak sağlamaktadır. SCHED_FIFO ve SCHED_RR çizelgeleme politikaları her zaman SCHED_OTHER politikasından daha baskındır. Yani sistemde o anda çalışabilecek bir SCHED_FIFO ya da SCHED_RR thread varsa bunlar bloke olmadıktan sonra ya da sonlanmadıktan sonra SCHED_OTHER thread'ler CPU'ya atanmazlar. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sistemde SCHED_FIFO ya da SCHED_RR thread'lerin "statik önceliği (static priority)" denilen bir öncelik derecesi vardır. Bu static öncelik SCHED_OTHER proseslerdeki nice değeri gibi değildir. POSIX standartları bu statik öncelik derecesinin alt ve üst limitleri konusunda bir belirlemede bulunmamıştır. Yani bu alt ve üst limitler sistemden sisteme değişebilmektedir. Ancak POSIX, bu alt ve üst limitlerin programın çalışma zamanı sırasında elde edilebilmesi için aşağıdaki iki fonksiyonu bulundurmaktadır: #include int sched_get_priority_max(int policy); int sched_get_priority_min(int policy); Fonksiyon parametre olarak çizelgeleme politikasının ismini almaktadır. Geri dönüş değeri olarak statik önceliğin alt ve üst limitlerini vermektedir. Fonksiyonlar başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyona parametre olarak SCHED_OTHER girilmemelidir. Burada parametre SCHED_FIFO ya da SCHED_RR olarak girilmelidir. Thread'lerin static önceliklerinin minimum değeri negatif olamamaktadır. Her ne kadar fonksiyonlar çizelgeleme politikalarını parametre olarak alıyorsa da SCHED_FIFO ve SCHED_RR aynı statik öncelik derecelerine sahiptir. Linux'ta SCHED_FIFO ve SCHED_RR thread'lerin minimum ve maksimum değerleri [1, 99] arasındadır. Statik öncelikler nice değeri gibi değildir. Statik önceliklerde gerçekten düşük değer düşük öncelik, yüksek değer yüksek öncelik belirtmektedir. Aşağıda ilgili sistemdeki minimum ve maksimum statik öncelikler yazdırılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { int priomin, priomax; if ((priomin = sched_get_priority_min(SCHED_FIFO)) == -1) exit_sys("sched_get_priority_min"); if ((priomax = sched_get_priority_max(SCHED_FIFO)) == -1) exit_sys("sched_get_priority_max"); printf("SCHED_FIFO primin: %d\n", priomin); printf("SCHED_FIFO primax: %d\n", priomax); if ((priomin = sched_get_priority_min(SCHED_RR)) == -1) exit_sys("sched_get_priority_min"); if ((priomax = sched_get_priority_max(SCHED_RR)) == -1) exit_sys("sched_get_priority_max"); printf("SCHED_RR primin: %d\n", priomin); printf("SCHED_RR primax: %d\n", priomax); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- SCHED_FIFO ve SCHED_RR çizelgeleme politikalarında Windows sistemlerinde olduğu gibi "öncelik sınıfına (priority class)" dayalı bir çizelgeleme kuyruğu oluşturulmaktadır. Yani her statik öncelik için ayrı bir kuyruk varmış gibi işlemler yürütülmektedir. Örneğin statik önceliği 10, 10, 10, 12, 12, 12, 12 olan 7 tane SCHED_FIFO ya da SCHED_RR thread olsun. Burada statik önceliği 10 olan thread'ler ayrı bir kuyrukta, 12 olan thread'ler ayrı bir kuyrukta bulundurulmaktadır. Mademki Linux'ta statik öncelikler [1, 99] arasındadır. O halde toplam 99 ayrı kuyruk olduğu varsayılabilir. Sistem her zaman yüksek öncelikli thread'lerin bulunduğu kuyruktaki thread'leri CPU'ya atamak ister. Örneğin sistemde 12 öncelikli SCHED_FIFO ya da SCHED_RR thread varsa bunlar bloke olmadan ya da sonlanmadan hiçbir zaman 10 öncelikli olanlar CPU'ya atanmazlar. Yani sistemde çalışmaya hazır yüksek statik öncelikli bir thread varsa hiçbir zaman düşük statik öncelikli bir thread CPU'ya atanmamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- SCHED_FIFO çizelgeleme politikasına sahip aynı statik öncelikte bir grup thread olsun. Bunların çizelgelemesi şöyle yapılmaktadır: 1) En öncelikli kuyruğun başındaki SCHED_FIFO thread CPU'ya atanır. Ancak quanta süresi diye bir durum yoktur. Bu thread sürekli CPU'da çalıştırılır. Ancak bloke olursa ya da biterse CPU'yu bırakır. 2) Bloke olmuş olan bir SCHED_FIFO thread'in blokesi çözüldüğünde bu thread kendi öncelik kuyruğunun sonuna yerleştirilir. 3) Eğer bir SCHED_FIFO thread çalışırken blokesi çözülen daha yüksek öncelikli bir SCHED_FIFO ya da SCHED_RR thread varsa bu SCHED_FIFO thread'in çalışmasına ara verilir ve CPU'ya o thread atanır. Örneğin sistemde 10, 10, 10, 12, 12, 12, 12 statik öncelik derecelerine sahip olan 7 tane thread olsun. Bunlar iki ayrı öncelik kuyruğunda bulunacaklardır: 10'un öncelik kuyruğu --> T1(10), T2(10), T3(10) 12'nin öncelik kuyruğu --> T4(12), T5(12), T6(12), T7(12) Burada işletim sistemi 12 kuyruğundaki önde bulunan T4 thread'ini CPU'ya atar. Bu T4 thread'i sürekli çalışır. Ancak bloke olursa sistem bu sefer T5 thread'ini CPU'ya atar ve onu sürekli çalıştırır. O da bloke olursa bu kez T6 thread'ini CPU'ya atar. Yani her zaman işletim sistemi o öncelik kuyruğundaki aynı önceliğe sahip olan kuyrukta önde bulunan thread'i CPU'ya atamaktadır. Tabii bu örnekte bloke olmuş olan T4 thread'inin blokesi çözülürse kuyruğun sonuna yerleştirilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- SCHED_RR çizelgeleme politikası SCHED_FIFO çizelgemeleme politikasına benzemektedir. Ancak bu çizelgeleme politikasına sahip olan thread'ler CPU'ya bir quanta süresi çalıştırılıp ilgili öncelik kuyruğunun sonuna yerleştirilmektedir. SCHED_FIFO ile SCHED_RR arasında tek farklılık şudur: SCHED_FIFO thread CPU'ya atandığında bloke olmadıkça, sonlanmadıkça ya da daha yüksek öncelikli bir thread'in blokesi çözülmedikçe o thread CPU'da çalışmaya devam eder. Ancak SCHED_RR bir thread bir quanta çalışıp kuyruğun sonuna atanmaktadır. Şimdi örneğin yine aşağıdaki gibi 7 tane SCHED_RR çizelgeleme politikasına sahip thread bulunuyor olsun: 10'un öncelik kuyruğu --> T1(10), T2(10), T3(10) 12'nin öncelik kuyruğu --> T4(12), T5(12), T6(12), T7(12) Burada işletim sistemi önce T4 thread'ini CPU'ya atar onu bir quanta çalıştırır ve kuyruğun sonuna atar. Sonra T5 thread'ini CPU'ya atar onu da bir quanta çalıştırır. (Tabii bu thread'ler daha quanta süresi dolmadan da bloke olabilirler.) SCHED_RR politikasına sahip thread'lerin quanta süreleri ile SCHED_OTHER politikasına sahip thread'lerin quanta sürelerinin bir ilgisi yoktur. SCHED_OTHER thread'lerin quanta süreleri onların nice değerine göre ayarlanabilmektedir. Halbuki SCHED_RR thread'lerin quanta süreleri onların statik öncelikleri ne olursa olsun hep aynıdır. Bugünkü Linux sistemlerinde tipik olarak SCHED_RR politikasına sahip thread'lerin quanta süreleri 100 milisaniye (0.1 saniye) kadardır. SCHED_RR thread'lerin quanta sürelerini almak için sched_rr_get_interval isimli bir POSIX fonksiyonu da bulundurulmuştur. Fonksiyonun prototipi şöyledir: #include int sched_rr_get_interval(pid_t pid, struct timespec *interval); Yukarıda da belirttiğimiz gibi güncel Linux sistemlerinde bu fonksiyondan elde edilecek değer 100 milisaniyedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz yukarıda SCHED_FIFO politikası ile SCHED_RR politikasını ayrı ayrı ele aldık. Aslında öncelik kuyruklarında (yani belli bir önceliğe ilişkin kuyrukta) SCHED_FIFO ve SCHED_RR politikalarına sahip thread'ler bir arada bulunabilirler. Örneğin aşağıdaki gibi thread'ler söz konusu olsun: 10'un öncelik kuyruğu --> T1(SCHED_FIFO), T2(SCHED_RR), T3(SCHED_RR) 12'nin öncelik kuyruğu --> T4(SCHED_RR), T5(SCHED_FIFO), T6(SCHED_RR), T7(SCHED_FIFO) Sistem her zaman en yüksek öncelik kuyruğunun başındaki thread'i CPU'ya atar. Örneğimizde bu thread T4 thread'idir. Bu thread SCHED_RR politikasına sahip olduğu için bir quanta (yani 100 milisaniye) çalıştırılacaktır. T4 thread'i bir quanta çalıştırıldıktan sonra kuyruğun sonuna alınır. Kuyruğun durumu şöyle olacaktır: 12'nin öncelik kuyruğu --> T5(SCHED_FIFO), T6(SCHED_RR), T7(SCHED_FIFO), T4(SCHED_RR) Şimdi sistem T5 thread'ini CPU'ya atar. T5 thread'i SCHED_FIFO politikasına sahip olduğu için CPU'da çalışmaya devam eder. Ta ki bloke olana kadar, sonlanana kadar ya da daha yüksek öncelikli bir SCHED_FIFO ya da SCHED_RR thread uyanana kadar. Şimdi T5 thread'inin bloke olduğunu düşünelim. Bu durumda kuyruğun sonuna alınacaktır. Kuyruğun durumu şöyle olacaktır: 12'nin öncelik kuyruğu --> T6(SCHED_RR), T7(SCHED_FIFO), T4(SCHED_RR), T5(SCHED_FIFO) Şimdi sistem T6 thread'ini CPU'ya atar. Bu thread SCHED_RR politikasına sahip olduğu için bir quanta çalıştırılır. Sonra kuyruğun sonuna yerleştirilir. Çalışma böyle devam eder. Burada bir kez daha şu durumu vurgulamak istiyoruz: SCHED_OTHER thread'ler default politikaya sahip thread'lerdir. Sistemde SCHED_FIFO ya da SCHED_RR thread varsa hiçbir zaman çizelgelenmezler. Linux çekirdeğinde gerçekleştirimi kolaylaştırmak için sanki SCHED_OTHER thread'lerin bir statik önceliği varmış ve sıfırmış gibi onlar için bir kuyruk oluşturulmaktadır. (Linux'ta SCHED_FIFO ve SCHED_RR politikalarına sahip thread'lerin en düşük önceliklerinin 1'den başlatıldığını anımsayınız.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi çok işlemcili ya da çekirdekli sistemlerde yukarıda açıkladığımız kurallar nasıl işletilmektedir? POSIX standartları bu konuda bir şey söylememiştir. Ancak her CPU ya da çekirdek diğerlerinden bağımsız ayrı bir birim gibi ele alınmaktadır. Örneğin iki çekirdekli bir bilgisayarda şu thread'ler bulunuyor olsun: T1(SCHED_OTHER), T2(10, SCHED_RR), T3(SCHED_OTHER) İşletim sistemi bu durumda örneğin SCHED_RR politikasına sahip olan T2 thread'ini bir çekirdeğe atar. Ancak diğer çekirdek boş kaldığı için onları da diğer çekirdeğe atayabilir. Yani adeta sanki iki ayrı bilgisayar varmış gibi işlemler yürütülmektedir. İki çekirdekli sistemde aşağıdaki gibi thread'ler söz konusu olsun: T1(SCHED_OTHER), T2(10, SCHED_RR), T3(SCHED_OTHER), T4(10, SCHED_FIFO) Burada işletim sistemi thread'leri önce çekirdeklere atayıp sonra yukarıdaki çizelgeleme kurallarını uygulamaktadır. İşletim sistemi burada isterse bir çekirdeğe T2 ve T4 thread'lerini diğerine de T1 ve T3 thread'lerini atayabilir. Ya da SCHED_RR ve SCHED_FIFO thread'lerini farklı çekirdeklere atayıp eş zamanlı da çalıştırabilir. Özetle birden fazla CPU ya da çekirdek varsa önce thread'ler bu CPU ya da çekirdeklere atanıp sonra sanki bu CPU ya da çekirdekler bağımsızmış gibi yukarıdaki kurallara uyularak bir çizelgeleme yapılmaktadır. Windows işletim sistemindeki çizelgeleme algoritması UNIX/Linux sistemlerindeki SCHED_RR politikasına çok benzemektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread'lerin default çizelgeleme politikaları SCHED_OTHER olduğuna göre SCHED_FIFO ya da SCHED_RR hangi durumlarda kullanılmalıdır? SCHED_FIFO ve SCHED_RR politikaları nispeten "soft realtime" uygulamalarda tercih edilmelidir. Bir olay gerçekleştiğinde o olayın hemen ele alınması gerekiyorsa bu politikalar tercih edilmelidir. Örneğin bir ısı sensöründe ısının belli bir değere geldiğinde derhal bir müdahalenin yapılması gereksin. Isıyı kontrol eden bir aygıt sürücünün olduğunu düşünelim ve ilgili thread'in blokede beklediğini varsayalım. Isı belli bir dereceye geldiğinde thread uyanacak ancak uyanır uyanmaz hemen CPU'ya atanacaktır. İşte bunu sağlamak için yüksek öncelikli bir SCHED_FIFO thread oluşturulabilir. SCHED_FIFO thread'lerin CPU yoğun olması diğer thread'lerin çalışamamalarına yol açabilmektedir. Bu nedenle SCHED_FIFO thread'lerin bloke olması ancak blokeden hızlı bir biçimde uyanıp CPU'ya atanması arzu edilir. Tabii bazen bir gömülü sistemde bir thread'in yoğun bir işlemi izlemesi gerekebilmektedir. Bu durumda thread'i bir çekirdeğe bağlayıp SCHED_FIFO önceliği verilebilir. Bu durumda adeta thread tek işlemli (single processing) bir sistemde olduğu gibi sürekli çalıştırılacaktır. SCHED_RR politikası bir grup thread'in "soft realtime" işlemler için kendi aralarında zaman paylaşımlı bir biçimde çalıştırılması için kullanılabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi proseslerin ve thread'lerin çizelgeleme politikaları ve SCHED_FIFO, SCHED_RR thread'lerin statik öncelikleri nasıl değiştirilmektedir. İşte bu işlemler için birkaç POSIX fonksiyonu bulundurulmuştur. İzleyen paragraflarda bu fonksiyonlar ele alınacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- sched_getparam ve sched_setparam POSIX fonksiyonları SCHED_FIFO ve SCHED_RR proseslerin statik önceliklerini alıp set etmek için kullanılmaktadır. Bu fonksiyonları SCHED_OTHER politikasına ilişkin thread'lerle kullanmaya çalışmayınız. Yine POSIX sistemlerinde bir prosesin statik önceliği değiştirildiğinde bu durum onun SCHED_FIFO ya da SCHED_RR olan tüm thread'lerine yansıtılmaktadır. Ancak Linux yalnızca prosesin ana thread'i için bu işlemi yapmaktadır. Tabii Linux'ta gettid fonksiyonu ile thread'e özgü pid değeri (tid değeri) bu fonksiyonlara verilirse thread'e özgü işlemler yapılabilmektedir. Fonksiyonların prototipleri şöyledir: #include int sched_getparam(pid_t pid, struct sched_param *param); int sched_setparam(pid_t pid, const struct sched_param *param); Fonksiyonun birinci parametresi işlemin yapılacağı prosese ilişkin pid değeridir. Bu değer 0 girilirse fonksiyonu çağıran proses üzerinde (Linux'ta çağıran thread üzerinde) işlem yapılmaktadır. İkinci parametre sched_param isimli bir yapı türündendir. Her ne kadar fonksiyonlar böyle bir yapı kullansalar da mevcut durumda bu yapının tek bir elemanı vardır. İleriye doğru uyumu korumak için burada böyle bir yapı kullanılmıştır. Yapının mevcut durumu şöyledir: struct sched_param { int sched_priority; }; 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. Yukarıda da belirttiğimiz gibi bu fonksiyonları SCHED_OTHER proses'ler için kullanmamalıyız. Linux'ta sched_getparam, SCHED_OTHER prosesler için her zaman 0 değerini vermektedir. Linux'ta herhangi bir prosesin statik önceliğinin alınması için bir koşul gerekmemektedir. Ancak diğer işletim sistemlerinde bunun için koşullar gerekebilmektedir. Proseslerin statik önceliklerinin set edilmesi için prosesin uygun önceliğe (root önceliği ya da Linux'ta capability'e) sahip olması gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir prosesin çizelgeleme politikasını almak ya da değiştirmek için sched_getscheduler ve sched_setscheduler POSIX fonksiyonları kullanılmaktadır. sched_getscheduler fonksiyonu yalnızca prosesin çizelgeleme politikasını almaktadır. Ancak sched_setscheduler fonksiyonu hem çizelgeleme poltikasını set etmekte hem de eğer politika SCHED_FIFO ya da SCHED_RR ise onun statik önceliğini set etmektedir. Fonksiyonların prototipleri şöyledir: #include int sched_getscheduler(pid_t pid); int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param) Fonksiyonların birinci parametreleri hedef prosesin pid değerini belirtir. Bu değer 0 geçilirse çağıran proses üzerinde işlem yapılır. sched_setscheduler fonksiyonunun ikinci parametresi çizelgeleme politikasını, üçüncü parametresi ise SCHED_FIFO ve SCHED_RR thread'lerin statik önceliklerini belirtmektedir. Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönerler. Prosesin çizelgeleme politikasının değiştirilmesi için prosesin "uygun önceliğe (appropriate priviledge)" sahip olması gerekmektedir. Prosesin çizelgeleme politikası SCHED_OTHER yapılırken sched_param yapısının sched_priority değeri Linux sistemlerinde 0 olarak ayarlanmalıdır. Aksi takdirde Linux sistemleri EINVAL errno değeri ile başarısız olmaktadır. POSIX standartlarında bu durum işletim sistemlerini yazanların isteğine bırakılmıştır. (implementation defined) POSIX standartlarına göre bu iki fonksiyon, prosesin tüm thread'leri üzerinde etkili olmaktadır. Ancak Linux sistemlerinde yalnızca ilgili thread üzerinde etkili olmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte proses kendisinin çizelgeleme politikasını SCHED_FIFO olarak statik öncelik derecesini de 1 olarak değiştirmektedir. Tabii bu programı root önceliği ile "sudo" yaparak çalıştırmalısınız. Prosesin politikasının ve öncelik derecesinin değiştirilebilmesi için prosesin uygun önceliğe sahip olması gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { struct sched_param sparam; int policy; sparam.sched_priority = 10; if (sched_setscheduler(0, SCHED_FIFO, &sparam) == -1) exit_sys("sched_setscheduler"); printf("success...\n"); if ((policy = sched_getscheduler(0)) == -1) exit_sys("sched_getscheduler"); switch (policy) { case SCHED_FIFO: printf("SCHED_FIFO policy\n"); break; case SCHED_RR: printf("SCHED_RR policy\n"); break; case SCHED_OTHER: printf("SCHED_OTHER policy\n"); break; default: printf("Any other policy\n"); break; } getchar(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 68. Ders 23/07/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bazen özellikle SCHED_FIFO ya da SCHED_RR thread'lerin kendi isteği ile CPU'yu bırakması istenebilmektedir. Örneğin bir SCHED_FIFO thread CPU'yu tekeline aldığı için hiç bloke olmadan CPU'yu bırakmak isteyebilir. Tabii bu tür gereksinimlerle oldukça seyrek bir biçimde karşılaşılmaktadır. İşte bu işlem sched_yield isimli POSIX fonksiyonuyla yapılmaktadır. Fonksiyonun prototipi şöyledir: #include int sched_yield(void); Fonksiyon her zaman o anda çalışmakta olan thread'in çalışmasına ara vererek hiç bloke olmadan ilgili thread'in kendi önceliğindeki çalışma kuyruğunun sonuna yerleştirilmesine yol açmaktadır. Örneğin sistemde şu thread'ler bulunuyor olsun: T1(12, FIFO), T2(12, FIFO), T3(12, RR) Burada işletim sistemi kuyruğun önündeki FIFO thread'i CPU'ya atayacaktır. Şimdi bu thread hiç bloke olmazsa hep CPU'da çalışmaya devam edecektir. İşte programcı sched_yield fonksiyonunu çağırırsa bu durumda bu thread kuyruğun sonuna yerleştirilecektir: T2(12, FIFO) T3(12, RR), T1(12, FIFO) Bu durumda artık kuyruğun başında T2 thread'i bulunduğu için o thread çizelgelenecektir. sched_yield fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Tabii fonksiyonun başarısız olma olasılığı aslında yoktur. POSIX standartlarında başarısızlık durumunda herhangi bir errno kodu da belirtilmemiştir. Linux sistemlerinde fonksiyon her zaman başarılı olmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar hep proses temelinde çalışan çizelgeleme fonksiyonlarını gördük. Ancak yukarıda da belirttiğimiz gibi bu fonksiyonlar Linux sistemlerinde yalnızca prosesin tüm thread'leri üzerinde değil yalnızca tek bir thread üzerinde etkili oluyordu. (Yani bu bağlamda Linux, POSIX standartlarını tam olarak desteklememektedir.) Pekiyi biz belli bir thread'in çizelgeleme özellikleri değiştirebilir miyiz? Linux sistemlerinde bunu thread'e özgü pid değerinden hareketle yapabiliyorduk. Aslında POSIX standartlarında thread'e özgü çizelgeleme belirlemelerinin yapılması da mümkündür. POSIX standartlarına göre belli bir thread'in çizelgeleme özellikleri thread yaratılırken özellik bilgisi yoluyla belirlenebilmektedir ya da bazı özellikler sonra da değiştirilebilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread yaratılırken thread özellikleri ile thread'lerin bazı özelliklerinin set edilebildiğini görmüştük. İşte belli bir thread'in çizelgeleme özellikleri de thread yaratılırken thread özellikleri ile belirlenebilmektedir. Bunun için kullanılan POSIX fonksiyonları şöyledir: #include int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy); int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param); int pthread_attr_setschedparam(pthread_attr_t * attr, const struct sched_param *param); Belli bir thread için çizelgeleme politikasının set edilmesi pthread_attr_setschedpolicy fonksiyonuyla yapılmaktadır. SCHED_FIFO ve SCHED_RR politikalarına ilişkin thread'lerin statik öncelikleri ise pthread_attr_setschedparam fonksiyonu ile set edilmektedir. Bu bilgiler yine özellik nesnesinin içerisinden pthread_attr_getschedpolicy ve pthread_attr_getschedparam fonksiyonlarıyla elde edilebilmektedir. Tabii bu özellik bilgilerinin değiştirilebilmesi için prosesin uygun önceliğe sahip olması gerekmektedir. Tabii kontrol bu özellik bilgilerinin set edilmesi sırasında değil thread'in yaratılması sırasında yapılmaktadır. Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. POSIX standartlarında SCHED_OTHER politikasına sahip bir thread'in nice değerini değiştirebilecek bir fonksiyon bulunmamaktadır. Yukarıdaki fonksiyonlar SCHED_FIFO ve SCHED_RR politikaları için düşünülmüştür. Ancak Linux sistemlerinde anımsanacağı gibi sched_setpriority fonksiyonu thread'e özgü pid değeri ile bu işlemi yapabilmektedir. Ancak yukarıdaki yöntemle thread'lerin çizelgeleme bilgilerini değiştirebilmek için öncelikle pthread_attr_setinheritsched fonksiyonu ile çizelgeleme özelliklerinin üst thread'ten alınmayacağı belirtilmelidir. Bunun için pthread_attr_setinheritsched fonksiyonu PTHREAD_EXPLICIT_SCHED parametresiyle çağrılmalıdır. Bu fonksiyonun (ve get eden fonksiyonun) prototipi şöyledir: #include int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched); int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched); Aşağıdaki örnekte SCHED_FIFO çizelgeleme politikası ile statik önceliği 10 olan bir thread yaratılmıştır. Programı sudo ile çalıştırmayı unutmayınız. Programı çalıştırdıktan sonra diğer bir terminalden durumu gözlediğimizde aşağıdaki gibi bir durum ortaya çıkmaktadır: $ ps -T --tty pts/2 -o pid,pri,policy,cmd,tid PID PRI POL CMD TID 9251 19 TS sudo ./sample 9251 9252 19 TS ./sample 9252 9252 50 FF ./sample 9253 Burada prosesin ana thread'inin SCHED_OTHER olduğunu ancak bizim yarattığımız thread'in SCHED_FIFO olduğunu görüyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #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; pthread_attr_t tattr; struct sched_param sparam; if ((result = pthread_attr_init(&tattr)) != 0) exit_sys_errno("pthread_attr_init", result); if ((result = pthread_attr_setinheritsched(&tattr, PTHREAD_EXPLICIT_SCHED)) != 0) exit_sys_errno("pthread_attr_setinheritsched", result); if ((result = pthread_attr_setschedpolicy(&tattr, SCHED_FIFO)) != 0) exit_sys_errno("pthread_attr_setschedpolicy", result); sparam.sched_priority = 10; if ((result = pthread_attr_setschedparam(&tattr, &sparam)) != 0) exit_sys_errno("pthread_attr_setschedparam", result); if ((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_attr_destroy(&tattr)) != 0) exit_sys_errno("pthread_attr_destroy", result); printf("Press Ctrl+C to exit...\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { pause(); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bir thread yaratıldıktan sonra da onun çizelgeleme politikaları statik öncelikleri değiştirilebilmektedir. Bu işlem pthread_setschedparam fonksiyonuyla yapılmaktadır. Thread'e ilişkin bu bilgiler pthread_getschedparam fonksiyonu ile elde edilebilmektedir. Bu fonksiyonlar hem çizelgeleme politikasında hem de statik öncelikler konusunda etkili olmaktadır. Fonksiyonların prototipleri şöyledir: #include int pthread_getschedparam(pthread_t thread, int *policy, struct sched_param *param); int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param); Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Tabii pthread_setschedparam fonksiyonunun başarılı olması için prosesin uygun önceliğe sahip olması gerekmektedir. Aşağıdaki örnekte önce bir thread yaratılmış daha sonra yaratılmış olan thread'in çizelgeleme politikası SCHED_FIFO, statik önceliği de 10 olacak biçimde pthread_setschedparam fonksiyonu ile değiştirilmiştir. Yine değişikliği başka bir terminalden ps komutu ile görebilirsiniz: $ ps -T --tty pts/2 -o pid,pri,policy,cmd,tid PID PRI POL CMD TID 9909 19 TS sudo ./sample 9909 9910 19 TS ./sample 9910 9910 50 FF ./sample 9911 ---------------------------------------------------------------------------------------------------------------------------*/ #include #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; struct sched_param sparam; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); sparam.sched_priority = 10; if ((result = pthread_setschedparam(tid, SCHED_FIFO, &sparam)) != 0) exit_sys_errno("pthread_setschedparam", result); printf("Press Ctrl+C to exit...\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { pause(); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- SCHED_FIFO ve SCHED_RR thread'lerin yalnızca statik önceliklerinin değiştirilmesi için pthread_setschedprio fonksiyonu kullanılmaktadır. Bu fonksiyonun bir get versiyonu yoktur. Fonksiyonun prototipi şöyledir: #include int pthread_setschedprio(pthread_t thread, int prio); Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Bu fonksiyonlar SCHED_OTHER politikası için kullanılmamalıdır. Aşağıdaki örnekte bir thread'in çizelgeleme politikası SCHED_FIFO yapılıp, statik önceliği de 10'ayarlanmıştır. Sonra pthread_setschedprio fonksiyonu ile bu öncelik 20 olarak değiştirilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; struct sched_param sparam; int policy; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); sparam.sched_priority = 10; if ((result = pthread_setschedparam(tid, SCHED_FIFO, &sparam)) != 0) exit_sys_errno("pthread_setschedparam", result); if ((result = pthread_getschedparam(tid, &policy, &sparam)) != 0) exit_sys_errno("pthread_getschedparam", result); printf("%d\n", sparam.sched_priority); if ((result = pthread_setschedprio(tid, 20)) != 0) exit_sys_errno("pthread_setschedprio", result); if ((result = pthread_getschedparam(tid, &policy, &sparam)) != 0) exit_sys_errno("pthread_getschedparam", result); printf("%d\n", sparam.sched_priority); printf("Press Ctrl+C to exit...\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { pause(); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da defalarca söylendiği gibi POSIX standartlarında belli bir SCHED_OTHER thread'in nice değerini (yani dinamik önceliğini) değiştirmenin standart bir yolu yoktur. setpriority ve getpriority fonksiyonları POSIX'te proses temelinde çalışmaktadır ve bu fonksiyonlar prosesin tüm thread'leri üzerinde etkili olmaktadır. Ancak defalarca belirttiğimiz gibi Linux'ta bu fonksiyonlar thread'e özgü işlemler yapabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 69. Ders 29/07/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Çok işlemcili ya da çok çekirdekli sistemlerde belli bir thread'in hangi işlemci ya da çekirdeğin çalışma kuyruğuna atanacağı işletim sisteminin sorumluluğundadır. İşletim sistemleri toplam faydayı göz önüne alarak bu atamayı yapar. Örneğin sistemimizde dört çekirdek olsun. Biz de 4 thread'li bir program yazmış olalım. İşletim sistemi bizim prosesimizin bu dört thread'ini ayrı çekirdeklere atamayabilir. Çekirdeklerin cache'leri birbirinden ayrı olduğu için aynı prosesin thread'lerinin aynı çekirdeğe atanması aslında toplam fayda bakımından işletim sisteminin tercih edeceği bir durumu oluşturabilmektedir. Ancak programcı özel bazı nedenlerden dolayı belli bir thread'in belli bir işlemci ya da çekirdeğin çalışma kuyruğuna atanmasını işletim sisteminden isteyebilir. Örneğin her ne kadar aynı prosesin thread'lerinin aynı işlemci ya da çekirdeğin çalışma kuyruğuna atanması toplam fayda bakımından daha iyi olabiliyorsa da bu durum paralel programlamayı sekteye uğratabilmektedir. Prosesin farklı thread'lerinin farklı işlemci ya da çekirdeklerin çalışma kuyruklarına atanması onların eş zamanlı bir biçimde çalışabilmelerine olanak sağlayabilmektedir. İşletim sistemlerinde belli bir thread'in belli bir işlemci ya da çekirdek tarafından çalıştırılmasının sağlanmasına İngilizce "prosessor affinity" denilmektedir. UNIX/Linux sistemlerinde "processor affinity" bazı fonksiyonlarıyla sağlanmaktadır. "Processor affinity" konusu taşınabilir bir konu olmadığı için POSIX standartlarına yansıtılmamıştır. Dolayısıyla bu işlemler işletim sistemine özgü sistem fonksiyonlarıyla ya da onları sarmalayan kütüphane fonksiyonlarıyla sağlanmaktadır. GNU libc kütüphanesinde bu konuyla ilgili iki fonksiyon bulunmaktadır: #define _GNU_SOURCE #include int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask); int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask); Fonksiyonların birinci parametresi ilgili thread'in pid değerini belirtmektedir. (Yani bu fonksiyonlara biz gettid fonksiyonundan elde edilen thread'e özgü pid değerlerini geçirebiliriz.) Bu pid parametresi 0 geçilirse fonksiyonu çağıran thread anlaşılmaktadır. Fonksiyonların üçüncü parametresi cpu_set_t türündedir. Bu tür aslında default olarak 1024 bite sahip olan bir veri yapısıdır. Bu veri yapısı muhtemelen aşağıdakine benzer biçimde typedef edilmiştir: typedef struct { unsigned long bitarray[16]; } cpu_set_t; Bu türden bir nesnenin bitlerini set ya da reset etmek için çeşitli makrolar bulundurulmuştur. Bu makroların önemli olanları şunlardır: void CPU_ZERO(cpu_set_t *set); void CPU_SET(int cpu, cpu_set_t *set); void CPU_CLR(int cpu, cpu_set_t *set); int CPU_ISSET(int cpu, cpu_set_t *set); int CPU_COUNT(cpu_set_t *set); CPU_ZERO makrosu tüm bit dizisini sıfırlamaktadır. CPU_SET makrosu bit dizisi dizisi içerisindeki belli bir biti 1 yapmaktadır. CPU_CLR makrosu ise bit dizisi içerisindeki belli bir biti 0 yapmaktadır. Bit dizisi içerisindeki belli bir bitin durumunu almak için CPU_ISSET makrosu kullanılmaktadır. CPU_COUNT makrosu ise bit dizisinde kaç bit olduğunu vermektedir. Aslında içerisinde daha pek çok makro vardır. Bunları dokümanlardan inceleyebilirsiniz. Makinemizdeki işlemci ya da çekirdeklerin numaraları 0'dan başlamaktadır. sched_setaffinity ve sched_getaffinity fonksiyonlarındaki ikinci parametre olan cpusetsize, üçüncü parametredeki bit dizisinin byte uzunluğunu belirtmektedir. Bu parametreye tipik olarak üçüncü parametredeki yapı nesnesinin sizeof değeri geçirilir. Fonksiyonlar başarı durumunda 0, başarısızlık durumunda -1 değerine geri dönmektedir. Tabii sched_setaffinity fonksiyonuna verdiğimiz bit dizisindeki elemanların yalnızca CPU sayısı kadar olanları dikkate alınmaktadır. Biz bu fonksiyon ile kendi thread'lerimizin CPU ayarını değiştirebiliriz. Ancak başkalarına ait thread'lerin ayarlarını değiştiremeyiz. sched_setaffinity fonksiyonunun başarılı olabilmesi için fonksiyonu çağıran prosesin etkin kullanıcı id'sinin hedef thread'e ilişkin prosesin gerçek kullanıcı id'si ile ya da etkin kullanıcı id'si ile aynı olması gerekmektedir. Tabii proses uygun önceliğe sahipse (appropriate privileged) bu durumda herhangi bir thread'in CPU ayarını değiştirebilir. Fonksiyonların ikinci parametresine neden gereksinim duyulduğu konusunda tereddütler oluşabilmektedir. Aslında cpu_set_t türünü kütüphaneyi yazanlar typedef ettiği için onun sizeof değerini zaten biliyor durumdadırlar. Ancak her ne kadar bu cpu_set_t türü 128 byte yani 1024 bit uzunluğundaysa da CPU ya da çekirdek sayısının çok fazla olduğu sistemlerde bunun artırılması gerekebilmektedir. Bunun artırılması için CPU_ALLOC makrosu kullanılmaktadır. Bu biçimde tahsis edilmiş olan cpu_set_t nesnesinin byte uzunluğu CPU_ALLOC_SIZE makrosuyla elde edilmektedir. İşte aslında programcının daha büyük cpu_set_t kullanabileceği olasılığı nedeniyle bu fonksiyonlar bu bit dizisinin byte uzunluğunu da bizden istemektedir. Linux sistemlerinde fork işlemiyle birlikte üst prosesin CPU ayarları alt prosese aktarılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte hem ana thread'in hem de yaratılan thread'in kullanabileceği CPU değiştirilmiştir. Ana thread 1 numaralı CPU'yu, yaratılan thread ise 2 numaralı CPU'yu kullanacak biçimde ayarlanmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #define _GNU_SOURCE #include #include #include #include #include void *thread_proc(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int result; cpu_set_t cpuset; pthread_t tid; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); CPU_ZERO(&cpuset); CPU_SET(1, &cpuset); if (sched_setaffinity(0, sizeof(cpuset), &cpuset) == -1) exit_sys("sched_setaffinity"); printf("Ok\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { pid_t pid; cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(3, &cpuset); if (sched_setaffinity(0, sizeof(cpuset), &cpuset) == -1) exit_sys("sched_setaffinity"); printf("Ok\n"); return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Linux'ta ayrıca pthread fonksiyonlarına benzer biçimde thread'in CPU ayarını değiştiren ve alan iki fonksiyon daha bulundurulmuştur: #define _GNU_SOURCE /* See feature_test_macros(7) */ #include int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize, const cpu_set_t *cpuset); int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize, cpu_set_t *cpuset); Bu fonksiyonların yukarıdaki fonksiyonlardan farkı thread'e ilişkin pid değeri yerine bizzat pthread_t türüyle belirtilen thread id değerini almasıdır. Böylece biz başka bir thread'in akışı içerisinde olmadan onların CPU ayarlarını değiştirebiliriz. Tabii bu iki fonksiyon da Linux'a özgüdür. Yani taşınabilir fonksiyonlar değildir. Fonksiyonlar diğer thread fonksiyonlarında olduğu gibi başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Aşağıdaki örnekte yine ana thread'in ve yaratılmış olan thread'in CPU ayarı değiştirilmiştir. Ancak bu değişiklik pthread_setaffinity_np fonksiyonuyla yapılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #define _GNU_SOURCE #include #include #include #include void *thread_proc(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int result; cpu_set_t cpuset; pthread_t tid; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); CPU_ZERO(&cpuset); CPU_SET(1, &cpuset); if ((result = pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset)) != 0) exit_sys_errno("pthread_setaffinity_np", result); CPU_ZERO(&cpuset); CPU_SET(2, &cpuset); if ((result = pthread_setaffinity_np(tid, sizeof(cpuset), &cpuset)) != 0) exit_sys_errno("pthread_setaffinity_np", result); printf("Ok\n"); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc(void *param) { printf("Ok\n"); return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi biz işin başında makinemizde kaç CPU ya da çekirdek olduğunu anlayabilir miyiz? Bunu anlamının bir yolu komut satırında nproc ya da lscpu komutlarını kullanmaktır. Örneğin: $ nproc 3 Örneğin: $ lscpu Mimari: x86_64 İşlemci işlem-kipi: 32-bit, 64-bit Address sizes: 45 bits physical, 48 bits virtual Bayt Sıralaması: Little Endian İşlemciler: 3 Çevrimiçi işlemci(ler) listesi: 0-2 Sağlayıcı Kimliği: AuthenticAMD Modem ismi: AMD Ryzen 7 5700G with Radeon Graphics İşlemci ailesi: 25 Model: 80 Çekirdek başına iş parçacığı: 1 Soket başına çekirdek: 1 Soket(ler): 3 Adımlama: 0 BogoMIPS: 7585.55 Bayraklar: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflus h mmx fxsr sse sse2 syscall nx mmxext pdpe1gb rdtscp lm constant_tsc rep_good nopl tsc_reliable nonstop_tsc cpuid extd_apicid tsc_known_freq pni pclmulqdq s sse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hype rvisor lahf_lm cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw topoext ibp b vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 xsaves clzero arat umip vaes vpclmulqdq r dpid overflow_recov succor fsrm Virtualization features: Hypervizör sağlayıcı: VMware Sanallaştırma tipi: tam Caches (sum of all): L1d: 96 KiB (3 instances) L1i: 96 KiB (3 instances) L2: 1,5 MiB (3 instances) L3: 48 MiB (3 instances) NUMA: NUMA düğümü(leri): 1 NUMA düğüm0 işlemci: 0-2 Vulnerabilities: L1tf: Not affected Mds: Not affected Meltdown: Not affected Mmio stale data: Not affected Retbleed: Not affected Spec store bypass: Vulnerable Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization Spectre v2: Mitigation; Retpolines, IBPB conditional, STIBP disabled, RSB filling, PBRSB-e IBRS Not affected Srbds: Not affected Tsx async abort: Not affected itlb multihit: Not affected Aslında bu komutlar proc dosya sisteminden bu bilgileri almaktadır. proc dosya sisteminde /proc/cpuinfo dosyasında bu bilgiler bulunmaktadır. Doğrudan cat komutu ile bu bilgiler alınabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında o anda sistemde kaç işlemci ya da çekirdek olduğu Linux'a özgü get_nprocs, get_nprocs_conf fonksiyonlarıyla elde edilebilmektedir. Fonksiyonların prototipleri şöyledir: #include int get_nprocs(void); int get_nprocs_conf(void); Buradaki get_nprocs_conf fonksiyonu makinemizdeki işlemci ya da çekirdek sayısını, get_nprocs fonksiyonu ise hali hazırda işletim sisteminin dikkate alarak kullandığı işlemci ya da çekirdek sayısını belirtmektedir. Genel olarak işletim sistemlerinde biz işletim sistemine "makinede şu kadar işlemci ya da çekirdek var ancak sen bunların şu kadarını kullan, diğerlerini görmezden gel" diyebilmekteyiz. Bu fonksiyonlar başarısız olamamaktadır. Aşağıdaki örnekte bu fonksiyonların kullanımına örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include int main(void) { printf("get_nprocs: %d\n", get_nprocs()); printf("get_nprocs_conf: %d\n", get_nprocs_conf()); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Linux'a özgü sched_getcpu isimli fonksiyon o anda fonksiyonu çağıran thread'in hangi işlemci ya da çekirdekte çalışmakta olduğu bilgisini vermektedir. Fonksiyonun prototipi şöyledir: #define _GNU_SOURCE #include int sched_getcpu(void); Aşağıdaki örnekte bu fonksiyonun kullanımına örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #define _GNU_SOURCE #include #include int main(void) { printf("CPU: %d\n",sched_getcpu()); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Thread güvenliliği (thread safety) genel olarak fonksiyonlar için kullanılan bir kavramdır. Bir fonksiyonun thread güvenli olması "o fonksiyonu birden fazla thread aynı anda çağırırsa bir sorunun oluşmaması" anlamına gelmektedir. Thread güvenli fonksiyonları biz farklı thread'lerden aynı anda çağırabiliriz. Bu durumda bir sorun oluşmaz. Pekiyi bir fonksiyonu thread güvenli (thread safe) olmaktan çıkartan şeyler nelerdir? Şüphesiz bir fonksiyon yalnızca yerel değişkenleri ve parametre değişkenlerini kullanıyorsa o fonksiyon zaten thread güvenlidir. Fonksiyonları thread güvenli olmaktan çıkartan unsur statik veri (data) ya da kaynak kullanmaktır. Örneğin bir fonksiyon bir global değişkeni değiştiriyorsa ve bu işlem için bir senkronizasyon uygulamamışsa bu fonksiyon thread güvenli olamaz. Çünkü bu fonksiyon birden fazla thread tarafından tesadüfen aynı anda çağrılırsa global değişkenin değeri bozulur. Benzer biçinde statik yerel değişkenleri kullanan fonksiyonlar da thread güvenli değildir. Örneğin C'nin localtime fonksiyonu bizden epoch'tan geçen saniye sayısını parametre olarak alır ve onu ayrıştırarak statik bir time_t yapı nesnesinin içerisine yerleştirir ve o nesnenin adresini bize verir. Örneğin: time_t t; struct tm *ptm; ... t = time(NULL); ptm = localtime(&t); Burada localtime fonksiyonu şöyle yazılmıştır: struct tm *localtime(time_t *pt) { static struct tm result; return &result; } Burada result değişkeninin toplamda tek bir kopyası olduğuna göre farklı thread'ler aslında aynı nesne üzerinde işlem yapmaktadır. Bu da fonksiyonun iki farklı thread tarafından aynı anda çağrıldığında soruna yol açacağı anlamına gelir. Bir fonksiyon statik ömürlü nesne kullanmadığı halde ortak başka kaynakları da kullanıyor olabilir. Bu tür fonksiyonlar da thread güvenli değildir. Pekiyi biz başkaları tarafından yazılmış olan foo fonksiyonunun thread güvenli olmadığını biliyorsak onu nasıl kullanmalıyız? Tabii burada tek seçeneğimiz bir mutex nesnesi ile onu kilitleyerek kullanmaktır. Örneğin: pthread_mutex_lock(...); foo(); pthread_mutex_unlock(...); Tabii bu kilitleme işleminin de bir zaman maliyeti vardır. Biz başkaları tarafından yazılmış olan kütüphaneleri kullanırken onların dokümantasyonlarına bakarak oradaki fonksiyonların thread güvenli olup olmadığını öğrenmeliyiz. Eğer kullanacağımız fonksiyon thread güvenli değilse onu yukarıda belirttiğimiz gibi bir mutex nesnesi ile kilitlemeliyiz. Pekiyi static nesne kullanan standart C fonksiyonları tasarımları gereği thread güvenli olamayacağına göre onları çok thread'li uygulamalarda nasıl kullanmalıyız? Microsoft 2004 yılına kadar standart C kütüphanesinin thread güvenli versiyonuyla thread güvenli olmayan versiyonunu ayrı ayrı bulunduruyordu. Ancak 2004 yılında makinelerin hızlandığı gerekçesiyle artık yalnızca thread güvenli standart C kütüphanesini bulundurmaya başlamıştır. Yani biz Micosoft C derleyicilerinde çalışıyorsak ilgili standart C fonksiyonları özünde thread güvenli bir tasarıma sahip değilse de Microsoft tarafından thread güvenli hale getirilmiştir. Pekiyi UNIX/Linux sistemlerinde durum nasıldır? İşte bu sistemlerde thread güvenlilik konusunda problemli fonksiyonların iki ayrı versiyonu yazılmıştır. Default versiyonlar thread güvenli değildir. Ancak sonu _r ile biten fonksiyonlar bunların thread güvenli versiyonlarıdır. Örneğin localtime fonksiyonu thread güvenli değildir. Ancak localtime_r fonksiyonu localtime fonksiyonunun thread güvenli versiyonudur. Burada dikkat edilmesi gereken nokta şudur: POSIX sistemlerinde xxx_r fonksiyonları orijinal fonksiyonlardan farklı parametrik yapılara sahiptir. Bu xxx_r fonksiyonlarında statik nesne kullanımı ortadan kaldırıldığı için bu fonksiyonların parametrik yapıları da orijinalinden farklıdır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 70. Ders 30/07/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Örneğin rand fonksiyonu statik veri kullandığı için thread güvenli değildir. Bilindiği gibi srand fonksiyonu "tohum (seed)" diye belirtilen bir global değişkeni set eder. Sonra rand fonksiyonu da bu global değişkenden hareketle yeni değerleri elde eder. Her rand çağrıldığında bu global değişkenin değeri güncellenmektedir. D. Ritchie ve B. Kernighan'ın "The C Programming Language" kitabında rand fonksiyonun olası bir gerçekleştirimi şöyle verilmiştir (değişken isimleri değiştirilmiştir): unsigned long int g_seed = 1; void srand(unsigned int seed) { g_seed = seed; } int rand(void) { g_seed = g_seed * 1103515245 + 12345; return (unsigned int)(g_seed / 65536) % 32768; } Statik veri kullandığı için UNIX/Linux sistemlerinde iki thread'in bu rand ve srand fonksiyonlarını kullanması uygun değildir. İşte rand fonksiyonunun thread güvenli biçimi rand_r ismiyle oluşturulmuştur. rand_r fonksiyonunun prototipi şöyledir: #include int rand_r(unsigned int *seedp); Burada fonksiyon tohum değere ilişkin nesnenin adresini almaktadır. Böylece fonksiyonun static veri kullanmasına gerek kalmamaktadır. Tabii aslında fonksiyona istenirse global bir nesnenin de adresi verilebilir. Ancak burada önemli olan nokta farklı thread'lerin farklı tohum nesnelerini kullanıyor olmasıdır. Aşağıdaki örnekte iki thread de rand_r fonksiyonunu kullanmıştır. Ancak bu thread'ler farklı tohum değerleri (seed) kullandığı için bir sorun olmayacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #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); 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); return 0; } void *thread_proc1(void *param) { unsigned seed = time(NULL) + 123; int randval; for (int i = 0; i < 10; ++i) { printf("thread-1: %d\n", i); randval = rand_r(&seed); usleep(randval % 500000); } return NULL; } void *thread_proc2(void *param) { unsigned seed = time(NULL) + 567; int randval; for (int i = 0; i < 10; ++i) { printf("thread-2: %d\n", i); randval = rand_r(&seed); usleep(randval % 500000); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Örneğin localtime fonksiyonu bize statik bir struct tm nesnesinin adresi vermektedir. Bu nedenle bu fonksiyon thread güvenli değildir. Fonksiyonun thread güvenli biçimi şöyledir: #include struct tm *localtime_r(const time_t *timep, struct tm *result); Burada localtime_r fonksiyonu kendi struct tm nesnesini alarak onun adresine geri dönmektedir. Yani kullanım şöyle olmalıdır: time_t t; struct tm tmval, *ptm; t = time(NULL); ptm = localtime_r(&t, &tmval); printf("%02d:%02d:%02d\n", ptm->tm_hour, ptm->tm_min, ptm->tm_sec); Tabii aslında localtime_r fonksiyonunun geri dönüş değerini kullanmak zorunda değiliz. Zaten bu geri dönüş değeri bizim verdiğimiz nesnenin adresidir. Yani kullanım şöyle de olabilirdi: time_t t; struct tm tmval; t = time(NULL); localtime_r(&t, &tmval); printf("%02d:%02d:%02d\n", tmval.tm_hour, tmval.tm_min, tmval.tm_sec); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde yalnızca bazı standart C fonksiyonlarının xxx_r'li versiyonları yoktur. Aynı zamanda statik veri kullanan bazı POSIX fonksiyonlarının da xxx_r'li versiyonları vardır. Örneğin getpwnam, getpwuid gibi fonksiyonlar statik nesnelerin adreslerini geri döndürmektedir. Aşağıda bu iki fonksiyonun normal ve _r'li versiyonlarının prototipleri verilmiştir: #include #include 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 */ }; struct passwd *getpwnam(const char *name); struct passwd *getpwuid(uid_t uid); int getpwnam_r(const char *name, struct passwd *pwd, char *buf, size_t buflen, struct passwd **restrict result); int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf, size_t buflen, struct passwd **restrict result); Fonksiyonların _r'li versiyonları /etc/passwd dosyasındaki satırların yerleştirileceği tampon alanın adresini ve uzunluğunu da istemektedir. Bu fonksiyonlara biz aynı zamanda struct passwd nesnelerinin adreslerini veririz. Fonksiyonların son parametrelerine başarı durumunda bizim verdiğimiz struct passwd nesnesinin adresi yerleştirilir. Başarısızlık durumunda ise bu nesneye NULL adres yerleştirilmektedir. Fonksiyonların bu _r'li versiyonları başarı durumunda 0, başarısızlık durumunda errno değerine geri dönmektedir. Fonksiyona vereceğimiz tamponun büyüklüğü sysconf(_SC_GETPW_R_SIZE_MAX) çağrısıyla elde edilebilmektedir. Ancak biz aşağıdaki örnekte geniş bir tampon tutarak fonksiyonu çağırıyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include int main(void) { char buf[4096]; struct passwd pwd, *ppwd; int result; result = getpwnam_r("kaan", &pwd, buf, 4096, &ppwd); if (result != 0) { fprintf(stderr, "getpwnam_r: %s\n", strerror(result)); exit(EXIT_FAILURE); } if (ppwd == NULL) { fprintf(stderr, "no user found...\n"); exit(EXIT_FAILURE); } printf("%s, %lld\n", pwd.pw_name, (long long)pwd.pw_uid); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Statik veri kullanan standart C fonksiyonlarının ve POSIX fonksiyonlarının xxx_r'li versiyonlarının nasıl kullanıldığını dokümanlardan öğrenebilirsiniz. Biz burada yalnızca birkaç fonksiyonu açıkladık. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Global nesneler ve heap'te yaratılan nesneler thread'ler arasında ortak bir biçimde kullanılıyordu. Ancak thread'lerin stack'leri birbirinden ayrılmıştı. Pekiyi thread'e özgü global değişken gibi bir kavram olabilir mi? Örneğin aşağıdaki gibi bir foo fonksiyonu olsun: void foo(void) { ... g_a = ...; ... } Burada g_a'nın global bir değişken olduğunu düşünelim. Bu durumda hangi thread foo fonksiyonunu çağırmış olursa olsun toplamda bir tane g_a olduğu için bu thread'lerin hepsi aynı global değişkeni kullanıyor olacaktır. Şimdi bu g_a global değişkeninin tıpkı stack'teki yerel değişkenlerde olduğu gibi thread'e özgü bir kopyasının bulunduğunu varsayalım. Bu durumda bu foo fonksiyonunu hangi thread çağırmışsa bu global değişken o thread'in global değişkeni olacaktır. İşte bu biçimde thread'e özgü statik veri kullanımını sağlamaya UNIX/Linux dünyasında "Thread Specific Data (TSD)", Windows dünyasında "Thread Local Storage (TLD)" denilmektedir. İşletim sistemi bir thread yaratıldığında yalnızca o thread için bir stack yaratmaz. Aynı zamanda o thread için bir TSD (Thread Specific Data) alanı da yaratır. Her thread için nasıl bir stack alanı varsa ve o stack alanı o thread'e özgü ise her thread için aynı zamanda bir TSD alanı da vardır ve o TSD alanı o thread'e özgüdür. Thread'ler için işletim sisteminin ayırdığı TSD alanı slotlardan oluşmaktadır. Alanın tipik veri yapısı şöyledir: Slot No Pointer 0 -----> 1 NULL 2 -----> 3 NULL ... ... Burada her slotun bir numarası vardır. Eğer slot doluysa o slot'un pointer elemanı gerçek nesneyi göstermektedir. Eğer slot boşsa o slotun pointer elemanı NULL pointer değerine sahiptir. TSD alanının kullanımı için dört POSIX bulundurulmuştur: #include int pthread_key_create(pthread_key_t *key, void (*destructor)(void*)); int pthread_key_delete(pthread_key_t key); void *pthread_getspecific(pthread_key_t key); int pthread_setspecific(pthread_key_t key, const void *value); Burada slotlar pthread_key_t türü ile temsil edilmiştir. POSIX standartlarına göre bu tür herhangi bir tür olarak typedef edilebilirse de tipik olarak int ya da unsigned int gibi bir tamsayı türü biçiminde typedef edilmektedir. TSD kullanımı şöyledir: 1) Önce pthread_key_create fonksiyonu ile boş bir slot elde edilir. Bu slot yaratılmış olan ve yaratılacak olan her thread'de var olacaktır. Fonksiyonun birinci parametresi slot bilgisinin (tipik olarak numarasının) yerleştirileceği pthread_key_t türünden nesnenin adresini almaktadır. Bu pthread_key_t nesnesinin tipik olarak global bir biçimde tanımlanması uygun olmaktadır. Fonksiyonun ikinci parametresi slot yok edilirken çağrılacak fonksiyonu belirtmektedir. Bu parametre NULL geçilebilir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: pthread_key_t g_key; ... if ((result = pthread_key_create(&g_key, NULL) != 0)) != 0) exit_sys_errno("pthread_key_create", result); Artık biz her thread'te geçerli olan bir slot tahsis etmiş olduk. Bu slot numarasını da g_key değişkenine yerleştirmiş olduk. 2) Programcı thread'e özgü statik verileri bir yapı olarak oluşturur. Bu yapı türünden dinamik tahsisat yapar ve tahsis ettiği alanın adresini pthread_setspecific fonksiyonu ile ilgili thread'in ilgili slotuna yerleştirir. pthread_setspecific fonksiyonunu hangi thread çağırmışsa adres o thread'in ilgili slotuna yerleştirilmektedir. pthread_setspecific fonksiyonunun birinci parametresi slotu belirten pthread_key_t değerini, ikinci parametresi de slota set edilecek adresi belirtmektedir. Fonksiyon yine başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: typedef struct tagTHREAD_SPECIFIC_DATA { int count; /* ... */ } THREAD_SPECIFIC_DATA; ... void *thread_proc1(void *param) { THREAD_SPECIFIC_DATA *tsd; int result; if ((tsd = (THREAD_SPECIFIC_DATA *)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } tsd->count = 10; // ... if ((result = pthread_setspecific(g_key, tsd)) != 0) exit_sys_errno("pthread_setspecific", result); // ... return NULL; } 3) Thread'in TSD alanındaki belli bir slota yerleştirilmiş olan adresin alınması pthread_getspecific fonksiyonu ile yapılmaktadır. Fonksiyon slotu belirten pthread_key_t değerini parametre olarak alır ve oradaki adresi bize geri dönüş değeri olarak verir. Ancak eğer böyle bir slot yoksa ya da henüz slota bir değer set edilmemişse fonksiyon NULL adrese geri dönmektedir. Başarısızlık için herhangi bir errno kodu verilmemiştir. Örneğin foo fonksiyonunu herhangi bir thread akışı çağırabiliyor olsun. Bu durumda ilgili thread'in TSD'de slotundan ona ilişkin adres şöyle edilecektir: void foo(void) { THREAD_SPECIFIC_DATA *tsd; if ((tsd = (THREAD_SPECIFIC_DATA *)pthread_getspecific(g_key)) == NULL) { fprintf(stderr, "cannot get thread specific data!...\n"); exit(EXIT_FAILURE); } printf("%d\n", tsd->count); // ... } 4) İlgili slot kullanım bitince iade edilebilir. pthread_key_delete fonksiyonu her thread'in TSD alanındaki slotu boşaltır. Fonksiyon pthread_key_t ile belirtilen slotu parametre olarak almaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. 5) Pekiyi tahsis edilen dinamik alan nasıl ve ne zaman serbest bırakılacaktır? Normal olarak buradaki dinamik alan thread tarafından tahsis edildiğine göre yine thread tarafından bırakılması uygun olur. Bu işlem manuel olarak yapılabileceği gibi pthread_key_create fonksiyonunda "destructor" fonksiyonu girilerek de yapılabilmektedir. Buraya girilen destructor fonksiyonu her yaratılmış ve adresi set edilmiş slot için sistem tarafından bir kez çağrılmaktadır. Fonksiyon çağrılırken parametresine slot içerisindeki set edilmiş olan adres geçirilmektedir. Örneğin pthread_key_create fonksiyonunda key_delete_proc isimli fonksiyonu parametre olarak vermiş olalım: void key_delete_proc(void *ptr) { free(ptr); } Aşağıda thread specific data kullanımına bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void foo(void); void key_delete_proc(void *ptr); void exit_sys_errno(const char *msg, int eno); pthread_key_t g_key; typedef struct tagTHREAD_SPECIFIC_DATA { int count; /* ... */ } THREAD_SPECIFIC_DATA; int main(void) { pthread_t tid1, tid2; int result; if ((result = pthread_key_create(&g_key, key_delete_proc) != 0) != 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_key)) != 0) exit_sys_errno("pthread_key_delete", result); return 0; } void *thread_proc1(void *param) { THREAD_SPECIFIC_DATA *tsd; int result; if ((tsd = (THREAD_SPECIFIC_DATA *)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } tsd->count = 10; if ((result = pthread_setspecific(g_key, tsd)) != 0) exit_sys_errno("pthread_setspecific", result); foo(); return NULL; } void *thread_proc2(void *param) { THREAD_SPECIFIC_DATA *tsd; if ((tsd = (THREAD_SPECIFIC_DATA *)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } tsd->count = 20; if ((result = pthread_setspecific(g_key, tsd)) != 0) exit_sys_errno("pthread_setspecific", result); foo(); return NULL; } void foo(void) { THREAD_SPECIFIC_DATA *tsd; if ((tsd = (THREAD_SPECIFIC_DATA *)pthread_getspecific(g_key)) == NULL) { fprintf(stderr, "cannot get thread specific data!...\n"); exit(EXIT_FAILURE); } printf("%d\n", tsd->count); } void key_delete_proc(void *ptr) { free(ptr); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 71. Ders 05/08/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bazen bir thread akışının aynı yerden geçtiği halde bir işlemi yalnızca bir kez yapması istenebilir. Bunun için pthread_once isimli bir fonksiyon bulundurulmuştur. Fonksiyonun prototipi şöyledir: #include int pthread_once(pthread_once_t *once_control, void (*init_routine)(void)); Fonksiyon pthread_once_t türünden bir nesnesnin adresini ve bir de çağrılacak fonksiyonun adresini alır. Fonksiyonun birinci parametresine geçirilecek olan pthread_once_t nesnesine aşağıdaki gibi ilk değer verilmiş olması gerekmektedir: pthread_once_t tonce = PTHREAD_ONCE_INIT; Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Thread akışı bu fonksiyona kaç kere girerse girsin yalnızca burada verilen fonksiyon ilk girişte bir kez çağrılmaktadır. Fonksiyonun birinci parametresindeki nesne bu bir kez çağırmayı sağlamaktadır. pthread_once fonksiyonunda asıl amaç birden fazla thread'in bir işi toplamda bir kez yapmasını sağlamaktır. Tabii aslında bu işlemi ileride görecek olduğumuz mutex nesneleriyle de basit bir biçimde yapılabiliriz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void init_proc(void); void exit_sys_errno(const char *msg, int eno); pthread_once_t g_tonce = PTHREAD_ONCE_INIT; 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); return 0; } void *thread_proc1(void *param) { int result; if ((result = pthread_once(&g_tonce, init_proc)) != 0) exit_sys_errno("pthread_once", result); printf("Thread-1\n"); return NULL; } void *thread_proc2(void *param) { int result; if ((result = pthread_once(&g_tonce, init_proc)) != 0) exit_sys_errno("pthread_once", result); printf("Thread-2\n"); return NULL; } void init_proc(void) { printf("init proc\n"); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Tabii aslında pthread_once fonksiyonunu oluşturmak oldukça kolaydır. Bir static mutex nesnesi alınır. pthread_once_t bir bayrak olarak kullanılır. mutex kontrolü içerisinde bu bayrak set edilir. Aşağıda buna ilişkin bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void init_proc(void); void exit_sys_errno(const char *msg, int eno); #define MYPTHREAD_ONCE_INIT 0 pthread_once_t g_tonce = MYPTHREAD_ONCE_INIT; int mypthread_once(pthread_once_t *once_control, void (*init_routine)(void)) { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int result; if ((result = pthread_mutex_lock(&mutex)) != 0) return result; if (*once_control == 0) { init_routine(); *once_control = 1; } if ((result = pthread_mutex_unlock(&mutex)) != 0) return result; return 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); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc1(void *param) { int result; if ((result = mypthread_once(&g_tonce, init_proc)) != 0) exit_sys_errno("mypthread_once", result); printf("Thread-1\n"); return NULL; } void *thread_proc2(void *param) { int result; if ((result = mypthread_once(&g_tonce, init_proc)) != 0) exit_sys_errno("mypthread_once", result); printf("Thread-2\n"); return NULL; } void init_proc(void) { printf("init proc\n"); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- TSD kullanan fonksiyonları biz yazıyorsak bu durumda TSD anahtarını (yani slotunu) daha thread'leri yaratmadan pthread_key_create fonksiyonu ile yaratabiliriz. Böylece thread'lerimiz zaten yaratılmış olan TSD slotlarını kullanabilir. Zaten biz yukarıdaki örnekte böyle yaptık. Pekiyi TSD kullanan fonksiyonları biz yazmamışsak örneğin bunlar bir kütüphanede bulunuyorsa ve bu fonksiyonları biz kütüphaneden çağırıyorsak bu fonksiyonlar TSD slotunu nasıl yaratmaktadır ve bu TSD alanına nasıl ilk değerlerini vermektedir? Bunun bir yolu eğer ilgili kütüphane standart C kütüphanesi gibi temel bir kütüphane ise TSD slotlarının derleyicilerin başlangıç kodlarında (start-up codes) yaratılması olabilir. Tabii bunun için söz konusu kütüphanenin derleyici ile bağlantılı aşağı seviyeli bir kütüphane olması gerekir. Örneğin Microsoft, standart C fonksiyonlarını bu biçimde thread güvenli hale getirmiştir. Aslında bir C programında ilk çalışan kod main fonksiyonu değildir. Aslında program derleyicinin yerleştirdiği ismine "start-up code" denilen bir koddan başlamaktadır. Bu kod main fonksiyonu çağırmaktadır. Yani bir C programının çalışması aşağıdakine benzerdir: ... ... ... call main call exit Buradan da görüldüğü gibi main fonksiyonu bittiğinde zaten exit fonksiyonu çağrılmaktadır. İşte eğer derleyici ile bağlantılı aşağı seviyeli bir kütüphane TSD kullanacaksa TSD slotları daha main fonksiyonu çağrılmadan start-up kodda yaratılabilir. Bazı derleyiciler start-up kodda main fonksiyonundan önce programcının fonksiyonlarının çağrılmasına izin verebilmektedir. C++ gibi bazı dillerde de main fonksiyonundan önce global değişkenler yoluyla kod çalıştırmak da mümkün olabilmektedir. Pekiyi biz C'de başkaları için TSD kullanılarak thread güvenli hale getirilmiş fonksiyonları nasıl yazabiliriz? Buradaki sorun bu TSD slotlarının pthread_key_create fonksiyonu ile ne zaman yaratılacağıdır. Bunun iki yolu olabilir: 1) Kütüphanemize lib_init gibi bir fonksiyon yerleştirip o fonksiyonun mutlaka programcı tarafından işin başında çağrılması gerektiğini belirtiriz. 2) pthread_once fonksiyonunu kullanarak yalnızca ilk thread bu fonksiyonu kullandığında bir kez TSD slotlarını yaratırız. Birinci yöntem daha etkindir. Ancak bu durumda programcının bu lib_init gibi bir fonksiyonu çağırmayı unutmaması gerekir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- TSD kullanımı ile aslında thread güvenli olmayan fonksiyonlar hiç parametre değişikliği yapılmadan thread güvenli hale getirilebilir. Örneğin Windows sistemlerinde bu biçimde bir işlem uygulanmıştır. Aşağıdaki örnekte srand ve rand fonksiyonları hiç parametre değişikliği yapılmadan thread güvenli hale getirilmiştir. Bu örnekte bir thread'te ilk kez srand ya da rand fonksiyonu çağrıldığında TSD slotu yaratılmıştır. Sonraki çağırmalarda zaten var olan slot kullanılmıştır. Yalnızca ilk çağırmada pthread_setspecific fonksiyonu ile slota yerleştirme yapılmıştır. Tabii büyük kütüphanelerde her fonksiyon için ayrı bir TSD slotunun tahsis edilmesine gerek yoktur. Kütüphanenin tamamı için bir slot yeterlidir. Kütüphanedeki tüm fonksiyonlar aslında aynı slottaki TSD alanını kullanabilirler. Programın testi için derlemeyi şöyle yapabilirsiniz: $ gcc -o sample sample.c rand.c -lpthread ---------------------------------------------------------------------------------------------------------------------------*/ /* rand.h */ #ifndef RAND_H_ #define RAND_H_ void my_srand(unsigned seed); int my_rand(void); #endif /* rand.c */ #include #include #include #include #include "rand.h" #define SEED_INIT 12345 typedef struct tagTSD_RAND { unsigned seed; /* ... */ } TSD_RAND; static void destructor(void *ptr); static void rand_init_once(void); static TSD_RAND *rand_init(void); static void exit_sys_errno(const char *msg, int eno); pthread_once_t g_rand_once; pthread_key_t g_rand_key; static void destructor(void *ptr) { free(ptr); } static void rand_init_once(void) { int result; if ((result = pthread_key_create(&g_rand_key, destructor)) != 0) exit_sys_errno("Fatal error pthread_key_create", result); } static TSD_RAND *rand_init(void) { int result; TSD_RAND *tsdrand; if ((result = pthread_once(&g_rand_once, rand_init_once)) != 0) exit_sys_errno("Fatal error pthread_once", result); if ((tsdrand = pthread_getspecific(g_rand_key)) == NULL) { if ((tsdrand = (TSD_RAND *)malloc(sizeof(TSD_RAND))) == NULL) { fprintf(stderr, "Fatal error: cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if ((result = pthread_setspecific(g_rand_key, tsdrand)) != 0) exit_sys_errno("Fatal error pthread_setspecific", result); tsdrand->seed = SEED_INIT; } return tsdrand; } static void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } void my_srand(unsigned seed) { TSD_RAND *tsdrand; tsdrand = rand_init(); tsdrand->seed = seed; } int my_rand(void) { TSD_RAND *tsdrand; tsdrand = rand_init(); tsdrand->seed = tsdrand->seed * 1103515245 + 12345; return (unsigned int)(tsdrand->seed / 65536) % 32768; } /* sample.c */ #include #include #include #include #include #include "rand.h" void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); 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); return 0; } void *thread_proc1(void *param) { int val; for (int i = 0; i < 10; ++i) { val = my_rand() % 100; printf("Thread-1: %d\n", val); sleep(1); } return NULL; } void *thread_proc2(void *param) { int val; for (int i = 0; i < 10; ++i) { val = my_rand() % 100; printf("Thread-2: %d\n", val); sleep(1); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Biz thread örneklerimizde exit_sys_errno fonksiyonunda strerror fonksiyonunu kullanmıştık. Anımsanacağı gibi strerror fonksiyonunun prototipi şöyleydi: #include char *strerror(int errnum); Görüldüğü gibi fonksiyon statik alanın adresiyle geri dönmektedir. O halde bu fonksiyonun aynı anda farklı thread'lerden çağrılması uygun değildir. Çünkü bu fonksiyon thread güvenli değildir. O halde aslında strerror yerine bizim strerror_t fonksiyonunu kullanmamız gerekir. Ancak strerror yerine strerror_r fonksiyonun kullanılması da biraz zahmetlidir. Bu nedenle UNIX/Linux sistemlerinde standart C kütüphanelerinin bir bölümü zaten kendi içerisinde TSD kullanarak yukarıdaki yaptığımız gibi yöntemlerle bu strerror fonksiyonunu thread güvenli hale getirmiştir. Örneğin GNU'nun glibc kütüphanesinde bu fonksiyon belli bir versiyondan sonra thread güvenlidir. Yani bu fonksiyonu biz iki farklı thread'ten çağırıyor olsak da aslında fonksiyon bize farklı adresler vermektedir. strerror_r fonksiyonunun prototipi şöyledir: #include int strerror_r(int errnum, char *buf, size_t buflen); Burada fonksiyon static bir alanın adresi ile geri dönmez. Bizzat verilen adrese yazıyı yerleştirir. Bu durumda yukarıda yazdığımız sys_exit_errno fonksiyonunun thread güvenli versiyonu şöyle oluşturulabilir: int strerror_r(int errnum, char *buf, size_t buflen); void exit_sys_errno(const char *msg, int eno) { char buf[256]; strerror_r(eno, buf, 256); fprintf(stderr, "%s:%s\n", msg, buf); exit(EXIT_FAILURE); } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Thread'e özgü global değişken oluşturmak için programlama dillerine zamanla özel bazı belirleyiciler (specifers) de eklenmiştir. Örneğin C'ye C11 ile birlikte _Thread_local isimli bir yer belirleyicisi eklenmiştir. Bu yer belirleyicisi C23 ile birlikte thread_local biçiminde değiştirilmiştir. C11'de thread_local ismi dosyası içerisinde bir makro biçiminde bulunmaktadır. C++11 ile birlikte de yine C++'a aynı işlevde thread_local isimli yer belirleyicisi de eklenmiştir. Örneğin: _Thread_local int g_x; Burada g_x global değişkeninin thread'e özgü kopyaları bulunmaktadır. Yani bir thread akışı bu g_x global değişkenini kullandığında o thread'e özgü olan g_x global değişkenini kullanmış olur. gcc ve clang derleyicilerinde ayrıca __thread biçiminde aynı işlevde bir uzantı (extension) niteliğinde belirleyici C11 öncesinde de bulunuyordu. gcc derleyicileri _thread_local, thread_local ya da __thread belirleyicisi ile tanımlanan nesneleri "thread local storage" biçiminde isimlendirilen bir teknikle thread'lerin stack alanlarının biz uzantısında saklamaktadır. Bu konudaki dokümanlar aşağıdaki bağlantıdan incelenebilir: https://gcc.gnu.org/onlinedocs/gcc/Thread-Local.html Aşağıda C11 ile ile C'ye eklenen _Thread_local belirleyicisinin kullanımına bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void disp(const char *name); void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); _Thread_local int g_x; 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); pthread_exit(NULL); return 0; } void disp(const char *name) { printf("%s: %d\n", name, g_x); // bu fonksiyonu hangi thread çağırırsa o thread'in g_x'i yazdırılıyor } void *thread_proc1(void *param) { g_x = 10; // thread-1'in g_x'ine 10 yerleştiriliyor disp("thread-1"); return NULL; } void *thread_proc2(void *param) { g_x = 20; // thread-1'in g_x'ine 20 yerleştiriliyor disp("thread-2"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi fopen fonksiyonunun bize verdiği FILE adresi FILE türünden bir yapı nesnesini gösteriyordu ve o FILE nesnesinin içerisinde o dosyanın kullandığı tamponun adresi vardı. Pekiyi biz global bir FILE türünden gösterici alıp bu göstericiyle iki farklı thread'te dosya işlemi yaparsak bu işlemler thread güvenli midir? Örneğin: FILE *g_f; int main(void) { g_f = fopen(...); ... return 0; } Burada g_f dosya bilgi göstericisini (stream) farklı thread'lerden aynı anda kullanırsak bir sorun çıkar mı? İşte POSIX standartlarına göre stream işlemleri thread güvenlidir. Yani programcının özel olarak bu konuda bir şey yapmasına gerek kalmaz. Bir dosya işlemi bitmeden diğeri devreye girip iç içe geçme olmamaktadır. Ancak C standartlarında böyle bir thread güvenlilik garanti edilmemiştir. Benzer biçimde Windows sistemlerinde de stream işlemleri thread güvenlidir. Örneğin biz iki thread'te fprintf fonksiyonu ile aynı dosyaya bir şeyler yazmak isteyelim. Bu fonksiyonlarda iç içe geçme olmayacaktır. Yani birinin yazdığı şeyler ile diğerinin yazdığı şeyler sanki atomik işlemlermiş gibi ele alınacaktır. Aşağıdaki örnekte global bir dosya bilgi gösterici yoluyla fprintf fonksiyonu kullanılarak farklı thread'lerden aynı dosyaya bir şeyler yazılmıştır. Dosyanın içeriğini incelediğinizde iç içe geçmelerin olmadığını göreceksiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #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); fclose(g_f); return 0; } void *thread_proc1(void *param) { for (int i = 0; i < 10000; ++i) fprintf(g_f, "thread-1: %d\n", i); return NULL; } void *thread_proc2(void *param) { for (int i = 0; i < 10000; ++i) fprintf(g_f, "thread-2: %d\n", i); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 72. Ders 06/08/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Server uygulamalarında server programın çok sayıda client'tan gelen mesajları işlemesi gerekebilmektedir. Bu tür uygulamalarda server program client'tan bilgiyi alır, onu işler, işlemlerin sonuçlarını client'a gönderir ve sonra yeniden aynı işlemleri yapar. Yani bu tür programlardaki tipik döngü aşağıdaki gibidir: for (;;) { msg = get_msg(); process_msg(msg); } Burada get_msg client'tan mesajı alan fonksiyonu, process_msg ise o mesajı işleyen fonksiyonu temsil ediyor olsun. Server programın amacı client'ları bekletmeden onların talep ettiği hizmetleri hızlıca yapmaktır. Bu tür client-server uygulamalar genellikle TCP/IP soket programlamada karşımıza çıkmaktadır. TCP/IP soket programlama izleyen bölümlerde ele alınacaktır. Pekiyi buradaki döngü nasıl hızlandırılabilir? İlk akla gelen process_msg fonksiyonunun başka bir thread'e yaptırılması ve server'ın hemen dönerek bir sonraki client'ın isteklerini işlemeye çalışmasıdır. Ancak burada önemli bir sıkıntı şudur: Biz her client mesajı için bir thread yaratmak durumundayız. Ancak bu thread'lerin yaratılması ve yok edilmesi de önemli bir zaman almaktadır. Yani bir client'ın isteğini yerine getirmek için o anda bir thread'in yaratılmasının ve sonra da o thread'in yok edilmesinin de bir zaman maliyeti vardır. İşte bu tür durumlarda iki yöntem uygulanabilmektedir: 1) Yukarıdaki döngüyü çalıştıran thread'in client'tan aldığı mesajları bir kuyruk sistemine yazması ve üretici-tüketici problemi biçiminde n tane client thread'in aynı kuyruktan istekleri alıp işlemesi yöntemi. Bu yöntemde işin başında n tane thread yaratılır. Bu n tane thread senkronize edilmiş bir kuyruktan client isteklerini alıp onları bağımsız bir biçimde işler. Böylece thread'lerin her client isteğinde yeniden yaratılıp yok edilmesi gerekmeyecektir. 2) Thread Havuzu (Thread Pool) yöntemi. Bu yöntemde n tane thread yaratılmış bir biçimde suspend durumda bekletilir. Yeni bir client isteği geldiğinde bekleyen thread uyandırılarak client isteği bu thread'e yaptırılır. Burada thread'ler işin başında yaratılmaktadır. Client isteği bu thread'ler tarafından işlendikten sonra thread'ler yok edilmemekte ve yeniden suspend durumda yeni client isteğinin gelmesini beklemektedir. Yukarıdaki iki yöntem de aslında birbirine benzer yöntemlerdir. Birinci yöntemdeki üretici-tüketici problemi için bir kuyruk sisteminin oluşturulması ve bu kuyruk sisteminin senkronize edilmesi gerekir. Client isteklerinin kuyruk sistemine yazılması ve oradan alınması da belli bir zaman kaybına yol açmaktadır. Thread havuzu yönteminde daha doğrudan bir işlem söz konusudur. Çünkü burada araya bir kuyruk sistemi sokulmamaktadır. Bu tür durumlarda her iki yöntem de kullanılabiliyor olsa da thread havuzları daha fazla tercih edilmektedir. Pekiyi yukarıdaki her iki yöntemde de n tane thread'in işin başında yaratılacağını belirtmiştik. Bu n sayısı ne olmalıdır? Tek işlemcili ya da çekirdekli sistemlerde bu n sayısının büyütülmesi bir fayda sağlayabilir mi? Aslında genel olarak bu yöntemler tek işlemcili ya da çekirdekli sistemlerde önemli bir hız kazancı sağlamayacaktır. Tabii eğer bu sistemlerde bu proses ile rekabet eden başka prosesler varsa bu prosesin fazla thread yaratması bu rekabette bu prosesin öne geçmesine yol açabilecektir. Ancak ilgili makinede bu server programın dışında onunla rekabet edebilecek başka programların olmadığını varsaydığımızda buradaki hız kazancı sanıldığı kadar fazla olmayabilecektir. Tabii yukarıdaki döngünün tek thread yerine birden fazla thread tarafından işletilmesi bloke konusunda bir avantaj sağlayacaktır. Eğer döngü tek thread tarafından işletilirse process_msg içerisindeki blokeden tüm client'lar etkilenir. Ancak process_msg fonksiyonu farklı thread'ler tarafından işletilirse bu fonksiyondaki blokeden tüm client'lar etkilenmez. Bu tür uygulamalarda thread sayısını belirten n değeri aslında mesaj işlenirken önemli blokeler oluşmuyorsa işlemci ya da çekirdek sayısı kadar olması uygundur. Hatta programcı bu thread'leri "processor affinity" işlemleriyle farklı işlemci ya da çekirdeklere de bağlayabilir. Tabii mesaj işlenirken (process_msg'yi kastediyoruz) blokeler oluşacaksa bu n değeri yükseltilebilir. (Tabii bu tür durumlarda Linux sistemlerinde "processor affinity" uygulanmadığı durumda çizelgeleyicinin CFS algoritması işlemcilerin çalışma kuyruklarını (run queue) toplam fayda çerçevesinde dengelemeye çalışmaktadır.) Yukarıdaki açıklamalarımızdan çıkan sonuçları özetleyelim. Aşağıdaki gibi bir server döngüsü söz konusu olsun: for (;;) { msg = get_msg(); process_msg(msg); } 1) Burada tek işlemcili ve tek çekirdekli sistemlerde process_msg fonksiyonlarının n tane thread'e yaptırılması eğer bu fonksiyon içerisinde blokeler söz konusu ise fayda sağlar. Ancak bu fonksiyon CPU yoğun ise ve sistemde bu proses ile rekabet etme iddiasında olan başka prosesler yoksa bu durum sanıldığı kadar fayda sağlamayabilir. 2) Çok işlemcili ve çok çekirdekli sistemlerde eğer process_msg CPU yoğun ise n değeri tipik olarak işlemci ya da çekirdek sayısı kadar olabilir. Ancak process_msg blokelere yol açabiliyorsa n değeri büyütülebilir. Thread havuzu yönteminde n tane thread'in baştan yaratılarak havuzda bekletilmesi ve onların gerektiğinde uyandırılarak belli bir fonksiyonu çalıştırması sağlanmaktadır. Thread havuzunda baştan yaratılacak thread sayısı önceden belirlenmekte ancak dinamik biçimde artırılıp azaltılabilmektedir. Yani client istekleri artıp havuzda bunları işleyecek thread'ler kalmadıysa bu thread havuzu sistemi yeni thread'ler yaratarak onları havuza ekler. Client istekleri azaldığında havuzdaki thread'lerin bir bölümünü sisteme iade eder. Tabii çok değişik thread havuzu gerçekleştirimleri vardır. Her birinin özellikleri diğerlerinden farklı olabilmektedir. Thread havuzları C'nin ve C++'ın standart kütüphanesinde yer almamaktadır. gcc ve clang derleyicilerinin kullandığı glibc kütüphanesinde de thread havuzları için fonksiyonlar yoktur. Ancak Qt gibi, Java ve .NET gibi platformlarda o platformların sınıf kütüphanelerinde thread havuzları hazır bir biçimde bulunmaktadır. Thread havuzları Windows sistemlerinde Windows API fonksiyonları tarafından desteklenmektedir. (Windows API fonksiyonları POSIX kütüphanesine benzer bir düzeydedir.) C için başkaları tarafından yazılmış thread havuzu kütüphaneleri kullanılabilmektedir. Bunun için çeşitli seçenekler bulunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Defalarca belirttiğimiz gibi POSIX'teki pthread kütüphanesi taban bir kütüphanedir. Diğer platformlardaki thread işlemleri aslında UNIX/Linux sistemlerinde bu pthread kütüphanesi kullanılarak gerçekleştirilmektedir. Yani örneğin biz Java'da C#'ta, C++'ta onların sağladığı kütüphaneler yoluyla thread işlemleri yaptığımızda aslında bu kütüphaneler UNIX/Linux sistemlerinde pthread fonksiyonlarıyla, Windows sistemlerinde Windows API fonksiyonlarıyla gerçekleştirmektedir. macOS sistemlerinde de yine pthread kütüphanesi kullanılmaktadır. Ancak bu dillerin ve platformların asıl sağladığı fayda taşınabilirliktir. Örneğin biz C++'ın thread kütüphanesini kullandığımızda o programı Windows'ta da Linux'ta da aynı kodlarla yeniden derleyerek çalıştırabilmekteyiz. Oysa pthread kütüphanesini kullanarak yazdığımız bir programı Windows sistemlerine götürdüğümüzde derlenemeyecektir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- C Programlama Dili'ne, 2011 versiyonuyla (ISO/IEC 9899:2011 ya da kısaca C11) isteğe bağlı mini bir thread kütüphanesi eklenmiştir. Bu thread kütüphanesi henüz Microsoft derleyicileri tarafından desteklenmemektedir. gcc ve clang derleyicilerinin kullandığı glibc kütüphanesinin son versiyonları bu thread kütüphanesini destekler hale gelmiştir. Biz de burada kısaca bu kütüphane hakkında temel bilgiler vereceğiz. Kütüphanedeki tüm fonksiyonların prototipleri, sembolik sabitler ve typedef isimleri dosyası içerisindedir. Dolayısıyla bu dosyanın include edilmesi gerekmektedir. Bu dosyanın kendi içerisinde dosyasını include etmesi garanti edilmiştir. gcc ve clang derleyicilerinde C11 thread kütüphanesini kullanan programları derlerken -pthread seçeneğinin kullanılması gerekmektedir. (pthread kütüphanesi için -lpthread seçeneğini kullandığımızı anımsayınız.) Örneğin: $ gcc -o sample sample.c -pthread ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- - Kütüphanedeki fonksiyonların geri dönüş değerleri genellikle int türdendir. Bu fonksiyonlar başarı durumunda thrd_success, başarısızlık durumunda thrd_error ya da thrd_nomem değerlerine geri dönerler. Başarı kontrolü şöyle yapılabilir: if (thrd_xxx(...) != thrd_success) { ... } - C11'de thread yaratmak için thrd_create fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int thrd_create(thrd_t *thr, thrd_start_t func, void *arg); Fonksiyonun birinci parametresi thread'i temsil eden id değerinin yerleştirileceği thrd_t türünden nesnesini adresini, ikinci parametresi thread akışının başlatılacağı fonksiyonun adresini ve üçüncü parametresi de thread fonksiyonuna geçirilecek argümanı belirtir. thrd_start_t türü şöyle typedef edilmiştir: typedef int (*thrd_start_t)(void *); - Thread yine yaratıldıktan sonra thrd_join fonksiyonu ile beklenebilir: #include int thrd_join(thrd_t thr, int *res); Thread'in exit kodunun int türden olduğuna dikkat ediniz. - Thread thrd_detach fonksiyonu ile detached duruma sokulabilmektedir: #include int thrd_detach(thrd_t thr); - Thread'i belli bir süre blokede bekletmek için thrd_sleep fonksiyonu kullanılmaktadır: #include int thrd_sleep(const struct timespec *duration, struct timespec *remaining); Aşağıda bu fonksiyonların kullanılmasına yönelik bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int thread_proc(void *param); int main(void) { thrd_t tid; if (thrd_create(&tid, thread_proc, "other thread") != thrd_success) { fprintf(stderr, "cannot create thread!...\n"); exit(EXIT_FAILURE); } struct timespec ts; ts.tv_sec = 1; ts.tv_nsec = 0; for (int i = 0; i < 10; ++i) { printf("Main thread %d\n", i); thrd_sleep(&ts, NULL); } if (thrd_join(tid, NULL) != thrd_success) { fprintf(stderr, "cannot join thread!...\n"); exit(EXIT_FAILURE); } return 0; } int thread_proc(void *param) { char *name = (char *)param; struct timespec ts; ts.tv_sec = 1; ts.tv_nsec = 0; for (int i = 0; i < 10; ++i) { printf("%s: %d\n", name, i); thrd_sleep(&ts, NULL); } return 0; } /*-------------------------------------------------------------------------------------------------------------------------- C11'de senkronizasyon için yalnızca mutex ve durum değişkenleri nesneleri bulundurulmuştur. Bu mutex nesneleri mtx_t türü ile temsil edilmektedir. Nesnenin kullanılması aşağıdaki fonksiyonlarla yapılmaktadır: #include int mtx_init(mtx_t *mtx, int type); void mtx_destroy(mtx_t *mtx); int mtx_lock(mtx_t *mtx); int mtx_unlock(mtx_t *mtx); int mtx_timedlock(mtx_t *restrict mtx, const struct timespec *restrict ts); int mtx_trylock(mtx_t *mtx); Aşağıdaki mutex nesnelerinin kullanılmasına bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int thread_proc1(void *param); int thread_proc2(void *param); int g_count; mtx_t g_mutex; int main(void) { thrd_t tid1, tid2; if (mtx_init(&g_mutex, mtx_plain) != thrd_success) { fprintf(stderr, "cannot initialize mutex!...\n"); exit(EXIT_FAILURE); } if (thrd_create(&tid1, thread_proc1, NULL) != thrd_success) { fprintf(stderr, "cannot create thread!...\n"); exit(EXIT_FAILURE); } if (thrd_create(&tid2, thread_proc1, NULL) != thrd_success) { fprintf(stderr, "cannot create thread!...\n"); exit(EXIT_FAILURE); } if (thrd_join(tid1, NULL) != thrd_success) { fprintf(stderr, "cannot join thread!...\n"); exit(EXIT_FAILURE); } if (thrd_join(tid2, NULL) != thrd_success) { fprintf(stderr, "cannot join thread!...\n"); exit(EXIT_FAILURE); } printf("%d\n", g_count); return 0; } int thread_proc1(void *param) { for (int i = 0; i < 1000000; ++i) { mtx_lock(&g_mutex); ++g_count; mtx_unlock(&g_mutex); } return 0; } int thread_proc2(void *param) { for (int i = 0; i < 1000000; ++i) { mtx_lock(&g_mutex); ++g_count; mtx_unlock(&g_mutex); } return 0; } /*-------------------------------------------------------------------------------------------------------------------------- C11'deki diğer thread fonksiyonları için C standartlarını inceleyebilirsiniz. C11'in thread fonksiyonları oldukça minimalist tasarlanmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- C++'a da C++11 (ISO/IEC 14882: 2011 ya da kısaca C++11) ile birlikte şablon temelli bir thread kütüphanesi eklenmiştir. C++'ın kütüphanesi, C'nin kütüphanesinden daha ayrıntıldır. Tabii C++'daki thread kütüphanesi sınıfsal bir tasarıma sahiptir. C++'ta thread işlemleri için thread isimli bir sınıf bulundurulmuştur. Bu sınıf şablon tabanlı olduğu için thread fonksiyonu herhangi bir parametreye sahip biçimde sınıfın yapıcı fonksiyonuna verilebilmektedir. Thread nesneleri kesinlikle join üye fonksiyonuyla beklenmeli ya da detach üye fonksiyonuyla detach duruma sokulmalıdır. Aşağıda C++'ta bir thread yaratımı örneği verilmiştir. Yine derleme işlemi sırasında Linux sistemlerinde -pthread seçeneğinin bulundurulması gerekmektedir. Programın testi için derlemeyi şöyle yapabilirsiniz: g++ -o sample sample.cpp -pthread ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; void thread_proc(int count); int main() { thread t(thread_proc, 5); for (int i = 0; i < 10; ++i) { this_thread::sleep_for(chrono::milliseconds(1000)); cout << "main thread: " << i << endl; } t.join(); return 0; } void thread_proc(int count) { for (int i = 0; i < count; ++i) { this_thread::sleep_for(chrono::milliseconds(1000)); cout << "other thread: " << i << endl; } } /*-------------------------------------------------------------------------------------------------------------------------- C++'ın standart kütüphanesinde de çeşitli senkronizasyon nesneleri bulunmaktadır. Örneğin mutex nesnesi yine bir sınıf biçiminde bulundurulmuştur. lock ve unlock işlemleri mutex sınıfının üye fonksiyonlarıyla yapılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include using namespace std; int g_count; mutex g_mutex; void thread_proc1(); void thread_proc2(); int main() { thread t1(thread_proc1); thread t2(thread_proc2); t1.join(); t2.join(); cout << g_count << endl; return 0; } void thread_proc1() { for (int i = 0; i < 1000000; ++i) { g_mutex.lock(); ++g_count; g_mutex.unlock(); } } void thread_proc2() { for (int i = 0; i < 1000000; ++i) { g_mutex.lock(); ++g_count; g_mutex.unlock(); } } /*-------------------------------------------------------------------------------------------------------------------------- C++'ın thread kütüphanesi, C'nin thread kütüphanesinden daha geniştir. Kütüphane şablon tabanlı olduğu için kullanımı konusunda dikkatli olmak gerekir. Tabii C++'ın bu kütüphanesi UNIX/Linux ve macOS sistemlerinde pthread kütüphanesi kullanılarak, Windows sistemlerinde ise Windows API fonksiyonları kullanılarak gerçekleştirilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sinyal (signal) işlemleri UNIX/Linux sistemlerinde, sistem programlama etkinliklerinde yoğun bir biçimde kullanılmaktadır. Bu nedenle bu sistemlerde programlama yapan programcıların sinyal işlemleri konusunda bilgi sahibi olması gerekmektedir. Sinyal işlemleri kapsamlı bir konudur. Biz kursumuzun bu bölümünde bu işlemlerin temelleri ve ayrıntıları üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sinyal mekanizması kesme (interrupt) mekanizmasına benzetilebilir. Sinyaller UNIX/Linux sistemlerinde asenkron işlem yapılmasına olanak sağlayan bir mekanizmadır. Sinyaller normal olarak proseslere gönderilmektedir. Ancak thread konusunun işletim sistemlerine eklenmesiyle thread'lere de sinyal gönderilmesi mümkün hale getirilmiştir. (Ancak thread'lerin yalnızca ilgili proses içerisinde erişilebilen bir kaynak olduğunu anımsayınız. Thread'lerin id değerleri ilgili proseste anlamlı değerlerdir.) Bir sinyal prosese gönderildiğinde prosesin akışı kesilir, ismine sinyal fonksiyonu (signal handler) denilen bir fonksiyon çalıştırılır. Sinyal fonksiyonu bitince akış kalınan yerden devam eder. Bu mekanizma kod çalışırken araya asenkron biçimde başka işlemlerin girebilmesine olanak sağlamaktadır. Sinyalin oluşmasına yol açan çeşitli durumlar söz konusu olabilmektedir. Örneğin sinyal işletim sistemi tarafından prosese belli koşullar altında gönderiliyor olabilir. Programcının yaptığı çeşitli ihlallerde de işletim sistemi tarafından prosese sinyaller gönderilebilmektedir. Bazı sinyaller bazı aygıt sürücüleri tarafından prosese gönderilebilmektedir. Örneğin terminal aygıt sürücüsü, prosesin ilişkin olduğu terminalde kullanıcı Ctrl+C gibi Ctrl+Backspace gibi tuşlara bastığında oturumun ön plan proses grubuna bazı sinyalleri gönderebilmektedir. Sinyaller programlama yoluyla da bir prosesten diğerine gönderilebilmektedir. Bazı POSIX fonksiyonları da kendi içlerinde bazı koşullarda ilgili proseslere sinyaller gönderebilmektedir. Bir sinyal bir prosese gönderildiğinde prosesin hangi thread'inin çalışmasına ara verilip sinyal fonksiyonunu çalıştıracağı POSIX standartlarında işletim sisteminin isteğine bırakılmıştır. Yani örneğin bizim toplamda beş thread'imiz varsa prosese bir sinyal gönderildiğinde bu beş thread'ten herhangi biri çalışmasına ara verip sinyal fonksiyonunu çalıştırabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Her sinyalin bir numarası vardır. Sinyallere ilişkin numaralar POSIX sistemlerinde işletim sistemini yazanların isteğine bırakılmıştır. Ancak taşınabilirlik sağlamak için sinyal numaraları dosyası içerisinde SIGXXX biçiminde sembolik sabitlerle define edilmiştir. Konuşurken ve program yazarken sinyallerin numaraları değil (çünkü taşınabilir değiller) bu sembolik sabit isimleri kullanılmaktadır. UNIX/Linux sistemlerinde kullanılan tipik sinyaller şunlardır: SIGABRT SIGALRM SIGBUS SIGCANCEL SIGCHLD SIGCONT SIGEMT SIGFPE SIGFREEZE SIGHUP SIGILL SIGINFO SIGINT SIGIO SIGIOT SIGJVM1 SIGJVM2 SIGKILL SIGLOST SIGLWP SIGPIPE SIGPOLL SIGPROF SIGPWR SIGQUIT SIGSEGV SIGSTKFLT SIGSTOP SIGSYS SIGTERM SIGTHAW SIGTHR SIGTRAP SIGTSTP SIGTTIN SIGTTOU SIGURG SIGUSR1 SIGUSR2 SIGVTALRM SIGWAITING SIGWINCH SIGXCPU SIGXFSZ SIGXRES ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir sinyal oluştuğunda eğer programcı o sinyal için bir "sinyal fonksiyonu (signal handler)" set etmişse bu sinyal fonksiyonu çağrılmaktadır. Eğer programcı sinyal için bir sinyal fonksiyonu set etmemişse bu durumda "default eylem (default action)" uygulanmaktadır. Default eylem sinyalden sinyala değişebilmektedir. Bazı sinyallerde default eylem "sinyalin görmezlikten gelinmesi (ignore)" iken, bazı sinyallerde "prosesin sonlandırılması (terminate)" biçimindedir. Bazı sinyallerde default eylem "prosesin sonlandırılması ve bir core dosyasının oluşturulması" biçimindedir. Core dosyaları "teşhis amacıyla" oluşturulan ve debugger altında incelenebilen özel dosyalardır. Tabii programcı bu default eylemleri sinyal temelinde öğrenmelidir. Aşağıda sinyallerin default eylemlerinin ne olduğuna ilişkin bir liste verilmiştir: Sinyal Default Eylem ------- --------------- SIGABRT terminate+core SIGALRM terminate SIGBUS terminate+core SIGCANCEL ignore SIGCHLD ignore SIGCONT continue/ignore SIGEMT terminate+core SIGFPE terminate+core SIGFREEZE ignore SIGHUP terminate SIGILL terminate+core SIGINFO ignore SIGINT terminate SIGIO terminate/ignore SIGIOT terminate+core SIGJVM1 ignore SIGJVM2 ignore SIGKILL terminate SIGLOST terminate SIGLWP terminate/ignore SIGPIPE terminate SIGPOLL terminate SIGPROF terminate SIGPWR terminate/ignore SIGQUIT terminate+core SIGSEGV terminate+core SIGSTKFLT terminate SIGSTOP stop process SIGSYS terminate+core SIGTERM terminate SIGTHAW ignore SIGTHR terminate SIGTRAP terminate+core SIGTSTP stop process SIGTTIN stop process SIGTTOU stop process SIGURG ignore SIGUSR1 terminate SIGUSR2 terminate SIGVTALRM terminate SIGWAITING ignore SIGWINCH ignore SIGXCPU terminate or terminate+core SIGXFSZ terminate or terminate+core SIGXRES ignore ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir sinyal oluşturulduğunda önce sinyal prosese teslim edilir ("deliver" işlemi). Proses kendine gelen sinyali mümkün olduğu kadar çabuk işlemek ister. Eğer ilgili sinyal için bir sinyal fonksiyonu set edilmişse proses çok gecikmeden bu sinyal fonksiyonunu çağırmak isteyecektir. Sinyalin oluşmasıyla prosese teslim edilmesi arasındaki ara duruma "sinyalin askıda (pending durumda olması)" denilmektedir. Eğer akış o anda bir sistem fonksiyonunun içerisindeyse ve o sistem fonksiyonu uzun sürecek bir eylem başlatmışsa ya da açıkça bloke olmuşsa bu durumda işletim sistemi ilgili sistem fonksiyonunu başarısızlıkla sonuçlandırır ve bir an evvel sinyal fonksiyonunu çalıştırır. Tabii biz sistem fonksiyonlarını doğrudan değil POSIX fonksiyonları yoluyla kullanmaktayız. Böylece bu tür durumlarda çağırdığımız POSIX fonksiyonları sinyal dolayısıyla başarısız olabilmektedir. Eğer bir POSIX fonksiyonu sinyal dolayısıyla başarısız olursa bu durumda errno değişkeni EINTR özel değeriyle set edilmektedir. Örneğin biz bir borudan read fonksiyonuyla okuma yapmak isteyelim. Ancak boruda okunacak hiç byte olmasın. Bu durumda read fonksiyonu blokeye yol açacaktır. İşte bu sırada prosese bir sinyal gelirse read fonksiyonu başarısızlıkla (yani -1 değeriyle) geri döner ve errno değişkeni EINTR değeriyle set edilir. Bu tür durumlarda biz fonksiyonun sinyal dolayısıyla başarısız olduğunu anlayıp onu yeniden çağırmamız gerekir. Örneğin: while ((result = read(...)) == -1 && errno == EINTR) ; if (result == -1) exit_sys("read"); Ancak programcı isterse bu tür POSIX fonksiyonlarınının "otomatik biçimde yeniden çağrılmasını (automatic restart)" da sağlayabilmektedir. Eğer programcı bunu sağlamışsa bu durumda fonksiyon hiç geri dönmez, ancak sinyal fonksiyonu çalıştırılır. Bu otomatik çalıştırma işlemi kütüphane tarafından değil çekirdek tarafından sağlanmaktadır. Tabii bir sistem fonksiyonunun ya da onu çağıran POSIX fonksiyonunun sinyal nedeniyle başarısız olması için o fonksiyonun "yavaş bir fonksiyon" olması gerekir. Programcı, çağırdığı sistem fonksiyonlarının ya da onu çağıran POSIX fonksiyonlarının sinyal karşısındaki davranışını bilmek zorundadır. Tabii bir sinyal için sinyal fonksiyonu set edilmemişse ve default eylem prosesin sonlanmasıysa zaten bu durumda ilgili sistem fonksiyonunun ya da onu çağıran POSIX fonksiyonunun başarısızlığının bir önemi de kalmamaktadır. Yavaş fonksiyon "sistem fonksiyonunun içerisinde göreli biçimde uzun zaman beklenebilmesi" anlamına gelmektedir. Örneğin read fonksiyonu ile biz bir disk dosyasından (regular file) okuma yaparken sinyal oluştuğunda bu yavaş bir işlem değildir. Genellikle işletim sistemlerinin çekirdekleri okuma bitip, fonksiyon başarıyla sonlandıktan sonra set edilmiş olan sinyal fonksiyonunu çağırmaktadır. Ancak örneğin read fonksiyonu ile bir borudan okuma yapıyorsak ve boruda hiç bilgi yoksa bu durumda read fonksiyonu yavaş bir sistem fonksiyonu durumundadır. Çünkü read fonksiyonu bu durumda blokeye yol açıp uzun süre bekleme oluşturabilmektedir. Yani sistem fonksiyonunun yavaş olması demekle genellikle blokeye yol açabilmesi kastedilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 73. Ders 13/08/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir sinyal oluştuğunda programcının kendi belirlediği fonksiyonun çağrılması (yani sinyal fonksiyonunun set edilmesi) iki POSIX fonksiyonu ile sağlanmaktadır: signal fonksiyonu ve sigaction fonksiyonu. Maalesef POSIX standartları oluşturulduğunda signal fonksiyonunun davranışı konusunda UNIX türevi sistemler arasında (özellikle AT&T ve Berkeley sistemleri arasında) farklılıklar söz konusuydu. POSIX standartları oluşturulurken bu farklılıklar bilindiği için signal fonksiyonu "öyle de davranabilir böyle de davranabilir" biçiminde standartlara sokuldu. Bu durum da tabii sistemler arasında taşınabilirlik sorunları oluşturmaktaydı. İşte signal fonksiyonunun bu taşınabilirlik sorunu sigaction fonksiyonuyla çözülmüştür. sigaction fonksiyonu POSIX standartlarına sokulduğunda fonksiyonun semantiği sistemler arasında farklılık oluşturmayacak biçimde tanımlanmıştır. Biz kursumuzda önce signal fonksiyonunu sonra sigaction fonksiyonunu göreceğiz. Yukarıda da belirttiğimiz gibi signal fonksiyonundaki semantik taşınabilir değildir. Bu nedenle programcıların sigaction fonksiyonunu kullanması iyi bir tekniktir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- signal fonksiyonunun prototipi şöyledir: #include void (*signal(int sig, void (*func)(int)))(int); signal fonksiyonunun birinci parametresi set edilecek sinyale ilişkin sinyal numarasını belirtmektedir. İkinci parametre ise sinyal oluştuğunda çağrılacak sinyal fonksiyonunun adresini almaktadır. Sinyal fonksiyonlarının geri dönüş değeri void parametreleri int türden olmak zorundadır. signal fonksiyonunun geri dönüş değeri de "parametresi int, geri dönüş değeri void olan" bir fonksiyon adresidir. Fonksiyon başarı durumunda önceki sinyal fonksiyonunun adresi ile geri döner. Başarısızlık durumunda SIG_ERR özel değeri ile geri dönmektedir. SIG_ERR içerisinde başarısızlığı anlatan bir değer olarak define edilmiştir. Örneğin: ... if (signal(SIGINT, sigint_handler) == SIG_ERR) exit_sys("signal"); ... void sigint_handler(int sno) { ... } Sinyal fonksiyonunun parametresi ne anlam ifade etmektedir? İşletim sistemi oluşan sinyalin numarasını sinyal fonksiyonuna parametre olarak aktarmaktadır. Bu sayede programcı farklı sinyaller için aynı sinyal fonksiyonunu set edebilir. Fonksiyonun içerisinde bu parametre yardımıyla hangi sinyal nedeniyle fonksiyonun çağrılmış olduğunu belirlenebilir. signal fonksiyonunun ikinci parametresi için SIG_DFL ve SIG_IGN özel değeri girilebilir. SIG_DFL sinyali "default duruma" çekmek için kullanılmaktadır. Yani bu değer "sanki hiç sinyal fonksiyonu set edilmemiş gibi" bir etki oluşturmaktadır. SIG_IGN ise "sinyali görmezden gelme yani ignore etme" için kullanılmaktadır. Bir sinyal SIG_IGN ile ignore edilirse bu sinyal oluştuğunda sanki sinyal oluşmamış gibi bir davranış gösterilir. İleride de açıklayacağımız gibi her sinyal ignore edilememektedir. Tabii signal fonksiyonunun geri dönüş değeri sinyale göre SIG_DFL ve SIG_IGN biçiminde de olabilmektedir. Örneğin biz bir sinyali ilk kez set ediyorsak önceki sinyal fonksiyonu muhtemelen SIG_DFL ya da SIG_IGN biçiminde olacaktır. void (*old_handler)(int); if ((old_handler = signal(SIGINT, sigint_handler)) == SIG_ERR) exit_sys("signal"); if (old_handler == SIG_DFL) printf("yes, old handler is SIG_DFL\n"); Aşağıdaki örnekte SIGINT sinyali için bir sinyal fonksiyonu set edilmiştir. Daha önceden de belirttiğimiz gibi SIGINT sinyali terminal aygıt sürücüsü tarafından Ctrl+C tuşlarına basıldığında prosese gönderilmektedir. Bu sinyalin default davranışı (default action) prosesin sonlandırılmasıdır. Tabii biz SIGINT için bir sinyal fonksiyonu set edersek proses sonlandırılmaz ve bizim set ettiğimiz fonksiyon çağrılır. Aşağıdaki programı çalıştırınca artık Ctrl+C tuşu ile programı sonlandıramayacağız. Bunun için terminali kapatabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void sigint_handler(int sno); void exit_sys(const char *msg); int main(void) { if (signal(SIGINT, sigint_handler) == SIG_ERR) exit_sys("signal"); for (;;) ; return 0; } void sigint_handler(int sno) { printf("signal occurred...\n"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- alarm isimli POSIX fonksiyonu belli bir süre dolduğunda onu çağıran prosese (yani kendi prosesine) SIGALRM isimli sinyali göndermektedir. Bu sinyalin default davranışı (default action) prosesin sonlandırılması biçimindedir. alarm fonksiyonunu birden fazla çağırdığımızda bunlar biriktirilmez. Her yeni çağrı, eski çağrıyı devre dışı bırakarak yeniden alarmı set etmektedir. Fonksiyonun prototipi şöyledir: #include unsigned alarm(unsigned seconds); Fonksiyon saniye sayısını parametre olarak almaktadır. Yani bu saniye dolduğunda fonksiyon SIGALRM sinyalini oluşturacaktır. Fonksiyon eğer daha önce alarm fonksiyonu çağrılıp set işlemi yapıldıysa o set işlemi için kalan saniye sayısını vermektedir. Eğer daha önce alarm fonksiyonu çağrılmamışsa ya da çağrıldığı halde zaten süre dolmuşsa fonksiyon 0 değeri ile geri dönmektedir. Fonksiyon başarısız olamaz. alarm fonksiyonuna argüman olarak 0 değeri geçilirse bu durum önceki alarm işleminin devre dışı bırakılacağı anlamına gelmektedir. Yani bu durum sanki alarm fonksiyonu daha önce hiç çağrılmamış gibi bir durum oluşturmaktadır. Aşağıdaki örnekte alarm fonksiyonu 5 saniyeye kurulmuştur. 5 saniye dolduktan sonra SIGALRM sinyali prosese gönderilecektir. Program bu sinyal oluştuğunda bir sinyal fonksiyonu set ederek ekrana "ALARM" yazısının çıkartılmasını sağlamıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void sigalrm_handler(int sno); void exit_sys(const char *msg); int main(void) { if (signal(SIGALRM, sigalrm_handler) == SIG_ERR) exit_sys("signal"); alarm(5); for (int i = 0; i < 10; ++i) { printf("%d\n", i); sleep(1); } return 0; } void sigalrm_handler(int sno) { printf("ALARM\n"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirtiğimiz gibi signal fonksiyonun semantiği maalesef taşınabilir değildir. Bu fonksiyonunun davranışı çeşitli sistemlerde aşağıda açıklayacağımız gibi farklılıklar gösterebilmektedir. signal fonksiyonundaki problemler ve semantik farklılıklar şunlardır: 1) Eski AT&T UNIX sistemlerinde ve o kökten gelen sistemlerde signal fonksiyonu ile bir sinyal set edilip sinyal oluştuğu zaman sinyal yeniden default duruma çekiliyordu. Böylece sanki sinyal set edilmemiş gibi bir etki oluşuyordu. Bu sistemlerde eskiden bu etkiyi ortadan kaldırmak için programcılar sinyal fonksiyonunun (signal handler) hemen başında sinyal default'a çekildiği için yeniden set işlemi yapıyorlardı. Örneğin: void signal_handler(int sno) { if (signal(SIGXXX, signal_handler) == SIG_ERR) exit_sys("signal"); ... } Böylece sinyal default'a çekildiğinde yeniden sinyal set ediliyordu. Ancak üst üste aynı sinyalin oluştuğu durumlarda prosesin sonlandırılmasına yönelik bir durum her zaman mümkün olabilmekteydi: void signal_handler(int sno) { ----> BU NOKTADA AYNI SİNYALDEN OLUŞSA SİNYAL DEFAULT'A ÇEKİLDİĞİ İÇİN PROSES SONLANDIRILACAKTIR! if (signal(SIGXXX, signal_handler) == SIG_ERR) exit_sys("signal"); ... } AT&T ve türevlerinde yukarıdaki duruma ilişkin bir önlem alınamıyordu. Ancak daha sonra BSD UNIX sistemleri bu problemi "sinyalin default'a çekilmemesi" biçiminde değiştirerek çözmeye çalışmıştır. BSD ve türevlerinde sinyal default'a çekilmemektedir. Linux sistemlerinde, signal sistem fonksiyonu AT&T semantiğini uygulamakta ve sinyali default'a çekmektedir. Ancak signal fonksiyonu glibc kütüphanesinin belli versiyonundan sonra signal sistem fonksiyonu yerine sigaction sistem fonksiyonu kullanılarak yazıldığı için sinyali default'a çekmemektedir. Yani Linux'ta signal POSIX fonksiyonu AT&T değil, BSD semantiğini uygulamaktadır. POSIX standartları, AT&T ve BSD sistemlerindeki farklılıkların geçerli olabilmesi için sinyal oluştuğunda oluşan sinyalin default'a çekilip çekilmeyeceğini işletim sistemlerini yazanların isteğine bırakmıştır. 2) AT&T ve bu kökten gelen UNIX türevi sistemlerde bir sinyal oluştuğunda o sinyal, sinyal fonksiyonu çalıştığı sürece bloke edilmiyordu. Yani aynı sinyalden üst üste gelebiliyordu. Bu durum sinyal fonksiyonun iç içe birden fazla kez çalıştırılmasına yol açabilmektedir. Bunun da "stack taşması (stack overflow)" gibi bazı sakıncaları söz konusu olabilmektedir. BSD UNIX sistemleri bu problemi çözmüştür. BSD sistemlerinde bir sinyal oluştuğunda sinyal fonksiyonundan çıkılana kadar aynı sinyal bir daha oluşamamaktadır. Bu duruma ileride de göreceğimiz gibi "sinyalin bloke edilmesi" denilmektedir. Yani BSD sistemleri sinyal fonksiyonu çalıştığı sürece aynı sinyali bloke etmekte ve böylece iç içe sinyalin oluşmasını engellemektedir. Linux'un signal sistem fonksiyonu bu konuda da AT&T semantiğini uygulamaktadır. Ancak signal fonksiyonu glibc kütüphanesinin belli bir versiyonundan sonra sigaction sistem fonksiyonunu çağırmaktadır ve BSD semantiğini uygulamaktadır. Yani Linux sistemlerinde signal fonksiyonu ile sinyal set edildiğinde sinyal fonksiyonu çalıştığı sürece proses ilgili sinyale bloke edilmektedir. POSIX standartları sinyal fonksiyonu çalıştığı sürece aynı sinyalin bloke edilip edilmeyeceğini işletim sistemini yazanların isteğine bırakmıştır. 3) signal fonksiyonunun diğer bir problemi de yavaş sistem fonksiyonlarında "otomatik restart" yapılıp yapılmayacağının sistemler arasında değişebilmesidir. AT&T ve bu kökten gelen UNIX türevi sistemlerde yavaş sistem fonksiyonları başarısız olmakta ve errno değeri EINTR olarak set edilmektedir. Halbuki BSD sistemlerinde signal fonksiyonu ile sinyal fonksiyonu set edildiğinde otomatik restart işlemi yapılmaktadır. Linux sistemlerinde signal sistem fonksiyonu AT&T semantiğini kullandığı için otomatik restart yapmamaktadır. Ancak yukarıda da belirttiğimiz gibi signal fonksiyonu glibc kütüphanesinin belli bir versiyonundan sonra sigaction sistem fonksiyonu çağrılarak yazılmıştır ve otomatik restart işlemi yapılmaktadır. POSIX standartlarında signal fonksiyonu ile bir sinyal set edildiğinde otomatik restart işleminin yapılıp yapılmayacağı işletim sistemlerini yazanların isteğine bırakılmıştır. Bu durumda Linux sistemlerindeki glibc kütüphanesinde bulunan signal fonksiyonu "sinyali default'a çekmemekte, sinyali sinyal fonksiyonu çalıştığı sürece bloke etmekte ve otomatik restart işlemini uygulamaktadır." ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 74. Ders 13/08/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- sigaction fonksiyonu signal fonksiyonunun oldukça iyileştirilmiş bir biçimidir. sigaction fonksiyonunda signal fonksiyonundaki semantik belirsizlikler kaldırılmıştır. Dolayısıyla programcıların sinyal fonksiyonlarını signal fonksiyonu yerine sigaction fonksiyonu ile set etmesi iyi bir tekniktir. Ancak sigaction fonksiyonunun kullanımı signal fonksiyonundan daha zordur. sigaction fonksiyonunun prototipi şöyledir: #include int sigaction(int sig, const struct sigaction *act, struct sigaction *oact); Fonksiyonun birinci parametresi yine set edilecek sinyalin numarasını belirtmektedir. Fonksiyonun ikinci parametresi sigaction isimli bir yapı nesnesinin adresini almaktadır. Programcı sinyal set işlemi ile ilgili bazı bilgileri sigaction türünden yapı nesnesinin içerisine yerleştirir. Sonra bu nesnenin adresini fonksiyona verir. Fonksiyonun üçüncü parametresi NULL geçilebilir. Ancak NULL geçilmezse daha önceki sinyal set özellikleri bu parametreye adresi geçirilen sigaction türünden nesneye yerleştirilecektir. Fonksiyonun ikinci parametresi de NULL adres geçilebilmektedir. Bu durumda programcı eski sinyal set bilgilerini alır, ancak onu değiştirmez. Fonksiyon 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 edilmektedir. sigaction fonksiyonun kullanımındaki en önemli nokta şüphesiz sigaction yapı nesnesinin içinin doldurulmasıdır. sigaction yapısı başlık dosyasında şöyle bildirilmiştir: struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; }; Yapının sa_handler elemanı sinyal oluştuğunda çağrılacak fonksiyonun (signal handler) adresini almaktadır. Bu elemana geri dönüş değeri void, parametresi int olan bir fonksiyonun adresi geçirilmelidir. UNIX/Linux sistemlerine daha sonraları "güvenilir sinyaller (reliable signals)" adı altında bazı semantik eklemeler yapılmıştır. Bu eklemelerden biri de sinyal fonksiyonunun (signal handler) detaylandırılmasıdır. Yapının sa_sigaction elemanı da sinyal oluştuğunda çağrılacak fonksiyonu belirtmektedir. Ancak bu fonksiyonun parametrik yapısı daha değişiktir. Tabii programcı yapının iki elemanına da fonksiyon girmez. Bunlardan birine fonksiyon girer. sigaction fonksiyonun sinyal oluştuğunda yapının hangi elemanındaki sinyal fonksiyonunu çağıracağı yapının sa_flags elemanında belirtilmektedir. Default durum yapının sa_handler elemanında belirtilen klasik tarzda sinyal fonksiyonunun çağrılmasıdır. Yapının sa_handler ya da sa_sigaction elemanlarına yine signal fonksiyonunda olduğu gibi SIG_DFL ve SIG_IGB özel değerleri girilebilmektedir. sigaction yapısının sa_mask elemanı sinyal bloke (mask) kümesini belirtmektedir. Default durumda bir sinyal oluştuğu zaman sinyal fonksiyonu çağrıldığında zaten artık aynı numaralı sinyal, sinyal fonksiyonu çalıştığı sürece bloke olmaktadır. Bir sinyalin bloke olması, o sinyal oluştuğunda işletim sisteminin sinyali prosese teslim etmemesi ve askıda (pending durumda) bekletmesi anlamına gelmektedir. Ancak sinyaller biriktirilmez. Yani örneğin bir sinyal oluşup sinyal fonksiyonu çağrıldığında o sinyalden beş kez daha oluşsa bloke açıldığında sinyal fonksiyonu yalnızca bir kez çalıştırılmaktadır. Ancak programcı isterse "sinyal fonksiyonu çalıştığı sürece" oluşan sinyalin yanı sıra başka sinyallerin de bloke edilmesini sağlayabilmektedir. Bunun için yapının sa_mask elemanı kullanılmaktadır. sa_mask elemanının sigset_t türünden olduğuna dikkat ediniz. Bu sigset_t türü bir bit dizisi gibi düşünülmelidir. Yani bu türün çeşitli bitleri çeşitli sinyallerin bloke edilip edilmeyeceğini belirtmektedir. İşte bu türün çeşitli bitlerini set etmek ve reset etmek için bazı fonksiyonlar bulundurulmuştur. Bu fonksiyonlar makro biçiminde de yazılabilmektedir. Bu fonksiyonların (ya da makroların) listesi şöyledir: #include int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set, int signum); int sigdelset(sigset_t *set, int signum); int sigismember(const sigset_t *set, int signum); sigemptyset fonksiyonu bit dizisi içerisindeki tüm bitleri reset etmektedir. sigfillset fonksiyonu ise tüm bitleri set etmektedir. sigaddset fonksiyonu bit dizisi içerisindeki ilgili sinyale ilişkin biti set etmektedir. sigdelset fonksiyonu ise bit dizisi içerisindeki ilgili sinyale ilişkin biti reset etmektedir. sigismember fonksiyonu belli bir sinyale ilişkin bitin bit dizisi içerisindeki değerini yani (yani set ya da reset olduğunu) bize vermektedir. Fonksiyonlar başarı durumunda 0, başarısızlık durumunda -1 değerine geri dönmektedir. Ancak sigismember başarı durumunda 0 ya da 1 değerine, başarısızlık durumunda -1 değerine geri döner. Tabii bu fonksiyonların başarısının kontrol edilmesine gerek yoktur. Örneğin biz sigset_t bit dizisinde yalnızca SIGINT ve SIGTERM sinyali için ilgili bitleri set etmek isteyelim. Bu işlemi aşağıdaki gibi yapabiliriz: sigset_t sset; sigemptyset(&sset); sigaddset(&sset, SIGINT); sigaddset(&sset, SIGTERM); Biz burada herhangi bir bloke işlemi yapmadık. Yalnızca bir belirleme yaptık. İşte sigaction yapısının sa_mask elemanında sinyallere ilişkin set edilen bitler sinyal fonksiyonu çalıştığı sürece bloke edilecektir. Örneğin: struct sigaction sa; sa.sa_handler = signal_handler; sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGINT); sigaddset(&sa.sa_mask, SIGTERM); ... if (sigaction(SIGALRM, &sa, NULL) == -1) exit_sys("sigaction"); Burada SIGALRM sinyali için set edilmiş olan sinyal fonksiyonu çalıştığı sürece SIGINT ve SIGTERM sinyalleri bloke edilecektir. Zaten default durumda ilgili sinyalin kendisinin de sinyal fonksiyonu çalıştığı sürece bloke edildiğini belirtmiştik. Burada bloke sürekli değil yalnızca sinyal fonksiyonu çalıştığı sürece söz konusu olmaktadır. Sinyal fonksiyonun çalışması bittiğinde sinyal işleme sokulacaktır. Programcı, yapının sa_mask elemanına değer atamalıdır. Bildiğiniz gibi yerel nesnelerin içerisinde rastgele değerler vardır. Yapı nesnesi global olsa bile içi sıfırlanan nesnelerin bu bağlamda içi boş bir bit dizisi belirtmesi garanti değildir. Örneğin biz yapının bu elemanına şöyle değer atayabiliriz: sigemptyset(&sa.sa_mask); Bu durumda sinyal fonksiyonu çalışırken ilgili sinyal dışındaki hiçbir sinyal bloke edilmeyecektir. sigaction yapısının sa_flags elemanı bazı sembolik sabitlerin bit OR işlemine sokulmasıyla oluşturulmaktadır. Bu bayrakların her birinin bir anlamı vardır: SA_RESETHAND: Bu bayrak set edilirse sinyal fonksiyonu çalıştırıldığında sinyal otomatik olarak default'a çekilir. Default durumda bu bayrağın set edilmediğine dikkat ediniz. Bu bayrak AT&T semantiğinin uygulanabilmesi için bulundurulmuştur. SA_RESTART: Bu bayrak set edilirse yavaş POSIX fonksiyonlarında (yani onların çağırdığı sistem fonksiyonlarında) sinyal oluştuğunda sinyal fonksiyonu çağrılır ancak fonksiyon başarısızlıkla sonuçlanmaz. Çünkü sistem fonksiyonunun restart edilmesi çekirdek tarafından otomatik yapılmaktadır. Bu biçimde set edilmiş bir sinyal fonksiyonu söz konusu olduğunda ilgili sistem fonksiyonları sinyal dolayısıyla başarısız olmayacaktır. Anımsanacağı gibi AT&T'nin signal fonksiyonu otomatik restart işlemi yapmamaktadır. Ancak BSD'lerin ve Linux'un signal fonksiyonu zaten otomatik restart işlemi yapmaktadır. SA_SIGINFO: Bu bayrak belirtilirse sinyal fonksiyonu için sigaction yapısının sa_handler elemanı değil, sa_sigaction elemanı dikkate alınmaktadır. Tabi bu durumda sinyal fonksiyonun da (signal handler) parametrik yapısı değişmektedir. Bu konu "gerçek zamanlı sinyalleri (realtime signals)" ele alacağımız paragraflarda açıklanacaktır. SA_NODEFER: Anımsanacağı gibi default durumda her zaman bir sinyal oluştuğunda sinyal fonksiyonu çalıştığı sürece o sinyal bloke edilmektedir. Yani iç içe aynı sinyalden oluşamamaktadır. Ancak bu bayrak kullanılırsa sinyal fonksiyonu çalıştığı sürece o sinyal bloke edilmeyecek ve sinyal fonksiyonu iç içe çağrılabilecektir. Anımsanacağı gibi signal fonksiyonundaki AT&T semantiği zaten böyleydi. Ancak BSD ve Linux sistemlerindeki semantik, aynı sinyalin sinyal fonksiyonu çalıştığı sürece bloke edilmesi biçimindeydi. SA_NOCLDWAIT: Bu bayrak SIGCHLD sinyali için anlamlıdır. Otomatik olarak zombie proses oluşmasını engellemek için kullanılmaktadır. Bu bayrak set edilip proses sonlandığında artık kaynaklarını wait fonksiyonlarını beklemeden serbest bırakır. Tabii programcı da böyle prosesler için artık wait işlemi yapmaz. Bu konu ayrı bir paragrafta açıklanacaktır. SA_NOCLDSTOP: Bu bayrak belirtildiğinde alt proses durdurulduğu zaman ya da yeniden çalışmaya devam ettirildiği zaman üst prosese SIGCHLD sinyalini göndermemektedir. Bu konu da ileride ele alınacaktır. SA_ONSTACK: İç içe sinyaller oluştuğunda mevcut stack için taşma problemleri çok seyrek de olsa teorik olarak söz konusu olabilmektedir. Bunun için alternatif stack kullanımı da mümkündür. İşte bu bayrak ilgili sinyal fonksiyonu çalışırken alternatif stack kullanılacağını belirtmektedir. Alternatif stack'in ayrıca setaltstack fonksiyonu ile set edilmesi gerekmektedir. Aşağıda sigaction fonksiyonunun kullanımına bir örnek verilmiştir. Bu örnekte yapının sa_mask elemanı sigemptyset fonksiyonu ile tamamen reset edilmiştir. Yani ilgili sinyal oluştuğunda, sinyal fonksiyonu çalıştığı sürece başka bir sinyal bloke edilmeyecektir. Yapının sa_flags elemanına da 0 atanmıştır. Yani bu eleman için herhangi bir bayrak belirtilmemiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void sigalrm_handler(int sno); void exit_sys(const char *msg); int main(void) { struct sigaction sa; sa.sa_handler = sigalrm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGALRM, &sa, NULL) == -1) exit_sys("sigaction"); alarm(5); for (int i = 0; i < 10; ++i) { printf("%d\n", i); sleep(1); } return 0; } void sigalrm_handler(int sno) { printf("ALARM\n"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi glibc kütüphanesindeki signal POSIX fonksiyonu belli bir versiyondan sonra signal sistem fonksiyonunu çağırarak değil, sigaction fonksiyonunu çağırarak yazılmıştı. signal fonksiyonu "sinyali default'a çekmiyordu", "sinyal fonksiyonu çalıştığı sürece aynı sinyali bloke ediyordu" ve "otomatik restart işlemi yapıyordu". O halde glibc kütüphanesindeki signal POSIX fonksiyonu sigaction fonksiyonu kullanılarak aşağıdaki gibi yazılabilir: void (*mysignal(int sig, void (*handler)(int)))(int) { struct sigaction sa, sa_old; sa.sa_handler = sigalrm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGALRM, &sa, &sa_old) == -1) return SIG_ERR; return sa_old.sa_handler; } Aşağıda yazdığımız fonksiyonun kullanımına örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void sigalrm_handler(int sno); void exit_sys(const char *msg); void (*mysignal(int sig, void (*handler)(int)))(int) { struct sigaction sa, sa_old; sa.sa_handler = sigalrm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGALRM, &sa, &sa_old) == -1) return SIG_ERR; return sa_old.sa_handler; } int main(void) { if (mysignal(SIGALRM, sigalrm_handler) == SIG_ERR) exit_sys("mysignal"); alarm(5); for (int i = 0; i < 10; ++i) { printf("%d\n", i); sleep(1); } return 0; } void sigalrm_handler(int sno) { printf("ALARM\n"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Sinyallerin proseslere gönderildiğini ve prosesin herhangi bir thread'i tarafından işletildiğini anımsayınız. İşte biz de istersek bir prosese sinyal gönderebiliriz. Bunun için kill isimli POSIX fonksiyonu kullanılmaktadır. Maalesef bu fonksiyon yanlış isimlendirilmiştir. Kill sözcüğü İngilizce "öldürmek" ya da bu bağlamda "sonlandırmak" anlamına gelmektedir. Oysa kill fonksiyonunun böyle bir amacı yoktur. Proseste ilgili sinyal için sinyal fonksiyonu set edilmemişse pek çok sinyalde zaten default davranış (default action) prosesin sonlandırılması biçimindedir. kill fonksiyonunun prototipi şöyledir: #include int kill(pid_t pid, int sig); Fonksiyonun birinci parametresi sinyalin gönderileceği prosesin id değerini, ikinci parametresi gönderilecek sinyalin numarasını almaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyonun birinci parametresi aslında daha geniş bir kullanım alanına sahiptir. Fonksiyonun birinci parametresinin ayrıntılı açıklaması şöyledir: - Eğer bu parametre sıfırdan büyük bir değer olarak girilmişse (en çok kullanılan durum), sinyal yalnızca belirtilen id'ye sahip prosese gönderilmektedir. - Eğer bu parametre 0 girilirse sinyal, sinyali gönderen prosesin proses grup id'si ile aynı proses grup id'ye sahip olan sinyal gönderme hakkının olabildiği tüm proseslere gönderilmektedir. Yani biz kendi proses grubumuzun tüm proseslerine bu biçimde sinyal gönderebiliriz. - Eğer bu parametre -1 geçilirse sinyal, "sinyal gönderme hakkı" olan sistemdeki tüm proseslere gönderilmektedir. - Eğer bu parametre negatif bir değer olarak girilirse bu değerin mutlak değeri (yani pozitif hali) bir proses grup id kabul edilerek sinyal o proses grubunun "sinyal gönderme hakkına sahip olunan" tüm proseslerine gönderilmektedir. Burada sözünü ettiğimiz proses grubu kavramı izleyen bölümlerde ele alınacaktır. Genellikle kill ile sinyaller, bir prosesin sonlandırılması için gönderilmektedir. Ancak başka amaçlarla da sinyallerin gönderilmesi söz konusu olabilmektedir. kill fonksiyonuyla bir prosese sinyal gönderebilmek için sinyali gönderen prosesin "gerçek ya da etkin kullanıcı id'sinin sinyalin gönderildiği prosesin gerçek ya da saklanmış kullanıcı id'si (saved-set-user id)" ile aynı olması gerekmektedir. Saklanmış kullanıcı id'si (saved-set-user id) izleyen bölümlerde ele alınacaktır. Tabii eğer proses uygun önceliğe (appropriate priviledge) sahipse (yani Linux'ta root ya da uygun yeteneğe sahipse) proses herhangi bir prosese sinyal gönderebilmektedir. Buradan çıkan özet şudur: Biz ancak kendi proseslerimize sinyal gönderebiliriz. Herhangi bir prosese sinyal gönderebilmemiz için etkin kullanıcı id'mizin 0 olması (yani root proses olmamız) gerekir. Aşağıdaki örnekte "ssender.c" programı komut satırı argümanıyla aldığı sinyali, yine komut satırı argümanıyla aldığı id'ye sahip prosese kill POSIX fonksiyonuyla göndermektedir. Sinyal numaralarının UNIX türevi sistemlerde aynı olmayabileceğine dikkat ediniz. Örneğin 12 numaralı sinyal farklı sistemlerde farklı olabilmektedir. Bu nedenle daha önce de belirttiğimiz gibi sinyallarin numaraları yerine dosyası içerisindeki sembolik sabitler kullanılmalıdır. Tabii komut satırı argümanları birer yazıdır. Sinyal isimlerini taşınabilir bir biçimde sinyal numaralarına dönüştüren standart bir POSIX fonksiyonu yoktur. (Ancak glibc kütüphanesinde bu amaçla kullanılabilecek standart olmayan bir fonksiyon bulunmaktadır.) Bu nedenle biz örnek programda, bir yapı dizisi oluşturup sinyal isimlerini manuel bir biçimde sinyal numaralarına dönüştürdük. Örnek için iki terminal açınız. Terminalin birinde "sample" programını diğerinde "ssender" programını çalıştırınız. "ssender" programından diğer programa sinyal gönderiniz. Tabii bunu yapabilmeniz için öncelikle "sample" programının proses id'sini bilmeniz gerekir. Bunun için terminalde "ps -u" komutunu kullanabilirsiniz. Programın örnek bir kullanımı şöyle olabilir: ./ssender TERM 12404 Örneğimizde sinyal isimlerinde "SIG" önekinin kullanılmadığına dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* ssender.c */ #include #include #include #include void exit_sys(const char *msg); typedef struct tagSIGNAL_INFO { const char *name; int sig; } SIGNAL_INFO; SIGNAL_INFO g_signal_info[] = { {"INT", SIGINT}, {"TERM", SIGTERM}, {"KILL", SIGKILL}, {"USR1", SIGUSR1}, {NULL, 0}, /* ... */ }; int main(int argc, char *argv[]) { pid_t pid; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } pid = (pid_t)atol(argv[2]); for (int i = 0; g_signal_info[i].name != NULL; ++i) if (!strcmp(argv[1], g_signal_info[i].name)) if (kill(pid, g_signal_info[i].sig) == -1) exit_sys("kill"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* sample.c */ #include #include #include #include void sigusr1_handler(int sno); void exit_sys(const char *msg); int main(void) { struct sigaction sa, sa_old; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGUSR1, &sa, &sa_old) == -1) exit_sys("sigaction"); for (int i = 0; i < 60; ++i) { printf("%d\n", i); sleep(1); } return 0; } void sigusr1_handler(int sno) { printf("SIGUSR1 handler running...\n"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Sinyal numaralarını alarak hata mesajları için sinyali betimleyen bir yazı veren strsignal isimli bir POSIX fonksiyonu bulunmaktadır: #include char *strsignal(int signum); Ayrıca glibc kütüphanesinde standart olmayan iki fonksiyon da vardır: #include const char *sigdescr_np(int sig); const char *sigabbrev_np(int sig); sigdescr_np fonksiyonu strsignal fonksiyonuna benzerdir. sigabbrev_np fonksiyonu ise sinyal numarasından hareketle sinyal ismini başında "SIG" öneki olmadan vermektedir. Bu fonksiyonları kullanmadan önce _GNU_SOURCE sembolik sabiti dosyasının yukarısında define edilmelidir. Ayrıca glibc kütüphanesinde eskiden bütün sinyal isimleri sys_siglist isimli bir dizide toplanmıştı. (Bu dizinin uzunluğu NSIG sembolik sabiti kadardır.) Ancak bu dizi daha sonra deprecated yapılıp kaldırılmıştır. Aşağıda bu fonksiyonlarının kullanımına yönelik bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #define _GNU_SOURCE #include #include #include #include void exit_sys(const char *msg); int main(void) { printf("%s\n", strsignal(SIGTERM)); printf("%s\n", sigabbrev_np(SIGTERM)); printf("%s\n", sigdescr_np(SIGTERM)); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bir prosese komut satırından sinyal göndermek için kill isimli bir kabuk komutu da bulunmaktadır. Bu kill kabuk komutu zaten kill fonksiyonu kullanılarak yukarıda bizim yaptığımıza benzer biçimde kullanılmaktadır. kill komutu ile prosese sinyal gönderirken sinyal ismi -TERM, -INT, _USR1 gibi başında "SIG" öneki olmadan belirtilmelidir. Örneğin: kill -TERM 12767 (SIGTERM sinyali gönderiliyor) kill -USR1 12767 (SIGUSR1 sinyali gönderiliyor) kill komutunda sinyalin ismi değil numarası da belirtilebilmektedir. Ancak sinyal numaralarının UNIX sistemleri genelinde taşınabilir olmadığına dikkat ediniz. Örneğin: kill -15 12801 (Linux sistemlerinde SIGTERM sinyali gönderiliyor) kill -10 12801 (Linux sistemlerinde SIGUSR1 sinyali gönderiliyor) kill komutu hiç sinyal ismi ya da numarası belirtilmeden kullanılırsa default olarak SIGTERM sinyalini göndermektedir. Örneğin: kill 12801 Bu durumda prosese SIGTERM sinyali gönderilmektedir. Ancak örneğin: kill -KILL 12801 Bu durumda prosese SIGKILL sinyali gönderilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 75. Ders 26/08/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir prosesi sonlandırmak için (kill etmek için) iki sinyal bulundurulmuştur: SIGTERM sinyali ve SIGKILL sinyali. Bu iki sinyal birbirine benzerdir. Bu iki sinyalin de amacı prosesi sonlandırmaktır. Ancak bu iki sinyal arasında şöyle bir farklılık vardır: SIGTERM sinyali proses tarafından bloke edilebilir, ignore edilebilir ya da bu sinyal için sinyal fonksiyonu set edilebilir. Ancak SIGKILL sinyali ignore edilemez, bloke edilemez ve bu sinyal için sinyal fonksiyonu da set edilemez. (Eğer signal ya da sigaction fonksiyonu ile SIGKILL sinyali için bir set işlemi yapılmaya çalışılırsa bu fonksiyonlar başarısız olur ve errno değeri EINVAL ile set edilir.) Bu durumda bir prosesi garantili sonlandırmak için SIGTERM sinyali değil, SIGKILL sinyali gönderilmelidir. Örneğin: kill -KILL 12801 SIGTERM sinyalinin Linux sistemlerindeki numarası 15, SIGKILL sinyalinin ise 9'dur. Örneğin: kill -9 12801 ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir proses id'ye ilişkin prosesin hala bulunuyor olduğunu, yani sonlanmamış olduğunu test etmenin çeşitli yöntemleri söz konusu olabilmektedir. Çok kullanılan yöntemlerden biri, kill fonksiyonu ile prosese 0 numaralı sinyali göndermektir. 0 numaralı sinyal aslında yoktur. 0 numara, sinyaller için bu amaçla kullanılmaktadır. Yani aslında 0 numaralı sinyal, prosese hiç gönderilmemekte yalnızca bu tür test işlemleri için kullanılmaktadır. Biz kill fonksiyonu ile prosese 0 numaralı sinyali gönderdiğimizde kill fonksiyonu başarılı olursa o prosesin sistemde bulunduğunu anlarız. Tabii proses sistemde bulunduğu halde uygun önceliğe sahip olmadığından dolayı da başarısız olabilir. Bu durumda errno değişkeni EPERM değeri ile set edilmektedir. Eğer prosesin var olmadığından dolayı kill fonksiyonu başarısız olmuşsa, bu durumda errno değişkeni ESRCH değeri ile set edilmektedir. Bu durumda prosesin hala yaşadığının testi şöyle yapılabilir: if (kill(pid, 0) == -1) { if (errno == ESRCH) { /* proses sonlanmış */ } exit_sys("kill); } /* proses sonlanmamış */ ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İşletim sistemleri dünyasında bir fonksiyonun "senkron işlem" yapması demekle "fonksiyon geri döndüğünde o işin bitmiş olmasının garanti edilmesi" anlaşılmaktadır. Örneğin read, write gibi fonksiyonlar senkron işlem yapmaktadır. read fonksiyonu geri döndüğünde okuma da bitmiş durumdadır. Bir fonksiyonun "asenkron işlem" yapması "fonksiyon geri döndüğünde o işlemin bitmek zorunda olmadığı" anlamına gelmektedir. Örneğin kill fonksiyonu geri döndüğünde sinyalin ilgili proses tarafından işlenmiş olduğunun bir garantisi yoktur. Bu durumda kill fonksiyonu "asenkron" bir işlem yapmaktadır. Örneğin "asenkron IO" işleminde biz bir fonksiyonla IO işlemini başlatırız ancak fonksiyon geri döndüğünde bu IO işlemi bitmiş olmak zorunda değildir. Hala devam ediyor olabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- raise fonksiyonu bir prosesin kendisine sinyal göndermesi için kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int raise(int sig); Bu fonksiyonun eşdeğeri aşağıdaki gibi kill fonksiyonu kullanılarak da elde edilebilir. Fakat kill fonksiyonu asenkron işlem sağlarken, raise fonksiyonu senkron bir işlem sağlamaktadır. kill(getpid(), sig); Fonksiyon gönderilecek sinyalin numarasını parametre olarak almaktadır. Başarı durumunda 0 değerine, başarısızlık durumunda sıfır dışı herhangi bir değere (yani -1 olmak zorunda değil) geri dönmektedir. Tabii fonksiyon her zaman kendine sinyal gönderebilir. Bu durumda başarısızlığın kontrol edilmesi gerekmez. (Tabii fonksiyona biz yanlış bir sinyal numarası geçersek fonksiyon başarısız olabilmektedir.) POSIX standartlarına göre çok thread'li bir ortamda raise fonksiyonu hangi thread tarafından kullanılmışsa senkronluğu sağlamak için sinyal fonksiyonu o thread tarafından çalıştırılmaktadır. Yani fonksiyon senkron işlem yapmasının dışında aşağıdaki ile eşdeğerdir: pthread_kill(pthread_self(), sig); pthread_kill fonksiyonu izleyen paragraflarda ele alınacaktır. raise fonksiyonu aynı zamanda standart bir C fonksiyonudur. Ancak C standartlarında sinyal konusu ayrıntılarıyla ele alınmış bir konu değildir. Yani C standartlarında bir sinyal olgusundan bahsedilmiş ancak hiçbir ayrıntıya girilmemiştir. Anımsanacağı gibi Linux sistemlerinde aslında her thread'in bir "task struct" yapısı vardı ve prosess id aslında task struct yapısından elde ediliyordu. Yani aslında Linux sistemlerinde her thread'in bir pid değeri vardır. Bu değeri biz daha önce gettid fonksiyonu ile elde etmiştik. O halde biz aslında kill fonksiyonunda thread'e ilişkin pid değerini kullanırsak zaten sinyal adeta o thread'e gönderilmiş gibi bir etki oluşacaktır. Böylece biz Linux sistemlerinde başka bir prosesten, başka bir prosesin thread'ine bu yolla sinyal gönderebilmekteyiz. Aşağıdaki örnekte proses kendine raise fonksiyonu ile SIGUSR1 sinyalini göndermektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void sigusr1_handler(int sno); void exit_sys(const char *msg); int main(void) { struct sigaction sa, sa_old; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGUSR1, &sa, &sa_old) == -1) exit_sys("sigaction"); for (int i = 0; i < 10; ++i) { printf("%d\n", i); if (i == 5) raise(SIGUSR1); sleep(1); } return 0; } void sigusr1_handler(int sno) { printf("SIGUSR1 handler running...\n"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Proses kendini bazı sinyallere bloke edebilir. Eğer proses kendini bir sinyale bloke ederse o sinyal oluştuğunda işletim sistemi, sinyali prosese teslim etmez (deliver etmez). Onu "askıda (pending durumda)" bekletir. Eğer proses o sinyalin blokesini kaldırırsa işletim sistemi sinyali prosese teslim eder. Ancak sinyaller biriktirilmemektedir. Yani bir proses kendini bir sinyale bloke etmiş ise o sırada o sinyal birden fazla kez prosese gönderilirse, proses sinyal blokesini kaldırdığında yalnızca tek bir sinyal prosese teslim edilmektedir. Yani işletim sistemi askıda bekletilen sinyaller için bir sayaç tutmamaktadır. Daha önceden de belirttiğimiz gibi default durumda zaten bir sinyal oluştuğunda, sinyal fonksiyonu çalıştığı sürece aynı numaralı sinyal otomatik bloke edilmekte sinyal fonksiyonu sonlandığında blokesi açılmaktaydı. Thread'ler öncesinde UNIX türevi işletim sistemleri her proses için bir "signal mask" kümesi tutuluyordu. Prosesin "signal mask" kümesi prosesin o anda hangi sinyallere bloke edilmiş olduğunu belirtmekteydi. Thread'ler konusu UNIX sistemlerine sokulduğunda ayrıca her thread için de bir "signal mask" kümesi söz konusu olmuştur. Maalesef sigprocmask fonksiyonunun çok thread'li uygulamalardaki davranışı tanımlanmamıştır. POSIX standartları fonksiyonun çok thread'li uygulamalardaki davranışını "unspecified" olarak belirtmektedir. Linux sistemlerinde sigprocmask yalnızca fonksiyonun çağrıldığı thread'in signal mask kümesini değiştirmektedir. Prosesin "signal mask" kümesini değiştirmek için yani onu belli sinyallere bloke etmek ya da blokesini açmak için sigprocmask isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int sigprocmask(int how, const sigset_t *set, sigset_t *oset); Fonksiyonun birinci parametresi "signal mask kümesi" üzerinde hangi işlemin yapılacağını belirtmektedir. Bu parametre şunlardan biri olabilir: SIG_BLOCK SIG_UNBLOCK SIG_SETMASK Fonksiyonun ikinci parametresi daha önce görmüş olduğumuz sigset_t türündendir. Anımsanacağı gibi bu tür aslında bitsel olarak sinyalleri ifade etmek için kullanılmaktadır. İşte eğer fonksiyonun birinci parametresi SIG_BLOCK biçiminde girilirse ikinci parametrede belirtilen sinyal kümesi prosesin "signal mask" kümesine dahil edilir. Yani artık bu sinyaller de bloke edilmiş olur. Eğer birinci parametre SIG_UNBLOCK biçiminde girilirse bu durumda ikinci parametrede belirtilen sinyal kümesindeki sinyaller prosesin signal mask kümesinden çıkartılmaktadır. Eğer birinci parametre SIG_SETMASK biçiminde girilirse bu durumda ikinci parametredeki sinyal kümesi prosesin "signal mask" kümesi haline getirilmektedir. Üçüncü parametre prosesin daha önceki signal mask kümesinin yerleştirileceği nesneyi belirtmektedir. Aslında ikinci ve üçüncü parametreler NULL adres olarak da geçirilebilir. Bu durumda bu parametreler fonksiyon tarafından kullanılmamaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Örneğin biz prosesimizi SIGTERM sinyaline bloke etmek isteyelim: sigset_t sset, osset; ... sigemptyset(&sset); sigaddset(&sset, SIGTERM); if (sigprocmask(SIG_BLOCK, &sset, &osset) == -1) exit_sys("sigprocmask"); Burada sigprocmask fonksiyonunun birinci parametresi SIG_BLOCK biçiminde geçilmiştir. Bu durumda ikinci parametrede belirtilen sinyal kümesindeki sinyaller prosesin sinyal bloke kümesine (yani signal mask kümesine) dahil edilecektir. Böylece proses bu sinyale bloke edilmiş olacaktır. Pekiyi bu blokeyi nasıl kaldırabiliriz? Bunun için iki yöntem kullanabiliriz. Örneğin: if (sigprocmask(SIG_UNBLOCK, &sset, NULL) == -1) exit_sys("sigprocmask"); Bunun diğer bir yolu eski sinyal mask kümesini yeniden set etmektir: if (sigprocmask(SIG_SETMASK, &osset, NULL) == -1) exit_sys("sigprocmask"); Burada biz eski signal mask kümesini yeniden set ettik. Böylece daha önce yapmış olduğumuz SIG_BLOCK işlemi de devre dışı kalmış oldu. sigprocmask fonksiyonunda prosesin signal mask kümesinde SIGKILL ya da SIGSTOP sinyalleri bulunsa bile bu durumda başarısız olmaz. Yalnızca bu sinyaller için bloke işlemi yapılmaz. Aşağıdaki örnekte proses 30 saniye kadar SIGTERM sinyaline bloke edilmiş sonra da blokesi açılmıştır. Bu süre içerisinde başka bir terminalden kill komutuyla prosese SIGTERM sinyali göndermeyi deneyiniz. Bu süre zarfında sinyalin "askıda (pending)" kaldığını prosese teslim edilmediğini göreceksiniz. Ancak 30 saniye geçtikten sonra örneğimizde prosesin blokesi açılmıştır. Bu durumda işletim sistemi SIGTERM sinyalini prosese gönderecek ve proses bu sinyal için sinyal fonksiyonu set etmediğinden dolayı sonlandırılacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { sigset_t sset; sigemptyset(&sset); sigaddset(&sset, SIGTERM); if (sigprocmask(SIG_BLOCK, &sset, NULL) == -1) exit_sys("sigprocmask"); printf("sleep for 30 seconds by blocikng SIGTERM...\n"); for (int i = 0; i < 30; ++i) { printf("%d\n", i); sleep(1); } if (sigprocmask(SIG_UNBLOCK, &sset, NULL) == -1) exit_sys("sigprocmask"); printf("program continues running...\n"); for (int i = 0; i < 30; ++i) { printf("%d\n", i); sleep(1); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pek gereksinim duyulmasa da programcı o anda "askıda (pending)" olan sinyallerin kümesini de elde etmek isteyebilir. Bunun için sigpending isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int sigpending(sigset_t *set); Fonksiyon askıdaki sinyalleri argüman olarak girilen sigset_t nesnesi içerisine yerleştirmektedir. Programcı belli bir sinyalin bu kümede olup olmadığını sigismember fonksiyonu ile öğrenebilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- pause isimli POSIX fonksiyonu bir sinyal oluşana kadar ilgili thread'i blokede bekletmektedir. Biz de aslında bu fonksiyonu daha önce kullanmıştık. Fonksiyonun prototipi şöyledir: #include int pause(void); Fonksiyon her zaman -1 değerine geri döner. errno değişkeni de her zaman EINTR olarak set edilmektedir. Fonksiyonun başarısının kontrol edilmesine gerek yoktur. Sinyal oluştuğunda eğer sinyal fonksiyonu set edilmemişse pause fonksiyonu zaten geri dönmemektedir. Ancak sinyal fonksiyonu set edilmişse önce sinyal fonksiyonu çalıştırılmakta, sinyal fonksiyonunun çalışması bittikten sonra pause fonksiyonu geri dönmektedir. Örneğin: for (;;) pause(); Burada aslında programcının arzu ettiği sinyal oluştukça iş yapan, diğer durumlarda uykuda bekleyen bir akış söz konusudur. Tabii böylesi bir programı sonlandırmanın bir yolu sinyal fonksiyonu set edilmemiş bir sinyali prosese göndermek olabilir. Aşağıdaki örnekte proses SIGUSR1 sinyalini işlemiş ve sonsuz döngüde pause ile beklemiştir. Burada başka bir terminalden prosese aşağıdaki gibi SIGUSR1 sinyallerini gönderebilirsiniz: $ kill -USR1 26969 $ kill -USR1 26969 $ kill -USR1 26969 $ kill -USR1 26969 $ kill -USR1 26969 $ kill -USR1 26969 $ kill -USR1 26969 $ kill -USR1 26969 $ kill -USR1 26969 $ kill 26969 Tabii buradaki proses id programın her çalıştırılmasında farklı olabilecektir. Bunun için ps -u komutundan faydalanabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void sigusr1_handler(int sno); void exit_sys(const char *msg); int main(void) { struct sigaction sa; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); for(;;) pause(); return 0; } void sigusr1_handler(int sno) { printf("SIGUSR1 handler running...\n"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Sinyaller ilk UNIX sistemlerinden beri var olan bir kavramdır. Ancak thread'ler 90'ların ortalarında işletim sistemlerinde yaygınlaşmaya başlamıştır. Dolayısıyla sinyaller eskiden yalnızca proseslerle ilgili bir kavramdı. Yani sinyaller proseslere gönderiliyordu. Zaten eskiden proseslerin tek bir akışı (yani thread'i) vardı. Ancak 90'lı yılların ortalarına doğru thread'ler işletim sistemlerine eklenince sinyaller konusu üzerinde thread'lere yönelik bazı revizyonlar yapılmıştır. Daha önceden de belirttiğimiz gibi sinyaller kill fonksiyonu ile ya da diğer kaynaklar yolu ile proseslere gönderilmektedir. Prosesin hangi thread'inin, sinyal fonksiyonunu çalıştıracağı POSIX standartlarında işletim sisteminin isteğine bırakılmıştır. Prosesin bir thread'i kendi prosesinin başka bir thread'ine sinyal gönderebilir. (Tabii bir proses başka bir prosesin spesifik bir thread'ine sinyal gönderememektedir.) Bir thread'e sinyal göndermek demek aslında sinyal fonksiyonunun o thread tarafından çalıştırılmasını sağlamak demektir. Bunun için pthread_kill fonksiyonu kullanılmaktadır. #include int pthread_kill(pthread_t thread, int sig); Fonksiyonun birinci parametresi thread'in id değerini, ikinci parametresi ise gönderilecek sinyalin numarasını almaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda ise errno değerine geri dönmektedir. Tabii ilgili sinyal için sinyal fonksiyonu set edilmemişse yalnızca ilgili thread değil, tüm proses sonlandırılmaktadır. Aşağıdaki örnekte prosesin ana thread'i, diğer thread'e pthread_kill fonksiyonu ile SIGUSR1 sinyalini göndermektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void sigusr1_handler(int sig); void *thread_proc(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; struct sigaction sa; int result; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); 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 running: %d\n", i); if (i == 5) if ((result = pthread_kill(tid, SIGUSR1)) != 0) exit_sys_errno("pthread_kill", result); sleep(1); } if ((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void sigusr1_handler(int sig) { printf("SIGUSR1 handler\n"); } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("Other thread running: %d\n", i); sleep(1); } return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Belli bir thread de sinyallere bloke edilebilir. UNIX türevi sistemlerde her thread'in de ayrıca bir signal mask kümesi vardır. Belli bir thread'i belli sinyallere bloke etmek için tamamen sigprocmask fonksiyonunun benzeri olan pthread_sigmask fonksiyonu kullanılmaktadır: #include int pthread_sigmask(int how, const sigset_t *set, sigset_t *oset); Fonksiyonun parametrik yapısı tamamen sigprocmask fonksiyonundaki gibidir. Fonksiyon hangi thread tarafından çağrılmışsa o thread'in signal mask kümesi bu işlemden etkilenir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Belli bir thread'in belli sinyallere bloke edilmesinin bazı gerekçeleri söz konusu olabilmektedir. Örneğin thread kesilmemesi gereken işlemler yapıyor olabilir. Bu durumda thread sinyallere bloke edilebilir. Tabii bir thread bir sinyale bloke edildiğinde artık o sinyal oluştuğunda, sinyal fonksiyonu prosesin diğer thread'lerinden biri tarafından çalıştırılacaktır. Bazen programcılar ilgili sinyalin prosesin belli bir thread'i tarafından işlenmesini sağlamak için diğer thread'leri ilgili sinyale bloke edip yalnızca tek bir thread'de bu bloke işlemini yapmazlar. Sinyal oluştuğunda işletim sistemi de mecburen sinyal fonksiyonunu o thread ile çağırır. Tabii prosesin tüm thread'leri ilgili sinyale bloke edilirse bu durum sanki prosesin o sinyale bloke edilmesi gibi bir etki oluşturacaktır. UNIX/Linux sistemlerinde bir thread yaratıldığında thread'in signal mask kümesi onu yaratan thread'ten (üst thread'ten) aktarılmaktadır. Yani örneğin bu sistemlerde biz prosesin ana thread'inde bir sinyali bloke edersek bu durumda bu sinyal ana thread tarafından yaratılan tüm thread'lerde bloke edilmiş olacaktır. Benzer biçimde fork işlemi sırasında da yaratılan alt prosesin thread'inin signal mask kümesi onu yaratan üst prosesten aktarılacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 76. Ders 27/08/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Seyrek olarak programcı kritik birtakım işlemler yaparken sinyalleri pthread_sigmask fonksiyonuyla bloke edip sonra açarak pause ile bekleyebilmektedir. Örneğin: siprocmask(); siprocmask(); ---> Problem var! pause(); Programcı sinyalleri açıp pause beklemesini yapmak istediği sırada sinyal prosese gönderilirse henüz akış pause fonksiyona girmeden sinyal teslim edilebilir. Daha sonra akış pause fonksiyonuna girdiğinde buradan çıkılamayacaktır. Çünkü sinyali gönderen kişi pause ile bekleyenin yoluna devam edebilmesi için sinyali göndermiştir. Bu problem atomik bir biçimde sinyalleri açarak pause yapan bir fonksiyonla çözülebilir. İşte sigsuspend fonksiyonu bunu yapmaktadır. sigsuspend fonksiyonunun prototipi şöyledir: #include int sigsuspend(const sigset_t *sigmask); Fonksiyon yeni uygulanacak signal mask kümesini parametre olarak alır. Bu kümeyi fonksiyonu çağıran thread'in (prosesin değil) signal mask kümesi yapar ve atomik bir biçimde pause işleminde bekler. Böylece yukarıda bahsetmiş olduğumuz blokeyi açarak pause fonksiyonunda bekleme atomik hale getirilmiş olmaktadır. Tabii sinyal gelip fonksiyondan çıkıldığında, thread'in eski sinyal kümesi yeniden set edilmektedir. POSIX standartları sigsuspend fonksiyonunun yalnızca ilgili thread'in signal mask kümesini etkilediğini belirtmektedir. Eskiden thread'ler yokken bu fonksiyon prosesin signal mask kümesini etkiliyordu. Tabii thread'lerin olmadığı devir ile tek thread'li uygulamalar aynı anlama gelmektedir. Yani başka bir deyişle fonksiyonu biz thread'siz bir programda çağırdığımızda fonksiyon eski zamanlardaki gibi prosese özgü işlem yapıyor gibi olacaktır. Ancak mevcut standartlarda sigsuspend fonksiyonu yalnızca fonksiyonun çağrıldığı thread'in signal mask kümesi üzerinde etkili olmaktadır. sigsuspend fonksiyonu eğer proses ilgili sinyal için bir sinyal fonksiyonu set etmemişse proses sonlanacağı için hiç geri dönmeyebilir. Eğer proses bir sinyal fonksiyonu set etmişse bu durumda fonksiyon -1 ile geri döner ve errno değişkeni EINTR değeri ile set edilir. Fonksiyonun geri dönüş değerinin kontrol edilmesine gerek yoktur. O halde sigsuspend fonksiyonunun eşdeğeri aşağıdaki gibidir: pthread_sigmask(SIG_SETMASK, &sset, &oldsset); pause(); pthread_sigmask(SIG_SETMASK, &oldsset, NULL); Tabii sigsuspend fonksiyonu bütün bunları kendi içerisinde atomik bir biçimde yapmaktadır. sigsuspend fonksiyonunun kullanımının neden gerektiği programcılar tarafından zor anlaşılmaktadır. Çünkü bu fonksiyona oldukça seyrek ihtiyaç duyulur. Konu ile ilgili soru ve cevaplar genellikle şunlardır: SORU: sigsuspend fonksiyonuna gereksinim duyulan bir senaryo nasıldır? YANIT: Tipik bir senaryo şöyledir: Programcı başka bir prosesten bir sinyal beklemektedir. Ancak o sinyal geldiğinde koduna devam etmek istemektedir. Ancak bu sinyali beklemeden önce sinyalleri bloke ederek bazı işlemler yapmak isteyebilir. Dolayısıyla bu süre zarfında o sinyal oluşursa pending durumda kalacaktır. Programcı sonra sinyalleri açarak pause işleminde diğer prosesin sinyalini beklemek ister. Ancak daha önce sinyal oluşmuş da olabilir. Bu durumda sinyali açarak pause işlemi uygularken sinyali açtığı noktada akış henüz pause fonksiyonuna gelmeden sinyal prosese teslim edilirse akış pause fonksiyonuna geldiğinde sonsuz bir bekleme oluşmaktadır. İşte sinyallerin açılarak pause beklenmesinin atomik bir biçimde yapılması gerekmektedir. SORU: sigsuspend öncesinde sinyallerin bloke edilmesinin nedeni nedir? Çünkü sigsuspend sinyalleri açarak pause uygulamak için kullanılmaktadır. YANIT: Bunun çeşitli nedenleri olabilir. Ancak başka bir prosesten sinyal beklerken birtakım başka şeylerin yapılması gerekebilmektedir. Bu durumda diğer proses sinyal gönderirse sinyalin boşa gitmemesi için programcı en azından ilgili sinyali bloke ederek sinyal gelmişse bile onun pending durumda kalmasını sağlayabilir. SORU: Zaten bir prosesin, bir işi yapmasını beklemek için senkronizasyon nesneleri kullanılmıyor mu? Neden ayrıca bir sinyal yoluyla böyle bir bekleme yapılmak istensin? Yani örneğin prosesler arasında kullanılan bir durum değişkeni nesnesi ile de aynı şeyler yapılamaz mı? YANIT: Evet yapılabilir. Ancak bu tür durumlarda sinyal kullanımı oldukça pratiktir. Yani sinyaller bir anlamda proseslerarası haberleşme yöntemlerinden biri gibi düşünülebilir. Aşağıda sigsuspend fonksiyonunun kullanımına bir örnek verilmiştir. Örnekte proses SIGSUR1 sinyalini bloke ederek birtakım işlemler yapmaktadır. (Örneğimizde bu işlemler 30 kere, saniyede bir ekrana bir şeyler yazdırılmasıdır.) Bu sırada prosese sinyal gelse bile sinyal prosese iletilmeyecek ve askıda (pending) kalacaktır. Sonra program bu sinyalleri açarak pause işleminde beklemek istemiştir. Bunun nedeni başka bir prosesin bir işi yapmasını beklemek olabilir. Yani başka bir proses bir işi bitirince bizim programımız yoluna devam edecektir. Burada diğer proses ilgili sinyali (SIGUSR1) 30 saniyelik işlemler sırasında göndermiş olabileceği gibi daha sonra da gönderecek olabilir. Program sinyalleri açarak pause işleminde beklemek istediğinde sinyalleri açar açmaz askıda olan sinyal prosese iletilebilir ve henüz akış pause fonksiyonuna gelmeden sinyal boşa gidebilir. Bu durumda akış pause fonksiyonuna geldiğinde zaten sinyal oluştuğu için ve bir daha da bu sinyal gönderilmeyeceği için sonsuz bir bekleme durumu oluşabilir. Buradaki sigsuspend çağrısının amacı sinyallerin blokesinin çözülmesi ile pause arasında açık bir pencerenin kapatılmasını sağlamaktır. Aşağıdaki örneği çalıştırdıktan sonra başka bir terminalden prosese kill komutu ile SIGUSR1 sinyalini gönderip durumu inceleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void sigusr1_handler(int sno); void exit_sys(const char *msg); int main(void) { struct sigaction sa; sigset_t sset, oldsset; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); sigfillset(&sset); if (sigprocmask(SIG_BLOCK, &sset, &oldsset) == -1) exit_sys("sigprocmask"); for (int i = 0; i < 30; ++i) { printf("%d\n", i); sleep(1); } sigsuspend(&oldsset); printf("Ok\n"); return 0; } void sigusr1_handler(int sno) { printf("SIGUSR1 handler running...\n"); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Biz bir fonksiyonun içerisindeyken bir sinyal oluşsa ve sinyal fonksiyonumuz çalıştırılsa ve sinyal fonksiyonumuz da aynı fonksiyonu çağırsa ne olur? Bu tür durumlarda iç içe çağırma söz konusu olacaktır. Bu durum thread güvenlilik durumuna benzemektedir. POSIX standartları bu biçimde sinyal dolayısıyla iç içe çağrılabilecek fonksiyonları "asenkron sinyal güvenli fonksiyonlar (async-signal-safe functions)" ismiyle belirtmektedir. POSIX standartlarında tüm asenkron sinyal güvenli fonksiyonların listesi "System Interfaces/General Information/Signal Concepts" başlığında listelenmektedir. Bu listede belirtilen asenkron sinyal güvenli fonksiyonlar gönül rahatlığı ile sinyal fonksiyonlarında ve dışarıda aynı anda kullanılabilirler. Pekiyi daha önce görmüş olduğumuz thread güvenli sonu _r ile biten fonksiyonlar da sinyal güvenli midir? Genel olarak asenkron sinyal güvenli fonksiyonlar, thread güvenli fonksiyonlardan biraz daha katı bir güvenliliğe sahiptir. Çünkü asenkron sinyal güvenli fonksiyonlar aynı thread akışı tarafından iç içe çağrılabilecek fonksiyonlardır. Bu nedenle genel olarak asenkron sinyal güvenli fonksiyonlar thread güvenli olma eğilimindeyken; asenkron thread güvenli fonksiyonlar, asenkron sinyal güvenli olmayabilmektedir. Aslında bir fonksiyon thread güvenli olduğu halde sinyal güvenli olmayabilir, sinyal güvenli olduğu halde thread güvenli olmayabilir. Örneğin bir fonksiyon TSD (Thread Specific Data) kullanılarak thread güvenli hale getirilmiş olabilir. Ancak sinyaller iç içe aynı thread tarafından işletilebileceği için bu fonksiyon sinyal güvenli olmayabilir. Yukarıda da belirttiğimiz gibi her zaman olmasa da çoğu zaman sinyal güvenli fonksiyonlar aynı zamanda thread güvenli olma eğilimindedir. Fonksiyonun asenkron sinyal güvenli (async-signal-safe) olması genel olarak "reentrant" olması anlamına gelmektedir. Reentrant terimi sinyal yoluyla ya da başka yolla (örneğin kesme yoluyla) bir fonksiyonunun iç içe çalışabilirliği anlamına gelmektedir. Dolayısıyla reentrant fonksiyonlar aynı zamanda asenkron sinyal güvenli fonksiyonlardır. Ancak POSIX standartlarında "reentrant" terimi kullanılmamıştır. Reentrant ve thread safe kavramları arasındaki benzerlikler ve farklılıklar için Wikipedia'daki aşağıdaki sayfayı gözden geçirebilirsiniz: https://en.wikipedia.org/wiki/Reentrancy_(computing) Biz örneklerimizde sinyal fonksiyonu içerisinde printf fonksiyonunu kullanmıştık. POSIX standartlarına göre C'nin tüm dosya fonksiyonları thread güvenli olduğu halde sinyal güvenli değildir. Biz örneklerimizde kullanmış olsak da printf gibi fonksiyonlarını sinyal fonksiyonlarından kullanmayınız. printf fonksiyonunun thread güvenli (thread safe) olduğu halde asenkron sinyal güvenli olmadığına dikkat ediniz. Yani printf fonksiyonunu yazanlar onun farklı thread'lerden çağrılmasına karşı önlemler almışlardır ancak onun aynı thread tarafından çağrılmasına karşı herhangi bir önlem almamışlardır. Başka bir deyişle örneğin printf fonksiyonu thread güvenli olduğu halde "reentrant" değildir. Tabii bu yalnızca printf fonksiyonu için değil, C'nin tampon kullanan tüm fonksiyonları için geçerlidir. Pekiyi biz sinyal fonksiyonları içerisinde ekrana bir şeyler yazdırmak istesek bunu nasıl sağlayabiliriz? Burada ilk akla gelecek yöntem yazdırılacak şeyleri sprintf ile bir tampona yazdırıp, onu write fonksiyonu ile 1 numaralı betimleyiciyi kullanarak ekrana yazdırmaktır. Biz sonraki örneklerde printf fonksiyonunun aslında sinyal güvenli olmadığını yalnızca örneklerimizde pratiklik sağladığı için kullandığımızı belirtmek amacıyla onun yanına bir UNSAFE notu da ekleyeceğiz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sinyal fonksiyonlarını yazarken dikkat edilmesi gereken diğer bir nokta da "errno" sorunudur. Sinyal fonksiyonu içerisinde errno değişkeninin değerini değiştirebilecek bir fonksiyon çağrısı varsa bu durum sorunlara yol açabilmektedir. (Anımsanacağı gibi bir POSIX fonksiyonu başarılı olduğu halde errno değişkenini yine de set edebilmektedir.) Buradaki sorunun kaynağını şöyle bir örnekle açıklayabiliriz: if (some_posix_func() == -1) { // Bizim exit_sys fonksiyonumuz zaten bunu yapıyor ---> Bu sırada sinyal gelirse ne olur? perror("some_posix_func"); exit(EXIT_FAILURE); } Burada ok ile belirtilen noktada bir sinyal gelirse, sinyal fonksiyonu errno değişkenini değiştirebileceği için perror ile rapor edilen mesaj da yanlış bir mesaj olabilecektir. Pekiyi bu sorunu nasıl giderebiliriz? İşte tipik olarak yapılması gereken şey sinyal fonksiyonunun başında errno değişkenini geçici olarak saklayıp sinyal fonksiyonunun sonunda yeniden set etmektir. Örneğin: void signal_handler(int sig) { int errno_temp = errno; // birtakım işlemler errno = errno_temp; } Aşağıdaki örnekte yukarıda açıkladığımız problem simüle edilmiştir. Programda önce SIGUSR1 sinyali sigaction fonksiyonu ile set edilmiştir. Sonra olmayan bir dosya açılmak istenmiştir. Olmayan bir dosya open ile açılmak istendiğinde errno değişkeninin ENOENT değeri ile ("No such file or directory") set edilmesi gerekir. Ancak o arada bir sinyal oluşup da sinyal fonksiyonu çalıştırıldığında sinyal fonksiyonu da errno değişkenini değiştirirse sorun ortaya çıkacaktır. Biz örneğimizde raise fonksiyonu ile bu durumu yapay bir biçimde sağlamaya çalıştık: if ((fd = open("file_not_found", O_RDONLY)) == -1) { raise(SIGUSR1); // only for example exit_sys("open"); } Sinyal fonksiyonun aşağıdaki gibi olduğunu varsayalım: void sigusr1_handler(int sno) { printf("SIGUSR1 handler running...\n"); // UNSAFE kill(1, SIGKILL); // kill will fail } Burada kill başarısız olacak ve errno değişkenini EPERM ("Operation not permitted") değeri ile set edecektir. Bu durumda exit_sys fonksiyonu yanlış mesajı rapor edecektir. Bu durumu düzeltmek için sinyal fonksiyonu şöyle düzeltilmelidir: void sigusr1_handler(int sno) { int errno_temp = errno; printf("SIGUSR1 handler running...\n"); // UNSAFE kill(1, SIGKILL); // kill will fail errno = errno_temp; } Sinyal fonksiyonunun düzeltilmiş biçimine ilişkin örnek program aşağıda verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void sigusr1_handler(int sno); void exit_sys(const char *msg); int main(void) { struct sigaction sa; int fd; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); if ((fd = open("file_not_found", O_RDONLY)) == -1) { raise(SIGUSR1); /* only for example */ exit_sys("open"); } return 0; } void sigusr1_handler(int sno) { int errno_temp = errno; printf("SIGUSR1 handler running...\n"); /* UNSAFE */ kill(1, SIGKILL); /* kill will fail */ errno = errno_temp; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bir sinyal fonksiyonu yazılırken global bir değişkenin kullanılması gerekebilmektedir. Bu durumda tıpkı çok thread'li uygulamalarda olduğu gibi bu global değişkenin kararsız durumda kalması engellenmelidir. Tabii bu tür durumlarda nesneyi mutex gibi mekanizmalarla korumak uygun değildir. Çünkü bu mekanizmalar farklı thread'ler tarafından erişimde koruma amaçlı oluşturulmuştur. Eğer bir sinyal fonksiyonu global bir değişkeni kullanacaksa onu atomik bir biçimde kullanmalıdır. Örneğin: int g_count; ... void signal_handler(int sig) { ... ++g_count; ---> Bu artırma tek bir makine komutu ile yapılmak zorunda değil ... } Bu örnekte akış ++g_count işleminde kesilip, yeniden aynı fonksiyon çalıştırılırsa g_count değeri uygun biçimde artırılamayabilecektir. İşte bu tür durumlarda mutex gibi nesneler bize bir koruma sağlamamaktadır. Burada yapılması gereken bu artırımın atomik bir biçimde yani tek bir makine komutuyla yapılmasıdır. İşte C99 ile birlikte C'ye sig_atomic_t türü de eklenmiştir. Bu türden global bir nesne tanımlandığında, bu nesne üzerinde atama işlemleri atomik bir biçimde yapılmaktadır. Ancak ++, -- gibi operatör işlemlerinin atomik yapılmasının bir garantisi yoktur. Thread'ler konusunda gördüğümüz gcc ve clang derleyicilerinin built-in fonksiyonlarını bu amaçla kullanabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir prosese SIGSTOP isimli gönderilirse proses durdurulur ve SIGCONT sinyali gönderilene kadar suspend biçimde kalır. SIGSTOP sinyali tıpkı SIGKILL sinyalinde olduğu gibi bloke edilememekte ve ignore edilememektedir. Ayrıca SIGSTOP sinyali için sinyal fonksiyonu da set edilememektedir. SIGCONT sinyali için ise sinyal fonksiyonu set edilebilir ve bu sinyal bloke edilebilir. SIGCONT sinyali bloke edilmiş olsa bile ya da SIGCONT sinyali için sinyal fonksiyonu set edilmiş olsa bile durdurulmuş proses yine çalışmaya devam ettirilmektedir. Aslında prosesi durduran tek sinyal SIGSTOP sinyali değildir. Terminal ile ilgili SIGTTIN ve SIGTTOU sinyalleri de prosesin durdurulmasına yol açabilmektedir. Biz bunlara "stop sinyalleri (stop signals)" diyeceğiz. Stop sinyalleriyle durdurulmuş bir prosese başka bir sinyal gönderilse bile proses bunu işlemez. Bu durumda sinyal askıda (pending durumda) kalır. Askıdaki sinyaller prosese SIGCONT sinyali gönderilip proses yeniden çalıştırıldıktan sonra (resume edildikten sonra) prosese teslim edilmektedir. Ancak SIGCONT sinyali sonrasında askında stop sinyalleri prosese teslim edilmez, doğrudan atılır. Benzer biçimde prosese bir stop sinyali teslim edildiğinde eğer o anda askıda olan SIGCONT sinyali varsa bu sinyal doğrudan atılmaktadır. Durdurulmuş bir proses her zaman SIGKILL sinyali ile sonlandırılabilmektedir. SIGKILL sinyali ile sonlandırma için prosesin SIGCONT sinyali ile çalışmaya devam ettirilmesine gerek yoktur. Aşağıdaki programı bir terminalden çalıştırıp diğer bir terminalden ona kill komutuyla SIGSTOP ve SIGCONT sinyallerini göndererek durumu gözlemleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include int main(void) { for (int i = 0; i < 60; ++i) { printf("%d\n", i); sleep(1); } return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Stop edilmiş prosesler ps komutunda Status bilgisi T ile gösterilmektedir. Örneğin: $ ps -u USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND kaan 24891 0.0 0.1 14176 5628 pts/1 Ss+ 01:01 0:00 /usr/bin/bash --init-file /us kaan 25747 0.0 0.1 14292 5680 pts/2 Ss+ 01:05 0:00 bash kaan 31684 0.0 0.1 14292 5776 pts/0 Ss 10:45 0:00 bash kaan 32827 0.0 0.0 2772 944 pts/2 T 12:57 0:00 ./mample kaan 32831 0.0 0.0 15428 1568 pts/0 R+ 12:58 0:00 ps -u ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Klavyeden Ctrl+Z tuşuna basıldığında terminal aygıt sürücüsü oturumun (session) ön plan (foreground) proses grubuna SIGSTOP sinyali göndermektedir. Yani biz klavyeden Ctrl+Z tuşlarına basarak da o anda çalışmakta olan programa SIGSTOP sinyalini göndertebilmekteyiz. Bu biçimde kabuk üzerinden durdurulan prosesler "fg (foreground)" kabuk komutuyla kaldığı yerden çalışmaya devam ettirilebilmektedir. Tabii "fg" kabuk komutu aslında ilgili prosese (ya da proseslere) SIGCONT sinyalini göndermektedir. Biz kabuk üzerinden Ctrl+Z tuşuna basarak bir prosesi durdurduğumuzda ona bir numara verilmektedir. fg komutunda o numara kullanılmalıdır. Örneğin: $ ./mample 0 1 2 3 4 ^Z [3]+ Durdu ./mample $ fg 3 ./mample 5 6 7 8 9 10 11 12 ... Eğer "fg" komutu argümansız kullanılırsa en son stop ettirilen proses çalıştırılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz komut satırında klavyeden Ctrl+C ya da Ctrl+Z tuşlarına bastığımızda SIGINT sinyali ve SIGSTOP sinyali tek bir prosese değil ön plan proses grubundaki proseslerin hepsine gönderilmektedir. Proses grupları ve oturum (session) kavramı sonraki paragraflarda ele alınacaktır. Ancak burada temel bir açıklamayı da yapmak istiyoruz. Biz programımızda fork işlemi yaptığımızda bizim üst prosesimiz ve alt prosesimiz aynı proses grubu içerisinde bulunur. Bu durumda Ctrl+C tuşlarına bastığımızda SIGINT sinyali bu üst prosese de alt prosese de gönderilecek ve iki proses de sonlandırılacaktır (Tabii bu prosesler SIGINT sinyalini set etmemişlerse). Aynı durum Ctrl+Z tuşlarına basıldığında da söz konusu olmaktadır. Ctrl+Z tuşlarına basıldığında SIGSTOP sinyali hem üst prosese hem de alt prosese gönderilecektir. Benzer biçimde "fg" komutunu uyguladığımızda SIGCONT sinyali de hem üst prosese hem de alt prosese gönderilmektedir. Aşağıdaki programı çalıştırınız ve sonra Ctrl+C tuşlarına basınız. Hem üst prosesin hem de alt prosesin sonlandığını göreceksiniz. Sonra programı yeniden çalıştırıp bu kez de Ctrl+Z tuşlarına basınız. Hem üst prosesin hem de alt prosesin durdurulduğunu göreceksiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #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) { for (int i = 0; i < 60; ++i) { printf("Parent: %d\n", i); sleep(1); } if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); } else { for (int i = 0; i < 60; ++i) { printf("Child: %d\n", i); sleep(1); } _exit(0); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- wait fonksiyonu ile alt proses beklenirken, wait fonksiyonu bu beklemeden ancak alt proses sonlandığında çıkabilmektedir. Alt prosesin sonlanması da iki biçimde olabilmektedir: Normal olarak exit ya da _exit fonksiyonlarının çağrılmasıyla ve bir sinyal dolayısıyla. Anımsanacağı gibi biz wait fonksiyonunun status parametresini WIFEXITED ve WIFSIGNALED makrolarına sokarak bu durumu anlayabiliyorduk. Zaten anımsanacağı gibi prosesin exit kodunun oluşabilmesi için onun normal biçimde sonlanmış olması gerekiyordu. Fakat ayrıca bir de waitpid fonksiyonu vardı. İşte bu fonksiyonun üçüncü parametresine WUNTRACED ve/veya WCONTINUED bayrakları geçilirse bu durumda waitpid fonksiyonu alt proses durdurulduğunda ve yeniden çalıştırıldığında sonlanacaktır. Bu durumda status parametresindeki değer WIFSTOPPED ve WIFCONTINUED makrolarına sokularak bu durum anlaşılabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir prosesi terminal yoluyla sonlandırmanın Ctrl+C tuşlarının dışında diğer bir yolu da Ctrl+\ tuşlarını (ya da bazı sistemlerde bunun yerine Ctrl+Backspace tuşları da kullanılabilmektedir) kullanmaktır. Bu durumda terminal aygıt sürücüsü oturumun ön plan proses grubuna SIGQUIT isimli bir sinyal göndermektedir. SIGQUIT sinyali prosese gönderildiğinde eğer proses bu sinyali ele almamışsa, proses sonlandırılır ve bir core dosyası oluşturulur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 77. Ders 02/09/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- abort aynı zamanda bir standart C fonksiyonudur. abort fonksiyonu "abnormal" bir sonlandırma için kullanılmaktadır. Pekiyi programcı exit yerine neden programını abort fonksiyonuyla sonlandırmak istesin? İşte bazen içinde bulunulan duruma bağlı olarak normal sonlandırma bile yapılamayabilir. Yani exit fonksiyonunun başarılı bir biçimde işlem yapması bile içinde bulunulan duruma göre mümkün olmayabilir. abort fonksiyonu UNIX/Linux sistemlerinde aslında kendi prosesi üzerinde SIGABRT sinyalinin oluşmasına yol açmaktadır. SIGABRT sinyalinin default eylemi de prosesin sonlandırılması ve core dosyasının oluşturulmasıdır. Yani abort fonksiyonu bir core dosyasının oluşumuna yol açtığı için abnormal sonlanmanın debugger altında incelenmesine de olanak sağlamaktadır. SIGABRT sinyali için sinyal fonksiyonu set edilmiş olsa bile sinyal fonksiyonunun çalışması bittikten sonra yine de proses sonlandırılmaktadır. Ayrıca SIGABRT sinyali bloke edilmişse ve ignore edilmişse bile proses yine de sonlandırılmaktadır. SIGABRT sinyali ile prosesin sonlandırılmasının engellenmesinin tek yolu sinyal fonksiyonu içerisinde "long jump" işlemi uygulamaktır. "long jump" konusu izleyen paragraflarda ele alınacaktır. abort fonksiyonunun prototipi şöyledir: #include void abort(void); Aşağıdaki örnekte SIGABRT sinyali için bir sinyal fonksiyonu set edilmiştir. Ancak proses yine de sonlanacak ve core dosyası oluşturulacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void sigabrt_handler(int sig); void exit_sys(const char *msg); int main(void) { struct sigaction sa; sa.sa_handler = sigabrt_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGABRT, &sa, NULL) == -1) exit_sys("sigaction"); for (int i = 0; i < 60; ++i) { printf("%d\n", i); sleep(1); if (i == 5) abort(); } return 0; } void sigabrt_handler(int sig) { printf("SIGABRT handler\n"); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde birkaç sinyal, işlemci tarafından oluşturulan içsel kesmelerle işletim sistemi tarafından prosese gönderilmektedir. Bunların en çok karşılaşılanı SIGSEGV isimli sinyaldir. Bu sinyal tahsis edilmemiş bir bellek bölgesine erişildiğinde işletim sistemi tarafından prosese gönderilmektedir. Bu sinyalin default eylemi prosesin sonlandırılmasıdır. Bu sinyal bloke edilemez ve ignore edilemez. Programcı nadiren bu sinyalde prosesinin sonlandırılmasını istemeyebilir. Bunun tek yolu "long jump" uygulamaktır. Linux sistemlerinde SIGSEGV sinyali için bir sinyal fonksiyonu set edildiyse sinyal oluştuğunda bu sinyal fonksiyonu çalıştırılır. Ancak sinyal fonksiyonunda proses, exit ya da _exit fonksiyonuyla sonlandırılmamışsa aynı sinyal yeniden oluşturulmaktadır. Bazı UNIX türevi sistemlerde sinyal fonksiyonu sonlandığında proses işletim sistemi tarafından sonlandırılmaktadır. (Bu sinyalin oluşmasına yol açan makine komutlarına Intel işlemcilerinde "fault" denilmektedir. Bu tür fault işlemlerinde sinyal fonksiyonu geri döndüğünde fault'a yol açan makine komutuyla akış devam ettirilir. Böylece yeniden fault oluşmaktadır.) Aşağıdaki örnekte tahsis edilmemiş bir bellek alanına erişilmesinden dolayı SIGSEGV sinyali oluşturulmuştur. Bu sinyal için sinyal fonksiyonu set edilmiş ancak sinyal fonksiyonu içerisinde exit fonksiyonuyla proses sonlandırılmıştır. (Proses exit fonksiyonu ile sonlandırılmasaydı aynı sinyal oluşturulmaya devam edecekti.) ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void sigsegv_handler(int sig); void exit_sys(const char *msg); int main(void) { struct sigaction sa; char *str = (char *)0x12345678; sa.sa_handler = sigsegv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGSEGV, &sa, NULL) == -1) exit_sys("sigaction"); *str = 'x'; printf("unreachable code!...\n"); return 0; } void sigsegv_handler(int sig) { printf("SIGSEGV handler\n"); /* UNSAFE */ _exit(EXIT_FAILURE); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- SIGSEGV sinyali ile aynı biçimde ele alınan diğer sinyaller SIGBUS, SIGEMT, SIGFPE, SIGILL, SIGTRAP sinyalleridir. SIGFPE sinyali "floating point birimi (FPU)" tarafından oluşturulmaktadır. SIGILL sinyali user mode'da çalışan prosesin özel makine komutlarını kullanmasından dolayı oluşmaktadır (illegal instruction). Bu sinyallerin davranışı SIGSEGV sinyalinde olduğu gibidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Diğer önemli bir sinyal de SIGCHLD isimli sinyaldir. Aslında biz bu sinyalden proses yaratımı kısmında bahsetmiştik. UNIX/Linux sistemlerinde bir alt proses sonlanırken üst prosese SIGCHLD sinyalini göndermektedir. Eskiden bu sinyalin ismi SIGCLD biçimindeydi. Linux sistemlerinde her iki sinyal eşdeğerdir ve aynı numaraya sahiptir. Ancak POSIX standartlarında SIGCLD sinyali yoktur. Anımsanacağı gibi wait fonksiyonları alt prosesi bekleyerek onu zombie olmaktan kurtarıyordu. İşte zombie proses oluşumunun engellenmesinin bir yolu da SIGCHLD sinyalini kullanmaktır. Bu yöntemde üst proses alt prosesleri yaratmadan önce SIGCHLD sinyalini set eder. Böylece alt prosesler sonlandığında programcının set etmiş olduğu sinyal fonksiyonu çalışır. İşte bu sinyal fonksiyonu içerisinde programcı wait fonksiyonlarını uygular. SIGCHLD sinyalinin default eylemi sinyalin "ignore" edilmesidir. Yani biz bu sinyal için herhangi bir şey yapmamışsak alt proses bittiğinde yine bu sinyal oluşur. Ancak işletim sistemi tarafından sinyal "ignore" edilir. Zombie proses oluşumunun, wait fonksiyonlarıyla beklemeden engellenmesi için yukarıda da belirttiğimiz gibi wait fonksiyonlarının SIGCHLD sinyalinde uygulanması gerekmektedir. Ancak bu uygulama sırasında dikkat edilmesi gereken bir nokta vardır. SIGCHLD sinyali geldiğinde sinyal fonksiyonu çalıştırılırken birden fazla alt proses de o sırada sonlanmış olabilir. Sinyaller biriktirilmediği için SIGCHLD için set edilen sinyal fonksiyonundan çıkıldığında askıda (pending) olan SIGCHLD sinyali prosese yalnızca bir kez gönderilecektir. Halbuki birden fazla alt proses o sırada sonlanmış olabilmektedir. İşte bu nedenle SIGCHLD sinyal fonksiyonunda wait işlemi bir kez değil, bir döngü içerisinde uygulanmalıdır. Tabii wait işleminin blokeye yol açmaması için waitpid fonksiyonu son parametresinde WNOHANG değeriyle çağrılmalıdır. Örneğin: while (waitpid(-1, NULL, WNOHANG) > 0) ; Anımsanacağı gibi waitpid fonksiyonu eğer beklenecek herhangi bir alt proses yoksa -1 değerine, WNOHANG değeri geçildiğinde ancak henüz bir alt proses sonlanmamışsa 0 değerine geri dönmektedir. Yukarıdaki döngüde sonlanan bütün altprosesler beklenmiş olacaktır. Tabii bu sırada errno değişkeni değer değiştirebileceği için fonksiyonun başında alınıp çıkışta yeniden set edilmesi uygun olur. Örneğin: void sigchld_handler(int sig) { int errno_temp = errno; while (waitpid(-1, NULL, WNOHANG) > 0) ; errno = errno_temp; } waitpid fonksiyonu aslında başka nedenlerle de başarısız olabilir. Gerçi bu durumlar programcı her şeyi doğru yapmışsa söz konusu olmamaktadır. Ancak bazı programcılar waitpid başarısız olmuşsa ve başarısızlık nedeni beklenecek alt prosesin olmayışından dolayı değilse bir hata rapor edebilmektedir. Bu durumda sinyal fonksiyonu şöyle de oluşturulabilmektedir: void sigchld_handler(int sig) { int errno_temp = errno; int result; while ((result = waitpid(-1, NULL, WNOHANG)) > 0) ; if (result == -1 && errno != ECHILD) exit_sys("waitpid"); errno = errno_temp; } Aşağıdaki örnekte üst proses 10 tane alt proses yaratmıştır. Bu alt proseslerin zombie oluşturmasını, SIGCHLD sinyal fonksiyonunda yukarıda belirtildiği gibi engellemiştir. Bu programı çalıştırıp başka bir terminalden "ps -u" komutu ile zombie prosesin oluşmadığını gözlemleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void sigchld_handler(int sig); void exit_sys(const char *msg); int main(void) { struct sigaction sa; pid_t pid; sa.sa_handler = sigchld_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); for (int i = 0; i < 10; ++i) { if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { printf("child process\n"); usleep(rand() % 500000); _exit(EXIT_SUCCESS); } } printf("Press ENTER to continue...\n"); getchar(); return 0; } void sigchld_handler(int sig) { int errno_temp = errno; int result; while ((result = waitpid(-1, NULL, WNOHANG)) > 0) ; if (result == -1 && errno != ECHILD) exit_sys("waitpid"); errno = errno_temp; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Zombie proses oluşumunun otomatik engellenmesinin bir yolu da SIGCHLD sinyalini "ignore" etmektir. Her ne kadar bu sinyalin default eylemi zaten "ignore" ise de bu sinyali açıkça "ignore" etmek başka bir anlama gelmektedir. SIGCHLD sinyali, signal fonksiyonu ile ya da sigaction fonksiyonu ile ignore edilirse bu durumda işletim sistemi alt proses bittiğinde onun kaynaklarını hemen boşaltır. Böylece zombie oluşumu engellenmiş olur. Tabii biz SIGCHLD sinyalini açıkça ignore edersek artık wait fonksiyonlarını uygulayamayız. Ancak bu sinyalin ignore işlemi bazı alt proseslerin yaratılmasından sonra yapılırsa bu durumda bu alt proseslerin otomatik kaynak boşaltımının yapılıp yapılmayacağı işletim sisteminden işletim sistemine farklılık gösterebilmektedir. Linux sistemlerinde daha önce yaratılmış olan alt prosesler için otomatik boşaltım yapılmamaktadır. Ayrıca POSIX standartlarında sigaction fonksiyonunda SA_NOCLDWAIT bayrağı bulundurulmuştur. Bu bayrak yalnızca SIGCHLD sinyali için kullanılabilir. Programcı isterse bu bayrak yoluyla da aynı şeyi yapabilir. Ancak bu bayrak kullanıldığında hala SIGCHLD sinyali için set edilmiş olan sinyal fonksiyonunun çalıştırılıp çalıştırılmayacağı işletim sisteminden işletim sistemine farklılık gösterebilmektedir. Linux sistemlerinde bu durumda sinyal fonksiyonu çalıştırılmaktadır. Aşağıdaki örnekte yine üst proses 10 tane alt proses yaratmıştır. Ancak üst proses bu işlemlerden önce SIGCHLD sinyalini sigaction fonksiyonu ile "ignore" etmiştir. Bu örneği çalıştırıp diğer bir terminalden alt zombie proses oluşmadığını gözlemleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void sigchld_handler(int sig); void exit_sys(const char *msg); int main(void) { struct sigaction sa; pid_t pid; sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); /* zaten bu elemana bakılmayacak, bu satır olmasa da olur */ sa.sa_flags = SA_RESTART; /* zaten bu elemana bakılmayacak, bu satır olmasa da olur */ if (sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); for (int i = 0; i < 10; ++i) { if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { printf("child process\n"); usleep(rand() % 500000); _exit(EXIT_SUCCESS); } } printf("Press ENTER to continue...\n"); getchar(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- SIGUSR1 ve SIGUSR2 sinyalleri tamamen programcılar kendi uygulamalarında kullansınlar diye bulundurulmuştur. Örneğin bu sinyalleri biz proseslerarası haberleşme amacıyla kullanabiliriz. Bu sinyallerin default eylemi prosesin sonlandırılmasıdır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- POSIX'e 90 yılların ortalarında "realtime extensions" başlığı altında "gerçek zamanlı (realtime)" sinyal kavramı da eklenmiştir. Gerçek zamanlı sinyallere ayrı isimler verilmemiştir. Gerçek zamanlı sinyallarin numaraları [SIGRTMIN, SIGRTMAX] değerleri arasındadır. Gerçek zamanlı sinyallerin normal sinyallerden (ilk 32 sinyal numarası normal sinyaller için ayrılmıştır) farkları şunlardır: 1) Gerçek zamanlı sinyaller kuyruklanmaktadır. Yani birden fazla gerçek zamanlı aynı sinyal oluştuğunda kaç tane oluşmuş olduğu tutulur ve o sayıda sinyal prosese teslim edilir. Halbuki normal sinyallerde bir sayaç (biriktirme) mekanizması yoktur. Örneğin biz bir sinyali bloke etmiş olalım. Bu arada o sinyal 10 kere oluşmuş olsun. Sinyalin blokesini açtığımızda bu askıdaki sinyalden yalnızca bir tane prosese teslim edilecektir. Halbuki gerçek zamanlı sinyallerde bu 10 kez oluşmuş sinyal için prosese 10 sinyal teslim edilecektir. 2) Gerçek zamanlı sinyallerde bir bilgi de sinyale iliştirilebilmektedir. Bu bilgi ya int bir değer ya da bir gösterici olur. Gösterici kullanıldığında bu göstericinin gösterdiği yerin hedef proseste anlamlı olması gerekmektedir. Yani bu adresin tipik olarak paylaşılan bir bellek alanındaki (shared memory) bir adres olması gerekir. (Tabii paylaşılan bellek alanları farklı proseslerde farklı adreslere map (ya da attach) edilmiş olabilir. Bu durumda bu tür adreslerin göreli bir biçimde oluşturulması uygun olur.) 3) Gerçek zamanlı sinyallerde bir öncelik ilişkisi (priority) vardır. Birden fazla farklı numaralı gerçek zamanlı sinyal bloke edildiği durumda bloke açılınca bunların oluşma sırası küçük numaradan büyük numaraya göredir. Yani gerçek zamanlı sinyallerde küçük numara yüksek öncelik belirtmektedir. Gerçi Linux kernel kodları incelendiğinde Linux'un da bu tür durumlarda önce düşük numaralı sinyali prosese teslim ettiği görülmektedir. Ancak ne olursa olsun bu UNIX türevi sistemlerde standart bir özellik değildir. Gerçek zamanlı sinyaller kill fonksiyonu ile değil, sigqueue isimli fonksiyonla gönderilmektedir. Eğer bu sinyaller kill ile gönderilirse kuyruklama yapılıp yapılmayacağı işletim sisteminden işletim sistemine değişebilmektedir. Linux gerçek zamanlı sinyaller için bu kuyruklamayı yapmaktadır. Ancak gerçek zamanlı sinyaller kill POSIX fonksiyonu ile değil, sigqueue POSIX fonksiyonuyla gönderilmelidir. Gerçek zamanlı sinyaller set edilirken sigaction fonksiyonu kullanılmak zorundadır. Gerçek zamanlı sinyaller signal fonksiyonu ile set edilememektedir. Bu fonksiyonda sinyal fonksiyonunun adresi artık sigaction yapısının sa_handler elemanına değil, sa_sigaction elemanına yerleştirilmelidir. Tabii fonksiyonun bunu anlaması için flags parametresine de ayrıca SA_SIGINFO eklenmelidir. (Yani başka bir deyişle fonksiyon sa_flags parametresinde SA_SIGINFO değerini gördüğünde artık sinyal fonksiyonu için yapının sa_sigaction elemanına bakar.) Örneğin: struct sigaction sa; ... sa.sa_sigaction = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART|SA_SIGINFO; sigaction yapısının sa_sigaction elemanına girilecek fonksiyonun parametrik yapısı aşağıdaki gibi olmak zorundadır: void signal_handler(int signo, siginfo_t *info, void *context); Aşağıdaki örnekte proc1 programı n tane gerçek zamanlı sinyal gönderir. proc2 ise bunları almaktadır. Programları çalıştırarak kuyruklamanın yapıldığına dikkat ediniz. sigqueue fonksiyonunda iliştirilen sinyal bilgisi siginfo_t yapısının si_value elemanından alınmaktadır. sigqueue fonksiyonuyla set edilen sinyal fonksiyonundaki siginfo_t yapısının diğer elemanlarını ilgili dokümanlardan inceleyıniz. (Örneğin burada sinyali gönderen proses id'si, gerçek kullanıcı id'si, sinyalin neden gönderildiği gibi bilgiler vardır.) ---------------------------------------------------------------------------------------------------------------------------*/ /* proc1.c */ #include #include #include void exit_sys(const char *msg); /* ./prog1 */ int main(int argc, char *argv[]) { int signo; pid_t pid; int count; int i; union sigval val; if (argc != 4) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } signo = (int)strtol(argv[1], NULL, 10); pid = (pid_t)strtol(argv[2], NULL, 10); count = (int)strtol(argv[3], NULL, 10); for (i = 0; i < count; ++i) { val.sival_int = i; if (sigqueue(pid, SIGRTMIN + signo, val) == -1) exit_sys("sigqueue"); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void sigusr1_handler(int sno) { printf("sigusr1 occurred...\n"); /* UNSAFE */ } /* proc2.c */ #include #include #include #include void exit_sys(const char *msg); void sigrt_handler(int signo, siginfo_t *info, void *context); /* ./prog2 */ int main(int argc, char *argv[]) { struct sigaction act; int signo; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } signo = (int)strtol(argv[1], NULL, 10); act.sa_sigaction = sigrt_handler; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO; if (sigaction(SIGRTMIN + signo, &act, NULL) == -1) exit_sys("sigaction"); for(;;) pause(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void sigrt_handler(int signo, siginfo_t *info, void *context) { printf("SIGRTMIN + 0 occurred with %d code\n", info->si_value.sival_int); /* UNSAFE */ } /*-------------------------------------------------------------------------------------------------------------------------- 78. Ders 03/09/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi gerçek zamanlı sinyallere farklı isimler karşı getirilmemiştir. Gerçek zamanlı sinyallerin minimum numaraları SIGRTMIN ve maksimum numaraları SIGRTMAX sembolik sabitleriyle içerisinde define edilmiştir. Kursun yapıldığı Linux sistemlerinde SIGRTMIN değeri 34, SIGRTMAX değeri 64'tür. Aslında Linux'ta gerçek zamanlı sinyallerin numaraları 32'den başlamaktadır. Ancak pthread kütüphanesi bunların ilk iki tanesini kullandığı için SIGRTMIN değeri 34'tür. POSIX standartlarında bir sistemin minimum destekleyeceği gerçek zamanlı sinyal sayısı _POSIX_RTSIG_MAX sembolik sabitiyle belirtilmiştir. Bu sembolik sabit 8'dir. Yani UNIX türevi bir sistem en azından 8 gerçek zamanlı sinyali desteklemelidir. Tabii aslında bu minimum değerdir. Pek çok UNIX türevi sistem bundan daha fazla sayıda gerçek zamanlı sinyali desteklemektedir. Bir sistemde, o sistem tarafından desteklenen gerçek zamanlı sinyallerin sayısı ayrıca içerisinde RTSIG_MAX sembolik sabitiyle belirtilmiştir. Yani RTSIG_MAX aslında _POSIX_RTSIG_MAX sembolik sabitinin ilgili sistemdeki gerçek değerini belirtmektedir. Ancak bu RTSIG_MAX sembolik sabiti aslında define edilmek zorunda değildir. Bu durumda bu değer sysconf fonksiyonuyla argüman olarak _SC_RTSIG_MAX değeri geçirilmek suretiyle elde edilmektedir. (Sistem limitleri biraz karmaşık bir konudur. Zaten bu konu ayrı bir başlık altında ileride ele alınacaktır.) Linux sistemlerinde dosyası içerisinde RTSIG_MAX değeri 32 olarak define edilmiştir. Yukarıda da belirttiğimiz gibi gerçek zamanlı sinyaller kuyruklanmaktadır. Yani bir sinyal birden fazla kez oluştuğunda bunlar işletim sistemi tarafından bir kuyruk sisteminde saklanmaktadır. İşte bu kuyruğun uzunluğu da sistemden sisteme değişebilmektedir. POSIX standartları bir sistemin desteklemesi gereken en az kuyruk uzunluğunu _POSIX_SIGQUEUE_MAX sembolik sabitiyle dosyası içerisinde belirtmiştir. Bu sembolik sabitin değeri 32'dir. Ancak buradaki kuyruk uzunluğu toplam kuyruk uzunluğudur. Yani her sinyal için ayrı bir kuyruk düşünülmemelidir. Belli bir sistemdeki gerçek değer SIGQUEUE_MAX sembolik sabitiyle içerisinde belirtilmektedir. Ancak bu sembolik sabit de define edilmek zorunda değildir. Linux sistemlerinde SIGQUEUE_MAX sembolik sabiti define edilmemiştir. Linux'ta bu değer eskiden 1024'tü. Sonra NPROC sayısına hizalanmıştır. Programcı bunu Linux sistemlerinde en az 1024 olarak düşünebilir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int get_queuemax(void) { int queue_max; #ifdef SIGQUEUE_MAX queue_max = SIGQUEUE_MAX; #else queue_max = sysconf(_SC_SIGQUEUE_MAX); #endif return queue_max; } int main(void) { printf("%d\n", SIGRTMIN); /* 34 */ printf("%d\n", SIGRTMAX); /* 64 */ printf("%d\n", RTSIG_MAX); /* 32 */ // printf("%d\n", SIGQUEUE_MAX); /* Undeclared */ printf("%d\n", get_queuemax()); /* 30231 */ return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Gerçek zamanlı sinyalleri gönderebilmek için sigqueue isimli bir POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int sigqueue(pid_t pid, int signo, union sigval value); Fonksiyonun birinci parametresi sinyalin gönderileceği prosesin id değerini almaktadır. İkinci parametre gönderilecek sinyalin numarasını belirtmektedir. Üçüncü parametre ise sinyale iliştirilecek ekstra bilgiyi belirtmektedir. sigval isimli birlik (union) şöyle bildirilmiştir: union sigval { int sival_int; void *sival_ptr; }; Görüldüğü gibi sinyale int bir bilgi de bir adres bilgisi de iliştirilebilmektedir. Tabii sinyale adres bilgisi iliştirilecekse bu adresin hedef proseste anlamlı olması gerekmektedir. Fonksiyon başarı durumunda 0, başarısızlık durumunda -1 değerine geri dönmektedir. Bir fonksiyona sigqueue fonksiyonuyla sinyal gönderebilmek için tıpkı kill fonksiyonunda olduğu gibi sinyal gönderen prosesin gerçek ya da etkin kullanıcı id'sinin, hedef prosesin gerçek ya da saklı kullanıcı id'si aynı olması ya da prosesin uygun önceliğe sahip olması gerekmektedir. sigqueue fonksiyonu ile gerçek zamanlı sinyallerin dışında normal sinyaller de gönderilebilmektedir. Ancak normal sinyaller sigqueue fonksiyonu ile gönderilse bile kuyruklanmayacaktır. Benzer biçimde kill fonksiyonu ile gerçek zamanlı sinyaller de gönderilebilmektedir. Ancak kill fonksiyonunun bu kuyruklamaya yol açıp açmayacağı sistemden sisteme değişebilmektedir. sigqueue fonksiyonunda, kill fonksiyonunda olduğu gibi "proses grubuna" sinyal gönderebilme gibi bir özellik yoktur. sigqueue fonksiyonu ile gönderilen sinyalin sigaction fonksiyonunda SA_SIGINFO bayrağı ile üç parametreli fonksiyon tarafından alınması da zorunlu değildir. Yani sigqueue fonksiyonu ile gönderilen sinyal, normal sinyal fonksiyonu ile de elde edilebilir. Ancak bu durumda sinyale iliştirilen bilgi elde edilemeyecektir. Özetle: 1) sigqueue ile gerçek zamanlı sinyal göndermek zorunda değiliz. Ancak gerçek zamanlı olmayan sinyaller kuyruklanmaz. 2) sigqueue ile gönderilen sinyalin üç parametreli sinyal fonksiyonuyla ele alınması da zorunlu değildir. Ancak bu durumda sinyale iliştirilen bilgi elde edilemektedir. 3) Ters bir biçimde kill fonksiyonuyla gönderilen sinyal üç parametreli sinyal fonksiyonu ile de ele alınabilir. Tabii bu durumda bir değer elde edilmeyecektir. 4) kill fonksiyonuyla gerçek zamanlı sinyal gönderilebilir. Ancak bunun semantiği açıkça belirtilmemiştir. Yani gönderilen sinyal kuyruklanmayabilir. kill fonksiyonuyla gerçek zamanlı sinyal göndermeye çalışmayınız. kill kabuk komutu default durumda kill fonksiyonu kullanılarak yazılmıştır. Ancak kill kabuk komutunda -q seçeneği kullanılırsa bu durumda kill kabuk komutu sigqueue fonksiyonunu kullanacaktır. Aşağıda bir prosese sigqueue fonksiyonu ile sinyal gönderen bir örnek program verilmiştir. Program üç komut satırı argümanı almaktadır. Kullanımı şöyledir: ./sq Linux'ta ilk gerçek zamanlı sinyalin 34 numara olduğunu anımsayınız. Normal olarak gerçek zamanlı sinyaller sigqueue fonksiyonu ile gönderilirken numara SIGRTMIN + n biçiminde belirtilmektedir. Örneğin: if (sigqueue(atoi(argv[1]), SIGRTMIN + 1, sv) == -1) exit_sys("sigqueue"); Normal sinyallerin Linux'taki numaraları için signal(7) man sayfasına başvurabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* sq.c */ #include #include #include #include /* ./sq */ void exit_sys(const char *msg); int main(int argc, char *argv[]) { union sigval sv; if (argc != 4) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } sv.sival_int = atoi(argv[3]); if (sigqueue(atoi(argv[1]), atoi(argv[2]), sv) == -1) exit_sys("sigqueue"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- sigqueue fonksiyonu ile gönderilen sinyalin üç parametreli sinyal fonksiyonu ile alınması uygundur. Anımsanacağı gibi üç parametreli sinyal fonksiyonunun parametrik yapısı şöyleydi: void signal_handler(int signo, siginfo_t *info, void *context); Burada fonksiyonun birinci parametresi yine sinyalin numarasını belirtir. İkinci parametresi ise oluşan sinyal hakkındaki ayrıntılı bilgilerin bulunduğu siginfo_t türünden bir yapı nesnesinin adresini belirtir. Bu yapının pek çok elemanı vardır: siginfo_t { int si_signo; /* Signal number */ int si_errno; /* An errno value */ int si_code; /* Signal code */ int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */ pid_t si_pid; /* Sending process ID */ uid_t si_uid; /* Real user ID of sending process */ int si_status; /* Exit value or signal */ clock_t si_utime; /* User time consumed */ clock_t si_stime; /* System time consumed */ union sigval si_value; /* Signal value */ int si_int; /* POSIX.1b signal */ void *si_ptr; /* POSIX.1b signal */ int si_overrun; /* Timer overrun count; POSIX.1b timers */ int si_timerid; /* Timer ID; POSIX.1b timers */ void *si_addr; /* Memory location which caused fault */ long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */ int si_fd; /* File descriptor */ short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */ void *si_lower; /* Lower bound when address violation occurred (since Linux 3.19) */ void *si_upper; /* Upper bound when address violation occurred (since Linux 3.19) */ int si_pkey; /* Protection key on PTE that caused fault (since Linux 4.6) */ void *si_call_addr; /* Address of system call instruction (since Linux 3.5) */ int si_syscall; /* Number of attempted system call (since Linux 3.5) */ unsigned int si_arch; /* Architecture of attempted system call (since Linux 3.5) */ } Bu yapının önemli elemanları ve anlamları şunlardır: int si_signo: Oluşan sinyalin numarası. int si_errno: O andaki errno değeri (saklayıp geri yüklemek için gerekebilmektedir). pid_t si_pid: Sinyali gönderen prosesin proses id'sini belirtmektedir. uid_t si_uid: Sinyali gönderen prosesin gerçek kullanıcı id'sini belirtir. int si_int: sigqueue fonksiyonundaki sinyale iliştirilen int değer. void *si_ptr: sigqueue fonksiyonundaki sinyale iliştirilen adres bilgisi. sinyal fonksiyonunun üçüncü parametresi (context parametresi) sinyal oluşmadan önceki durum bilgisinin bulunduğu yerin adresini belirtmektedir. Bu parametre Linux sistemlerinde ucontext_t türünden bir yapı nesnesinin adresini tutmaktadır. Bu parametreye çok nadir biçimde gereksinim duyulmaktadır. Aşağıdaki örnekte "sample" programı üç parametreli sinyal fonksiyonunu kullanmaktadır. Program hangi sinyal için sinyal fonksiyonunu set edeceğini belirten bir komut satırı argümanı almaktadır. Program sonsuz bir döngüde pause çağrılarıyla beklemektedir. Dolayısıyla programı Ctrl+C tuşları ile sonlandırabilirsiniz. Programın kullanımı şöyledir: ./sample 34 Bu örnekte 34 Linux'taki ilk gerçek zamanlı sinyal numarasıdır. Bu programa yukarıdaki programı kullanarak sigqueue fonksiyonu ile sinyal gönderebilirsiniz. Örneğin: ./sq 34183 34 123 sample programı ona iliştirilen int bilgiyi de sinyal geldiğinde ekrana yazdırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void signal_handler(int signo, siginfo_t *info, void *context); void exit_sys(const char *msg); int main(int argc, char *argv[]) { struct sigaction sa; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } printf("Process id: %jd\n", (intmax_t)getpid()); sa.sa_sigaction = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART|SA_SIGINFO; if (sigaction(atoi(argv[1]), &sa, NULL) == -1) exit_sys("sigaction"); printf("waiting for signals...\n"); for (;;) pause(); return 0; } void signal_handler(int signo, siginfo_t *info, void *context) { printf("#%d signal handler: %d\n", signo, info->si_int); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekle gerçek zamanlı sinyallerin kuyruklandığını ancak gerçek zamanlı olmayan sinyallerin kuyruklanmadığını gözlemleyebilirsiniz. Buradaki "sample" programını önce 10 numaralı sinyal ile (SIGUSR1) sonra da 34 numaralı sinyal ile (SIGRTMIN + 0) çalıştırınız. Diğer bir terminalden "sample" programı 30 saniye beklerken birkaç sinyal gönderiniz. "sample" programı 30 saniye sonra blokeyi açtığında gerçek zamanlı olmayan sinyaller için bunlardan bir tanesinin teslim edildiğini, gerçek zamanlı sinyaller için hepsinin teslim edildiğini göreceksiniz. Örneğin 10 numaralı sinyal ile (SIGUSR1) şöyle bir durum oluşmuştur: Birinci terminal $ ./sample 10 Process id: 46480 sleep 30 seconds... #10 signal handler: 12 ^C İkinci Terminal $ ./sq 46480 10 12 $ ./sq 46480 10 13 $ ./sq 46480 10 14 $ ./sq 46480 10 15 $ ./sq 46480 10 16 $ ./sq 46480 10 17 Örneğin 34 numaralı sinyal ile (SIGRTMIN + 0) şöyle bir durum oluşmuştur: Birinci Terminal $ ./sample 34 Process id: 46528 sleep 30 seconds... #34 signal handler: 17 #34 signal handler: 18 #34 signal handler: 100 #34 signal handler: 120 #34 signal handler: 60 #34 signal handler: 10 İkinci Terminal $ ./sq 46528 34 17 $ ./sq 46528 34 18 $ ./sq 46528 34 100 $ ./sq 46528 34 120 $ ./sq 46528 34 60 $ ./sq 46528 34 10 ---------------------------------------------------------------------------------------------------------------------------*/ /* sample.c */ #include #include #include #include #include void signal_handler(int signo, siginfo_t *info, void *context); void exit_sys(const char *msg); int main(int argc, char *argv[]) { struct sigaction sa; sigset_t ss; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } printf("Process id: %jd\n", (intmax_t)getpid()); sa.sa_sigaction = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART|SA_SIGINFO; if (sigaction(atoi(argv[1]), &sa, NULL) == -1) exit_sys("sigaction"); sigfillset(&ss); sigdelset(&ss, SIGINT); if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("sleep 30 seconds...\n"); sleep(30); if (sigprocmask(SIG_UNBLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); for (;;) pause(); return 0; } void signal_handler(int signo, siginfo_t *info, void *context) { printf("#%d signal handler: %d\n", signo, info->si_int); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* sq.c */ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { union sigval sv; if (argc != 4) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } sv.sival_int = atoi(argv[3]); if (sigqueue(atoi(argv[1]), atoi(argv[2]), sv) == -1) exit_sys("sigqueue"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 79. Ders 09/09/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Senkron biçimde sinyal oluşmasını bekleyen iki ilginç fonksiyon vardır: sigwait ve sigwaitinfo. sigwaitinfo fonksiyonu POSIX'e "Realtime Extensions" eklemeleri sırasında dahil edilmiştir. sigwait fonksiyonu uzun süredir zaten bulunmaktadır. Fonksiyonların prototipleri şöyledir: #include int sigwait(const sigset_t *set, int *sig); int sigwaitinfo(const sigset_t *set, siginfo_t *info); Bu fonksiyonlar birinci parametresiyle belirtilmiş olan sinyal kümesindeki herhangi bir sinyal oluşana kadar thread'i blokede bekletmektedir. Yani bu fonksiyonlarda biz hangi sinyaller için bekleme yapacağımızı fonksiyonun birinci parametresiyle fonksiyona veririz. Fonksiyon da bu sinyallerden biri oluşana kadar çağrıyı yapan thread'i bloke bekletir. Fonksiyonlar bizim belirttiğimiz kümedeki hangi sinyal dolayısıyla sonlanmışsa o sinyalin bilgilerini bizim ikinci parametreyle verdiğimiz nesneye yerleştirmektedir. sigwait ile sigwaitinfo arasındaki tek fark sigwait fonksiyonunun yalnızca oluşan sinyalin numarasını bize vermesi ancak sigwaitinfo fonksiyonunun oluşan sinyalin pek çok bilgisini siginfo_t yapısı eşliğinde bize vermesidir. sigwait fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda errno değerinin kendisine geri dönmektedir. Zaten POSIX standartları başarısızlık durumunda yalnızca EINVAL errno değerini tanımlamıştır. Ancak sigwaitinfo fonksiyonu başarı durumunda 0, başarısızlık durumunda -1 değerine geri dönmekte ve errno değişkenini uygun biçimde set etmektedir. (sigwait fonksiyonunun errno değişkenini set etmediğine ancak sigwaitinfo fonksiyonunun set ettiğine dikkat ediniz.) Biz bu fonksiyonların sinyal kümelerine normal sinyalleri de gerçek zamanlı sinyalleri de ekleyebiliriz. Gerçek zamanlı sinyaller kuyruklandığı için kuyruktaki yalnızca ilk sinyalin bilgisi elde edilecektir. Ancak POSIX standartları pending duruma geçen birden fazla sinyal olduğunda gerçek zamanlı sinyallerden düşük numarada (yüksek öncelikte) olanın bilgilerinin elde edileceğini belirtmiş olsa da pending durumda olan hem normal sinyal hem de gerçek zamanlı sinyal olduğunda bunların hangisinin bilgilerinin elde edileceği konusunda bir belirlemede bulunmamıştır (unspecified). Normal olarak fonksiyonların birinci parametrelerinde belirtilen sinyallerin daha önceden bloke edilmiş olması gerekmektedir. POSIX standartları eğer bu sinyaller bloke edilmediyse fonksiyon çağrısının tanımsız davranış oluşturacağını belirtmektedir. Görüldüğü gibi biz sinyalleri bloke edip sigwait ya da sigwaitinfo fonksiyonlarıyla bekleyerek işlediğimizde aslında o sinyalleri asenkron değil, senkron bir biçimde işlemiş olmaktayız. Aşağıdaki örnekte önce SIGINT ve SIGUSR1 sinyalleri bloke edilmiş sonra da sigwait fonksiyonu ile bu sinyaller oluşana kadar bekleme yapılmıştır. Bu sinyallerden biri gerçekleştiğinde gerçekleşen sinyalin numarası da stdout dosyasına yazdırılmıştır. Daha sonra bloke edilen sinyallerin blokelerinin de açıldığına dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { sigset_t sset; int signo; int result; sigaddset(&sset, SIGUSR1); sigaddset(&sset, SIGINT); if (sigprocmask(SIG_BLOCK, &sset, NULL) == -1) exit_sys("sigprocmask"); printf("waiting for signal...\n"); if ((result = sigwait(&sset, &signo)) != 0) { fprintf(stderr, "sigwait: %s\n", strerror(result)); exit(EXIT_FAILURE); } printf("%d signal occured and processing...\n", signo); if (sigprocmask(SIG_UNBLOCK, &sset, NULL) == -1) exit_sys("sigprocmask"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bilindiği gibi C'de goto işlemi aynı fonksiyon içerisinde yapılabilen bir işlemdir. Yani biz C'de bir fonksiyondan başka bir fonksiyonun bir yerine goto yapamayız. Zaten goto etiketleri de C'de "fonksiyon faaliyet alanına (function scope)" sahiptir. İşte bir fonksiyondan başka bir fonksiyona goto yapmaya C dünyasında "long jump" denilmektedir. Her ne kadar bu "long jump" konusu aslında C Programlama Dili'ne ilişkin bir konu ise de genellikle bu eğitimlerde çok ayrıntı olduğu için bu fonksiyon üzerinde durulmamaktadır. Ancak long jump işlemleri sinyaller söz konusu olduğunda UNIX/Linux Sistem Programlama faaliyetlerinde kullanılabilmektedir. Biz de burada önce "long jump" işlemini ele alacağız. Sonra da onun sinyaller konusundaki kullanımları üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir fonksiyondan başka bir fonksiyona goto yapılamamasının temel nedenleri şunlardır: 1) goto işleminin yapıldığı fonksiyondaki yerel değişkenler stack'tan nasıl boşaltılacaktır? (Bu problem kısmen çözülebilir) 2) goto yapılan fonksiyondaki yerel değişkenler goto yapıldığı noktada yaratılmış olmayacağı için bu durumda ne olacaktır? 3) goto yapılan fonksiyon return işlemi yaptığında nereye geri dönecektir? Bu fonksiyon çağrılmadığına göre nereye geri döneceği belli değildir. Ancak program akışının daha önce geçilmiş olan başka bir fonksiyondaki bir noktaya aktarılmasında teknik bir sorun yoktur. Çünkü daha önce geçilen noktadaki CPU yazmaçlarının konumu saklanırsa ve "long jump" sırasında bu yazmaçların konumu o değerlerle yüklenirse adeta akış sanki zamanda geri gitmiş ve geçmişteki o noktaya geri dönülmüş gibi olmaktadır. İşte biz C'de long jump işlemi ile ancak daha önce geçmiş olduğumuz bir noktaya geri dönebiliriz. C'de long jump işlemi oldukça basit olarak iki standart C fonksiyonuyla yapılmaktadır: setjmp ve longjmp. Fonksiyonların prototipleri şöyledir: #include int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val); setjmp fonksiyonu, fonksiyonun çağrıldığı noktadaki CPU yazmaçlarının konumunu alarak jmp_buf ile belirtilen alana yerleştirmektedir. setjmp fonksiyonuna geçirilen jmp_buf nesnesinin global bir biçimde oluşturulmuş olması gerekir. Çünkü bu nesne geriye dönüşte başka bir fonksiyonda longjmp yaparken de kullanılacaktır. longjmp işleminde geri dönüş aslında setjmp fonksiyonunun içerisine yapılmaktadır. Böylece setjmp fonksiyonunun geri dönmesi akışın ilk kez geçişi sırasında olabileceği gibi longjmp ile geri dönüş sırasında da olabilmektedir. Tabii programcının sonsuz döngüye girmemek için setjmp fonksiyonundan akışın ilk kez geçmesinden dolayı mı yoksa longjmp işleminden dolayı mı çıkıldığını anlaması gerekir. İşte setjmp fonksiyonundan ilk kez çıkılırken setjmp fonksiyonu 0 ile geri dönmekte ancak setjmp fonksiyonundan longjmp nedeniyle çıkıldığında setjmp fonksiyonu, longjmp fonksiyonunun ikinci parametresi için girilen argümanla geri dönmektedir. Böylece programcı setjmp fonksiyonundan hangi nedenle çıkıldığını anlayıp geri dönüşte sonsuz döngü oluşmasını engelleyebilir. Örneğin: jmp_buf g_jbuf; ... void foo(void) { ... if (setjmp(g_jbuf) == 1) { ... } else bar(); ... } void some_func(void) { ... longjmp(g_jbuf, 1); ... } Burada foo fonksiyonunun içerisinde geri dönüş için geri dönüş noktası setjmp fonksiyonunda kaydedilmiştir. Bu kayıttan sonra programcı bar fonksiyonunu çağırarak yoluna devam etmiştir. Ancak akış bar tarafından çağrılan fonksiyon zincirinden biri olan some_func tarafından yeniden longjmp fonksiyonu ile geçmişe yani foo içerisindeki setjmp içerisine aktarılmıştır. Bu durumda artık setjmp'den 1 değeri ile çıkılacak ve tekrar bar fonksiyonu çağrılmayacaktır. Aşağıda longjump kullanımına bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include void foo(void); void bar(void); void tar(void); jmp_buf g_jbuf; int main(void) { printf("main begins...\n"); foo(); printf("main ends...\n"); return 0; } void foo(void) { printf("foo begins...\n"); if (setjmp(g_jbuf) != 1) bar(); printf("foo ends...\n"); } void bar(void) { printf("bar begins...\n"); tar(); printf("bar ends...\n"); } void tar(void) { printf("tar begins...\n"); longjmp(g_jbuf, 1); printf("tar ends...\n"); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi biz setjmp yaptığımızda prosesteki bazı sinyaller bloke edilmişse ve daha sonra bu sinyallerin blokesi açılmışsa biz longjmp ile eski noktaya dönerken sinyallerin bloke durumu ne olacaktır? İşte POSIX standartları setjmp işleminde kayıt yapılırken prosesin sinyal bloke kümesinin de kaydedilip longjmp sırasında geri yükleneceği konusunda bir garanti vermemiştir (unspecified). Bu nedenle eğer geri dönüşte aynı sinyal bloke kümesinin de yüklenmesi isteniyorsa setjmp ve longjmp yerine sigsetjmp ve siglongjmp fonksiyonları kullanılmalıdır. Bu fonksiyonların prototipleri şöyledir: #include int sigsetjmp(sigjmp_buf env, int savemask); void siglongjmp(sigjmp_buf env, int val); Görüldüğü gibi fonksiyonların genel kullanımı setjmp ve longjmp fonksiyonu ile benzerdir. Ancak sigsetjmp fonksiyonu iki parametre almaktadır. Bu ikinci parametre (savemask parametresi) 0 ise prosesin sinyal bloke kümesi kaydedilip geri yükleme yapılmaz, sıfır dışı ise kaydedilip geri yükleme yapılır. (Yani ikinci parametre 0 geçilirse fonksiyon setjmp gibi davranmaktadır.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi "long jump" işlemine neden gereksinim duyulmaktadır? İşte bazı durumlarda programcı bulunan durumdan kaçıp geçmişteki daha güvenli bir noktaya dönmek isteyebilir. Örneğin sinyal fonksiyonlarının içerisinde long jump işlemine sıkça rastlanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 80. Ders 10/09/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Örneğin biz SIGSEGV sinyali oluştuğunda programı sonlandırmayıp başka birtakım işlemlerle programın çalışmasına devam etmesini isteyebiliriz. Bu durumda SIGSEGV sinyali için sinyal fonksiyonu set ederiz, ancak bu sinyal fonksiyonunda daha önceki bir noktaya "long jump" yapabiliriz. Aşağıda bu duruma bir örnek verilmiştir. Bu örnekte sigsetjmp ve siglongjmp kullanmanın özellikle bir nedeni yoktur. Yani örneğimizde setjmp ve longjmp fonksiyonlarını da kullanabilirdik. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void sigsegv_handler(int sig); void exit_sys(const char *msg); jmp_buf g_jbuf; int main(void) { struct sigaction sa; char *str = (char *)0x12345678; sa.sa_handler = sigsegv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGSEGV, &sa, NULL) == -1) exit_sys("sigaction"); if (sigsetjmp(g_jbuf, 1) == 1) { printf("SIGSEGV occures, but we continue...\n"); /* UNSAFE */ /* ... */ } else { *str = 'x'; /* ... */ } return 0; } void sigsegv_handler(int sig) { /* ... */ siglongjmp(g_jbuf, 1); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Biz sinyal fonksiyonları içerisinde yalnızca asenkron sinyal güvenli fonksiyonları kullanabiliyorduk. Sinyal fonksiyonu içerisinde longjmp yaparsak "long jump" yaptığımız yerde sinyal güvenli fonksiyonlar kullanmak zorunda mıyız? İşte sinyal fonksiyonu çalıştırılıp oradan "long jump" yapıldığında atlanılan yerde de sinyal güvenli fonksiyonların kullanılması gerekir. Çünkü bu bakımdan "reentancy" durumunda bir değişiklik yoktur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sinyal fonksiyonları içerisinde tüm programın sonlandırılması isteniyorsa bu işlem exit standart C fonksiyonu ile yapılmamalıdır. Çünkü exit fonksiyonu stdio dosyalarını kapatıp onların tamponlarını tazelemektedir. Dolayısıyla exit fonksiyonu asenkron sinyal güvenli bir fonksiyon değildir. Bu tür durumlarda doğrudan _exit POSIX fonksiyonu ile proses sonlandırılabilir. Anımsanacağı gibi _exit fonksiyonu aslında doğrudan işletim sisteminin prosesi sonlandıran sistem fonksiyonunu çağırmaktadır. Ayrıca C99 ile C'ye eklenen _Exit fonksiyonu da POSIX standartlarına göre asenkron sinyal güvenlidir. _Exit standart C fonksiyonunun stdio tamponlarını flush edip etmeyeceği derleyicileri yazanların isteğine bırakılmıştır. POSIX standartlarına göre bu fonksiyon tamamen _exit fonksiyonu ile eşdeğer işleve sahiptir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi bazı sinyallerdeki default eylem, prosesin sonlandırılmasıyla birlikte bir "core" dosyasının oluşturulmasıdır. Buradaki "core" terimi eski bir terimdir ve ana belleği belirtmektedir. Core dosyasının üretilmesinin amacı onun debugger altında incelenmesine olanak sağlanmasıdır. Böylece çöken bir programda, programın neden ve nerede çöktüğüne ilişkin bir analiz yapılabilmektedir. Core dosyasının incelenmesi çeşitli debugger'larla yapılabilmektedir. Tabii UNIX/Linux dünyasında en yaygın kullanılan debugger "gdb" isimli GNU projesi kapsamında oluşturulmuş olan debugger'dır. Biz izleyen paragraflarda Linux sistemlerinde "core dosyalarına" yönelik bazı açıklamalarda bulunacağız. gdb debugger'ının temel kullanımı başka bölümlerde ele alınacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux sistemlerinde sinyal yoluyla core dosyası oluşturulması için sistem limitlerinin uygun olması gerekmektedir. "ulimit -a" komutu ile sistem limitlerine bakılabilir. Eğer burada "core file size" 0 ise bunun artırılması gerekir. "ulimit -c" ile yalnızca "core file size" bilgisi de görüntülenebilmektedir. Bu sınırın "unlimited" hale getirilmesi şöyle yapılabilir: $ ulimit -c unlimited Core dosyasının üretildiği yer Linux sistemlerinde kullanılan bazı sistem paketlerine göre değişebilmektedir. Eskiden core dosyaları prosesin çalışma dizininde yaratılıyordu. Daha sonra "systemd" paketi ile birlikte core dosyalarının yaratılması biraz daha ayrıntılı hale getirilmiştir. Öncelikle core dosyalarının nasıl isimlendirildiğinin belirlenmesi gerekir. Bunun için proc dosya sisteminde "/proc/sys/kernel/core_pattern" dosyasına bakılmalıdır. Bu dosya core dosyasının hangi isimlendirme biçimiyle nerede yaratılacağını belirtmektedir. Örneğin bu dosyanın içeriği şöyle olabilir: |/lib/systemd/systemd-coredump %P %u %g %s %t 9223372036854775808 %h Burada core dosyasının üretilmesinde "systemd-coredump" isimli programın kullanılacağı belirtilmektedir. Bu programın dokümantasyonu SYSTEMD-COREDUMP(8) man sayfasında yapılmıştır. Buradaki core_pattern dosyasının içeriğinde bulunan % format karakterleri core dosyasının hangi isimlerle kombine edilerek üretileceğini belirtmektedir. core dosyasının yeri aslında systemd-coredump programının konfigürasyonunda belirlenmektedir. Default olarak /var/lib/systemd/coredump dizini kullanılmaktadır. Core dosyaları genellikle lz4 formatında sıkıştırılmış bir biçimde tutulmaktadır. Bazı sistemlerde "core dump" utility'si olarak apport denilen program da kullanılmaktadır. Bazı sistemlerde core_pattern dosyasının içeriği aşağıdaki gibi de olabilir: |/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E Burada core dosyasının yaratılma organizasyonu "apport" denilen programa devredilmiştir. Bu program da "core" dosyasını "/var/lib/systemd/coredump" dizininde ya da "/var/lib/apport/coredump" dizininde oluşturmaktadır. Core dosyalarını kolay yüklemek için ayrıca "coredumpctl" isimli bir programdan da faydalanılabilir. Ancak bu program default olarak kurulu durumda değildir. Bunun kurulabilmesi için aşağıdaki komut uygulanabilir: $ sudo apt install systemd-coredump coredumpctl programı core dosyaları üzerinde işlem yapmayı kolaylaştıran bir programdır. Örneğin üretilmiş olan core dosyalarının listeleri şöyle alınabilir: $ coredumpctl list Bu komutla tüm üretilmiş olan core dosyaları listelenecektir. En son üretilen dosya listenin sonunda olacaktır. Ancak ters sırada görüntüleme için -r seçeneği kullanılabilmektedir. Artık coredumptctl programı yüklendiğine göre ondan faydalanabiliriz. En son core dosyasını gdb ile yüklemek için şöyle yapılır: $ coredumpctl gdb Bu komut ile her zaman son core dosyası yüklenmektedir. Spesifik bir core dosyasının yüklenmesi de sağlanabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir grup prosesin oluşturduğu gruba "proses grubu" denilmektedir. Proses grubu kavramı bir grup prosese sinyal gönderebilmek için uydurulmuştur. Gerçekten de kill sistem fonksiyonunun birinci parametresi olan pid sıfırdan küçük bir sayı olarak girilirse abs(pid) numaralı proses grubuna sinyal gönderilmektedir. Bir sinyal bir proses grubuna gönderilirse o proses grubunun bütün üyeleri olan proseslere gönderilmiş olur. Anımsanacağı gibi kill fonksiyonun birinci parametresi 0 girildiğinde, sinyal kill fonksiyonunu uygulayan prosesin içinde bulunduğu proses grubuna gönderilmektedir. Yani proses kendi proses grubuna sinyali göndermektedir. Her proses grubunun bir id'si vardır. Bir proses grubunun id'si o proses grubundaki bir prosesin proses id'si ile aynıdır. İşte proses id'si proses grup id'sine eşit olan prosese, o proses grubunun "proses grup lideri (process group leader)" denilmektedir. Proses grup lideri genellikle proses grubunu yaratan prosestir. fork işlemi sırasında alt prosesin proses grubu onu yaratan üst prosesten alınmaktadır. Yani üst proses hangi proses grubundaysa fork işlemi sonucunda yaratılan proses de aynı proses grubunda olur. Bir prosesin ilişkin olduğu proses grubunun id'sini alabilmek için getpgrp ya da getpgid POSIX fonksiyonları kullanılır. #include pid_t getpgrp(void); pid_t getpgid(pid_t pid); getpgrp fonksiyonu prosesin kendi grup id'sini elde etmekte kullanılmaktadır. getpgid fonksiyonu ise herhangi bir prosesin proses grup id'sini elde etmekte kullanılmaktadır. getpgid fonksiyonunun parametresi 0 geçilirse fonksiyonu çağıran prosesin proses grup id'si alınmış olur. Yani aşağıdaki çağrı eşdeğerdir: pgid = getpgrp(); pgid = getpgid(0); Proses grup lideri olan proses sonlanmış olsa bile proses grubunun id'si aynı biçimde kalmaya devam eder. Yani işletim sistemi bu durumda prosesin id'sini o proses grubu bu id ile temsil edildiği için başka proses'te kullanmaz. Proses grubu gruptaki son prosesin sonlanması ya da grup değiştirmesiyle ömrünü tamamlamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kabuktan bir program çalıştırdığımızda kabuk fork işlemini yaptıktan sonra alt proses için yeni bir proses grubu oluşturur ve alt prosesi o proses grubunun grup lideri yapar. Artık bu program kendi içerisinde fork yaptığında oluşturulacak olan alt proseslerin hepsi aynı proses grubunun içerisinde olacaktır. Örneğin aşağıdaki gibi "sample" isimli bir programı bir terminalden çalıştırmış olalım: #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); pause(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Diğer bir terminalden giriş yaparak o terminaldeki (örneğimizde "pts/0") programların proses id'leri, üst proses id'leri ve proses grup id'leri hakknda bilgi edinelim: $ ps -t pts/0 -o pid,ppid,pgid,cmd PID PPID PGID CMD 26158 25044 26158 bash 29577 26158 29577 ./sample 29578 29577 29577 ./sample Burada ilk çalıştırdığımız sample programının yaratılan proses grubunun grup lideri olduğu görülmektedir. sample programında fork yapılarak oluşturulmuş olan prosesin de proses grup id'sinin aynı olduğuna dikkat ediniz. Proses grup id'leri fork işlemi sırasında üst prosesten alt prosese aktarılmaktadır. Kabuk üzerinden boru sembolü ile birden fazla programı çalıştırdığımızda kabuk birden fazla proses oluşturmaktadır. Örneğin: $ ls -l | grep "sample" Burada kabuk "ls" ve "grep" programını çalıştırmak için fork işlemleri yapacaktır. Ancak kabuk burada "ls" ve "grep" proseslerinin proses id'lerini aynı yapmaktadır. Örneğin pts/1 terminalinden aşağıdaki gibi bir komut çalıştırmış olalım: $ cat | grep "test" Burada kabuk programı bir proses grubu oluşturup "cat" ve "grep" komutlarını aynı proses grubuna atayacaktır. Diğer terminalden durumu inceleyelim: $ ps -t pts/0 -o pid,ppid,pgid,cmd PID PPID PGID CMD 34667 34658 34667 bash 49484 34667 49484 cat 49485 34667 49484 grep --color=auto test Burada "cat" programının yaratılan proses grubunun proses lideri olduğu görülmektedir. "grep" programı da aynı proses grubuna atanmıştır. Yani burada kabuk programı bir proses grubu yaratıp "cat" ve "grep" programlarını aynı proses grubuna atamıştır. Tabii burada eğer "cat" ve "grep" programları da kendi içlerinde fork yapsalardı onların alt prosesleri de aynı proses grubuna dahil olacaklardı. Thread'lerin ayrı proses id'leri yoktur. Yalnızca proseslerin proses id'leri vardır. Dolayısıyla bir prosesin herhangi bir thread'inde biz getpgrp fonksiyonunu uygularsak aynı değeri elde ederiz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yeni bir proses grubu yaratmak için ya da bir prosesin proses grubunu değiştirmek için setpgid POSIX fonksiyonu kullanılmaktadır. #include int setpgid(pid_t pid, pid_t pgid); Fonksiyonun birinci parametresi proses grup id'si değiştirilecek prosesi, ikinci parametresi de hedef proses grup id'sini belirtmektedir. Eğer bu iki parametre aynı ise yeni bir proses grubu yaratılır ve bu yeni grubun lideri de buradaki proses olur. Bir proses (uygun önceliğe sahip olsa bile) ancak kendisinin ya da kendi alt proseslerinin proses grup id'lerini değiştirebilir. Fakat üst proses, alt proses exec uyguladıktan sonra artık onun proses grup id'sini değiştirememektedir. Ayrıca setpgid fonksiyonu ile proses ancak kendisinin ya da alt proseslerinin proses grup id'lerini aynı "oturum (session)" içerisindeki bir proses grup id'si olarak değiştirebilmektedir. Oturum (session) kavramı izleyen paragraflarda ele alınmaktadır. setpgid fonksiyonunun birinci parametresi 0 girildiğinde fonksiyonu çağıran proses anlaşılmaktadır. İkinci parametresi 0 girildiğinde birinci parametresinde belirtilen proses anlaşılmaktadır. Yani aşağıdaki çağrılar eşdeğerdir: setpgid(getpid(), getpid()); setpgid(getpid(), 0); setpgid(0, getpid()); setpgid(0, 0); Örneğin kabuktan bir program çalıştırdırğımızda kabuk önce fork işlemini yapar sonra alt proseste setpgid fonksiyonu ile yeni bir proses grubu yaratır. Çalıştırılan programı da yeni yaratılan proses grubunun proses grup lideri yapar. Aşağıdaki örnekte üst proses fork yaparak alt prosesi oluşturmuştur. Alt proseste de setpgid fonksiyonu ile yeni bir proses grubu yaratılmıştır. Programın örnek çıktısı şöyledir: $ ./sample Parent process id: 49658 Parent process id of the parent: 34667 Parent process group id: 49658 Child process id: 49659 Parent process id of the child: 49658 Child process group id: 49659 Buradan şunlar anlaşılmaktadır: - Kabuk, sample programını çalıştırırken yeni bir proses grubu oluşturup sample programını bu proses grubunun grup lideri yapmıştır. - Alt proseste yeni bir proses grubu yaratılmış, alt proses de bu proses grubunun grup lideri olmuştur. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pgid; pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ printf("Parent process id: %jd\n", (intmax_t)getpid()); printf("Parent process id of the parent: %jd\n", (intmax_t)getppid()); pgid = getpgrp(); printf("Parent process group id: %jd\n", (intmax_t)pgid); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); } else { /* child process */ sleep(1); if (setpgid(getpid(), getpid()) == -1) /* setpgid(0, 0) */ exit_sys("setpgid"); printf("Child process id: %jd\n", (intmax_t)getpid()); printf("Parent process id of the child: %jd\n", (intmax_t)getppid()); pgid = getpgrp(); printf("Child process group id: %jd\n", (intmax_t)pgid); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirtildiği gibi kill POSIX fonksiyonuyla (ya da kill komutuyla) bir proses grubuna sinyal gönderildiğinde aslında proses grubundaki tüm proseslere sinyal gönderilmektedir. Bunu aşağıdaki programla test edebilirsiniz. Aşağıdaki programda üst proses SIGSUR1 sinyalini set ederek fork işlemi yapmıştır. Böylece aynı sinyal alt proseste de set edilmiş durumdadır. Bu programı çalıştırınca üst proses alt prosesi wait fonksiyonunda bekleyecek, alt proses de pause ile sinyal oluşana kadar blokede bekleyecektir. Diğer bir terminalden bu proses grubuna kill komutu ile SUGUSR1 sinyalini göndermeyi deneyiniz. Bunu yaparken kill komutunda proses grup id'sini negatif yapmayı unutmayınız. Aşağıda testin nasıl yapıldığına ilişkin örnek verilmiştir: - Terminallerden birinde "sample" programı çalıştırılır: $ ./sample Parent process id: 49792 Parent process id of the parent: 34667 Parent process group id: 49792 Child process id: 49793 Parent process id of the child: 49792 Child process group id: 49792 SIGUSR1 occurred in process 49793 SIGUSR1 occurred in process 49792 waitpid: Interrupted system call - Diğer bir terminalden proses grubuna SIGUSR1 sinyali gönderilir: $ kill -USR1 -49792 ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void sigusr1_handler(int sno); void exit_sys(const char *msg); int main(void) { pid_t pgid; pid_t pid; struct sigaction sa; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("siagaction"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ printf("Parent process id: %jd\n", (intmax_t)getpid()); printf("Parent process id of the parent: %jd\n", (intmax_t)getppid()); pgid = getpgrp(); printf("Parent process group id: %jd\n", (intmax_t)pgid); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); } else { /* child process */ sleep(1); printf("Child process id: %jd\n", (intmax_t)getpid()); printf("Parent process id of the child: %jd\n", (intmax_t)getppid()); pgid = getpgrp(); printf("Child process group id: %jd\n", (intmax_t)pgid); pause(); } return 0; } void sigusr1_handler(int sno) { printf("SIGUSR1 occurred in process %ld\n", (intmax_t)getpid()); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 81. Ders 16/09/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Oturum (session) kabuktaki arka plan çalışmayı düzene sokmak için uydurulmuş bir kavramdır. Bir oturum proses gruplarından oluşur. Oturumu oluşturan proses gruplarından yalnızca biri "ön plan (foreground)", diğerlerinin hepsi "arka plan (background)" proses gruplardır. İşte aslında terminal sürücüsü (tty) klavye sinyallerini oturumun ön plan proses grubuna yollamaktadır. Kabuk üzerinden bir komut yazıp komutun sonuna & karakteri getirilirse bu & karakteri "bu komutu arka planda çalıştır" anlamına gelmektedir. Böylece kabuk çalıştırılan komutu wait fonksiyonlarıyla beklemez. Yeniden prompt'a düşer. Ancak o komut çalışmaya devam etmektedir. İşte kabuk & ile çalıştırılan her komut için yeni bir proses grubu yaratır ve o proses grubunu oturumun arka plan proses grubu durumuna getirir. Sonunda & olmadan çalıştırılan komutlar ise ön plan proses grubunu oluşturur. Tabii oturumu yaratan aslında genellikle kabuk programlarıdır. Kabuk, oturumu ve proses grubunu yaratır. Kendisi de oturumda bir proses grubunda bulunuyor olur. Oturumların birer id'si vardır. Bir oturumun id'si onu yaratan prosesin içinde bulunduğu proses grubunun id'si olur. Oturumu yaratan bu prosese de "oturum lideri (session leader)" denilmektedir. (Oturum liderinin proses id'sinin, proses grup id'sinin ve oturum id'sinin aynı olması gerektiğine dikkat ediniz.) O halde durum özetle şöyledir: Kabuk bir oturum ve bir proses grubu yaratır. Kendisini bu proses grubunun lideri yapar. kendisi proses grubunun ve oturumun lideri durumundadır. (Bu işlemlerin nasıl yapıldığına ilişkin ayrıntılar izleyen paragraflarda açıklanmaktadır.) Sonra kabuk sonu & ile biten komutlar için proses grupları yaratıp bu proses gruplarını oturumun arka plan proses grupları yapar. Sonunda & olmayan komutları da oturumun ön plan proses grubu yapmaktadır. Böylece belli bir anda oturumun içerisinde bir ön plan proses grubu ve çeşitli arka plan proses grupları bulunacaktır. Tabii kabuğun kendisi de "sonunda & olmayan" bir komut uygulandığında oturumun arka plan proses grubu içerisinde bulunuyor olacaktır. Terminal sürücüsü de SIGINT ve SIGQUIT gibi sinyalleri oturumun ön plan proses grubuna göndermektedir. Örneğin biz mample programını sonunda & olacak biçimde sample programını da normal bir biçimde aşağıdaki gibi çalıştırmış olalım: $ ./mample & [1] 52630 $./sample Her iki program da pause fonksiyonunda bekleyecek biçimde yazılmıştır. Şimdi diğer bir terminalden bu proseslerin id'lerini, proses grup id'lerini, oturum id'lerini yazdıralım: $ ps -o pid,pgid,sid,cmd -t pts/0 PID PGID SID CMD 31684 31684 31684 bash 52630 52630 31684 ./mample 52632 52632 31684 ./sample Burada görülen şudur: Kabuğun kendisi, mample ve sample prosesleri için ayrı birer proses grubu yaratmıştır. Ancak bu proses grupları aynı oturum içerisindedir. Oturumun lideri ise kabuktur. Komut çıktısından göremesek de kabuk ve mample oturumun arka plan proses gruplarını, sample ise ön plan proses grubunu oluşturmaktadır. mample ve sample proseslerinin grup id'lerinin farklı olduğuna dikkat ediniz. Yukarıda da belirttiğimiz gibi oturumların da proses gruplarında olduğu gibi id'leri vardır. Oturumların id'leri (session id) oturum içerisindeki bir proses grubunun liderinin id'si ile aynıdır. Bu prosese aynı zamanda "oturum lideri (session leader)" denilmektedir. Kabuktaki bütün arka plan proses komutları "jobs" komutu ile görülebilmektedir. Örneğin: $ cat > x & [1] 14797 $ cat > y & [2] 14798 $ cat > z & [3] 14799 $ cat > z | grep "test" & [4] 14827 $ jobs [1] Running cat > x & [2] Running cat > y & [3]- Running cat > z & [4]+ Running cat > z | grep --color=auto "test" & Belli bir arka plan proses grubunu ön plana çekmek için "fg %n (n burada arka plandaki işin numarasını belirtmektedir) komutu uygulanır. % karakteri hiç kullanılmayabilir. Örneğin: $ fg %3 cat > z Belli bir arka plan işe (job) yani proses grubuna "kill %n" komutuyla sinyal de gönderebiliriz. Örneğin: $ kill %2 Oturum terminal sürücüsüyle ilişkili bir kavram olarak sisteme sokulmuştur. Oturumların bir "ilişkin olduğu terminal ya da terminal sürücüsü (controlling terminal)" vardır. Bu terminal gerçek terminal ise "/dev/ttynnn" (buradaki nnn bir sayıyı temsil ediyor) terminallerinden biridir. Sahte (pseudo) bir terminal ise "dev/pts/nnn" terminallerinden biridir. Pencere yöneticilerinin içerisinde açılan terminaller sahte (pseudo) terminallerdir. Ancak işlev olarak sahte terminallerin gerçek terminallerden bir farkları yoktur. Klavyeden Ctrl+C ve Ctrl+\ (ya da Ctrl + Backspace) tuşlarına basıldığında SIGINT ve SIGQUIT sinyalleri bu terminal sürücüsü tarafından oturumun ön plan proses grubuna gönderilmektedir. Örneğin: $ cat | grep "test" Bu komut terminalden uygulandıktan sonra biz Ctrl+c tuşlarına bastığımızda SIGINT sinyali ön plan proses grubuna gönderileceğinden dolayı burada hem cat prosesi hem de grep prosesi sonlandırılacaktır. Burada aktardığımız bilgiler üzerinde şu anahtar noktalara yeniden dikkatinizi çekmek istiyoruz: 1) Kabuk programları bir proses grubu ve oturum yaratmakta ve kendilerini proses grubunun ve oturumun oturum lideri yapmaktadır. Kabuğun çalıştığı terminal de oturumun "ilişkin olduğu terminal (controlling terminal)" durumundadır. 2) Kabuk "sonu & ile bitmeyen" her komut için bir proses grubu oluşturur ve o proses grubunu oturumun ön plan proses grubu yapar. Terminal sinyalleri bu proses grubuna gönderilmektedir. 3) Kabuk "sonu & ile biten" her komut için ayrı bir proses grubu oluşturur ve o proses grubunu oturumun arka plan proses grubu haline getirir. 4) Terminal tuşlarıyla oluşturulan SIGINT ve SIGQUIT gibi sinyaller oturumun ön plan proses grubuna gönderilmektedir. Yukarıda açıklandığı gibi çalışan kabuk programlarına "görev kontrol kabukları (job control shells)" denilmektedir. Eski "Bourne Shell" kabuklarında görev kontrol özelliği yoktu. Bugün kullanılan kabuk programlarında genel olarak görev kontrol özelliği bulunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir prosesin ilişkin olduğu oturum id'si (session id) getsid POSIX fonksiyonuyla alınmaktadır: #include pid_t getsid(pid_t pid); Fonksiyon parametre olarak prosesin id'sini almaktadır. Eğer bu id değeri 0 olarak girilirse fonksiyonu çağıran prosesin oturum id'si elde edilir. Aşağıdaki programda bir prosesin ve onun alt prosesinin id bilgileri stdout dosyasına yazdırılmıştır. Üst ve alt proseslerin aynı session id'ye sahip olduğuna onun da bash'in session id'si (dolayısıyla proses id'si) olduğuna dikkat ediniz. Programın çalıştırılması ile elde edilen bir çıktı şöyledir: Parent process id: 52812 Parent process id of the parent: 31684 Parent process group id: 52812 Parent process session id: 31684 Child process id: 52813 Parent process id of the child: 52812 Child process group id: 52812 Child process session id: 31684 ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pgid, sid; pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ pgid = getpgrp(); sid = getsid(0); printf("Parent process id: %jd\n", (intmax_t)getpid()); printf("Parent process id of the parent: %jd\n", (intmax_t)getppid()); printf("Parent process group id: %jd\n", (intmax_t)pgid); printf("Parent process session id: %jd\n", (intmax_t)sid); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); } else { /* child process */ sleep(1); pgid = getpgrp(); sid = getsid(0); printf("Child process id: %jd\n", (intmax_t)getpid()); printf("Parent process id of the child: %jd\n", (intmax_t)getppid()); printf("Child process group id: %jd\n", (intmax_t)pgid); printf("Child process session id: %jd\n", (intmax_t)sid); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi biz sıfırdan bir "job control shell" yazmak istersek bu oturum işlemlerini nasıl yaparız? Öncelikle bizim bir oturum yaratıp oturumumuzu bir terminal aygıt sürücü ile ilişkilendirmemiz gerekir. Yani oturumumuzun bir terminale ("controlling terminal) sahip olması gerekir. Yeni bir oturum (session) yaratmak için setsid fonksiyonu kullanılmaktadır: #include pid_t setsid(void); Fonksiyon şunları yapar: - Yeni bir oturum (session) oluşturur. - Bu oturum içerisinde yeni bir proses grubu oluşturur. - Oluşturulan oturumun ve proses grubunun lideri fonksiyonu çağıran prosestir. Görüldüğü gibi bir proses setsid fonksiyonunu çağırdığında bir oturumla birlikte yeni bir proses grubu da oluşturulmaktadır. Bu proses grubu oturumun lideri olmaktadır. setsid fonksiyonu tipik olarak kabuk programları tarafından işin başında çağrılmaktadır. Böylece kabuk yeni bir oturumun hem lideri olur hem de o oturum içerisinde yaratılmış olan bir proses grubunun lideri olur. O halde bir komut uygulanmamış durumdaki kabuk ortamında bir oturum ve bir de proses grubu vardır. Kabuk bu ikisinin de lideri durumundadır. Sonra kabukta sonu & ile bitmeyen bir komut çalıştırıldığında kabuk bu komuta ilişkin proses için yeni proses grubu yaratacak ve bu grubu oturumun ön plan proses grubu yapacaktır. setsid fonksiyonunu çağıran proses eğer zaten bir proses grubunun grup lideri ise fonksiyon başarısız olmaktadır. Örneğin biz kabuktan çalıştırdığımız bir programda setsid çağrısı yaparsak başarısız oluruz. O halde bir oturum yaratabilmemiz için bizim zaten bir proses grubunun proses grup lideri olmamamız gerekir. Bunu sağlayabilmek için tipik olarak proses önce fork yapar sonra alt proseste setsid fonksiyonunu çağırır. Böylece üst proses proses grup lideri olsa bile alt proses hiçbir zaman proses grup lideri olamayacaktır. Örneğin biz kabuktan çalıştırdığımız programda setsid fonksiyonunu çağırırsak fonksiyon başarısız olacaktır. Çünkü kabuk bizim programımız için bir proses grubu yaratıp bizi o proses grubunun grup lideri yapmaktadır. Aşağıdaki programı çalıştırarak hatayı inceleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { if (setsid() == -1) /* function possibly will fail! */ exit_sys("setsid"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi eğer yeni bir oturum yaratılmak isteniyorsa programın nasıl çalıştırılacağı bilinmediğine göre önce fork uygulayıp alt proseste setsid uygulamak gerekir. Çünkü alt proses hiçbir zaman zaten proses grup lideri olamayacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ #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) _exit(EXIT_SUCCESS); if (setsid() == -1) exit_sys("setsid"); printf("Ok, i am session leader of the new session!\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Biz sıfırdan bir kabuk programı oluşturmak istediğimizde fork yapıp alt proseste setsid fonksiyonunu çağırıp oturum yaratabildik. Pekiyi oturumumuzu nasıl bir terminal aygıt sürücüsü ile ilişkilendireceğiz? İşte oturum lideri open fonksiyonuyla O_NOCTTY bayrağı kullanılmadan bir terminal aygıt sürücüsünü açtığında ve elde ettiği dosya betimleyicisi ile ioctl(fd, TIOCSCTTY) çağrısı yaptığında artık o terminal oturumun ilişkin olduğu terminal (controlling terminal) durumuna gelir. Örneğin: if ((fd = open("/dev/tty1", O_RDWR)) == -1) _exit(EXIT_FAILURE); if (ioctl(fd, TIOCSCTTY) == -1) _exit(EXIT_FAILURE) ioctl fonksiyonu aygıt sürücülerdeki fonksiyonların çağrılması için kullanılan genel amaçlı bir fonksiyondur. Bu fonksiyonun kullanımını "aygıt sürücüler" konusunda göreceğiz. (Bazı sistemlerde bu ioctl işlemini yapmaya gerek kalmamaktadır. Ancak Linux sistemlerinde bu işlemin yapılması gerekmektedir.) Eğer ioctl işlemi yapılırken terminal o anda başka bir oturumun terminaliyse (controlling terminal) ve çağrıyı yapan proses uygun önceliğe de sahip değilse, çağrı EPERM errno değeri ile başarısız olmaktadır. Ancak ioctl çağrısı yapılırken eğer terminal başka bir oturumun terminaliyse ancak çağrıyı yapan proses uygun önceliğe sahipse bu durumda terminal o oturumdan koparılıp çağrının yapıldığı oturumun terminali (controlling terminal) haline getirilmektedir. Bu durumda ioctl işlemini yapan prosese de "terminali kontrol eden proses (controlling proscess)" biçiminde isimlendirilmektedir. Normal olarak kabuk programı (bash) terminali kontrol eden proses (controlling process) durumundadır. Dosyaların betimleyicileri üst prosesten alt prosese aktarıldığına göre bu terminal betimleyicisi her proseste gözükecektir. Yukarıda belirttiğimiz gibi buna "ilgili prosesin ilişkin terminal (process controlling terminal)" denilmektedir. Anımsanacağı gibi aslında 0 numaralı betimleyici terminal aygıt sürücüsünün O_RDONLY modunda açılmasıyla, 1 numaralı betimleyici aynı aygıt sürücünün O_WRONLY moduyla açılmasıyla ve stderr dosyası da 1 numaralı betimleyicinin dup yapılmasıyla oluşturulmaktadır. Yani aslında 0, 1 ve 2 betimleyiciler aynı terminale ilişkindir. Bir oturumun ilişkin olduğu terminalin oturumdan kopartılması işlemi de ioctl(fd, TIOCNOTTY) çağrısıyla yapılabilmektedir. Şimdiye kadar oturum ve terminale ilişkin pek çok terim gördük. Bu terimlerin neler olduğunu ve ne anlamlara geldiğini aşağıda topluca listelemek istiyoruz: - Oturum (Session): Proses gruplarından oluşan görev kontrol kabuklarının faydalandığı bir kavramdır. Yeni bir oturum oluşturmak için setsid fonksiyonu kullanılmaktadır. - Oturumun İlişkin Olduğu Terminal (Controlling Terminal): Oturumdaki proseslerin kullandığı terminali (terminal aygıt sürücüsünü) belirtmektedir. - Prosesin İlişkin Olduğu Terminal (Process Controlling Terminal): Belli bir prosesin kullandığı terminali (terminal aygıt sürücüsünü) belirtmektedir. - Oturum Lideri (Session Leader): Oturumu yaratan prosesi belirtir. Bu prosesin proses id'si, proses grup id'si ve oturum id'si aynıdır. - Oturum Id'si (Session Id): Oturumu temsil eden proses id değeridir. Oturum id'si normal olarak oturum içerisindeki bir proses grubunun, dolayısıyla da prosesin id'si ile aynıdır. - Ön Plan Proses Grubu (Foreground Process Group): Oturum içerisindeki özel klavye tuşları için sinyallerin gönderildiği proses grubu. Bu proses grubu doğrudan terminalle etkileşebilmektedir. Sonuna & getirilmeden uygulanan komutlardaki prosesleri kabuk aynı proses grubuna yerleştirmekte ve o proses grubunu da kabuğun ön plan proses grubu yapmaktadır. Belli bir anda oturumda yalnızca bir tane ön plan proses grubu bulunmaktadır. - Arka Plan Proses Grubu (Background Process Group): Sonuna & getirilerek uygulanan komutlardaki prosesleri kabuk aynı proses grubuna yerleştirmekte ve o proses grubunu da kabuğun arka plan proses grubu haline getirmektedir. Belli bir anda oturumda birden fazla arka plan proses grubu bulunabilmektedir. Aşağıdaki örnekte bir oturum yaratılmış ve "/dev/tty1" terminali oturumun ilişkin olduğu terminal (controlling terminal) yapılmıştır. Sonra prosesin açmış olduğu bütün dosyalar kapatılmış ve ilk üç betimleyicinin söz konusu terminale ilişkin stdini, stdout ve stderr betimleyicisi olması sağlanmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; int fd; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) exit_sys("setsid"); for (int i = 0; i < 1024; ++i) close(i); if ((fd = open("/dev/tty1", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if (open("/dev/tty1", O_WRONLY) == -1) _exit(EXIT_FAILURE); if (dup(1) == -1) _exit(EXIT_FAILURE); if (ioctl(fd, TIOCSCTTY) == -1) _exit(EXIT_FAILURE); for (int i = 0; i < 30; ++i) { printf("%d\n", i); sleep(1); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Oturumdaki ön plan proses grubunun hangisi olduğu tcgetpgrp POSIX fonksiyonuyla elde edilebilir. Oturumun ön plan proses grubu da tcsetpgrp POSIX fonksiyonuyla değiştirilebilir. #include pid_t tcgetpgrp(int fd); int tcsetpgrp(int fd, pid_t pgid_id); Fonksiyonlar terminal aygıt sürücüsüne ilişkin dosya betimleyicileri ile çalışmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Oturumun arka plan bir prosesi, prosesin ilişkin olduğu terminalden (controlling terminal) okuma yapmak isterse terminal sürücüsü o arka plan prosesin içinde bulunduğu proses grubuna SIGTTIN sinyalini göndermektedir. Bu sinyalin default eylemi (default action) prosesin durdurulmasıdır. Bu biçimde durdurulmuş olan prosesler SIGCONT sinyali ile yeniden çalıştırılmak istenebilir. Ancak yeniden okuma yapılırsa yine proses durdurulacaktır. Bu tür prosesler kabuk üzerinden fg %n komutuyla ön plana çekilebilir. Bu durumda kabuk önce prosesin proses grubunu ön plan proses grubu yapar sonra da onu SIGCONT sinyali ile yeniden proses grubunu çalışır duruma getirir. Aşağıdaki programı komut satırında sonuna & getirerek çalıştırınız. Program 10 saniye sonra stdin dosyasından okuma yapmaya çalışacak ve bu nedenden dolayı SIGTTIN sinyali gönderilerek durdurulacaktır. Prosesin durdurulmuş olduğunu "ps -l" komutu ile ya da "ps -o stat,cmd" komutuyla gözlemleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include int main(void) { int ch; sleep(10); ch = getchar(); putchar(ch); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Arka plan proses grubundaki bir proses SIGTTIN sinyalini işleyebilir. Bu durumda proses durdurulmaz. Ancak tabii eğer bu sırada sinyal yeniden başlatılabilir (SA_RESTART) biçimde set edilmemişse read fonksiyonu EINTR errno değeriyle başarısız olacaktır. Eğer SIGTTIN sinyali yeniden başlatılabilir biçimde (yani SA_RESTART bayrağı kullanılarak) set edilmişse bu durumda read fonksiyonu çekirdek tarafından yeniden başlatılacak ve yeniden aynı sinyal oluşacaktır. Dolayısıyla program sonsuz döngüye girecektir. Aşağıda programın sonuna & getirerek çalıştırıp log dosyasını inceleyiniz. Programda stdin dosyasından okuma yapılmak istendiğinde SIGTTIN sinyali oluşacak ve read fonksiyonu EINTR errno değeri ile başarısız olacaktır. Bu örnekte sigaction fonksiyonunda sinyalin SA_RESTART özelliği kullanılmadan set edildiğine dikkat ediniz. Eğer biz sinyal fonksiyonunu "otomatik olarak yeniden başlatılabilir biçimde" set etmiş olsaydık read fonksiyonu tekrar tekrar başarısız olacak ve sürekli bir biçimde sinyal fonksiyonu çağrılacaktı. Arka plan proseslerin stdout dosyasına yazmaya çalıştığındaki özel durum izleyen paragrafta ele alınmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void sigttin_handler(int sno); void exit_sys(const char *msg); int main(void) { struct sigaction sa; char ch; sa.sa_handler = sigttin_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGTTIN, &sa, NULL) == -1) exit_sys("sigaction"); if (read(STDIN_FILENO, &ch, 1) && errno == EINTR) printf("read terminated by signal!...\n"); return 0; } void sigttin_handler(int sno) { printf("SIGTTIN occurred!...\n"); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Arka plan proses grubundaki bir prosesin ilişkin olduğu terminale (process controlling terminal) bir şeyler yazmaya çalışması da uygun değildir. Bu durumda da terminal sürücüsü prosesin ilişkin olduğu arka plan proses grubuna SIGTTOU sinyalini göndermektedir. Bu sinyalin default eylemi yine prosesin durdurulmasıdır. Ancak bu sinyalin aygıt sürücüsü tarafından arka plan proses grubuna gönderilmesi için Linux'ta terminalin TOSTOP modunda olması gerekir. Eğer terminal bu modda değilse SIGTTOU sinyali gönderilmemektedir. Bu durumda write fonksiyonu işlemini başarıyla sonlandırabilecektir. Terminal sürücüsünün default davranışını şöyle öğrenebilirsiniz: $ stty -a speed 38400 baud; rows 27; columns 90; line = 0; intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = ; eol2 = ; swtch = ; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0; -parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel iutf8 opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc Burada -tostop SIGTTOU sinyalinin gönderilmeyeceğini belirtmektedir. Arka plan proseslerin terminal sürücüsüne bir şeyler yazdığında SIGTTOU sinyalini göndermesini istiyorsanız şu komutu uygulamalısınız: $ stty tostop Artık aynı komutu uyguladığımızda ilgili seçenek "-tostop" yerine "tostop" biçiminde görüntülenecektir. Terminal aygıt sürücüsünün sinyal göndermemesini sağlamak için ise aşağıdaki komutu uygulayabilirsiniz: $ stty -tostop Yukarıdaki komutları da kullanarak aşağıdaki programı sonuna & getirerek arka plan proses grubu olarak çalıştırmayı deneyiniz ve durumu gözlemleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { if (write(STDOUT_FILENO, "test\n", 5) == -1) exit_sys("write"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Eskiden UNIX/Linux sistemlerine RS232 gibi seri haberleşme arayüzleriyle terminaller bağlanırdı. Programcılar ve kullanıcılar da bu terminalleri kullanarak başka bir odadan ya da uzaktan modem ile bağlanarak işlemlerini yapardı. Bu eski terminaller bilgisayar gibi değildi. Yalnızca ekran ve klavyeden oluşuyordu. Bunlara o zamanlar "aptal terminaller (dummy terminals)" deniliyordu. Teknoloji gelişince bu aptal terminaller ortadan kalktı. Artık uzaktan bağlanma için aptal terminaller yerine aynı zamanda kendisi bilgisayar olan akıllı terminaller kullanılmaya başlandı. Sonra uzaktan kablolu bağlantı büyük ölçüde teknoloji dışı kaldı. Bağlantılar genellikle (ssh gibi protokollerle) uzaktan yapılır hale geldi. Bugün kullandığımız bilgisayarlarda eski terminallerin simüle edilmesini sağlayan iki temel mekanizma bulunmaktadır. Bunlardan birisi "Ctrl+F+N" tuşlarına basılarak açılan terminallerdir. Bu terminallere genellikle "sanal terminaller (virtual terminals)" denilmektedir. Diğeri ise GUI ortamında pencere yöneticilerinden açılan terminal pencereleridir. Bunlara ise "sahte terminaller (pseudo terminals)" denilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Terminal bağlantısı koptuğunda ya da sahte terminal penceresi kapatıldığında terminal aygıt sürücüsü o terminalin ilişkin olduğu oturumun liderine (aslında oturum liderinin proses grubuna) SIGHUP sinyali göndermektedir. Tipik komut satırlı çalışmada oturum lideri kabuk programıdır. Dolayısıyla terminal penceresi kapatıldığında SIGHUP sinyali kabuk programına (örneğin bash programına) gönderilmektedir. SIGHUP sinyalinin default eylemi prosesin sonlandırılmasıdır. Eğer terminalin ilişkin olduğu (controlling terminal) oturumun lideri bu sinyali "ignore" ederse bu durumda terminalden yapılacak okumalarda read fonksiyonu 0 ile (yani sanki EOF durumu oluşmuş gibi) geri dönmekte write fonksiyonu da EIO errno değeri ile başarısız olmaktadır. Terminal bağlantısı koptuğunda ya da sahte terminal penceresi kapatıldığında terminal aygıt sürücüsünün kabuk programına SIGHUP sinyali gönderdiğini belirtmiştik. İşte kabuk programları da bu SIGHUP sinyalini işleyerek oturumdaki tüm proses gruplarına SIGHUP sinyali göndermektedir. Sonuç olarak terminal bağlantısı koptuğunda ya da terminal penceresi kapatıldığında oturumdaki tüm proseslere SIGHUP sinyali gönderilmiş olur ve default durumda oturumun tüm prosesleri bu biçimde sonlandırılır. Ancak burada ince bir nokta da vardır. Oturumun arka plan bir proses grubundaki proses terminalden okuma yapmak istediğinde ona SIGTTIN sinyalinin gönderildiğini bu sinyalin de default durumda prosesi durdurduğunu (stop ettirdiğini) belirtmiştik. İşte terminal bağlantısı koptuğunda ya da terminal penceresi kapatıldığında kabuk programı tüm prposes gruplarına SIGHUP sinyalini gönderdiğinde arka plandaki durdurulmuş olan prosesler bu durumda sonlanmayacaktır. (Durdurulmuş bir prosesin SIGKILL dışında bir sinyali işlemediğini, o sinyal gönderilse bile "pending" durumda kaldığını anımsayınız.) İşte bunun için kabuk programları (ancak tüm kabuk programları değil) durdurulmuş proseslerin bulunduğu proses gruplarına yalnızca SIGHUP sinyalini değil, aynı zamanda SIGCONT sinyalini de göndermektedir. Böylece bu durdurulmuş prosesler çalışmaya başlar başlamaz sonlandırılmaktadır. Pekiyi terminale ilişkin proses (controlling process) sonlanırsa ne olacaktır? İşte bu durumda çekirdek oturuma ilişkin tüm prosesleri terminalden koparmaktadır ve oturumun ön plan proses grubuna SIGHUP sinyali göndermektedir. Tabii tipik olarak terminali kontrol eden proses kabuk programı olduğu için kabuk programları bu tür durumlarda özel işlemler uygulamaktadır. Örneğin bash programından "exit" komutu ile çıkmak isterseniz "bash" programı eğer arka planda durdurulmuş prosesler varsa bir uyarı mesajı çıkartmaktadır. Örneğin: $ cat & [1] 26338 $ exit exit Durmuş işler var. [1]+ Durdu cat $ Ancak bu tür durumlarda üst üste iki kez exit yapıldığında artık "bash" oturumun tüm ön plan ve arka plan proses gruplarına SIGHUP sinyali göndererek onları sonlandırmaktadır. Tabii durdurulmuş proseslere ilişkin proses grupları için aynı zamanda SIGCONT sinyalini de göndermektedir. Terminal kapatıldığında ya da kabuk programından çıkıldığında o terminalde çalışan programların çalışmasına devam etmesi isteniyorsa bunun için "nohup" ve "disown" isimli programlardan faydalanılmaktadır. nohup programı çalıştırdığı programın SIGHUP sinyalini "ignore" eder, disown ise prosesi oturumdan koparır. Örneğin: $ nohup ./sample & Burada terminal kapatılsa bile bu prosesler çalışmaya devam edecektir. nohup programı stdout dosyasını "nohup.out" isimli bir dosyaya yönlendirmektedir. Bu iki komut hakkında ayrıntılı açıklamalar için dokümanlara başvurabilirsiniz. Pekiyi terminal bağlantısı koptuğunda ya da terminal penceresi kapatıldığında oluşan SIGHUP sinyali "ignore" edilirse ne olur? İşte bir proses ister ön planda isterse arka planda çalışıyor olsun eeğer SIGHUP sinyalini "ignore" ederse terminal okumasında read fonksiyonu sanki EOF durumu oluşmuş gibi 0 değeri ile geri dönmektedir. Yine ister ön planda isterse arka planda çalışıyor olursa olsun SIGHUP sinyali "ignore" edildiğinde terminale yazma yapılırsa write fonksiyonu EIO errno değeri ile başarısız olmaktadır. Bu durum terminalin ilişkin olduğu proses için de (yani kabul prosesi için de) böyledir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Terminal ve oturum konusu ile ilgili diğer bir konu da "öksüz proses grupları (orphan process groups)" konusudur. Anımsanacağı gibi öksüz proses "kendisi devam ettiği halde üst prosesi sonlanmış olan proseslere" deniyordu. "Bir proses grubundaki her prosesin üst prosesi o proses grubundaysa ya da aynı oturumda değilse" böyle proses gruplarına öksüz proses grupları denilmektedir. Bu tanım kişilere biraz karışık gibi gelmektedir. De Morgan kuralına göre bunun değili alınırsa belki daha sade bir tanım elde edilecektir: "Eğer bir proses grubundaki en az bir prosesin üst prosesi aynı oturumda ancak farklı bir proses grubunda bulunuyorsa" o proses grubu öksüz değildir. Örneğin kabuktan bir program çalıştırmış olalım. Program da birkaç kez fork yapıp alt proses oluşturmuş olsun. Şimdi bu proses grubu öksüz değildir. Çünkü bu gruptaki tüm proseslerin üst prosesleri aynı gruptadır ya da aynı oturumdadır. Kabuktan çalıştırılan prosesin üst prosesinin kabuk olduğuna ve onun da aynı oturumda olduğuna dikkat ediniz. Şimdi kabuktan bir program çalıştıralım. Bu program fork işlemi yapıp kendisini sonlandırsın. Bu durumda alt prosesin üst prosesi "init" prosesi olacaktır. Böylece proses grubundaki söz konusu alt prosesin üst prosesi aynı grupta değildir. Yukarıdaki örneği yinelemek istiyoruz. Biz kabuktan "./sample" programını çalıştıralım. Kabuk bu komut için bir ön plan proses grubu oluşturacaktır. sample prosesi de bu ön plan proses grubunun lideri olacaktır. Şimdi biz bu sample prosesi içerisinde fork yapıp üst prosesi sonlandırırsak proses grubu yaşamaya devam eder ancak öksüz durumda olur. Çünkü alt prosesin artık üst prosesi init olacağı için init prosesi de aynı oturumda olmadığından yukarıdaki tanım sağlanmış olacaktır. Aşağıdaki örnekte "sample" programında bir kez fork yapılıp üst proses sonlandırılmıştır. ps komutuyla oluşan duruma dikkat ediniz: #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) _exit(EXIT_SUCCESS); alarm(30); // 30 saniye sonra proses sonlanacak pause(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Burada ps komutuyla elde edilen çıktılara bakınız: $ ps -o pid,ppid,pgid,sid,cmd PID PPID PGID SID CMD 26667 26642 26667 26667 bash 27410 1136 27409 26667 ./sample 27411 26667 27411 26667 ps -o pid,ppid,pgid,sid,cmd $ ps -p 1136,1 -o pid,ppid,pgid,sid,cmd PID PPID PGID SID CMD 1 0 1 1 /sbin/init splash 1136 1 1136 1136 /lib/systemd/systemd --user Buradan elde edilen değerlere bakıldığında üst prosesi sonlanmış prosesin üst prosesinin 1136 pid değerine sahip olan systemd isimli proses olduğu, systemd prosesinin de üst prosesinin init olduğu anlaşılmaktadır. Biz bir prosesin üst prosesi sonlandığında onun üst prosesinin init olacağını söylemiştik. Ancak günümüzdeki systemd init paketlerinde bu durum yukarıdaki gibi biraz farklıdır. Bu konu "servislerin (daemons) anlatıldığı" bölümde ele alınacaktır. Öksüz proses grubu tanımına dikkat edilirse aslında kabuk programının içinde bulunduğu proses grubunun da öksüz olduğu görülmektedir: $ ps -o pid,ppid,pgid,sid,cmd PID PPID PGID SID CMD 26667 26642 26667 26667 bash 27434 26667 27434 26667 ps -o pid,ppid,pgid,sid,cmd $ ps -p 26642 -o pid,ppid,pgid,sid,cmd PID PPID PGID SID CMD 26642 1136 26642 26642 /usr/libexec/gnome-terminal-server Burada bash kabuk programının üst prosesinin aynı oturuma dahil olmadığını görüyorsunuz. O halde bash prosesi de aslında oturumdaki öksüz bir proses grubundadır. Öksüz proses gruplarında şöyle bir ayrıntı da vardır: Kabuk programları terminal bağlantısı koptuğunda ya da sahte terminal penceresi kapatıldığında oturumun öksüz proses gruplarına SIGHUP sinyali göndermemektedir. Ancak bir proses grubu öksüz hale geldiğinde eğer o proses grubu içerisinde durdurulmuş olan (stop edilmiş olan) bir proses varsa terminal aygıt sürücüsü öksüz hale gelmiş olan bu proses grubuna SIGHUP ve SIGCONT sinyalleri göndermektedir. Öksüz proses gruplarındaki proseslerin kabuk programı sonlansa bile yaşamaya devam edeceğine dikkat ediniz. Pekiyi öksüz proses grubundaki bir proses terminalden okuma yapmaya çalışırsa ya da terminale yazma yapmaya çalışırsa ne olur? İşte bu durumda read fonksiyonu EIO errno değeri ile başarısız olmaktadır. write fonksiyonu ise terminal aygıt sürücünün TOSTOP ayarı aktif değilse normal olarak terminale yazmakta eğer aktif ise o da EIO errno değeri ile başarısız olmaktadır. Örneğin biz üst prosesi sonlanmış bir alt proseste terminalden okuma yapmaya çalışırsak read fonksiyonu başarısız olacaktır. Ancak terminale yazma yapmaya çalışırsak terminalin TOSTOP ayarında göre ya yazdıklarımız ekrana çıkacak ya da write fonksiyonu EIO errno değeri ile başarısız olacaktır. (Michael Kerrisk'in "The Linux Programming Environment" kitabının 730'uncu sayfasında sanki öksüz proses grubundaki proseslerin terminale yazma yapması durumunda write fonksiyonunun EIO errno değeriyle başarısız olacağı gibi bir cümle edilmiştir. Halbuki bu durum terminalin TOSTOP ayarı ile ilgilidir.) Öksüz proses grubundaki proseslere (arka planda çalıştırılsın ya da çalıştırılmasın) terminalden okuma yaptığında SIGTTIN sinyalinin gönderilmediğine, terminalin TOSTOP ayarı ne olursa olsun terminale yazma yapıldığında da SIGTTOU sinyalinin gönderilmediğine dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 82. Ders 17/09/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir thread'in akışını belli bir süre bekletmek için işletim sistemlerinde sleep fonksiyonları bulundurulmaktadır. Bu fonksiyonlar thread'i bloke ederek çalışma kuyruğundan çıkartır ve özel sleep kuyruklarına yerleştirir. İstenen zaman dolduğunda yeniden thread çalışma kuyruğuna yerleştirilir. Böylece istenilen miktarda bekleme CPU zamanı harcanmadan sağlanmış olur. UNIX türevi sistemlerde ilk zamanlardan beri sleep isimli bir bekleme fonksiyonu bulunmaktadır. Bu fonksiyon saniye cinsinden bir duyarlılığa sahiptir. Günümüzde saniye düşük bir çözünürlük durumuna gelmiştir. sleep fonksiyonunun prototipi şöyledir: #include unsigned sleep(unsigned seconds); Fonksiyon parametre olarak beklenecek saniye sayısını almaktadır. Fonksiyonun geri dönüş değeri sinyal dolayısıyla erken sonlanmada kalan saniye değerini belirtmektedir. Fonksiyonun 0 ile geri dönmesi normal bir sonlanma anlamına gelmektedir. Ancak fonksiyonun geri döndürdüğü değerin de saniye duyarlılığında olması kalan zaman hakkında detaylı bilgi verememektedir. sleep fonksiyonu sinyal geldiğinde hiçbir zaman otomatik yeniden çalıştırılmaz. Yani örneğin biz bir sinyali SA_RESTART bayrağı ile set etmiş olalım ve o anda sleep fonksiyonunda bekliyor olalım. İlgili sinyal geldiğinde sleep yeniden başlatılmaz. Programcılar genel olarak sleep fonksiyonunun geri dönüş değeri ile ilgilenmezler. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz daha önceki kodlarımızda kullanım kolaylığından dolayı kullanmış olsak da aslında usleep fonksiyonu bir POSIX fonksiyonu değildir. Ancak Linux ve bazı UNIX türevi sistemlerde glibc kütüphanesinin içerisinde bulunmaktadır. usleep fonksiyonu mikrosaniye çözünürlüğüne sahiptir. Prototipi şöyledir: #include int usleep(useconds_t usec); Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyon sinyalden dolayı başarısız olursa yeniden başlatılmaz ve errno değişkeni EINTR değeriyle set edilir. Fonksiyona uygunsuz argüman girildiğinde errno değişkeni EINVAL değeriyle set edilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Nanosaniye çözünürlüğe sahip ismine nanosleep denilen bir POSIX fonksiyonu da bulunmaktadır. Bu fonksiyon sonradan POSIX standartlarına eklenmiştir. Fonksiyonun prototipi şöyledir: #include int nanosleep(const struct timespec *rqtp, struct timespec *rmtp); Buradaki timespec yapısını daha önce de kullanmıştık. Bu yapı dosyası içerisinde aşağıdaki gibi bildirilmiştir: struct timespec { time_t tv_sec; long tv_nsec; }; nanosleep fonksiyonu da bir sinyal oluştuğunda sinyal fonksiyonu set edilmişse başarısız olur ve errno değeri EINTR ile set edilir. Bu durumda bekleme için kalan süre fonksiyonun ikinci parametresiyle belirtilen timespec yapısının içerisine yerleştirilmektedir. Fonksiyonun ikinci parametresi NULL adres girilebilir. Bu durumda bu yerleştirme yapılmaz. Birinci parametreyle ikinci parametreye aynı nesnenin adreslerinin girilmesinde de bir sakınca yoktur. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Aşağıdaki örnekte 3.5 saniyelik bir sleep uygulanmıştır. 1 saniyenin bir milyar nanosaniyeden oluştuğuna dikkat ediniz. Bu durumda yarım saniye 500000000 nanosaniyedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { struct timespec ts; printf("sleeping 3.5 seconds...\n"); ts.tv_sec = 3; ts.tv_nsec = 500000000; if (nanosleep(&ts, NULL) == -1) exit_sys("nanosleep"); printf("ok...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte SIGINT sinyali set edilmiş ve nanosleep fonksiyonu ile 10.5 saniye bekleme yapılmıştır. Bu programı iki durumda test ediniz. Birincisi klavyeden hiçbir tuşa basmadan zamanın dolmasını bekleyiniz. Bu durumda kalan zaman 0 olacaktır. İkinci durumda programı çalıştırdıktan sonra Ctrl+C tuşlarına basarak SIGINT sinyali oluşturunuz. Bu durumda kalan zamanın sıfırdan büyük olduğunu göreceksiniz. Örnek iki denemenin sonuçları şöyledir: $ ./mample sleeping 10.5 second... Left second: 0 Left nanosecond: 0 $ ./mample sleeping 10.5 second... ^Csignt handler... Left second: 9 Left nanosecond: 464386077 ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void sigint_handler(int sig); void exit_sys(const char *msg); int main(void) { struct timespec ts, tsleft; struct sigaction sa; sa.sa_handler = sigint_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGINT, &sa, NULL) == -1) exit_sys("sigaction"); printf("sleeping 10.5 second...\n"); ts.tv_sec = 10; ts.tv_nsec = 500000000; if (nanosleep(&ts, &tsleft) == -1 && errno != EINTR) exit_sys("nanosleep"); printf("Left second: %ju\n", (intmax_t)tsleft.tv_sec); printf("Left nanosecond: %ld\n", (intmax_t)tsleft.tv_nsec); return 0; } void sigint_handler(int sig) { printf("sigint handler...\n"); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- nanosleep fonksiyonu gerçek zamana (CLOCK_REALTIME) göre bekleme yapmaktadır. Beklemenin değişik zamanlamalara göre (bunlara saat (clock) da denilmektedir) yapılabilmesi için clock_nanosleep fonksiyonu POSIX standartlarına eklenmiştir. Fonksiyonun prototipi şöyledir: #include int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *rqtp, struct timespec *rmtp); Fonksiyonun birinci parametresi beklemede kullanılacak saatin cinsini belirtmektedir. POSIX standartlarında şu saat cinsleri bulunmaktadır: CLOCK_REALTIME: Bu saat kullanılırsa bekleme sistem zamanının değiştirilmesinden etkilenebilmektedir. Örneğin 30 saniye beklemek isterken sistem zamanı ileri alınırsa daha az bekleme söz konusu olabilmektedir. CLOCK_MONOTONIC: Sistem zamanının değiştirilmesinden ve diğer faktörlerden etkilenmeyen göreli bir saattir. Monotonic saat kararlı beklemeler için tercih edilmesi gereken saattir. CLOCK_PROCESS_CPUTIME_ID: Bu saat proses zamanının ölçülmesinde kullanılan saattir. Genel olarak bu saat timer tick'lerle ilerletilmektedir. CLOCK_THREAD_CPUTIME_ID: Bu saat de thread zamanının ölçülmesinde kullanılan saattir. Genel olarak bu saat de timer tick'lerle ilerletilmektedir. Linux sistemlerine özgü CLOCK_TAI ve CLOCK_BOOTTIME gibi başka saatler de bulunmaktadır. Fonksiyonun ikinci parametresine (flags) ya 0 ya da TIMER_ABSTIME değeri geçilebilir. Eğer bu parametreye TIMER_ABSTIME değeri geçilirse bu durumda bekleme göreli zaman ile değil, mutlak zaman ile yapılmaktadır. Yani başka bir deyişle bu durumda bekleme miktarını belirten timespec yapısında beklemenin sonlandırılacağı mutlak zaman bilgisi bulunmalıdır. (Aslında biz mutlak zamanlı beklemeleri thread konusunda bazı senkronizasyon nesnelerinin zaman aşımlı biçimlerini anlatırken görmüştük.) Mutlak zaman beklemesi için önce o andaki zaman bilgisinin clock_gettime fonksiyonuyla alınıp üzerine ekleme yapılması gerekmektedir. Örneğin: if (clock_gettime(CLOCK_REALTIME, &ts) == -1) exit_sys("clock_gettime"); ts.tv_sec += 10; Fonksiyonun üçüncü parametresi bekleme zamanını, dördüncü parametresi ise işlemin sinyal dolayısıyla sonlanması durumunda kalan zamanı belirtmektedir. Bu son parametre yine NULL adres geçilebilir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Aşağıda daha önce nanosleep fonksiyonu için yapılan örneğin clock_nanosleep kullanan biçimi verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void sigint_handler(int sig); void exit_sys(const char *msg); int main(void) { struct timespec ts, tsleft; struct sigaction sa; sa.sa_handler = sigint_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGINT, &sa, NULL) == -1) exit_sys("sigaction"); printf("sleeping 10.5 second...\n"); ts.tv_sec = 10; ts.tv_nsec = 500000000; if (clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, &tsleft) == -1 && errno != EINTR) exit_sys("clock_nanosleep"); printf("Left second: %ju\n", (intmax_t)tsleft.tv_sec); printf("Left nanosecond: %ld\n", (intmax_t)tsleft.tv_nsec); return 0; } void sigint_handler(int sig) { printf("sigint handler...\n"); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Şu andaki almak için ve zaman ölçmek için zaten C'de prototipleri içerisinde olan standart C fonksiyonları bulundurulmuştur. Ancak bu standart C fonksiyonları genel olarak düşük bir çözünürlüğe sahiptir. Bu fonksiyonlar C Programlama Dili kurslarında ele alındığı için biz yalnızca bir özet yapacağız. time isimli standart C fonksiyonu epoch'tan geçen (epoch göreli orijini belirten bir terimdir) zamanı time_t türünden vermektedir. Prototipi şöyledir: time_t time(time_t *timer); Buradaki time_t türü C standartlarına göre nümerik herhangi bir tür olabilmektedir. (Örneğin double ya da float da olabilmektedir.) Ancak POSIX standartlarında bu türün bir tamsayı türü olması gerektiği belirtilmiştir. Ayrıca C standartlarında "epoch" belirtilmemiştir. POSIX standartlarında epoch 01/01/1970 : 00:00:00 olarak belirlenmiştir. (Genel olarak C derleyicilerinin hemen hepsi zaten epoch olarak bu tarihi almaktadır.) localtime fonksiyonu time_t değerini alarak bunu bileşenlerine ayrıştırır ve struct tm yapısı biçiminde bize verir. Fonksiyonun prototipi şöyledir: struct tm *localtime(const time_t *timer); gmtime fonksiyonu localtime fonksiyonunun tarih ve zamanı UTC olarak (eski adıyla GMT) veren biçimidir. Fonksiyonun prototipi şöyledir: struct tm *gmtime(const time_t *timer); Türkiye'nin yerel saati UTC'ye göre "day light saving" durumuna bağlı olarak +2 ya da +3 durumundadır. ctime ve asctime fonksiyonları doğrudan tarih ve zamanı bize bir yazı olarak vermektedir. Bu iki fonksiyon arasındaki tek fark ctime fonksiyonu time_t parametresi alırken asctime fonksiyonunun struct tm parametresi almasıdır. Bu fonksiyonların prototipleri şöyledir: char *ctime(const time_t *timer); char *asctime(const struct tm *timeptr); mktime fonksiyonu epoch'tan (POSIX'te 01/01/1970'ten) belli bir tarih zamana kadar geçen zamanın elde edilmesinde kullanılmaktadır: time_t mktime(struct tm *timeptr); Programcı bir struct tm nesnesi tanımlar. Onun içini doldurur ve fonksiyona verir, fonksiyon da epoch'tan geçen zamanı time_t türünden vermektedir. C standartlarında epoch ve time_t türü açıkça belirtilmediği için iki time_t değerinin çıkartılması için difftime fonksiyonu bulundurulmuştur: double difftime(time_t time1, time_t time0); POSIX sistemlerinde zaten time_t saniye belirttiği için bu fonksiyonun kullanılmasına gerek kalmamaktadır. strftime fonksiyonu adeta snprintf fonksiyonunun tarih zaman üzerinde formatlama yapan bir biçimi gibidir. Fonksiyonun prototipi şöyledir: size_t strftime(char *s, size_t maxsize, const char *format, const struct tm *timeptr); Format karakterlerinin neler olduğu ilgili dokümanlardan görülebilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- C'de programın iki noktası arasındaki zamanı ölçmek için iki yöntem kullanılabilmektedir. Birincisi time fonksiyonu ile zamanı elde edip bunları çıkartmak olabilir. Örneğin: time_t start, stop; ... start = time(NULL); ... ... ... stop = time(NULL); POSIX standartlarında bu yöntem ancak saniye duyarlılıkta ölçüm yapılmasına izin vermektedir. C standartlarında time fonksiyonunun ne verdiği de belirsizdir. Dolayısıyla C standartlarında taşınabilir bir biçimde bu fonksiyonla zaman ölçmek aslında mümkün değildir. İkinci yöntem clock fonksiyonunu kullanmaktır. clock fonksiyonunun prototipi şöyledir: #include clock_t clock(void); Fonksiyon bize clock_t türünden bir timer tick sayısı verir. Ancak bir saniyenin kaç tick'ten oluştuğu CLOCKS_PER_SEC sembolik sabitiyle define edilmiştir. O halde programcı programın iki noktası arasında clock fonksiyonunu çağırıp bunları çıkartıp sonucu CLOCKS_PER_SEC değerine bölerse geçen zamanı elde edebilir. Örneğin: clock_t start, stop; double result; start = clock(); ... ... ... stop = clock(); result = (double)(stop - start) / CLOCKS_PER_SEC; Güncel Linux sistemlerinde CLOCKS_PER_SEC sembolik sabiti 1000000 olarak define edilmiştir. Dolayısıyla bu yöntemle Linux sistemlerinde mikrosaniye duyarlılıkta zaman ölçülebilir. Windows sistemlerinde ise CLOCKS_PER_SEC sembolik sabiti 1000 değerindedir. Yani bu yöntemle milisaniye duyarlılıkta ölçüm yapılabilmektedir. clock_t türünün C standartlarında ve POSIX standartlarında nümerik bir tür olarak (yani tamsayı ya da gerçek sayı türü) typedef edilmesi gerektiği belirtilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 83. Ders 23/09/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- POSIX standartlarında zaman ölçmek için en uygun fonksiyon clock_gettime fonksiyonudur. Biz zaten bu fonksiyonu daha önce senkronizasyon nesnelerinde zaman aşımlı bekleme yapmak için kullanmıştık. Fonksiyonun prototipi şöyledir: #include int clock_gettime(clockid_t clock_id, struct timespec *tp); Fonksiyonun birinci parametresi zaman ölçümünde kullanılacak saatin türünü almaktadır. Bu tür şunlardan biri olabilir: CLOCK_REALTIME CLOCK_MONOTONIC CLOCK_PROCESS_CPUTIME_ID CLOCK_THREAD_CPUTIME_ID Biz daha önce CLOCK_REALTIME ile CLOCK_MONOTONIC arasındaki farkı belirtmiştik. CLOCK_REALTIME sistem zamanının değişmesinden etkilenebilecek bir saati belirtirken CLOCK_MONOTONIC sistem zamanının değişmesinden etkilenmeyecek stabil bir saati temsil ediyordu. CLOCK_PROCESS_CPUTIME_ID belli bir prosesin tüm thread'lerinin harcadığı CPU zamanını ölçmek için, CLOCK_THREAD_CPUTIME_ID ise belli bir thread'in harcadığı CPU zamanını ölçmek için kullanılmaktadır. Fonksiyonun ikinci parametresi timespec yapı nesnesinin adresini almaktadır. Zaman bilgisi bu nesnenin içerisine yerleştirilmektedir. Fonksiyon başarı durumunda 0, başarısızlık durumunda -1 değerine geri dönmektedir. timespec yapısını yeniden anımsatmak istiyoruz: struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; Aşağıda clock_gettime ile programın iki noktası arasındaki zaman ölçümüne bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { struct timespec ts1, ts2; long long elapsed_time; if (clock_gettime(CLOCK_MONOTONIC, &ts1) == -1) exit_sys("clock_gettime"); for (int i = 0; i < 2000000000; ++i) ; if (clock_gettime(CLOCK_MONOTONIC, &ts2) == -1) exit_sys("clock_gettime"); elapsed_time = (ts2.tv_sec * 1000000000LL + ts2.tv_nsec) - (ts1.tv_sec * 1000000000LL + ts1.tv_nsec); /* elsapsed_time = (ts2.tv_sec - ts1.tv_sec) * 1000000000.0 + ts2.tv_nsec - ts1.tv_nsec; */ printf("Elapsed Nanosecond: %lld\n", elapsed_time); printf("Elapsed Second: %f\n", elapsed_time / 1000000000.); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- clock_gettime fonksiyonu ile elde edilen zaman her ne kadar nano saniye mertebesindeki timespec yapısının içerisine yerleştiriliyorsa da ölçüm duyarlılığı umulan kadar yüksek olmayabilir. Çünkü ölçümün duyarlılığı işlemciye ve işletim sisteminin çekirdeğine bağlı olabilmektedir. İşte ölçümün duyarlılığı ayrıca clock_getres fonksiyonu ile elde edilebilmektedir. #include int clock_getres(clockid_t clock_id, struct timespec *res); Fonksiyonun birinci parametresi duyarlılığı ölçülecek olan saat türünü belirtmektedir. İkinci parametre ise duyarlılığın yerleştirileceği timespec yapısının adresini belirtmektedir. Aşağıda fonksiyonun kullanımına bir örnek verilmiştir. Kursun yürütüldüğü sanal makinede bu fonksiyon 1 nanonasiye duyarlılık belirtmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { struct timespec ts; long long result; if (clock_getres(CLOCK_MONOTONIC, &ts) == -1) exit_sys("clock_getres"); result = (ts.tv_sec * 1000000000LL + ts.tv_nsec); printf("%lld\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- clock_gettime fonksiyonunda clockid_t olarak CLOCK_PROCESS_CPUTIME_ID geçilirse prosesin yalnızca CPU'da harcadığı zamanların toplamı verilir. Bu muhtemelen gerçek zamandan daha kısa olacaktır. Eğer proses çok thread'ten oluşuyorsa bu hesaba tüm thread'lerin CPU zamanları dahil edilmektedir. Fakat clockid_t olarak CLOCK_THREAD_CPUTIME_ID verilirse bu da spesifik bir thread'in (fonksiyonu çağıran) CPU zamanını ölçmekte kullanılır. Aşağıdaki örnekte aslında programın iki noktası arasında geçen zaman 5 saniyeden daha yüksek olduğu halde ölçüm CLOCK_PROCESS_CPUTIME_ID ile yapıldığından ve yalnızca prosesin CPU'da harcadığı zamanın ölçülmesinden dolayı işlemden çok küçük bir değer elde edilecektir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { struct timespec ts1, ts2; long long elapsed_time; if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts1) == -1) exit_sys("clock_gettime"); for (int i = 0; i < 5; ++i) { for (int k = 0; k < 10000000; ++k) ; sleep(1); } if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts2) == -1) exit_sys("clock_gettime"); elapsed_time = (ts2.tv_sec * 1000000000LL + ts2.tv_nsec) - (ts1.tv_sec * 1000000000LL + ts1.tv_nsec); /* elsapsed_time = (ts2.tv_sec - ts1.tv_sec) * 1000000000.0 + ts2.tv_nsec - ts1.tv_nsec; */ printf("Elapsed Nanosecond: %lld\n", elapsed_time); printf("Elapsed Second: %f\n", elapsed_time / 1000000000.); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- clock_gettime fonksiyonunda eğer clock id olarak CLOCK_THREAD_CPUTIME_ID verilirse yalnızca fonksiyonu çağıran thread'in CPU'da harcadığı zaman elde edilmektedir. Tabii tek thread'li uygulamalarda CLOCK_PROCESS_CPUTIME_ID ile CLOCK_THREAD_CPUTIME_ID arasında bir fark oluşmayacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Ayrıca POSIX standartlarında clock_settime isimli bir fonksiyon da vardır. Bu fonksiyon söz konusu saati set etmek için kullanılmaktadır. Ancak set işlemi yalnızca uygun önceliğe sahip prosesler tarafından yapılabilmektedir. Her saat türü de set edilemeyebilmektedir. Fonksiyonun prototipi şöyledir: #include int clock_settime(clockid_t clock_id, const struct timespec *tp); Biz burada bu fonksiyonun kullanımına örnek vermeyeceğiz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir prosesin user mode'da ve kernel mode'da harcadığı zamanlar işletim sistemi tarafından proses kontrol bloğunda saklanmaktadır. Proses kontrol bloğundan bu bilgiler times isimli POSIX fonksiyonu ile (doğrudan ilgili sistem fonksiyonunu çağırmaktadır) elde edilebilmektedir. Fonksiyonun prototipi şöyledir: #include clock_t times(struct tms *buffer); Fonksiyon çağrıldığı ana kadarki prosesin zaman bilgisini alarak tms isimli bir yapı nesnesinin içerisine yerleştirmektedir. Fonksiyonun geri dönüş değeri belli bir orijinden geçen gerçek zamanı belirten bir değerdir. (Yani bu değer tek başına bir anlam taşımaz.) Fonksiyon başarısızlık durumunda -1 değerine geri dönmektedir. tms yapısı şöyledir: struct tms { clock_t tms_utime; /* user time */ clock_t tms_stime; /* system time */ clock_t tms_cutime; /* user time of children */ clock_t tms_cstime; /* system time of children */ }; Yapının tms_utime elemanı prosesin user mode'da harcadığı zamanı, tms_stime elemanı kernel mode'da harcadığı zamanı vermektedir. tms_cutime elemanı wait fonksiyonlarıyla beklenen tüm alt proseslerin (ve onların alt proseslerinin de) user mode'daki zamanlarını tms_cstime da wait fonksiyonu ile beklenen tüm alt proseslerin (ve onların alt proseslerinin de) kernel mode'daki zamanlarını vermektedir. Fonksiyonun verdiği zamanlar proses kontrol bloktan alınmaktadır. Proses kontrol bloğa da zamanlar "timer tick" olarak yazılmaktadır. Timer tick değerleri günümüzün masaüstü sistemlerinde 1 milisaniye, yavaş sistemlerde 10 milisaniye periyottadır. Bu fonksiyon prosesin bloke olup uykuda beklediği zamanları bize herhangi bir biçimde vermemektedir. Yalnızca prosesin ve alt proseslerin user mode'da ve kernel mode'da harcadığı zamanları bize vermektedir. Standard time kabuk komutu bu fonksiyon ve muhtemelen clock_gettime fonksiyonu kullanılarak gerçekleştirilmiştir. Aşağıdaki örnekte komut satırından alınan program çalıştırılmış ve onun user ve kernel mode'daki zamanı yazdırılmıştır. Ancak programın çalışması için gereken gerçek zaman yazdırılmamıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { struct tms tms; 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]) == -1) _exit(EXIT_FAILURE); if (wait(NULL) == -1) exit_sys("wait"); if (times(&tms) == -1) exit_sys("times"); printf("User time: %f\n", (double)tms.tms_cutime / CLOCKS_PER_SEC); printf("Kernel time: %f\n", (double)tms.tms_cstime / CLOCKS_PER_SEC); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bazen belli periyotta sürekli işlemlerin yapılması gerekebilmektedir. Örneğin ekrana canlı bir saat çıkartmak istediğimizi düşünelim. Saatimizin duyarlılığı da saniye cinsinden olsun. Biz saniyede bir periyodik olarak bir fonksiyonumuzun çağrılmasını sağlarsak bu işlemi kolaylıkla yapabiliriz. Bu tür periyodik timer mekanizmalarına "interval timer" da denilmektedir. POSIX standartlarında "interval timer" oluşturmak için iki fonksiyon bulundurulmuştur. Şüphesiz interval timer oluşturmanın basit bir yolu bir thread yaratmak ve o thread içerisinde bir döngü oluşturup clock_nanosleep gibi bir fonksiyonla bekleme yapmak olabilir. Ancak bunun için bir thread'e gereksinim duyulmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Interval timer setitimer isimli POSIX fonksiyonu ile basit bir biçimde oluşturulabilmektedir. Ancak POSIX standartlarına izleyen paragraflarda ele alacağımız daha yetenekli bir "interval timer" mekanizması eklendiği için bu fonksiyon "obsolete" yani "deprecated" yapılmıştır. Fonksiyonun prototipi şöyledir: #include int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); setitimer fonksiyonunda periyodik işleme başlamak için gereken zaman ile periyot zamanı fonksiyona ayrı ayrı verilmektedir. setitimer fonksiyonu ile oluşturulan "interval timer" mekanizmasının türleri vardır. Her tür, zaman dolduğunda farklı bir sinyalin oluşmasına yol açmaktadır. Fonksiyonun birinci parametresi interval timer'ın türünü belirtmektedir. Bu tür şunlardan biri olabilir: ITIMER_REAL: Bu gerçek zamana dayalı ölçüm yapar. Zaman dolduğunda SIGALRM sinyali gönderilmektedir. ITIMER_VIRTUAL: Buradaki ölçüm prosesin çalışmasına göre yapılmaktadır. Yani proses çalışmadığı sürece saat artmamaktadır. Bu türde zaman dolduğunda SIGVTALRM sinyali gönderilmektedir. ITIMER_PROF: Burada ölçüm yine prosesin çalışmasına göre yapılmaktadır. Ancak bekleme zamanları da buna dahil edilmektedir. Zaman dolduğunda SIGPROF sinyali gönderilmektedir. Fonksiyonun ikinci parametresi itimerval isimli bir yapı türündendir. Bu yapı şöyle bildirilmiştir: struct itimerval { struct timeval it_interval; /* next value */ struct timeval it_value; /* current value */ }; Yapının elemanlarının struct timeval türünden olduğuna dikkat ediniz. Bu yapı da şöyle bildirilmiştir: struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; itimerval yapısının it_value elemanı ilk periyoda kadar geçen zamanı, it_interval elemanı da periyodik zamanı belirtmektedir. Fonksiyonun üçüncü parametresi bir önceki itimer çağrısında set edilen değerin elde edilmesi için kullanılmaktadır. Bu parametre NULL geçilebilir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. setitemer ile oluşturulan interval timer'ın disable hale getirilmesi için fonksiyonun ikinci parametresindeki yapının it_value elemanı sıfırlanarak çağrılması gerekmektedir. Aşağıdaki örnekte interval timer ilk periyoda kadar 5 saniye bekleyecek biçimde oluşturulmuştur. Timer periyodu da 1 saniye olarak ayarlanmıştır. 10 kere sinyal oluşturulduktan sonra interval timer disable edilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include int g_count; void sigalrm_handler(int signo); void exit_sys(const char *msg); int main(void) { struct sigaction sa; struct itimerval itval; sa.sa_handler = sigalrm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGALRM, &sa, NULL) == -1) exit_sys("sigaction"); itval.it_value.tv_sec = 5; itval.it_value.tv_usec = 0; itval.it_interval.tv_sec = 1; itval.it_interval.tv_usec = 0; if (setitimer(ITIMER_REAL, &itval, NULL) == -1) exit_sys("setitimer"); for (;;) { pause(); ++g_count; if (g_count == 10) { itval.it_value.tv_sec = 0; itval.it_value.tv_usec = 0; if (setitimer(ITIMER_REAL, &itval, NULL) == -1) exit_sys("setitimer"); break; } } printf("timer disabled, sleeps for extra 5 seconds before finish...\n"); sleep(5); return 0; } void sigalrm_handler(int signo) { printf("interval timer...\n"); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 84. Ders 24/09/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Periyodik timer oluşturabilmek için kullanılan setitimer fonksiyonunun bazı yetersizlikleri şunlardır: - Haberdar etme mekanizması yalnızca sinyalle yapılmaktadır. - Kullanılan sinyal gerçek zamanlı olmayan sinyallerdir. Bunların da biriktirilmesi söz konusu değildir. - Sinyal oluştuğunda sinyal gerçek zamanlı olmadığı için sinyale iliştirilecek bir bilgi de yoktur. - Thread yoluyla haberdar edilme mekanizması yoktur. - Zaman duyarlılığı mikrosaniye mertebesindedir. İşte bu eksikliklerden dolayı POSIX'e yeni bir interval timer mekanizması eklenmiştir. Bu interval timer mekanizmasının kullanılması setitimer mekanizmasına göre daha zordur. Bu yeni mekanizmada ilk yapılacak şey bir interval timer nesnesinin timer_create fonksiyonu ile yaratılmasıdır. timer_create fonksiyonunun prototipi şöyledir: #include int timer_create(clockid_t clockid, struct sigevent *evp, timer_t *timerid); Fonksiyonun birinci parametresi zamanlamada kullanılacak saatin türünü belirtmektedir. Bu tür daha önce gördüğümüz CLOCK_MONOTONIC, CLOCK_REALTIME, CLOCK_PROCESS_CPUTIME_ID, CLOCK_THREAD_CPUTIME_ID olabileceği gibi clock_getcpuclockid ve pthread_getcpuclockid fonksiyonlarından elde edilen clock id'ler kullanılabilmektedir. Fonksiyonun ikinci parametresi sigevent isimli bir yapı nesnesinin adresini almaktadır. sigevent yapısı şöyle bildirilmiştir: #include union sigval { /* Data passed with notification */ int sival_int; /* Integer value */ void *sival_ptr; /* Pointer value */ }; struct sigevent { int sigev_notify; /* Notification method */ int sigev_signo; /* Notification signal */ union sigval sigev_value; /* Data passed with notification */ void (*sigev_notify_function)(union sigval); /* Function used for thread notification (SIGEV_THREAD) */ void *sigev_notify_attributes; /* Attributes for notification thread (SIGEV_THREAD) */ pid_t sigev_notify_thread_id; /* ID of thread to signal (SIGEV_THREAD_ID); Linux-specific */ }; Yapının sigev_notify elemanı periyot dolduğunda haberdar edilmenin nasıl yapılacağını belirtmektedir. Bu elemana şu değerlerden biri yerleştirilebilir: SIGEV_NONE: Haberdar edilme yapılmaz. SIGEV_SIGNAL: Haberdar edilme sinyal yoluyla yapılır. SIGEV_THREAD: Haberdar edilme bu mekanizma tarafından yaratılan bir thread tarafından yapılır. SIGEV_THREAD_ID: Haberdar edilme prosesin spesifik bir thread'i ile sinyal yoluyla yapılmaktadır. (Bu yöntem POSIX standartlarında yoktur, dolayısıyla Linux'a özgüdür.) Yapının sigev_signo elemanı eğer haberdar edilme sinyal yoluyla yapılacaksa oluşturulacak sinyalin numarasını almaktadır. Bu sinyal numarası normal bir sinyal olabileceği gibi gerçek zamanlı bir sinyal de olabilmektedir. Yapının sigev_value elemanı gerçek zamanlı sinyallerde sinyale iliştirilecek bilgiyi belirtmektedir. Bu aynı zamanda thread yoluyla haberdar edilmede de kullanılmaktadır. Yapının sigev_notify_function elemanı eğer haberdar edilme thread yoluyla yapılacaksa haberdar edecek thread'in çağıracağı fonksiyonu belirtmektedir. Yapının sigev_notify_attributes elemanı ise yaratılacak thread'in özellik bilgilerini belirtmektedir. Bu elemana NULL adres geçilebilir. Yapının sigev_notify_thread_id elemanı Linux'a özgüdür. Eğer birinci parametreye Linux'a özgü olan SIGEV_THREAD_ID değeri geçilirse bu elemana da thread'in pid değeri girilmelidir. Aslında fonksiyonun ikinci parametresi NULL adres de geçilebilmektedir. Bu durumda haberdar edilme SIGALRM sinyali yoluyla yapılmaktadır. (Aslında POSIX standartları bu durumda sinyalin SIGALRM olduğunu açıkça belirtmemiştir, "default signal" ifadesini kullanmıştır. Bu "default signal" Linux sistemlerinde SIGABRT sinyalidir.) timer_create fonksiyonunun son parametresi yaratılan interval timer'ın id'sinin yerleştirileceği timer_t türünden nesnenin adresini almaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Örneğin: struct sigaction sa; struct sigevent se; timer_t itimer; sa.sa_sigaction = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART|SA_SIGINFO; if (sigaction(SIGRTMIN, &sa, NULL) == -1) exit_sys("sigaction"); se.sigev_notify = SIGEV_SIGNAL; se.sigev_signo = SIGRTMIN; se.sigev_value.sival_int = 100; if (timer_create(CLOCK_MONOTONIC, &se, &itimer) == -1) exit_sys("timer_create"); Interval timer nesnesi yaratıldıktan sonra artık periyot timer_settime fonksiyonu ile belirlenmelidir. Fonksiyonun prototipi şöyledir: #include int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value, struct itimerspec *old_value); Fonksiyonun birinci parametresi timer_create fonksiyonundan elde edilen "timer id" değeridir. İkinci parametre 0 ya da TIMER_ABSTIME biçiminde geçilebilir. Bu parametre 0 geçilirse "göreli zaman", TIMER_ABSTIME geçilirse mutlak zaman dikkate alınır. Programcılar hemen her zaman göreli zaman kullanırlar. Fonksiyonun üçüncü parametresi ilk haberdar edilmenin ve periyodik haberdar edilmenin zamanlamasının ayarlandığı itimerspec isimli yapı nesnesinin adresini almaktadır. Bu yapı programcı tarafından doldurulup fonksiyona verilmelidir. Yapı şöyle bildirilmiştir: #include struct itimerspec { struct timespec it_interval; /* Interval for periodic timer */ struct timespec it_value; /* Initial expiration */ }; Yapının it_value elemanı ilk haberdar edilmeye kadar geçecek zamanı, it_interval elemanı haberdar edilme periyodunu belirtmektedir. Anımsanacağı gibi timespec yapısı da şöyle bildirilmiştir: struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds [0, 999'999'999] */ }; Fonksiyonun son parametresi eski değerlerin yerleştirileceği yapı nesnesinin adresini almaktadır. Bu parametre NULL geçilebilir. Ya da ikinci parametre NULL geçilip eski değerler de alınabilir. timer_settime fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Örneğin: if (timer_create(CLOCK_MONOTONIC, &se, &itimer) == -1) exit_sys("timer_create"); ts.it_value.tv_sec = 5; ts.it_value.tv_nsec = 0; ts.it_interval.tv_sec = 1; ts.it_interval.tv_nsec = 0; if (timer_settime(itimer, 0, &ts, NULL) == -1) exit_sys("timer_settime"); Interval timer ile işimiz bitince onu timer_delete fonksiyonu ile yok etmemiz gerekir. Fonksiyonun prototipi şöyledir: #include int timer_delete(timer_t timerid); Fonksiyon parametre olarak timer_create fonksiyonundan elde edilen timer id değerini almaktadır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. Başarısının kontrol edilmesine normal olarak gerek yoktur. Örneğin: timer_delete(itimer); Aşağıda POSIX modern interval timer mekanizmasını sinyal yoluyla haberdar etmeye bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void signal_handler(int signo, siginfo_t *info, void *context); void exit_sys(const char *msg); jmp_buf g_jb; int g_count; int main(void) { struct sigaction sa; struct sigevent se; timer_t itimer; struct itimerspec ts; sa.sa_sigaction = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART|SA_SIGINFO; if (sigaction(SIGRTMIN, &sa, NULL) == -1) exit_sys("sigaction"); se.sigev_notify = SIGEV_SIGNAL; se.sigev_signo = SIGRTMIN; se.sigev_value.sival_int = 100; if (timer_create(CLOCK_MONOTONIC, &se, &itimer) == -1) exit_sys("timer_create"); ts.it_value.tv_sec = 5; ts.it_value.tv_nsec = 0; ts.it_interval.tv_sec = 1; ts.it_interval.tv_nsec = 0; if (timer_settime(itimer, 0, &ts, NULL) == -1) exit_sys("timer_settime"); for (;;) { if (setjmp(g_jb) == 1) break; pause(); ++g_count; } timer_delete(itimer); return 0; } void signal_handler(int signo, siginfo_t *info, void *context) { if (g_count == 10) longjmp(g_jb, 1); printf("interval timer code %d\n", info->si_int); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de thread yoluyla haberdar edilmeye bir örnek verelim. Bu yöntemde thread bu mekanizma tarafından yaratılıp bizim fonksiyonumuz çağrılmaktadır. Burada yaratılacak olan thread'in toplamda bir tane mi olacağı yoksa her periyot için ayrı bir thread'in yeniden mi yaratılacağı POSIX standartlarında işletim sistemini yazanların isteğine bırakılmıştır. Linux tek bir thread yaratıp tüm periyotlarda aynı thread'i kullanmaktadır. Bu durumda çağrılacak callback fonksiyonun sigevent yapısının sigev_notify_function elemanına girilmesi gerekmektedir. Fonksiyonun parametrik yapısı şöyle olmalıdır: void notification_proc(union sigval); Örneğin: struct sigevent se; timer_t itimer; struct itimerspec ts; se.sigev_notify = SIGEV_THREAD; se.sigev_notify_function = notification_proc; se.sigev_notify_attributes = NULL; se.sigev_value.sival_int = 100; if (timer_create(CLOCK_MONOTONIC, &se, &itimer) == -1) exit_sys("timer_create"); ts.it_value.tv_sec = 5; ts.it_value.tv_nsec = 0; ts.it_interval.tv_sec = 1; ts.it_interval.tv_nsec = 0; if (timer_settime(itimer, 0, &ts, NULL) == -1) exit_sys("timer_settime"); Aşağıda thread yaratılarak haberdar edilmeye bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include void notification_proc(union sigval sval); void exit_sys(const char *msg); int main(void) { struct sigevent se; timer_t itimer; struct itimerspec ts; se.sigev_notify = SIGEV_THREAD; se.sigev_notify_function = notification_proc; se.sigev_notify_attributes = NULL; se.sigev_value.sival_int = 100; if (timer_create(CLOCK_MONOTONIC, &se, &itimer) == -1) exit_sys("timer_create"); ts.it_value.tv_sec = 5; ts.it_value.tv_nsec = 0; ts.it_interval.tv_sec = 1; ts.it_interval.tv_nsec = 0; if (timer_settime(itimer, 0, &ts, NULL) == -1) exit_sys("timer_settime"); printf("Press ENTER to exit...\n"); getchar(); timer_delete(itimer); return 0; } void notification_proc(union sigval sval) { printf("interval timer code %d\n", sval.sival_int); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Biz sinyal yoluyla haberdar edilirken gerçek zamanlı bir sinyal kullanmamışsak bir biriktirme yapılmadığı için ilgili sinyal bloke edildiğinde ondan kaç periyot geçtiğini anlayamayız. İşte bunu anlayabilmek için POSIX interval mekanizmasında timer_getoverrun fonksiyonu bulundurulmuştur. Fonksiyonun prototipi şöyledir: #include int timer_getoverrun(timer_t timerid); Fonksiyon bize sinyal bloke edildiğinde kaç kez periyot geçtiğini vermektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX türevi sistemlerde çok sayıda sistemden sisteme değişebilecek parametrik değer vardır. Örneğin bir yol ifadesinin maksimum uzunluğu, bir prosesin açık durumda tutabileceği maksimum dosya sayısı (yani dosya betimleyici tablosunun büyüklüğü), bir kullanıcının yaratabileceği alt proseslerin sayısı, exec fonksiyonuna geçirilecek komut satırı argümanlarının sayısı, ek grupların (supplementary groups) sayısı (bu konu ileride ele alınmaktadır) gibi pek çok parametrik değer sistemden sisteme değişebilmektedir. Halbuki taşınabilir programlar oluşturabilmek için bu parametrik değerlerin o anda programın çalıştığı sistemde biliniyor olması gerekebilmektedir. Genel olarak sistemdeki parametrik değerler dosyası içerisinde sembolik sabitler biçiminde define edilmiştir. (C standartlarında da bir dosyası vardır, ama POSIX limits.h dosyası çok daha geniş kapsamlıdır.) dosyası içerisindeki sembolik sabitler POSIX standartlarında şu gruplara ayrılmıştır: - Runtime Invariant Values (Possibly Indeterminate) - Pathname Variable Values - Runtime Increasable Values - Maximum Values - Minimum Values - Numerical Limits (C standartlarında da var) - Other Invariant Values POSIX standartları bazı parametrik değerlerin POSIX uyumlu sistemler için olabilecek en küçük değerlerini "Minimum Values" kategorisi içerisinde define etmiştir. Buradaki sembolik sabitlerin hepsi _POSIX_ öneki başlatılmıştır ve genellikle sonunda MAX soneki bulunmaktadır. Yani bu sembolik sabitlerin isimleri tipik olarak _POSIX_XXX_MAX biçimindedir. Her ne kadar sembolik sabit isminde MAX soneki kullanılmışsa da aslında bu değerler her POSIX uyumlu sistemin destekleyeceği minimum değerlerdir. Bu minimum değerlerin hepsi açıkça POSIX standartlarında belirtilmiştir. Örneğin bir prosesin açık durumda tutabileceği dosya sayısı (yani dosya betimleyici tablosunun uzunluğu) _POSIX_OPEN_MAX sembolik sabitiyle belirtilmiştir ve bunun değeri 20'dir. Yine örneğin bir kullanıcının yaratabileceği maksimum alt proses sayısı _POSIX_CHILD_MAX sembolik sabitiyle belirtilmiştir ve bunun değeri de 25'tir. Buradaki isimsel karışıklığa dikkat ediniz. _POSIX_CHILD_MAX sembolik sabiti bir kullanıcının yaratabileceği maksimum alt proses sayısına ilişkindir. Ancak bu sembolik sabitin değeri POSIX uyumlu sistemdeki olabilecek minimum değerdir. Örneğin _POSIX_ARG_MAX sembolik sabiti exec fonksiyonlarında girilebilecek maksimum argüman ve çevre değişkenlerinin byte uzunluğu ile ilgilidir. Ancak bu sembolik sabit POSIX uyumlu bir sistemdeki minimal değeri belirtmektedir ve 4096 olarak belirlenmiştir. Başka bir deyişle bir POSIX uyumlu bir işletim sistemi yazacaksak bir kullanıcının yaratbileceği maksimum alt proses sayısının en az 25 olmasını, exec fonksiyonuna girilebilecek maksimum argüman sayısının ve çevre değişkeni sayısının en az 4096 olmasını sağlamalıyız. POSIX tarafından belirlenen ve _POSIX_XXX_MAX biçiminde içerisinde define edilmiş olan minimum değerler aslında çok küçük değerlerdir. Örneğin _POSIX_NAME_MAX bir dosyanın karakter uzunluğunu belirtir. Bunun minimum değeri 14'tür. Halbuki yaygın hiçbir sistem 14 karakterden çok daha fazla dosya isimlerini desteklemektedir. Benzer biçimde bir kullanıcının maksimum yaratacağı alt proses sayısının 25 olması da çok düşük bir değerdir. İşte bu sembolik sabitler tüm POSIX sistemlerindeki olabilecek en küçük değerleri belirtmektedir, ancak modern sistemler dikkate alındığında bir kullanımı yoktur. O halde programcının POSIX standartlarının desteklediği en küçük değere değil, o sistemdeki mevcut değere ihtiyacı vardır. dosyası içerisinde "Runtime Invariant Values (Possibly Indeterminate)" kategorisi bazı _POSIX_XXX_MAX değerlerinin ilgili sistemdeki gerçek değerlerini belirtmektedir. Bu gerçek değerler içerisinde başında _POSIX_ öneki olmadan bildirilmiştir. Örneğin _POSIX_CHILD_MAX minimum değerinin ilgili sistemdeki gerçek değeri CHILD_MAX sembolik sabitiyle, _POSIX_OPEN_MAX minimum değerinin ilgili sistemdeki gerçek değeri OPEN_MAX sembolik sabitiyle bildirilmiştir. Böylece içerisinde hem POSIX sistemlerindeki minimum değerler hem de ilgili sistemdeki gerçek değerler define edilmiş durumdadır. Ancak burada da önemli bir pürüz vardır. Bazı parametrik değerler sistemin çalışması sırasında değiştirilebilmektedir ya da bazı parametrik değerler o andaki sistem kaynaklarının miktarıyla ilgilidir. Bu durumda bu parametrelerin ilgili sistemdeki değerleri baştan tespit edilememektedir. Ayrıca bazı parametrelerin ilgili sistemde bir sınırı da olmayabilir. İşte POSIX standartları "Runtime Invariant Values (Possibly Indeterminate)" başlığı altında "eğer bir parametrik değer sistem çalışırken değiştirilebiliyorsa ya da onun bir sınırı yoksa" ona ilişkin sembolik sabitin içerisinde define edilmemesi gerektiğini belirtmektedir. Yani aslında OPEN_MAX gibi, ARG_MAX gibi, CHILD_MAX gibi sembolik sabitler içerisinde define edilmemiş de olabilirler. Yani bu parametrelere ilişkin sembolik sabitler ancak define edilmişse kullanılabilmektedir. Pekiyi bir sembolik sabitin define edilip edilmeyeceği belirsizse biz bundan faydalanabilir miyiz? İşte POSIX standartlarının örtük bir biçimde önerdiği yöntem şudur: Eğer bir parametrik değer içerisinde define edilmişse programcı onu doğrudan derleme zamanında kullanabilir. Ancak define edilmemişse o anki gerçek değer programın çalışma zamanı sırasında sysconf, pathconf ve fpathconf fonksiyonlarıyla elde edilmelidir. (Tabii hiç sembolik sabite bakılmadan doğrudan bu fonksiyonlar da çağrılabilir. Yalnızca fonksiyon çağırma maliyeti bir dezavantaj oluşturmaktadır.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 85. Ders 30/09/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- içerisindeki diğer bir grup da "Pathname Variable Values" isimli gruptur. Bu başlık altında belirtilen sembolik sabitlerin değerleri o anda kullanılan dosya sistemine bağlı olarak değişebilmektedir. UNIX/Linux sistemlerinde farklı dosya sistemleri farklı noktalara mount edilebilmektedir. Bu farklı dosya sistemlerinin isim uzunlukları gibi özellikleri de birbirinden farklı olabilmektedir. İşte bu tür büyüklükler içerisinde "Pathname Variable Values" başlığı altında toplanmıştır. Bu gruptaki en önemli sembolik sabitler NAME_MAX ve PATH_MAX sembolik sabitleridir. NAME_MAX bir dizin girişinin isminin olabilecek maksimum karakter sayısını belirtmektedir. PATH_MAX ise toplamda mutlak bir yol ifadesinin olabileceği en fazla karakter sayısını belirtir. Bir dosya isminin ya da bir yol isminin bir yerde saklanacağı durumlarda saklanacak yerin büyüklüğünün belirlenmesi için bu değerlere gereksinim duyulmaktadır. Yukarıda da belirttiğimiz gibi bu değerler dosya sisteminden sistemine değişebilecek değerlerdir. İşte POSIX standartları eğer bu değerler tüm dizin ağacı içerisinde sabit ise bu sembolik sabitlerin define edilmesi, ancak sabit değilse define edilmemesi gerektiğini belirtmektedir. Yani örneğin PATH_MAX sembolik sabiti bir UNIX türevi sistemde define edilmişken başka bir sistemde edilmemiş olabilir. İşte "Pathname Variable Values" grubundaki sembolik sabitler eğer içerisinde define edilmemişse bu durumda onların değerleri pathconf ya da fpathconf POSIX fonksiyonuyla alınmalıdır. içerisindeki "Runtime Increasable Values" grubundaki sembolik sabitler sistemden sisteme değişebilir ve işletiminin çalışma zamanı sırasında artırılabilir. Bu sembolik sabitlerin ilgili sistemdeki minimum değerleri bu başlık altında define edilmek zorundadır. Anımsanacağı gibi bazı sembolik sabitlerin tüm POSIX sistemleri genelindeki minimum değerleri _POSIX_XXX biçiminde "Minimum Values" grubunda belirtilmiştir. Özetle "Runtime Increasable Values" grubundaki değerler o sistemdeki minimum değerler olarak kullanılabilir. Ancak bunların gerçek değerleri programın çalışma zamanı sırasında sysconf fonksiyonu ile elde edilmelidir. içerisindeki "Numeric Limits" grubundaki sembolik sabitler aslında C'nin dosyası içerisinde de bulunmaktadır. Bu sembolik sabitler temel türlerin o sistemdeki maksimum ve minimum değerlerini belirtmektedir. Örneğin INT_MIN int türünün o sistemdeki minimum değerini, INT_MAX ise maksimum değerini belirtir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- sysconf fonksiyonu yukarıda da belirttiğimiz gibi "Runtime Invariant Values" ve "untime Increasable Values" grubundaki büyüklüklerin programın çalışma zamanı sırasında elde edilmesinde kullanılmaktadır. Tabii "Runtime Invariant Values" grubundaki sembolik sabitler eğer define edilmişse bu fonksiyonu çağırmaya gerek yoktur. Doğrudan o sembolik sabitlerin değeri programcı tarafından kullanılabilir. sysconf fonksiyonunun prototipi şöyledir: #include long sysconf(int name); Fonksiyon parametre olarak hangi büyüklüğün değerinin elde edilmek istendiğini almaktadır. Bu değerler _SC_XXX biçiminde define edilmiştir. Bu sembolik sabitlerin oluşturulmasındaki kurala dikkat ediniz: POSIX'teki minimum değerler _POSIX_XXX biçiminde, o sistemdeki değerler XXX biçiminde, sysconf fonksiyonun parametreleri ise _SC_XXX biçiminde isimlendirilmiştir. Örneğin dosya betimleyici tablosunun POSIX'teki minimum uzunluğu _POSIX_OPEN_MAX sembolik sabitiyle, ilgili sistemdeki uzunluğu OPEN_MAX sembolik sabitiyle, sysconf fonksiyonundaki parametre ismi ise _SC_OPEN_MAX ismiyle define edilmiştir. sysconf fonksiyonu başarı durumunda ilgili büyüklüğe, başarısızlık durumunda -1 değerine geri dönmektedir. sysconf ile elde edilmek istenen büyüklük belirli olmayabilir ve sınırsız (infinite) da olabilir. Bu durumda fonksiyon başarısız olur. Ancak errno, değer değiştirmez. (Yani bu durumu belirlemek için errno değişkenine sysconf fonksiyonunu çağırmadan önce 0 değeri yerleştirilir. Sonra fonksiyon başarısız olduğunda errno değişkenine bakılır. Eğer hala 0 değeri duruyorsa ilgili büyüklüğün belirsiz ya da sınırsız olduğu sonucu çıkartılır.) Örneğin dosya betimleyici tablosunun uzunluğunu (yani açılacak maksimum dosya sayısını) sysconf fonksiyonu ile şöyle elde edebiliriz: errno = 0; if ((result = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) fprintf(stderr, "infinite value...\n"); else exit_sys("sysconf"); else printf("%ld\n", result); ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { long result; errno = 0; if ((result = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) fprintf(stderr, "infinite value...\n"); else exit_sys("sysconf"); else printf("%ld\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi içerisindeki "Pathname Variable Values" grubunda bulunan sembolik sabitler eğer ilgili sistemde define edilmemişse bunların değerleri pathconf ya da fpathconf fonksiyonlarıyla elde edilmelidir. Bu fonksiyonların prototipleri şöyledir: #include long fpathconf(int fildes, int name); long pathconf(const char *path, int name); Fonksiyonların ikinci parametreleri değeri elde edilecek büyüklüğü belirtmektedir. Büyüklüğün POSIX sistemlerindeki minimum değeri _POSIX_XXX, ilgili sistemdeki değeri XXX olmak üzere buradaki büyüklük isimleri _PC_XXX biçiminde isimlendirilmiştir. Örneğin bir dosyanın maksimum karakter uzunluğunun POSIX'teki minimum değeri _POSIX_NAME_MAX biçimindedir. İlgili sistemdeki değeri NAME_MAX biçiminde pathconf ve fpathconf fonksiyonlarındaki isimleri ise _PC_NAME_MAX biçimindedir. path fonksiyonu yol ifadesiyle çalışırken, fpathconf fonksiyonu açık dosya betimleyicisi ile çalışmaktadır. Bazı büyüklükler için bu yol ifadesinin ya da betimleyicinin dizine ilişkin olması gerekir. Fonksiyon başarı durumunda ilgili büyüklük değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Eğer ilgili büyüklüğün o sistemde belli bir sınırı yoksa fonksiyonlar başarısız olmakta ancak errno değerini değiştirmemektedir. Biz yukarıda genel olarak açıklamış olsak da belli büyüklüklerin değerlerini elde ederken ayrıntılar için POSIX dokümanlarına başvurmanızı tavsiye ederiz. Örneğin _PC_NAME_MAX ve _PC_PATH_MAX değerlerini elde etmek için yol ifadesinin ya da betimleyicinin bir dizine ilişkin olması gerekmektedir. Aksi takdirde fonksiyonun çalışması sistemden sisteme değişebilmektedir. Ayrıca örneğin _PC_PATH_MAX ile verilen uzunluk o dizine göreli uzunluktur. Yani bu uzunluğa o dizine kadarki karakter uzunluğu da eklenmelidir. Ayrıca örneğin NAME_MAX sembolik sabitiyle ya da _PC_NAME_MAX ismiyle elde edilen uzunluğa null karakter dahil değildir. Halbuki PATH_MAX ya da _PC_PATH_MAX ile elde edilen uzunluğa null karakter dahildir. Örneğin PATH_MAX değerini pathconf fonksiyonu ile şöyle elde edebiliriz: long result; errno = 0; if ((result = pathconf("/", _PC_PATH_MAX)) == -1) if (errno == 0) fprintf(stderr, "infinite value...\n"); else exit_sys("pathmax"); else printf("%ld\n", result); Biz burada kök dizinden itibaren (yani kök dizine göreli biçimde) yol ifadesinin uzunluğunu elde ettik. Dolayısıyla mutlak yol ifadesi için gereken karakter sayısı burada elde edilenden 1 fazla olmalıdır. Çünkü bu fonksiyonların verdikleri değer bizim onlara geçtiğimiz dizine görelidir. Tabii Linux sistemlerinde NAME_MAX ve PATH_MAX sembolik sabitleri zaten define edilmiştir. Dolayısıyla aslında Linux sistemlerinde bu fonksiyonların bu amaçla çağrılmasına gerek yoktur. Yani yazacağınız kod yalnızca Linux sistemlerinde kullanılacaksa zaten siz NAME_MAX ve PATH_MAX sembolik sabitlerini doğrudan kullanabilirsiniz. Ancak taşınabilir programlar için bu fonksiyonlara gereksinim duyulabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { long result; errno = 0; if ((result = pathconf("/", _PC_PATH_MAX)) == -1) if (errno == 0) fprintf(stderr, "infinite value...\n"); else exit_sys("pathmax"); else printf("%ld\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi bazı büyüklüklere ilişkin değerler ilgili sistemde define edilmeyebileceğine göre taşınabilir programlar için nasıl bir yol izlenmelidir? Tabii yöntemlerden biri her zaman sysconf ya da pathcnf, fpathconf fonksiyonlarını çağırmak olabilir. Ancak ilgili sembolik sabitler o sistemde define edilmişse bu çağrı gereksiz zaman kaybına yol açacaktır. Taşınabilirliği sağlamak için Stevens "Advanced Programming in the UNIX Environment" kitabında aşağıdaki gibi bir yöntem önermektedir: #define PATH_MAX_GUESS 4096 #ifdef MAX_PATH static long g_max_path = MAX_PATH; #else static long g_max_path = 0; #endif long path_max(void) { if (!g_max_path) { errno = 0; if ((g_max_path = pathconf("/", _PC_PATH_MAX)) == -1) if (errno == 0) g_max_path = PATH_MAX_GUESS; else exit_sys("pathmax"); else ++g_max_path; } return g_max_path; } Burada path_max fonksiyonu toplamda sıfır kere ya da en fazla bir kere pathconf fonksiyonunu çağırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include void exit_sys(const char *msg); #define PATH_MAX_GUESS 4096 #ifdef MAX_PATH static long g_max_path = MAX_PATH; #else static long g_max_path = 0; #endif long path_max(void) { if (!g_max_path) { errno = 0; if ((g_max_path = pathconf("/", _PC_PATH_MAX)) == -1) if (errno == 0) g_max_path = PATH_MAX_GUESS; else exit_sys("pathmax"); else ++g_max_path; } return g_max_path; } int main(void) { char *path; if ((path = (char *)malloc(path_max())) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } free(path); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- İşletim sistemini bir kaynak yöneticisi olarak da düşünebiliriz. Prosesler işletim sisteminin sunduğu çeşitli kaynakları kullanmaktadır. Tabii işletim sistemleri proseslerin kaynak kullanımlarını sınırlandırmaktadır. Aksi takdirde bir proses çok fazla kaynak kullanıp diğer proseslerin o kaynağa erişimini güçleştirebilir. Prosesin kaynak limitleri (yani kaynakları hangi sınırlar içerisinde kullanabileceği) proses kontrol bloğu içerisinde saklanmaktadır. (Linux'ta bu bilgi task_struct yapısının signal elemanının gösterdiği struct signal yapısının içerisindeki struct rlimit dizisinde bulunmaktadır.) UNIX türevi işletim sistemleri her kaynak için bir "soft limit (buna current limit de denilmektedir)" bir de "hard limit" değeri tutmaktadır. Kontroller soft limit dikkate alınarak yapılır. Herhangi bir proses soft limiti yükseltebilir. Ancak en fazla hard limit kadar yükseltebilir. Yani hard limit, soft limit için tavan değeri belirtmektedir. Sıradan prosesler hard limiti yükseltemezler, ancak düşürebilirler. Eğer hard limit sıradan prosesler tarafından düşürülürse bir daha eski değerine bile yükseltilemez. Uygun önceliğe sahip prosesler hard limiti yükseltebilirler. Dolayısıyla soft limiti de yükseltebilirler. Bir özelliğin hard limiti, soft limitin aşağısına çekilememektedir. O halde bizim şu bilgileri edinmemiz gerekir: Prosesin kaynakları nelerdir ve bu kaynak limitleri nasıl elde edilip değiştirilmektedir? Prosesin kaynak limitlerini elde etmek için getrlimit POSIX fonksiyonu, set etmek için setrlimit POSIX fonksiyonu kullanılmaktadır. getrlimit fonksiyonunun prototipi şöyledir: #include int getrlimit(int resource, struct rlimit *rlp); Fonksiyonun birinci parametresi kaynağın türünü, ikinci parametresi kaynak bilgilerinin yerleştirileceği rlimit yapı nesnesinin adresini almaktadır. rlimit yapısı şöyle bildirilmiştir: struct rlimit { rlim_t rlim_cur; /* Soft limit */ rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */ }; Buradaki rlimit tür ismi işaretsiz bir tamsayı biçiminde typedef edilmek zorundadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. getrlimit fonksiyonunda dikkat edilmesi gereken birkaç nokta vardır: Fonksiyonun birinci parametresindeki kaynak belirten değer dosyası içerisinde RLIMIT_XXX sembolik sabitleriyle define edilmiştir. Fonksiyon ile elde edilmek istenen limitler sınırsız olabilir. Bu durumda rlimit yapısının rlim_cur ve rlim_max elemanlarına RLIM_INFINITY özel değeri yerleştirilir. Elde edilmek istenen limit o sistemde belirsiz olabilir ya da o limitin o sistemde elde edilme yolu olmayabilir (unspecifed). Bu durumda yapının rlim_cur elemanına RLIM_SAVED_CUR özel değeri, yapının rlim_max elemanına ise RLIM_SAVED_MAX değeri yerleştirilir. getrlimit fonksiyonu aşağıdaki gibi kullanılmalıdır: struct rlimit rl; if (getrlimit(RLIMIT_STACK, &rl) == -1) exit_sys("getrlimit"); if (rl.rlim_cur == RLIM_INFINITY) printf("soft limit infinite...\n"); else if (rl.rlim_cur == RLIM_SAVED_CUR) printf("soft limit unrepresentable...\n"); else printf("Soft limit: %ju\n", (uintmax_t)rl.rlim_cur); if (rl.rlim_max == RLIM_INFINITY) printf("hard limit infinite...\n"); else if (rl.rlim_max == RLIM_SAVED_MAX) printf("hard limit unrepresentable...\n"); else printf("hard limit: %ju\n", (uintmax_t)rl.rlim_max); ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { struct rlimit rl; if (getrlimit(RLIMIT_STACK, &rl) == -1) exit_sys("getrlimit"); if (rl.rlim_cur == RLIM_INFINITY) printf("soft limit infinite...\n"); else if (rl.rlim_cur == RLIM_SAVED_CUR) printf("soft limit unrepresentable...\n"); else printf("Soft limit: %ju\n", (uintmax_t)rl.rlim_cur); if (rl.rlim_max == RLIM_INFINITY) printf("hard limit infinite...\n"); else if (rl.rlim_max == RLIM_SAVED_MAX) printf("hard limit unrepresentable...\n"); else printf("hard limit: %ju\n", (uintmax_t)rl.rlim_max); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- setrlimit fonksiyonu prosesin kaynaklarındaki hard ve/veya soft limitleri değiştirmek için kullanılmaktadır. Yukarıda da belirttiğimiz gibi soft limit en fazla hard limit kadar yükseltilebilir. Hard limit ise ancak uygun önceliğe sahip prosesler tarafından yükseltilebilmektedir. Fonksiyonun prototipi şöyledir: #include int setrlimit(int resource, const struct rlimit *rlp); Fonksiyonun yine birinci parametresi kaynağı, ikinci parametresi yükseltilecek limitleri belirtmektedir. Örneğin: struct rlimit rl; if (getrlimit(RLIMIT_NOFILE, &rl) == -1) exit_sys("getrlimit"); rl.rlim_cur = 4096; if (setrlimit(RLIMIT_NOFILE, &rl) == -1) exit_sys("getrlimit"); Burada önce dosya betimleyici tablosunun soft ve hard limitleri getrlimit fonksiyonuyla elde edilmiştir. Daha sonra da setrlimit fonksiyonu ile yalnızca soft limit değiştirilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { struct rlimit rl; if (getrlimit(RLIMIT_NOFILE, &rl) == -1) exit_sys("getrlimit"); rl.rlim_cur = 4096; if (setrlimit(RLIMIT_NOFILE, &rl) == -1) exit_sys("setrlimit"); if (getrlimit(RLIMIT_NOFILE, &rl) == -1) exit_sys("getrlimit"); if (rl.rlim_cur == RLIM_INFINITY) printf("soft limit infinite...\n"); else if (rl.rlim_cur == RLIM_SAVED_CUR) printf("soft limit unrepresentable...\n"); else printf("Soft limit: %ju\n", (uintmax_t)rl.rlim_cur); if (rl.rlim_max == RLIM_INFINITY) printf("hard limit infinite...\n"); else if (rl.rlim_max == RLIM_SAVED_MAX) printf("hard limit unrepresentable...\n"); else printf("hard limit: %ju\n", (uintmax_t)rl.rlim_max); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 86. Ders 01/10/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Prosesin kaynak limitleri üst prosesten alt prosese fork işlemi sırasında aktarılmaktadır. Yani örneğin kabuk programının kaynak limiti değiştirilirse bu limit kabuktan çalıştırılan bütün programlara yansıtılacaktır. Ancak bu konuda genellikle yanlış anlaşılan bir nokta vardır. İşletim sistemi kaynak aşımına ilişkin kontrolleri o prosesin proses kontrol bloğundaki değerlere göre yapmaktadır. Örneğin bir kullanıcının yaratacağı maksimum proses sayısı RLIMIT_NPROC isimli kaynak ile belirlenmiştir. Biz bu değeri değiştirdiğimizde artık bizim prosesimiz ve bizim yarattığımız prosesler bu limitten etkilenir. Ancak bizim başka proseslerimizin proses kontrol bloğunda bu değişiklik yapılmamış olduğu için onlar bu değişiklikten etkilenmeyecektir. Oysa kişiler bu limitin kullanıcı tabanlı bir limit olduğunu gördüğünde sanki bir proses bu limiti değiştirdiğinde sistem genelinde bir değişiklik yapılıyormuş gibi düşünmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kabuk programının proses limitleri ulimit isimli içsel (internal) kabuk komutuyla görüntülenip değiştirilebilmektedir. (ulimit komutunun tıpkı cd komutu gibi dışsal bir komut olamayacağına dikkat ediniz.) ulimit komutu -a seçeneği ile kullanılırsa tüm soft limitler görüntülenir. Örneğin: $ ulimit -a real-time non-blocking time (microseconds, -R) unlimited core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 15070 max locked memory (kbytes, -l) 497904 max memory size (kbytes, -m) unlimited open files (-n) 1000 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 15070 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited Hard limitleri görüntülemek için -H, soft limitleri görüntülemek için (default) -S seçenekleri kullanılmaktadır. Örneğin: $ ulimit -H -a real-time non-blocking time (microseconds, -R) unlimited core file size (blocks, -c) unlimited data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 15070 max locked memory (kbytes, -l) 497904 max memory size (kbytes, -m) unlimited open files (-n) 1048576 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) unlimited cpu time (seconds, -t) unlimited max user processes (-u) 15070 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited Limitlerde değişiklik yapmak için limite özgü seçenekler kullanılmalıdır. Örneğin -n seçeneği dosya betimleyici tablosunun uzunluğu ile ilgilidir. Değişiklik şöyle yapılabilir: $ ulimit -n 2048 $ ulimit -n 2048 Burada default olarak hem hard hem de soft limit değiştirilmiştir. Yalnızca soft limitin değiştirilmesi için -S, yalnızca hard limitin değiştirilmesi için -H seçeneğinin de komuta eklenmesi gerekir. Örneğin: $ ulimit -H -n 5000 $ ulimit -H unlimited $ ulimit -a -H real-time non-blocking time (microseconds, -R) unlimited core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 15070 max locked memory (kbytes, -l) 497904 max memory size (kbytes, -m) unlimited open files (-n) 5000 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) unlimited cpu time (seconds, -t) unlimited max user processes (-u) 15070 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited Burada biz yalnızca hard limiti değiştirdik. Yukarıda da belirttiğimiz gibi hard limit, soft limitin aşağısına indirilememektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de bazı proses limitlerinden bahsedelim. Bunlar hakkında daha ayrıntılı bilgileri ilgili dokümanlardan edinebilirsiniz. - RLIMIT_AS: Prosesin maksimum kullanabileceği sanal bellek miktarını belirtir. Bu limitin Linux'taki default hard ve soft default RLIM_INFINITY biçimindedir. - RLIMIT_CORE: Bu limit üzerinde daha önce de "core dosyalarının oluşturulması" konusunda bazı şeyler söylemiştik. Bu limit core dosyalarının maksimum uzunluğunu belirtmektedir. Linux'ta default olarak soft ve hard değerleri 0 durumundadır. Yani core dosyalarının oluşturulabilmesi için bu limitin yükseltilmesi (örneğin RLIM_INFINITY yapılması) gerekmektedir. - RLIMIT_CPU: Bu limit prosesin harcayacağı CPU zamanını sınırlandırmak için bulundurulmuştur. Eğer proses burada belirtilen CPU zamanını saniye olarak aşarsa bu durumda çekirdek prosese SIGXCPU sinyalini göndermektedir. Bu sinyalin default eylemi prosesin sonlandırılması biçimindedir. Linux'ta bu sinyal için sinyal fonksiyonu yazıldığında proses hemen sonlandırılmaz ve her saniyede bu sinyal yeniden gönderilir. Soft limit değerine ulaşıldığında prosese SIGKILL sinyali gönderilerek proses sonlandırılmaktadır. - RLIMIT_DATA: Bu limit ptosesin "data segment" kısmının limitini belirlemektedir. malloc gibi tahsisat fonksiyonları bu data segment kısmını büyüttüğü için bu limitten etkilenmektedir. Bu limitin Linux'taki default hard ve soft değeri RLIM_INFINITY biçimindedir. - RLIMIT_FSIZE: Prosesin toplamda yaratabileceği maksimum dosya uzunluğudur. Örneğin bu limit 5 MB'a ayarlanmışsa proses toplamda (tüm yarattığı dosyaların uzunluğu) ancak 5 MB uzunluğunda dosya yaratabilecektir. Bu limitin Linux'taki default hard ve soft değerleri RLIM_INFINITY biçimindedir. Eğer bu limit aşılırsa işletim sistemi prosese SIGXFSZ sinyalini göndermektedir. Bu sinyalin defaut eylemi prosesin sonlandırılmasıdır. - RLIMIT_NICE: Bu değer SCHED_OTHER prosesler için nice değerine sınır koymak amacıyla düşünülmüştür. Anımsanacağı gibi normalize edilmiş olan nice değerleri [0, 39] arasındaydı. Buradaki değer 20 - RLIMIT_NICE biçiminde normalize edilmektedir. Bu değerin sıfır olması, prosesin nice değerini yükseltemeyeceği anlamına gelmektedir. Bu limitin Linux'taki default hard ve soft değerleri 0 biçimindedir. - RLIMIT_NOFILE: Prosesin dosya betimleyici tablosunun uzunluğunu belirtmektedir. Bu limitin Linux'taki default soft değeri 1024, hard değeri ise 1048576 biçimindedir. Biz bu limitin soft değerini değiştirdiğimizde kendi prosesimizin dosya betimleyici tablosunu otomatik olarak büyütmüş olmaktayız. Ayrıca fork yaptığımız proseslerin de artık dosya betimleyici tabloları büyümüş olacaktır. Linux'ta tüm proseslerin dosya betimleyici tablolarının uzunlukları toplamı için de bir kısıt vardır. Bu kısıt Linux'ta proc dosya sistemindeki /proc/sys/fs/file-max dosyasında belirtilmiştir. Güncel çekirdeklerde buradaki değer 9223372036854775807 biçiminde aşırı derecede büyüktür. Prosesin hard limiti olan 1048576 değeri de aslında proc dosya sistemi ile değiştirilebilmektedir. Bunun için proc dosya sistemindeki /proc/sys/fs/nr_open dosyası kullanılmaktadır. Bu dosyadaki değeri aşağıdaki gibi bir komutla değiştirebilirsiniz: $ sudo sh -c "echo 20 > /proc/sys/fs/mqueue/msg_max" Anımsanacağı gibi bu bilgi aynı zamanda sysconf fonksiyonundan da elde edilebilmektedir. - RLIMIT_NPROC: Belli bir kullanıcının yaratabileceği maksimum proses sayısıdır. Anımsanacağı gibi Linux sistemlerinde thread'ler için de task_struct yapısı ayrılmaktadır. Bu nedenle buradaki limitler Linux sistemlerinde thread'leri de kapsamaktadır. Yani biz thread yarattıkça da bu limite doğru ilerlemiş oluruz. Mevcut sistemlerde bu limitin hard ve soft değerleri default olarak 15070 biçimindedir. Anımsanacağı gibi bu bilgi aynı zamanda sysconf fonksiyonundan da elde edilebilmektedir. - RLIMIT_RTPRIO: Bu limit uygun önceliğe sahip olmayan SCHED_FIFO ve SCHED_RR proseslerin önceliklerini sınırlandırmak için kullanılmaktadır. (Anımsanacağı gibi bu çizelgeleme politikalarına sahip proseslerin Linux'taki öncelikleri [0, 99] arasında olabiliyordu). Bu limitin Linux'taki default hard ve soft değerleri 0 biçimindedir. - RLIMIT_SIGPENDING: Gerçek zamanlı sinyallerdeki kuyruk uzunluğunu belirtmektedir. Bu limitin Linux'taki default hard ve soft değerleri mevcut çekirdeklerde 15070 biçimindedir. - RLIMIT_STACK: Bir thread'in stack uzunluğunu sınırlandırmak için kullanılmaktadır. Linux'ta default durumda thread'lerin stack uzunlukları 8 MB'dir. Bu limitin Linux'taki default soft değeri 8 MB, hard değeri ise RLIM_INFINITY biçimindedir. (Yani biz thread yaratırken stack uzunluğunu bu soft limiti yukarı çekmeden artıramayız, ancak düşürebiliriz.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux'ta limitlerin hard ve soft değerlerini kalıcı bir biçimde değiştirebilmek için /etc/security/limits.conf dosyası bulundurulmuştur. Örneğin bu dosya sayesinde biz sistem açıldığında prosesimizin dosya betimleyici tablosunun default uzunluğunun (soft limitini) istediğimiz bir değerde olmasını sağlayabiliriz. Bu dosyanın formatı için man sayfalarına başvurabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir prosesin kaynak kullanımlarını elde etmek için getrusage isimli bir POSIX fonksiyonu bulundurulmuştur. Fonksiyonun prototipi şöyledir: #include int getrusage(int who, struct rusage *r_usage); Fonksiyonun birinci parametresi kaynak bilgilerinin elde edileceği prosesleri belirtmektedir. İkinci parametresi kaynak bilgilerinin yerleştirileceği yapı nesnesinin adresini almaktadır. Bu yapıda Linux'a özgü bazı elemanlar da vardır. Ayrıntıları için ilgili dokümanlara başvurabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sonraki konuya yardımcı olması için UNIX türevi sistemlerin dosya sistemlerinde kullandığı i-node yapısı üzerinde durmak istiyoruz. Biz daha önce dosya betimleyici tablosundaki slotların "dosya nesnesi" denilen yapı nesnelerini gösterdiğini belirtmiştik. Dosya nesneleri Linux kaynak kodlarında "struct file" yapısıyla temsil edilmektedir. Daha önceki modelimiz şöyleydi: Proses Kontrol Blok ... ----------------------------> Dosya Betimleyici Tablosu ... 0 1 2 3 --------------------------------> Dosya Nesnesi 4 ... İşte diskteki dosya için o dosya kullanılmaya başlandığı zaman işletim sistemi aynı zamanda i-node isimli bir nesne oluşturmaktadır. i-node nesnesi farklı prosesler aynı dosya dosyayı açsalar bile o dosya için toplamda bir tanedir. i-node nesnesi içerisinde tipik olarak bizim stat fonksiyonlarıyla elde ettiğimiz bilgiler bulunmaktadır. (Örneğin fstat fonksiyonu hiç disk işlemi yapmadan bilgileri doğrudan bu i-node nesnesinin içerisinden almaktadır.) Burada anahtar nokta toplamda bir dosya için işletim sisteminin sistem genelinde tek bir i-node nesnesi oluşturmasıdır. Çünkü i-node nesneleri işletim sisteminin diskte tek olan dosya bilgilerini yerleştirdiği nesnelerdir. O dosya diskte tek olduğuna göre o dosya bilgilerinin çekirdek tarafından bellekteki temsili de tek olmalıdır. Tabii modern işletim sistemleri dosyalara ilişkin bu i-node elemanlarını bir cache sistemi içerisinde tutmaktadır. Yani bir dosya artık hiçbir proses tarafından kullanılmıyor olsa da o dosyanın bilgileri i-node cache içerisinde bulunuyor olabilir. Linux i-node nesneleri için LRU (Least Recently Used) stratejisine sahip bir cache sistemi kullanmaktadır. Bu durumda yukarıdaki şeklin daha gerçekçi biçimi aşağıdaki gibi olabilir: Proses Kontrol Blok ... ----------------------------> Dosya Betimleyici Tablosu ... 0 1 2 3 --------------------------------> Dosya Nesnesi ------> i-node nesnesi 4 ... ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kursumuzun bu bölümünde birden fazla prosesin aynı dosyaya erişim yaptığı durumlardaki problemleri ve bu problemlerin nasıl çözülmesi gerektiği üzerinde duracağız. Bu konuya genel olarak işletim sistemlerinde "dosya kilitleme (file locking)" de denilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İki prosesin aynı anda, aynı dosyanın, aynı bölümü üzerinde işlem yaptığını düşünelim. Bu durumda ne olur? Örneğin bir proses dosyanın belli bir kısmına write fonksiyonu ile yazma yaparken diğer bir proses aynı kısımdan, aynı anda read fonksiyonu ile okuma yapmak istese bir problem oluşur mu? İşte UNIX türevi sistemlerde read ve write işlemleri sistem genelinde atomik işlemlerdir. Yani işletim sistemi read ve write işlemlerini sıraya sokarak onların biri tamamen bitince diğeri işleme sokulacak biçimde yapmaktadır. Bu nedenle bir proses, dosyanın belli bir bölümüne yazma yaparken diğer bir proses de aynı dosyanın, aynı bölümünden okuma yapıyorsa; okuyan proses ya yazan prosesin yazmış olduğu değeri okur ya da dosyadaki yazma işleminden önceki değeri okur. Karışık bir değer okumaz. Yani read ve write işlemleri iç içe geçmemektedir. POSIX standartları bunu garanti etmektedir. Benzer biçimde iki proses de aynı dosyanın aynı bölümüne yazma yapmak istese, dosyada ya birinin ya da diğerinin yazdığı gözükecektir. Karışık bir bilgi (yani biraz birinin, biraz ötekinin yazdığı değer) gözükmeyecektir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Veritabanı yönetim sistemleri ya da veritabanı kütüphaneleri bir dosyaya birbirleriyle ilgili birden fazla read ve write işlemi yapabilmektedir. Örneğin şöyle bir senaryo düşünelim. Siz bir dosyanın belli bir yerine bir bilgi yazmak isteyin, ancak o bilginin hash değeri de dosyanın başka bir yerine yazılacak olsun. Burada birbiriyle uyumlu iki farklı write işlemi söz konusudur. Siz birinci write işlemini yaptıktan sonra henüz ikinci write işlemini yapmadan başka bir proses bu iki write işlemini hızlı davranıp yaparsa, siz ikinci write işlemini yaptığınızda artık yazdığınız hash değeri ile asıl bilgi uyumsuz hale gelecektir. İşte veritabanı uygulamalarında bu biçimdeki işlemler çok yoğun yapılmaktadır. Veritabanı programı bir dosyaya önce kaydı yazıp sonra dosyanın başka bir yerine onun indeks bilgisini yazabilmektedir. Bu bilginin tutarlı olması gerekmektedir. Özetle tek bir read ve write sistem genelinde atomiktir, ancak birden fazla read ve write için böyle bir durum söz konusu değildir. Bu tür problemlerin çözümü için akla gelen ilk yöntem senkronizasyon nesnelerini kullanmaktır. Örneğin proses iki write işlemi öncesinde mutex nesnesini kilitler ve iki write işlemi bittiğinde kilidi açar. Diğer prosesler de aynı mutex nesnesini aynı biçimde kullanır. Tabii burada mutex nesnesinin prosesler arasında kullanılabiliyor olması gerekmektedir. Ancak bu çözüm hem yorucu hem de yavaş bir çözümdür. İşte bu tür problemler için "dosya kilitleme" denilen mekanizmalar kullanılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dosya kilitleme bütünsel olarak ve offset temelinde (yani dosyanın bir bölümünü kapsayacak biçimde) yapılabilmektedir. Bütünsel dosya kilitleme kullanışlı değildir ve seyrek uygulanmaktadır. Offset temelinde dosya kilitleme de kendi içerisinde ikiye ayrılmaktadır: İsteğe bağlı kilitleme (advisory lock) ve zorunlu kilitleme (mandatory lock). Biz bu bölümde önce bütünsel kilitlemeyi daha sonra offset temelinde kilitlemeyi ele alacağız. Bütünsel kilitleme adeta mutex benzeri bir senkronizasyon nesnesi ile dosyaya tüm ulaşımı engelleyecek bir mekanizma oluşturmaktadır. Offset temelinde kilitlemede dosyanın yalnızca istenilen bölümleri kilitlenmekte, diğer bölümleri üzerinde işlemler yapılabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bütünsel dosya kilitleme için Linux sistemlerinde flock isimli fonksiyon kullanılmaktadır. flock fonksiyonu ilk kez BSD sistemlerinde gerçekleştirilmiştir. POSIX standartlarında bulunmamaktadır. Fonksiyonun prototipi şöyledir: #include int flock(int fd, int operation); Fonksiyonun birinci parametresi bütünsel olarak kilitlenecek dosyaya ilişkin betimleyiciyi belirtmektedir. İkinci parametre kilitlemenin nasıl yapılacağını belirtir. İkinci parametre şunlardan biri olabilir: LOCK_SH: Okuma için kilidi alma LOCK_EX: Yazma için kilidi alma LOCK_UN: Kilidi bırakma Bu değerlerden biri ayrıca LOCK_NB (non-blocking) değeri ile de bit OR işlemine sokulabilmektedir. Buradaki mekanizma "reader-writer lock" mekanizmasına benzemektedir. Yani dosyaya birden fazla proses read işlemi yapabilir. Ancak bir proses write yapıyorsa diğerlerinin işlem bitene kadar beklemesi sağlanmalıdır. Buradaki LOCK_SH (shared lock) okuma eylemi için erişme anlamına gelmektedir. LOCK_EX ise yazma işlemi için erişmek anlamına gelir. LOCK_UN kilitlemeyi kaldırmaktadır. Bu durumda dosyaya yazma amaçlı erişilecekse önce LOCK_EX ve işlem bitince de LOCK_UN uygulanmalıdır. Dosyaya okuma amaçlı erişilecekse önce LOCK_SH ve işlem bitince de LOCK_UN uygulanmalıdır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Proseslerden bir dosyanın değişik yerlerine tutarlı bir biçimde yazma yapacak olsun. Kodunu şöyle organize etmelidir (kontroller yapılmamıştır): flock(fd, LOCK_EX); /* yazma işlemleri */ flock(fd, LOCK_UN); Bu durumda diğer bir proses flock fonksiyonunu LOCK_SH ya da LOCK_EX ile çağırdığında blokede bekler. Ta ki bu proses flock fonksiyonunu LOCK_UN ile çağırana kadar. Dosyadan okuma yapacak proses de kodunu şöyle organize etmelidir: flock(fd, LOCK_SH); /* okuma işlemleri */ flock(fd, LOCK_UN); Bu durumda başka bir proses de aynı dosyaya flock fonksiyonunu LOCK_SH ile çağırarak erişmek istese bloke oluşmaz. Ancak bir proses dosyaya flock fonksiyonunu LOCK_EX ile çağırarak erişmeye çalışırsa blokede bekler. Ta ki bu proses LOCK_UN yapana kadar. Mekanizma tamamen daha önce görmüş olduğumuz reader-writer lock senkronizasyon nesnesindeki gibidir. Bu biçimdeki kilitler alt prosese fork işlemi sırasında da geçirilmektedir ve exec işlemi sırasında bu kilitler korunmaktadır. Kilit bilgisi dosya nesnesinin içerisinde saklanmaktadır. Dolayısıyla birden fazla kez open fonksiyonu çağrılarak elde edilmiş olan dosya betimleyicilerinden biri ile kilit alındığında diğer betimleyici bu kilidi almamış gibi olmaktadır. Ancak aynı dosya nesnesini gösteren betimleyici ile birden fazla kez flock fonksiyonu çağrılırsa bu durumda ikinci flock birincinin etkisini kaldırır ve yeni etki oluşur. Bütünsel lock işlemi uygulanırken LOCK_NB bayrağı da kullanılırsa bu durumda uyuşmayan lock işlemleri blokeye yol açmaz. Fonksiyon başarısız olur ve errno değeri EWOULDBLOCK biçiminde set edilir. Eğer kilit hiç açılmazsa kilidin ilişkin olduğu dosya nesnesine ilişkin son betimleyici kapatıldığında kilit otomatik olarak açılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 87. Ders 07/10/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Offset temelinde dosya kilitleme işlemleri yukarıda da belirttiğimiz gibi kendi aralarında "isteğe bağlı (advisory)" ve "zorunlu (mandatory)" olmak üzere ikiye ayrılmaktadır. Offset temelinde lock işlemleri fcntl fonksiyonu ile gerçekleştirilmektedir. fcntl fonksiyonu bir dosya betimleyicisi ile bazı özel işlemler yapmakta kullanılan genel bir fonksiyondu. Biz bu fonksiyonu daha önce tanıtmıştık. Fonksiyonun prototipi şöyleydi: #include int fcntl(int fd, int cmd, ...); Fonksiyon iki argümanla ya da üç argümanla çağrılmaktadır. İkinci parametreye komut parametresi denilmektedir. Üçüncü parametrenin anlamı ikinci parametredeki komuta göre değişmektedir. Offset temelinde kilitleme için kilitlenecek dosyayı temsil eden bir dosya betimleyicisi olmalıdır (birinci parametre). fcntl fonksiyonunun ikinci parametresine F_SETLK, F_SETLKW ya da F_GETLK değerlerinden biri girilir. Üçüncü parametresine ise her zaman flock isimli bir yapı nesnesinin adresi girilmelidir. F_SETLK ve F_SETLKW komutlarında bu flock yapı nesnesinin içi programcı tarafından doldurulmalıdır. Ancak F_GETLK komutunda bu yapı nesnesinin içi fonksiyon tarafından doldurulmaktadır. flock yapısı şöyle bildirilmiştir: struct flock { short l_type; short l_whence; off_t l_start; off_t l_len; pid_t l_pid; }; Bu yapı nesnesinin elemanları pid dışında programcı tarafından doldurulur. l_type elemanı kilitlmenin cinsini belirtir. Bu elemana F_RDLCK, F_WRLCK, F_UNLCK değerlerinden biri girilmelidir. Yapının l_whence elemanı offset için orijini belirtmektedir. Bu elemana da SEEK_SET (0), SEEK_CUR (1) ya da SEEK_END (2) değerleri girilmelidir. l_start elemanı kilitlenecek bölgenin başlangıç offset'ini l_len ise uzunluğunu belirtmektedir. l_pid, F_GETLK komutu tarafından doldulmaktadır. Yapının l_type elemanının kilidin türünü belirttiğini söylemiştik. Bu tür tıpkı thread senkronizasyonu konusunda görmüş olduğumuz "okuma-yazma kilitleri (reader-writer lock)" gibi işlem görmektedir. Yani biz belli bir bölgeye F_WRLCK ile kilit yerleştirirsek bu durum "bizim o bölgeye başkalarının okuma ya da yazma yapmasını istemediğimiz" anlamına gelmektedir. Biz belli bir bölgeye F_RDLCK ile kilit yerleştirirsek bu durum da "bizim o bölgeye başkalarının yazma yapmasını istemediğimiz ancak okuma yapmalarına izin verdiğimiz" anlamına gelmektedir. F_UNLCK ise bizim o bölgedeki kilidi kaldırmak istediğimiz anlamına gelmektedir. Bu kilit türlerini şöyle özetleyebiliriz: F_WRLCK: Başkaları yazma da okuma da yapmamalı F_RDLCK: Başkaları okuma yapabilir ancak yazma yapmamalı F_UNLCK: Bu bölgedeki kilit kaldırılmak isteniyor l_len değeri 0 ise bu l_start değerinden itibaren dosyanın sonuna ve ötesine kadar kilitleme anlamına gelmektedir. (Bu durumda örneğin lseek ile dosya göstericisi EOF ötesine konumlandırılsa bile kilit geçerli olmaktadır.) Yani yapının l_len elemanı 0 olarak girilirse dosya ne kadar büyütülürse büyütülsün tüm büyütülen alanlar da kilitli olacaktır. Pekiyi fcntl fonksiyonunun ikinci parametresindeki F_SETLK, F_SETLKW ve F_GETLK komutları ne anlama gelmektedir? F_SETLK blokesiz, F_SETLKW blokeli kilitleme yapmaktadır. Blokesiz kilitlemede "çelişen (incompatible)" kilitleme isteklerinde thread bloke olmaz, ancak fcntl fonksiyonu başarısız olur. Blokeli kilitlemede ise çelişen (incompatible) kilitleme isteklerinde thread bloke olur. Ta ki bu çelişki ortadan kaldırılana kadar. Örneğin biz bir dosyanın 100 ile 110 offset'leri arasını F_WRLCK türü ile F_SETLK komutunu kullanarak kilitlemek isteyelim. Eğer bu bölge başka bir proses tarafından F_RDLCK ya da F_WRLCK türü ile kilitlenmişse kilitleme başarısız olur ve fcntl fonksiyonu -1 değeri ile geri döner. Ancak biz aynı bölgeyi F_SETLKW ile blokeli kilitlemek istersek bu durumda çelişkili bir kilitleme isteği söz konusu olduğu için thread bloke olur. Ta ki bu çelişki giderilene kadar. (Yani kilidi yerleştiren proses onu kaldırana kadar.) Özetle F_SETLK ile çelişkili kilit yerleştirilmek istenirse fcntl başarısız olmakta, ancak F_SETLKW ile çelişkili kilit yerleştirilmek istenirse fcntl blokeye yol açmaktadır. Genellikle uygulamalarda F_SETLKW kullanılmaktadır. Yukarıda da belirttiğimiz gibi F_SETLK ile bir bölgeyi kilitlemek isteyen proses eğer bu alan başka bir proses tarafından çelişki yaratacak biçimde kilitlenmişse fcntl fonksiyonu başarısız olur ve -1 değerine geri döner. Bu durumda errno değişkeni EACCES ya da EAGAIN değerlerinden biri ile set edilmektedir. Aynı proses kendisinin yerleştirmiş olduğu bir kilidin türünü değiştirebilir. Bu durumda bir çelişkiye bakılmamaktadır. Yani örneğin biz bir dosyanın belli bölgesine bir F_WRLCK yerleştirmiş olabiliriz. Sonra bunu F_RDLCK ile yer değiştirebiliriz. Bu işlem atomik düzeyde yapılmaktadır. F_GETLK komutu için de programcının flock nesnesini oluşturmuş olması gerekir. Bu durumda fcntl bu alanın isteğe bağlı biçimde kilitlenip kilitlenmeyeceğini bize söyler. Yani bu durumda fcntl kilitleme yapmaz ama sanki yapacakmış gibi duruma bakar. Eğer çelişki yoksa fcntl yalnızca yapının l_type elemanını F_UNLCK haline getirir. Eğer çelişki varsa bu çelişkiye yol açan kilit bilgilerini yapı nesnesinin içerisine doldurur. Fakat o alan birden farklı biçimlerde kilitlenmişse bu durumda fcntl bu kilitlerin herhangi birinin bilgisini bize verecektir. Örneğin kilit durumunu öğrenmek istediğimiz bölgede ayrık iki farklı kilit bulunuyor olabilir. Bu tür durumlarda fcntl fonksiyonu bu kilit bilgilerinin herhangi birini bize vermektedir. fcntl ile offset temelinde konulmuş olan kilitler fork işlemi sırasında alt prosese aktarılmazlar. Yani üst prosesin kilitlemiş olduğu alanlar alt proses tarafından da kilitlenmiş gibi olmazlar. (Halbuki flock fonksiyonu ile bütünsel kilitlemelerin fork işlemi sırasında alt prosese aktarıldığını anımsayınız.) exec işlemleri sırasında offset temelindeki kilitlemeler varlığını devam ettirmektedir. Dosyanın kilit bilgileri dosyanın diskteki varlığı üzerine kaydedilmemektedir. İşletim sisteminin çekirdek içerisinde oluşturduğu i-node elemanının içerisinde kaydedilmektedir. Dolayısıyla aynı dosyayı açmış olan prosesler aynı i-node nesnesini gördükleri için aynı kilit bilgilerine sahip olurlar. Genellikle UNIX türevi işletim sistemleri kilit bilgilerini i-node nesnesi içerisinde bir bağlı listede proses id'lere göre sıralı bir biçimde tutmaktadır. Prosesin yerleştirmiş olduğu bir kilit o proses tarafından kilit türü F_UNLCK yapılarak kaldırılabilmektedir. Ancak en kötü olasılıkla ilgili dosya kapatıldığında o prosesin o dosya üzerinde yerleştirmiş olduğu kilitler de kaldırılmaktadır. Burada önemli bir nokta bir dosya betimleyicisi dup yapıldığında onlardan herhangi biri close ile kapatılırsa kilidin kaldırılacağıdır. Yani kilidin kaldırılması için bir betimleyicinin close edilmesi yeterli olmaktadır. Benzer biçimde aynı proses, aynı dosyayı ikinci kez açtığı durumda, onlardan herhangi biri close işlemi yaptığında kilit kaldırılmaktadır. Proses biterken zaten betimleyiciler üzerinde close işlemleri yapılacağı için o prosesin açmış olduğu dosyalar üzerinde yerleştirdiği kilitler de ortadan kaldırılacaktır. Prosesin dosyaya F_WRLCK kilidi yerleştirebilmesi için dosyanın yazma modunda, F_RDLCK kilidi koyabilmesi için ise dosyanın okuma modunda açılmış olması gerekir. Offset temelinde kilitlemede "deadlock" oluşumuna dikkat edilmelidir. Yani biz bir prosesin kilidi açmasını beklerken o proses de bizim başka bir kilidi açmamızı bekliyorsa burada bir "deadlock" durumu söz konusudur. İşte deadlock durumu fcntl fonksiyonu tarafından otomatik olarak tespit edilmektedir. Eğer bir proses F_SETLKW ile (F_SETLK ile değil) kilit koymak isterken bir "deadlock" durumu söz konusu ise fcntl başarısız olmakta ve errno değeri EDEADLK değeri ile set edilmektedir. Kilitlenmek istenen bölgede birden fazla ayrık kilit bulunuyor olabilir. Bu durumda kilitlemenin yapılabilmesi için bu ayrık kilitlerin hiçbirinde bir çelişkinin olmaması gerekir. Aksi takdirde F_SETLK komutunda fcntl başarısız olur, F_SETLKW komutunda ise fcntl blokeye yol açar. Yani kilitleme işlemi "yap hep ya hiç" biçiminde yapılmaktadır. Bir bölgenin belli bir kilit türüyle kilitlenmiş olduğunu varsayalım. Aynı proses bu bölgenin bir alt bölgesini başka bir kilit türüyle kilitleyebilir ya da o alt bölgenin kilidini kaldırabilir. Bu durumda işletim sistemi otomatik olarak kilitleri birbirinden ayıracaktır. Örneğin: WWWWWWWWWWWWWWWWWWWWW Bu bölgenin F_WRLCK ile kilitlenmiş olduğunu düşünelim. Biz bu bölgenin ortasında bir bölgenin kilidini F_RDLCK olarak değiştirelim: WWWWWWWWWRRRRRWWWWWWW Burada artık üç kilit bölgesi oluşmaktadır. Kilidin kaldırılması eğer kilidi koyan proses tarafından yapılıyorsa her zaman başarılı olmaktadır. Yani biz yukarıdaki üç bölgenin kilidini tek bir F_UNLCK ile kaldırabiliriz. Kilitlenmemiş bölgeye F_UNLCK uygulansa bile fcntl fonksiyonu başarısız olmamaktadır. Pekiyi fcntl fonksiyonunun başarısı nasıl kontrol edilmelidir. Eğer kilitleme F_SETLK fonksiyonu ile yapılıyorsa EACCES ve EAGAIN değerleri test edilmelidir. Mesajlar bu durumlar için özel olarak verilmesi daha iyi bir tekniktir. Eğer kilitleme F_SETLKW ile yapılıyorsa EDEADLK değeri de test edilmelidir. Örneğin: if (fcntl(fd, F_SETLK, &fl) == -1) { if (errno == EACCES || errno == EAGAIN) { fprintf(stderr, "lock failed!...\n"); ... } else exit_sys("fcntl"); } Ya da örneğin: if (fcntl(fd, F_SETLKW, &fl) == -1) { if (errno == EDEADLK) { fprintf(stderr, "deadlock danger!...\n"); ... } else exit_sys("fcntl"); } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda kilitleme işlemlerini test etmek için "flock-test.c" isimli örnek bir program verilmektedir. Bu programı çalıştırırken komut satırı argümanı olarak kilitlemeye konu olacak dosyanın yol ifadesi verilmelidir. Örneğin: ./flock-test test.txt Program aşağıdaki gibi bir prompt'a düşmektedir: CSD (55306)> Parantezler içerisinde prosesin proses id'si bulunmaktadır. Buraya kullanıcı komutlar girerek fcntl ile kilit yerleştirebilir. Girilecek komutların biçimi şöyle olmalıdır: Örneğin: CSD (55306)>F_SETLK F_RDLCK 0 64 Bu komutla fcntl fonksiyonu ile "test.txt" dosyasının 0 offset'inden 64 byte uzunluktaki bölgeye F_RDLCK yerleştirilmek istenmiştir. Yerleştirme işlemi blokeye yol açmayan F_SETLK komut kodu ile yapılacaktır. Aynı programı başka bir terminalden girerek çalıştırmalısınız. Örneğin: CSD (55377)>F_SETLK F_WRLCK 0 64 Locked failed!.. F_GETLK komutunda aslında kilit türünün belirtilmesi gereksizdir. Ancak buradaki programı kısaltmak için kullanılmayacak olsa da bir kilit türünü yine belirtmek zorundayız. Örneğin: CSD (55471)>F_GETLK F_RDLCK 0 64 Write Lock Whence: 0 Start: 0 Length: 64 Process Id: 55469 Komutun ikinci argümanı aslında program tarafından kullanılmamaktadır. Ancak girilmek zorundadır. Programdan çıkmak için komut satırına "quit" yazılabilir. Bu programı kllanarak F_SETLKW komutuyla blokeli işlemleri de deneyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* fclock-test.c */ #include #include #include #include #include #include #define MAX_CMDLINE 4096 #define MAX_ARGS 64 void parse_cmd(void); int get_cmd(struct flock *fl); void disp_flock(const struct flock *fl); void exit_sys(const char *msg); char g_cmd[MAX_CMDLINE]; int g_count; char *g_args[MAX_ARGS]; int main(int argc, char *argv[]) { int fd; pid_t pid; char *str; struct flock fl; int fcntl_cmd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } pid = getpid(); if ((fd = open(argv[1], O_RDWR)) == -1) exit_sys("open"); for (;;) { printf("CSD (%ld)>", (long)pid), fflush(stdout); fgets(g_cmd, MAX_CMDLINE, stdin); if ((str = strchr(g_cmd, '\n')) != NULL) *str = '\0'; parse_cmd(); if (g_count == 0) continue; if (g_count == 1 && !strcmp(g_args[0], "quit")) break; if (g_count != 4) { printf("invalid command!\n"); continue; } if ((fcntl_cmd = get_cmd(&fl)) == -1) { printf("invalid command!\n"); continue; } if (fcntl(fd, fcntl_cmd, &fl) == -1) if (errno == EACCES || errno == EAGAIN) printf("Locked failed!...\n"); else perror("fcntl"); if (fcntl_cmd == F_GETLK) disp_flock(&fl); } close(fd); return 0; } void parse_cmd(void) { char *str; g_count = 0; for (str = strtok(g_cmd, " \t"); str != NULL; str = strtok(NULL, " \t")) g_args[g_count++] = str; } int get_cmd(struct flock *fl) { int cmd, type; if (!strcmp(g_args[0], "F_SETLK")) cmd = F_SETLK; else if (!strcmp(g_args[0], "F_SETLKW")) cmd = F_SETLKW; else if (!strcmp(g_args[0], "F_GETLK")) cmd = F_GETLK; else return -1; if (!strcmp(g_args[1], "F_RDLCK")) type = F_RDLCK; else if (!strcmp(g_args[1], "F_WRLCK")) type = F_WRLCK; else if (!strcmp(g_args[1], "F_UNLCK")) type = F_UNLCK; else return -1; fl->l_type = type; fl->l_whence = SEEK_SET; fl->l_start = (off_t)strtol(g_args[2], NULL, 10); fl->l_len = (off_t)strtol(g_args[3], NULL, 10); return cmd; } void disp_flock(const struct flock *fl) { switch (fl->l_type) { case F_RDLCK: printf("Read Lock\n"); break; case F_WRLCK: printf("Write Lock\n"); break; case F_UNLCK: printf("Unlocked (can be locked)\n"); } printf("Whence: %d\n", fl->l_whence); printf("Start: %ld\n", (long)fl->l_start); printf("Length: %ld\n", (long)fl->l_len); if (fl->l_type != F_UNLCK) printf("Process Id: %ld\n", (long)fl->l_pid); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi dosya kilitleme mekanizmasından nasıl faydalanılacaktır? Örneğin biz aynı dosyanın iki bölgesi üzerinde birbirleriyle ilgili iki güncelleme yapmak isteyelim. Tipik olarak bunun için önce iki bölgeyi kilitleriz. Sonra güncellemeleri yaparız. Sonra da kilitleri açarız. Örneğin (kontroller yapılmamıştır): fcntl(fd, F_SETLKW, ®ion1); /* F_WRLCK */ fcntl(fd, F_SETLKW, ®ion2); /* F_WRLCK */ write(fd, region1, size1); write(fd, region2, size2); fcntl(fd, F_SETLKW, ®ion1); /* F_UNLCK */ fcntl(fd, F_SETLKW, ®ion2); /* F_UNLCK */ Şimdi belli bir kaydın bir elemanını değiştirmek isteyelim: fcntl(fd, F_SETLKW, ®ion); /* F_WRLCK */ read(fd, buf, elem_size); write(fd, buf, elem_size); fcntl(fd, F_SETLKW, ®ion); /* F_UNLCK */ ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir programın tek bir kopyasının çalışması istenebilmektedir. Örneğin program bir kaynak kullanıyor olabilir. Programın kullandığı kaynak sistem genelinde tek olabilir. Ya da programın birden fazla kopyasının çalıştırılması tasarım bakımından anomalilere yol açıyor da olabilir. Bunu sağlamak için kayıt kilitleme mekanizması kullanılabilmektedir. Program çalışmaya başladığında bir dosyayı bütünsel olarak kilitlemeye çalışır. Eğer kilitleme başarısız olursa programın başka bir kopyasının çalışıyor olduğu sonucuna varılır. Aslında bu işlem isimli senkronizasyon nesneleriyle de yapılabilmektedir. Örneğin Windows sistemlerinde bunun için dosya kilitleme mekanizması yerine "isimli mutex nesneleri" tercih edilmektedir. Ancak UNIX/Linux sistemlerindeki geleneksel yaklaşım dosya kilitleme mekanizmasının kullanılmasıdır. Aşağıda programın tek bir kopyasının çalışmasını sağlayan basit bir örnek verilmiştir. Bu örnekte proses dosyayı bütünsel olarak "exclusive" bir biçimde kilitlemeye çalışmıştır. Dosya kapatıldığında zaten kilit de ortadan kalkmaktadır. Burada flock fonksiyonunda LOCK_NB bayrağını da kullandık. Çünkü bu tür durumlarda bloke oluşmasının engellenmesi gerekmektedir. Bu yöntemde içi boş bir dosyanın yaratılmış olduğuna dikkat ediniz. Bu tür dosyaların "/temp" dizini içerisinde yaratılması da sık karşılaşılan bir uygulamadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #define LOCK_FILE_PATH "lock.dat" void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) exit_sys("open"); if (flock(fd, LOCK_EX|LOCK_NB) == -1) if (errno == EWOULDBLOCK) { fprintf(stderr, "only one instance of this program can run...\n"); exit(EXIT_FAILURE); } else exit_sys("flock"); sleep(10); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Biz yukarıda "isteğe bağlı (advisory)" kilitleme işlemi yaptık. Burada "isteğe bağlı (advisory)" demekle aslında read/write fonksiyonlarının bu kilitlere hiç bakmaması anlatılmak istenmiştir. Örneğin biz dosyanın bir bölgesine F_WRLCK ile bir kilit yerleştirelim. Başka bir proses o bölgeye write ile yazma yapabilir. Tabii dosya erişen ilgili programların hepsi birbirleriyle koordineli yazıldığı için onlar da read/write yapmadan önce aynı kilit mekanizmasını kullanarak senkronizasyon sağlamaktadır. Özetle "isteğe bağlı kilitleme (advisory locking)" demekle read/write fonksiyonlarının bakmadığı, fcntl fonksiyonunun baktığı kilit anlaşılmaktadır. Pekiyi "zorunlu (mandatory)" kilit sistemi nedir? Zorunlu kilitlemede read/write fonksiyonları kilitlere bakarak çalışmaktadır. Yani örneğin biz bir bölgeye F_WRLCK ile zorunlu kilit yerleştirmişsek o bölgeden read işlemi ya da o bölgeye write işlemi yapıldığında bu fonksiyonlar otomatik blokeye yol açmakta ya da başarısız olmaktadır. Uygulamada "isteğe bağlı (advisory) kilitleme" çok daha yoğun kullanılmaktadır. Çünkü zorunlu kilitlemede read/write fonksiyonlarının her defasında kilitlere bakması (kilitler konulmamış bile olsa) zaman kaybına yol açmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi zorunlu (mandatory) kilitleme nasıl uygulanmaktadır? İsteğe bağlı kilitleme ile zorunlu kilitlemenin uygulanış biçimi arasında hiçbir farklılık yoktur. Kilitleme isteğe bağlı mı yoksa zorunlu mu olduğu "mount parametrelerine" ve "dosyanın erişim haklarına" bakılarak belirlenmektedir. Zorunlu kilitleme kavramı POSIX standartlarında bulunmamaktadır. POSIX standartları fcntl fonksiyonunun açıklamasında bunun gerekçelerini belirtmiştir. Zaten bazı işletim sistemleri zorunlu kilitlemeyi hiç desteklememektedir. Linux 2.4 çekirdeği ile birlikte zorunlu dosya kilitlemesini destekler hale gelmiştir. Yukarıda da belirttiğimiz gibi zorunlu kilitleme için aslında fcntl fonksiyonunda ek bir şey yapılmaz. Linux'ta zorunlu kilitleme için şu koşulların sağlanmış olması gerekmektedir: 1) Dosyanın içinde bulunduğu dosya sisteminin -o mand ile mount edilmiş olması gerekir. Mevcut mount edilmiş olan bir dosya sisteminin mount parametreleri aşağıdaki gibi değiştirilebilir: $ sudo mount -o mand,remount Hangi dosya sisteminin "mand" parametresi ile mount edileceğini tespit etmeniz gerekir. Bunun için df komutunu argümansız biçimde kullanabilirsiniz. Örneğin: $ df Dosyasistemi 1K-blok Dolu Boş Kull% Bağlanılan yer udev 1471368 0 1471368 0% /dev tmpfs 303552 1364 302188 1% /run /dev/sda5 81526200 41496476 35845404 54% / tmpfs 1517748 4 1517744 1% /dev/shm tmpfs 5120 4 5116 1% /run/lock tmpfs 1517748 0 1517748 0% /sys/fs/cgroup /dev/sda1 523248 4 523244 1% /boot/efi tmpfs 303548 104 303444 1% /run/user/1000 mount parametrelerini görebilmek için mount komutunu parametresiz olarak kullanabilirsiniz. Ancak burada karşınıza uzun bir liste çıkabilir. grep utility'si ile ilgili satırı görüntüleyebilirsiniz. Örneğin: $ mount | grep /dev/sda5 /dev/sda5 on / type ext4 (rw,relatime,mand,errors=remount-ro) Aslında mount komutu bu bilgileri /proc/mounts dosyasından elde etmektedir. Örneğin: $ sudo mount -o mand,remount /dev/sda5 / Sistem açıldığında ilgili dosya sisteminin "mand" parametresi ile mount edilebilmesi için bazı start-up dosyaları kullanabilirsiniz. Örneğin /etc/fstab dosyası bu amaçla kullanılabilmektedir. 2) İlgili dosyanın set-group-id bayrağı set edilip gruba x hakkı varsa reset edilmelidir. Bu işlem şöyle yapılmalıdır: $ chmod g+s,g-x Örneğin: $ chmod g+s,g-x test.txt Bir dosyanın bir proses tarafından zorunlu kilitlenmiş olması dosyanın silinmesini engellememektedir. Dosyanın truncate ve ftruncate fonksiyonu ile genişletilmesi ya da budanması işleminde zorunlu kilitleme mekanizması devreye girmektedir. Yani bu fonksiyonlar sanki dosyaya yazma yapıyormuş gibi etki göstermektedir. Benzer biçimde örneğin dosyaya zorunlu kilit konulmuşsa dosya O_TRUNC modunda açılamamaktadır. Ancak yukarıda belirttiğimiz gibi dosya remove ya da unlink fonksiyonlarıyla üzerinde zorunlu kilit olsa bile silinebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 88. Ders 08/10/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------* /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda zorunlu kilitlemeyi test etmek için bir örnek hazırlanmıştır. Bu örnekte iki program vardır. "flock-test.c" programı yukarıdaki programın aynısıdır. "rw-test.c" programı ise belli bir dosyanın belli bir offset'inden belli miktarda okuma ya da yazma yapan bir programdır. "flock-test.c" programını yukarıda açıklamıştık. "rw-test.c" programının komut satırı argümanları şöyledir: ./rw-test Burada "r" okuma, "w" ise yazma yapma anlamına gelmektedir. Örneğin: $ ./rw-test test.txt w 60 10 Bu çalıştırmayla program write fonksiyonu ile "test.txt" dosyasına 60'ıncı offset'inden itibaren 10 byte yazacaktır. Örneğin: $ ./rw-test test.txt r 0 64 Burada da program "test.txt" dosyasının 0'ıncı offset'inden 64 byte okuyacaktır. Aşağıdaki örneği test yaparken şu biçimde kullanabilirsiniz: Önce "flock-test" programı ile dosyanın belli bir bölgesine kilit yerleştiriniz. Sonra "rw-test" programı ile ilgili offset'i kapsayan bölgeden okuma/yazma yapmaya çalışınız. read ve write fonksiyonlarındaki blokeyi gözlemleyiniz. Sonra kilidi kaldırınca fonksiyonların blokeden çıkacağını göreceksiniz. Tabii örnekte test işleminde kullanacağınız dosyanın (örneğin "test.txt") yukarıda belirttiğimiz gibi zorunlu kilitlemeye hazırlanmış olması gerekir. Zorunlu kilitleme uygulanmış bir dosya open fonksiyonu ile O_NONBLOCK bayrağı kullanılarak açılmışsa (normal dosyaların bu bayrak kullanılarak açılması genellikle işlevsizdir, ancak burada bir istisna vardır) bu durumda read/write fonksiyonları çelişkili kilitle karşılaştığında bloke olmazlar ve başarısızlıkla (-1 değeriyle) geri dönerler. Bu durumda errno değeri EAGAIN (Resource temporarily unavailable) değeri ile set edilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /* flock-test.c */ #include #include #include #include #include #include #define MAX_CMDLINE 4096 #define MAX_ARGS 64 void parse_cmd(void); int get_cmd(struct flock *fl); void disp_flock(const struct flock *fl); void exit_sys(const char *msg); char g_cmd[MAX_CMDLINE]; int g_count; char *g_args[MAX_ARGS]; int main(int argc, char *argv[]) { int fd; pid_t pid; char *str; struct flock fl; int fcntl_cmd; F_SETLKW"wrong number of arguments!...\n"); exit(EXIT_FAILURE); } pid = getpid(); if ((fd = open(argv[1], O_RDWR)) == -1) exit_sys("open"); for (;;) { printf("CSD (%ld)>", (long)pid), fflush(stdout); fgets(g_cmd, MAX_CMDLINE, stdin); if ((str = strchr(g_cmd, '\n')) != NULL) *str = '\0'; parse_cmd(); if (g_count == 0) continue; if (g_count == 1 && !strcmp(g_args[0], "quit")) break; if (g_count != 4) { printf("invalid command!\n"); continue; } if ((fcntl_cmd = get_cmd(&fl)) == -1) { printf("invalid command!\n"); continue; } if (fcntl(fd, fcntl_cmd, &fl) == -1) if (errno == EACCES || errno == EAGAIN) printf("Locked failed!...\n"); else perror("fcntl"); if (fcntl_cmd == F_GETLK) disp_flock(&fl); } close(fd); return 0; } void parse_cmd(void) { char *str; g_count = 0; for (str = strtok(g_cmd, " \t"); str != NULL; str = strtok(NULL, " \t")) g_args[g_count++] = str; } int get_cmd(struct flock *fl) { int cmd, type; if (!strcmp(g_args[0], "F_SETLK")) cmd = F_SETLK; else if (!strcmp(g_args[0], "F_SETLKW")) cmd = F_SETLKW; else if (!strcmp(g_args[0], "F_GETLK")) cmd = F_GETLK; else return -1; if (!strcmp(g_args[1], "F_RDLCK")) type = F_RDLCK; else if (!strcmp(g_args[1], "F_WRLCK")) type = F_WRLCK; else if (!strcmp(g_args[1], "F_UNLCK")) type = F_UNLCK; else return -1; fl->l_type = type; fl->l_whence = SEEK_SET; fl->l_start = (off_t)strtol(g_args[2], NULL, 10); fl->l_len = (off_t)strtol(g_args[3], NULL, 10); return cmd; } void disp_flock(const struct flock *fl) { switch (fl->l_type) { case F_RDLCK: printf("Read Lock\n"); break; case F_WRLCK: printf("Write Lock\n"); break; case F_UNLCK: printf("Unlocked (can be locked)\n"); } printf("Whence: %d\n", fl->l_whence); printf("Start: %ld\n", (long)fl->l_start); printf("Length: %ld\n", (long)fl->l_len); if (fl->l_type != F_UNLCK) printf("Process Id: %ld\n", (long)fl->l_pid); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* rw-test.c */ #include #include #include #include #include void exit_sys(const char *msg); /* ./rwtest */ int main(int argc, char *argv[]) { int fd; int operation; off_t offset; off_t len; char *buf; ssize_t result; if (argc != 5) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (strcmp(argv[2], "r") && strcmp(argv[2], "w")) { fprintf(stderr, "invalid operation!\n"); exit(EXIT_FAILURE); } offset = (off_t)strtol(argv[3], NULL, 10); len = (off_t)strtol(argv[4], NULL, 10); if ((buf = (char *)calloc(len, 1)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], argv[2][0] == 'r' ? O_RDONLY : O_WRONLY)) == -1) exit_sys("open"); lseek(fd, offset, SEEK_SET); if (argv[2][0] == 'r') { if ((result = read(fd, buf, len)) == -1) exit_sys("read"); printf("%ld bytes read\n", (long)result); } else { if ((result = write(fd, buf, len)) == -1) exit_sys("write"); printf("%ld bytes written\n", (long)result); } free(buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Dosyalardaki kilitleri izlemek için proc dosya sistemindeki /proc/locks dosyasından faydalanılabilmektedir. Örneğin: $ cat /proc/locks 1: POSIX ADVISORY WRITE 34561 08:05:4194918 0 EOF 2: POSIX MANDATORY WRITE 57926 08:05:1207498 0 63 3: POSIX ADVISORY READ 34560 00:36:103 0 EOF 4: POSIX ADVISORY READ 34560 08:05:3031150 0 EOF 5: POSIX ADVISORY READ 34560 08:05:3050341 0 EOF 6: FLOCK ADVISORY WRITE 705 00:19:673 0 EOF Buradaki ilk sütun kilidin hangi fonksiyonlarla konulduğunu belirtmektedir. Bu sütunda POSIX belirteci kilidin fcntl ya da lockf fonksiyonu ile konulduğunu belirtir. flock fonksiyonu Linux'a özgü olduğu için burada LINUX belirteci ile görüntülenecektir. İkinci sütun kilidin isteğe bağlı mı zorunlu mu olduğunu belirtmektedir. Üçüncü sütun ise kilidin türünü belirtmektedir. Dördüncü sütun kilidi koyan prosesin proses id'sini belirtmektedir. Sonraki sütun ':' karakterleriyle ayrılmış üç alandan oluşmaktadır. İlk iki alan dosyanın içinde bulunduğu aygıtın majör ve minör numaralarını belirtmektedir. Üçüncü alanda kilit uygulanan dosyanın i-node numarası belirtilmektedir. Sonraki iki sütunda da kilidin başlangıç offset'i ve uzunluğu belirtilmektedir. UNIX/Linux sistemlerinde dosyanın yol ifadesinden hareketle stat, fstat, lstat fonksiyonlarıyla biz dosyanın i-node numarasını elde edebiliriz. Ancak bunun tersini yapan bir mekanizma yoktur. Bu işlem ancak find utility'si ile arama yöntemiyle yapılabilir. Örneğin: find / -inum 1207498 2> /dev/null Burada find utility'sine biz i-node numarası 1207498 olan dizin girişlerini kökten itibaren özyinelemeli biçimde aratmaktayız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dosya kilitleme için POSIX standartlarında lockf isimli bir fonksiyon da bulundurulmuştur. Bu fonksiyon aslında yukarıda görmüş olduğumuz fcntl fonksiyonuna sarma yapan bir fonksiyondur. lockf fonksiyonunun prototipi şöyledir: #include int lockf(int fd, int function, off_t size); Fonksiyonun birinci parametresi dosya betimleyicisini, ikinci parametresi uygulanacak lock işleminin türünü, üçüncü parametresi ise kilitlenecek alanının uzunluğunu belirtmektedir. Fonksiyonda başlangıç offset'inin olmadığına dikkat ediniz. Fonksiyon her zaman dosya göstericisinin gösterdiği yerden itibaren kilitleme yapmaktadır. İkinci parametrede belirtilen kilit türü şunlardan biri olabilir: Kilit Türü fcntl Karşılığı Anlamı F_ULOCK F_SETLK, F_UNLCK Kilidi kaldırır F_LOCK F_SETLK, F_WRLCK Blokesiz yazma kilidi yerleştirir F_TLOCK F_SETLKW, F_WRLCK Blokeli yazma kilidi yerleştirir F_TEST F_GETLK Kilidin yerleştirilmiş olup olmadığına bakar Görüldüğü gibi fonksiyon her zaman "yazma (yani "exclusive")" bir kilit yerleştirmektedir. Dolayısıyla bu fonksiyon fcntl fonksiyonuna göre daha yeteneksizdir. Programcılar tarafından pek tercih edilmemektedir. Fonksiyonun üçüncü parametresi 0 geçilirse bu durum "bulunulan offset'ten dosya sonuna kadar ve daha sonraki eklemeleri kapsayacak biçimde" kilit yerleştirilmesi anlamına gelir. Eğer F_ULOCK işleminde bu parametre 0 girilirse bulunulan offset'ten itibaren sonraki bütün kilitler kaldırılmaktadır. Fonksiyonun uzunluk belirten size parametresi negatif de girilebilir. Bu durumda dosya göstericisinin gösterdiği yerden geriye doğru alan kilitlenir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar proc dosya sistemindeki bazı dosyaları hedefe yönelik biçimde gördük. Şimdi proc dosya sistemini genel olarak ele almak istiyoruz. proc dosya sistemi disk tabanlı bir dosya sistemi değildir. Bu dosya sistemi bellek tabanlıdır. Genellikle bu dosya sistemindeki bilgiler çekirdek modülleri ve aygıt sürücüler tarafından talep edildiğinde verilmektedir. proc dosya sistemi standart bir dosya sistemi değildir. Dolayısıyla bu dosya sistemi POSIX standartlarında belirtilmemiştir. Linux gibi bazı UNIX türevi sistemler bu dosya sistemini desteklerken bazıları desteklememektedir. (Örneğin macOS sistemleri proc dosya sistemini desteklememektedir.) proc dosya sistemi boot işlemi sırasında "/proc" dizinine mount edilmektedir. proc dosya sistemindeki dosyaların ve dizinlerin listesi alındığında sanki onların uzunlukları "0" imiş gibi rapor edilmektedir. Bunun nedeni bu dizindeki dosyaların içeriklerinin talep edildiğinde oluşturulmasıdır. Örneğin: $ ls -l /proc dr-xr-xr-x 9 root root 0 Ağu 21 11:21 1 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 10 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 100 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 1001 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 1003 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 101 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 102 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 103 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 105 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 107 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 108 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 11 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 110 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 111 dr-xr-xr-x 9 root root 0 Ağu 21 11:21 112 ... proc dosya sistemi içerisinde dizinler ve dosyalar bulunmaktadır. Bu dizinlerin ve dosyaların nasıl oluşturulduğu aygıt sürücülerin ele alındığı bölümde açıklanmaktadır. proc dosya sistemi çekirdeğin ve aygıt sürücülerin birtakım bilgileri dış dünyaya iletmesini basit bir biçimde sağlamaktadır. Bu dosya sistemi sayesinde biz sistemde ne olup bittiğini buradaki dosyaların içeriklerini okuyarak anlayabiliriz. Gerçekten de pek çok UNIX/Linux komutu aslında çekirdeğe ilişkin birtakım bilgileri bu dosya sisteminden elde etmektedir. Örneğin "ps" komutu ile sistemdeki proseslerin bilgilerini elde edebiliyorduk. Aslında "ps" komutu bu bilgileri proc dosya sistemindeki dosyalardan elde etmektedir. Biz bazı konularda bazı bilgileri elde etmek için bu proc dosya sistemindeki birtakım dosyaları kullanmıştık. proc dosya sistemindeki dosyaların bazıları yazmaya da izin vermektedir. Eğer sistem yöneticisi bu dosyalarda değişiklik yaparsa çekirdek ve aygıt sürücüler bu değişikleri dikkate alıp çalışmasını bu değişiklere göre ayarlayabilmektedir. proc dosya sistemi içerisindeki dosyalar genel olarak "text dosyalar" biçimindedir. Yani bunların içeriklerini biz "cat" komutuyla elde edebiliriz. proc dosya sisteminde iç içe pek çok dizin ve dosya bulunmaktadır. Biz burada bu dosya sisteminin genel içeriği hakkında bazı bilgiler vereceğiz. proc dosya sisteminin kökünde (yani "/proc" dizininde) her proses için ayrı bir dizin yaratılmaktadır. Yaratılan dizinin ismi prosesin id değeri biçimindedir. Yani örneğin biz bir programı çalıştırdığımızda yaratılan prosesin proses id'sine ilişkin bir dizin "/proc" dizinin kökünde yaratılmaktadır: $ ./app press ENTER to exit... Şimdi yaratılan prosesin proses id'sine başka bir terminalden bakalım: $ ps -a PID TTY TIME CMD 16852 pts/0 00:00:00 app 16854 pts/3 00:00:00 ps İşte çekirdek "/proc" dizininde "16852" ismiyle bir dizin yaratıp o ilgili prosesin tüm bilgilerini bu dizinin içerisine yerleştirmektedir: $ ls -ld /proc/16852 dr-xr-xr-x 9 kaan study 0 Ağu 27 13:40 /proc/16852 Bu dizin içeriğini görüntülediğimizde aşağıdaki gibi bir dizin yapısıyla karşılaşmaktayız: $ ls /proc/16852 arch_status cmdline environ limits mounts oom_score root smaps_rollup task attr comm exe loginuid mountstats oom_score_adj sched stack timens_offsets autogroup coredump_filter fd map_files net pagemap schedstat stat timers auxv cpu_resctrl_groups fdinfo maps ns patch_state sessionid statm timerslack_ns cgroup cpuset gid_map mem numa_maps personality setgroups status uid_map clear_refs cwd Buradaki girişlerin bazıları "dizin" bazıları da dosyadır. Buradaki bazı dizin ve dosyalar hakkında kısa açıklamalar yapalım: - fd dizini prosesin açmış olduğu dosyalara ilişkin bilgileri bulundurmaktadır. Örneğin: $ ls -l toplam 0 lrwx------ 1 kaan study 64 Ağu 27 14:00 0 -> /dev/pts/0 lrwx------ 1 kaan study 64 Ağu 27 14:00 1 -> /dev/pts/0 lrwx------ 1 kaan study 64 Ağu 27 14:00 2 -> /dev/pts/0 Burada dosyaların sembolik bağlantı dosyaları olduğuna dikkat ediniz. - fdinfo isimli dizin içerisinde açılmış dosyaların bazı önemli bilgileri bulundurulmaktadır. - environ isimli dosya prosesin çevre değişken listesini bize vermektedir. - cmdline isimli dosyada program çalıştırılırken kullanılan komut satırı argümanları bulunmaktadır. proc dosya sisteminin kök dizininde sisteme ilişkin bilgi veren pek çok dosya dosya bulunmaktadır. Burada bunların birkaçı üzerinde duralım: - version isimli dosya yüklü olan çekirdek ve dağıtım hakkında temel bilgileri vermektedir. - devices dosyası yüklü olan aygıt sürücüler hakkında bilgiler vermektedir. - modules isimli dosya yüklü olan çekirdek modülleri hakkında bilgiler vermektedir. proc dosya sistemi oldukça fala dizine ve dosyaya sahiptir. Bazı dosyalar özel birtakım konularla ilgildir. Dolayısıyla bu dosyaların ve dizinlerin "konuya göre gerektikçe öğrenilmesi" yoluna gidilebilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar read/write fonksiyonlarıyla klasik IO işlemleri yaptık. UNIX/Linux sistemlerinde bloke durumlarında kullanılabilecek alternatif IO modelleri de vardır. Bunlara "ileri IO (advanced IO)" de denilmektedir. Bu bölümde özellikle client-server programların gerçekleştirilmesinde kullanılabilecek ileri IO modelleri üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi ileri IO işlemlerine neden gereksinim duyulmaktadır? Client-server bir sistem için bir server programı yazacak olalım. Server program N tane client'tan gelen istekleri okusun ve onlara yanıt göndersin. Haberleşme ortamı isimli borularla ya da sonraki bölümde göreceğimiz soketlerle yapılabilmektedir. Ancak isimli borulardan ve soketlerden okuma yapılırken eğer boruda ya da sokette hiçbir bilgi yoksa read fonksiyonu bloke olmaktadır. Bu nedenle böyle bir program aşağıdaki gibi bir döngüyle yazılamamaktadır: for (;;) { for (int i = 0; i < N; ++i) { read(...); write(...); } ... } Burada bir client'ın borusunda ya da soketinde hiç bilgi yoksa read fonksiyonu blokeye yol açacak ve diğer client'lardan gelen bilgiler işleme sokulamayacaktır. Tabii bu durum yalnızca borular ve soketler için değil, bloke yok açan diğer kaynaklar için de söz konusudur. Bu tür durumlarda ilk akla gelen şey blokesiz modda işlem yapmak olabilir. Örneğin: for (;;) { for (int i = 0; i < N; ++i) { result = read(...); if (result == -1 && errno == EAGAIN) continue; write(...); } ... } Burada problem çözülmüş gibi gözükmekle birlikte aslında başka bir sorun oluşmaktadır. Eğer hiçbir client'tan bilgi gelmemişse burada "meşgul bir döngü (busy loop)" oluşmaktadır. Bu tür durumlarda akla gelen en basit çözüm "thread ya da proses" modelini kullanmaktadır. Tabii proses yaratımı, thread yaratımına göre çok daha maliyetli olduğu için thread modeli tercih edilmektedir. Bu modelde her client için ayrı bir thread (ya da proses) oluşturulur. O client'ın istekleri o thread (ya da proses) tarafından sağlanır. Böylece bir client'a bilgi gelmediği zaman yalnızca o thread (ya da proses) bloke olacaktır. Diğerlerinin çalışması devam edecektir. Bu model basitliği nedeniyle az sayıda client'ın bulunduğu durumlarda uygun bir yöntemdir. Ancak ölçeklenebilir (scalable) değildir. Yani client sayısı arttığında çok fazla sistem kaynağı kullanılacağından dolayı olumsuzluk ortaya çıkacaktır. İşte ileri IO modelleri temel olarak yukarıdaki tarzda problemleri çözmek için geliştirilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- POSIX sistemlerinde ileri IO işlemleri dört bölüme ayrılarak incelenebilir: 1) Multiplexed IO: Bu modelde bir grup betimleyici izlemeye alınır. Bu betimleyicilerde ilgilenilen olay (read/write/error) gerçekleşmemişse blokede beklenir. Ta ki bu betimleyicilerden en az birinde ilgilenilen olay gerçekleşene kadar. Multiplexed IO için select ve poll POSIX fonksiyonları kullanılmaktadır. Ancak Linux epoll isimli daha yetenekli bir fonksiyona da sahiptir. 2) Sinyal Tabanlı (Signal Driven) IO: Burada belli betimleyiciler izlemeye alınır. Ancak blokede beklenmez. Bu betimleyicilerde olay gerçekleştiğinde SIGIO isimli sinyal oluşur. Programcı da bu sinyal oluştuğunda blokeye maruz kalmadan read/write işlemini yapılabilir. 3) Asenkron IO: Burada read/write işlemleri başlatılır. Ancak bir bloke oluşmaz. Arka planda çekirdek tarafından okuma ve yazma bir yandan devam ettirilir. Ancak aktarım bittiğinde programcı bundan haberdar edilir. Bunun signal driven IO'dan farkı şudur: Signal tabanlı IO'da aktarım yapılmamaktadır. Yalnızca okuma yazma yapılırsa bloke olunmayacağı prosese söylenmektedir. Halbuki asenkron IO'da okuma ve yazma işlemi bloke çözüldüğünde arka planda gerçekleştirilmekte ve yalnızca işlemin bittiği haber verilmektedir. 4) Scatter-Gather IO: Burada okuma birden fazla adrese, yazma ise birden fazla adresten kaynağa yapılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Multiplexed IO işlemlerinde select ve poll isimli POSIX fonksiyonları ve Linux sistemlerinde de Linux sistemlerine özgü epoll fonksiyonu kullanılmaktadır. select fonksiyonu çok eskiden beri var olan klasik fonksiyonlardan biridir. select ilk kez BSD sistemlerinde gerçekleştirilmiştir. POSIX standartları oluşturulduğunda doğrudan standartlarda bulundurulmuştur. Aslında select fonksiyonunun tasarımında bazı kusurlar vardır. Ancak fonksiyon hala en çok kullanılan ileri IO fonksiyonlarındandır. select fonksiyonunun prototipi şöyledir: #include int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout); Fonksiyonun parametrelerindeki fd_set türü bir bit dizisi belirtmektedir. Bu bit dizisinin belli bitini 1 yapmak için FD_SET, belli bitini 0 yapmak için FD_CLR, tüm bitlerini sıfır yapmak için FD_ZERO, belli bitini test etmek için ise FD_ISSET makroları kullanılmaktadır. Biz fonksiyonun ikinci (readfds), üçüncü (writefds) ve dördüncü (errorfds) parametrelerine bir grup betimleyiciyi o betimleyicilerin numaralarına karşı gelen bitleri 1 yaparak veririz. Örneğin biz 18, 23 ve 47 numaralı betimleyicilerle ilgilenmek isteyelim. Bunun için fd_set veri yapısının 18, 23 ve 47 numaralı bitlerini 1 yaparız. Fonksiyonun ikinci parametresi (readfds) "okuma amaçlı izlenecek betimleyicilerin kümesini", üçüncü parametresi (writefds) "yazma amaçlı izlenecek betimleyicilerin kümesini" ve dördüncü parametresi (errorfds) ise "hata (exception) oluşumu için izlenecek betimleyicilerin kümesini" belirtmektedir. Bu parametrelerin biri ya da birden fazlası NULL geçilebilir. Bu durum ilgili izlemenin yapılmayacağını belirtmektedir. Fonksiyonun birinci parametresi bu kümelerdeki en yüksek betimleyicinin bir fazla değerini almaktadır. Bu parametre aslında işlemleri hızlandırmak için düşünülmüştür. En yüksek betimleyici değeri FD_SETSIZE (1024) ile define edilmiş durumdadır. (Yani bu parametreye istersek doğrudan bu değeri de geçebiliriz.) Örneğin biz 18, 23 ve 47 numaralı betimleyicileri izlemek istiyorsak bu birinci parametreye 48 değerini (47 + 1) girebiliriz. Ancak buraya en yüksek betimleyici değeri olan FD_SETSIZE girilse de bir sorun oluşmayacaktır. Yukarıda da belirttiğimiz gibi bu birinci parametre yalnızca işlemleri hızlandırmak için bir ipucu niteliğindedir. Fonksiyonun son parametresi "zaman aşımı" belirtmektedir. Zaman aşımı için NULL adres girilebilir. Bu durum zaman aşımının uygulanmayacağı anlamına gelir. timeval yapısını biz daha önce kullanmıştık. Anımsatmak istiyoruz: struct timeval { time_t tv_sec; suseconds_t tv_usec; }; Bu yapı mikrosaniye çözünürlüğünde bir zaman aralığı belirtmek için oluşturulmuştur. Yapının her iki elemanı 0'da girilebilir. Bu durumda select fonksiyonu hemen testini yapar ve geri döner. select fonksiyonu ile bir grup betimleyiciyi okuma amaçlı izlemek isteyelim. İlk ayapacığımız şey izlenecek betimleyicileri bir fd_set nesnesi içerisinde belirtmek ve bu nesnesin adresini fonksiyonun ikinci parametresine vermektir. Birinci parametreye de buradaki en yüksek betimleyici değerinin 1 fazlası geçirilecektir. Örneğin: fd_set rset; ... FD_ZERO(&rset); FD_SET(fd1, &rset); // fd1 = 18 varsayalım FD_SET(fd2, &rset); // fd2 = 23 varsayalım FD_SET(fd3, &rset); // fd3 = 48 varsayalım maxfds = getmax(fd1, fd2, fd3); select(maxfds + 1, &rset, NULL, NULL, NULL); select fonksiyonu bizim ona verdiğimiz betimleyicileri okuma amaçlı izler. Bu betimleyicilere hiçbir bilgi gelmemişse select akışı blokede bekletmektedir. Ancak bu betimleyicilerin en az birine bir bilgi geldiyse bu durumda select blokeyi çözmektedir. Görüldüğü gibi select fonksiyonu "okunacak hiçbir bilgi betimleyicilerde oluşmamışsa akışı blokede bekletmekte ancak en az bir betimleyicide okunacak bilgi oluşmuşsa okuma işleminin yapılması için blokeyi çözmektedir. select fonksiyonun okumayı yapmadığına yalnızca okuma yapılabilecek bir durumun oluştuğunu tespit ettiğine dikkat ediniz. Tabii programıcının select'in blokesi çözüldüğünde hangi betimleyicilere bilgi gelmiş olduğunu anlaması gerekir. Çünkü gelen bilgileri read ile o betimleyicilerden okuyacaktır. Tabii select fonksiyonu aslında bir kez değil, genellikle bir döngü içerisinde kullanılmaktadır. Pekiyi select blokeyi çözdüğünde programcı ne yapmalıdır? Örneğin biz yine 18, 23 ve 48 numaralı betimleyicileri okuma amaçlı izlemek isteyelim. select fonksiyonu 23 numaralı betimleyiciye bilgi geldiğinden dolayı blokeyi çözmüş olsun. Bizim bunu anlayıp 23 numaralı betimleyiciden read fonksiyonu ile (ya da soketler söz konusu ise recv ya da recvfrom fonksiyonları ile) okuma yapmamız gerekir. İşte select bizim ona verdiğimiz fd_set nesnelerini çıkışta yeniden uygun biçimde set etmektedir. Hangi betimleyicilerde ilgili olay gerçekleşmişse o betimleyicilere ilişkin bitleri set edip diğerlerini reset etmektedir. Yani select bizim ona verdiğimiz fd_set nesnelerinin içeriğini bozup ilgili olayın gerçekleştiği betimleyicileri o nesnelerde belirtmektedir. O halde bizim select fonksiyonu geri döndüğünde bu fd_set nesnelerinin bitlerine bakıp hangi betimleyiciye ilişkin bitlerin set edildiğini anlamamız ve onlardan okuma yapmamız gerekir. select ile beklerken birden fazla betimleyicide olayın gerçekleşmiş olabileceğine dikkat ediniz. Bu nedenle kontrolü else-if ile değil, ayrık if deyimleriyle yapmalısınız. Aşağıdaki kod parçasında üç betimleyici select ile izlenmiş, select fonksiyonunun blokesi çözüldüğünde bu betimleyicilere ilişkin bitler tek tek FD_ISSET makrosu ile kontrol edilmiştir. Hangi betimleyicide okuma olayı gerçekleşmişse read fonksiyonu ile o betimleyiciden okuma yapılmıştır. Bu noktada artık read fonksiyonunun blokeye yol açmayacağına dikkat ediniz. fd_set rset; ... FD_ZERO(&rset); FD_SET(fd1, &rset); // fd1 = 18 varsayalım FD_SET(fd2, &rset); // fd2 = 23 varsayalım FD_SET(fd3, &rset); // fd3 = 48 varsayalım maxfds = getmax(fd1, fd2, fd3); if (select(maxfds + 1, &rset, NULL, NULL, NULL) == -1) exit_sys("select"); if (FD_ISSET(fd1, &rset)) { result = read(fd1, ...); ... } if (FD_ISSET(fd2, &rset)) { result = read(fd2, ...); ... } if (FD_ISSET(fd3, &rset)) { result = read(fd3, ...); ... } Programcı select yoluyla çok sayıda betimleyiciyi izliyorsa select çıkışında onlar için ayrı if deyimleriyle kontrol yapmak yorucu olabilmektedir. Bu durumda iki yol izlenebilir. Birincisi tüm betimleyicileri bir döngü içerisinde kontrol etmektir: ... if (select(maxfds + 1, &rset, NULL, NULL, NULL) == -1) exit_sys("select"); for (int fd = 0; fd <= maxfds; ++fd) if (FD_ISSET(fd, &rset)) { result = read(fd, ...); ... } Burada 0'dan maxfds değerine kadar rset nesnesindeki tüm bitlere bakılmış ve set edilen bitlere ilişkin betimleyicilerden okuma yapılmıştır. İkinci yöntem, izlenecek betimleyicileri aynı zamanda bir diziye yerleştirmektir. Örneğin: fd_set rset; int fds[3]; ... fds[0] = fd1; fds[1] = fd2; fds[2] = fd3; ... FD_ZERO(&rset); for (int i = 0; i < 3; ++i) FD_SET(fds[i], &rset); maxfds = getmax(fds, 3); if (select(maxfds + 1, &rset, NULL, NULL, NULL) == -1) exit_sys("select"); for (int i = 0; i < 3; ++i) if (FD_ISSET(fds[i], &rset)) { result = read(fds[i], ...); ... } Burada izlenecek betimleyicilerin numaraları fds dizisine yerleştirilmiştir. select geri döndüğünde yalnızca fds dizisindeki betimleyicilere ilişkin bitler FD_ISSET makrosuyla kontrol edilmiştir. Yukarıda da belirttiğimiz gibi select fonksiyonu genel olarak bir döngü içerisinde kullanılmaktadır. Fonksiyonu döngü içerisinde kullanırken bizim ona verdiğimiz fd_set kümesinin bozulacağına, dolayısıyla döngünün başında onun yeniden yüklenmesi gerektiğine dikkat ediniz. Örneğin: fd_set rset, orset; ... FD_ZERO(&rset); FD_SET(fd1, &rset); // fd1 = 18 varsayalım FD_SET(fd2, &rset); // fd2 = 23 varsayalım FD_SET(fd3, &rset); // fd3 = 48 varsayalım maxfds = getmax(fd1, fd2, fd3); for (;;) { orset = rset; if (select(maxfds + 1, &orset, NULL, NULL, NULL) == -1) exit_sys("select"); if (FD_ISSET(fd1, &orset)) { read(fd1, ...); } if (FD_ISSET(fd2, &orset)) { read(fd2, ...); } if (FD_ISSET(fd3, &orset)) { read(fd3, ...); } } Burada her döngünün başında izlemenin yapılacağı rset nesnesi orset (output rset) nesnesine atanmıştır. Böylece her defasında orset izlenmesi gereken betimleyicileri belirtmektedir. İki fd_set nesnesi birbirine atanabilmektedir. (fd_set türü tipik olarak bir yapı biçiminde oluşturulmuştur. İki yapı nesnesi birbirine atandığında karşılıklı elemanların atanacağına dikkat ediniz.) Pekiyi boru, soket gibi betimleyicilerden select eşliğinde okuma yapılırken karşı taraf bu betimleyicileri kapatırsa ne olacaktır? İşte bu durumda select fonksiyonu sanki o betimleyicide "okuma" olayı varmış gibi davranmaktadır. Örneğin boruyu karşı taraf kapattığında select fonksiyonu okuma olayı gerçekleşmiş gibi geri döner, biz de read fonksiyonu ile 0 byte okuruz ve artık karşı tarafın boruyu kapatmış olduğunu anlarız. select ile çok sayıda betimleyiciyi izlerken bu betimleyicileri karşı taraf kapattığında biz de read fonksiyonu ile bunu anladığımızda artık betimleyiciyi okuma kümesinden çıkartmamız gerekir. select fonksiyou başarısızlık durumunda -1 değerine geri döner. Eğer hiçbir betimleyicide olay gerçekleşmemiş, ancak zaman aşımı dolmuşsa fonksiyon 0 değerine geri dönmektedir. Eğer en az bir betimleyicide ilgili olay gerçekleşmişse select fonksiyonu bu durumda toplam gerçekleşen olay sayısına geri dönmektedir. (Aynı betimleyici örneğin hem okuma hem de yazma için izleniyorsa ve bu betimleyicide hem okuma hem de yazma olayı gerçekleşmişse bu değer 2 artırılmaktadır.) Fonksiyonun geri dönüş değeri genellikle programcılar tarafından kullanılmamaktadır. select fonksiyonunun normal disk dosyaları için kullanılması anlamsızdır. Eğer select normal disk dosyaları için kullanılırsa select her zaman olay gerçekleşmiş gibi geri dönecektir. select fonksiyonu uzun süre beklemeye yol açabilecek terminal gibi, boru gibi, soket gibi aygıtlar için kullanılmalıdır. Normal olarak select ile beklenecek betimleyicilere ilişkin kaynaklar "blokeli" modda açılmalıdırlar. select blokeyi kendisi uygulamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 89. Ders 14/10/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda select fonksiyonunun kullanımına basit bir örnek verilmiştir. Bu örnekte select ile stdin dosyası (0 numaralı betimleyici) okuma amaçlı izlenmektedir. Klavyeden giriş yapılıp ENTER tuşuna basıldığında select blokeyi çözmekte ve read ile artık bloke olmadan okuma yapılabilmektedir. select fonksiyonunun tek bir betimleyici için kullanılmasının bir anlamı yoktur. Bu örnek yalnızca select fonksiyonunun kullanımını ana hatlarıyla açıklamak için verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { fd_set rset, orset; char buf[BUFFER_SIZE + 1]; ssize_t result; FD_ZERO(&rset); FD_SET(0, &rset); for (;;) { orset = rset; if (select(0 + 1, &orset, NULL, NULL, NULL) == -1) exit_sys("select"); if (FD_ISSET(0, &orset)) { if ((result = read(0, buf, BUFFER_SIZE)) == -1) exit_sys("read"); if (result == 0) break; buf[result] = '\0'; printf("%s", buf); } } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bir grup betimleyici select fonksiyonu ile izlenirken karşı taraf ilgili betimleyiciyi kapatırsa ne olacaktır? Örneğin biz select fonksiyonu ile isimli boruları ya da soketleri izleyebiliriz. Böylece borunun ya da soketin karşı tarafındakilerin yazdıklarını etkin bir biçimde elde edebiliriz. Pekiyi ya karşı taraf boruyu ya da soketi kapatırsa ne olacaktır. İşte yukarıda da belirttiğimiz gibi karşı taraf boru ya da soketi kapattığında select sanki "okuma" olayı oluşmuş gibi davranır. Biz de bunu read fonksiyonu ile anlarız (read bu durumda 0 ile geri dönecektir). Böylece kendi betimleyicimizi kapatıp ilgili betimleyiciyi de izleme listesinden çıkartırız. Aşağıdaki örnekte program komut satırı argümanlarıyla aldığı isimli boruları okuma amacıyla select fonksiyonuyla izlemektedir. İsimli boruların O_RDONLY modda açılması sırasında karşı taraf boruyu O_WRONLY modunda (ya da O_RDWR modunda) açana kadar open fonksiyonunun blokeye yol açtığını anımsayınız. Bu programı kullanırken önce mkfifo komutuyla isimli boruları yaratmalısınız. Örneğin: mkfifo x y Z Burada x, y ve z isimli boruları yaratılacaktır. Daha sonra programı bu isimli boruların yol ifadeleriyle çalıştırmalısınız. Örneğin: ./sample x y z Artık bu isimli borular açıldığında program bir döngü içerisinde select fonksiyonunda bekleyecektir. Bu borulardan herhangi birine yazma yapıldığında select blokesi çözülecek ve artık program o borudan okuma yapacaktır. Bütün borular kapatıldığında program sonlandırılmaktadır. Aşağıdaki programı test etmek için en pratik yöntem başka terminaller açarak cat programını borulara yönlendirerek çalıştırmaktır. Örneğin: cat > x Artık klavyeden bir şeyler yazıp ENTER tuşuna bastığımızda cat onu boruya yazacaktır. cat programından Ctrl+D tuşu ile çıkabilirsiniz. Tabii aslında cat programı Ctrl+C ile sinyal yoluyla sonlandırılsa da bir sorun oluşmayacaktır. Bir proses nasıl sonlanırsa sonlansın işletim sistemi o prosesin açmış olduğu dosyaları kapatmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { fd_set rset, orset; char buf[BUFFER_SIZE + 1]; ssize_t result; int fds[MAX_SIZE]; int maxfd; int count; if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); FD_ZERO(&rset); maxfd = -1; for (int i = 1; i < argc; ++i) { if ((fds[i] = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); printf("%s opened...\n", argv[i]); if (fds[i] > maxfd) maxfd = fds[i]; FD_SET(fds[i], &rset); ++count; } for (;;) { orset = rset; printf("waiting at select...\n"); if (select(maxfd + 1, &orset, NULL, NULL, NULL) == -1) exit_sys("select"); for (int i = 0; i <= maxfd; ++i) if (FD_ISSET(fds[i], &orset)) { if ((result = read(fds[i], buf, BUFFER_SIZE)) == -1) exit_sys("read"); if (result == 0) { printf("peer closed descriptor...\n"); close(fds[i]); FD_CLR(fds[i], &rset); --count; if (count == 0) goto EXIT; } buf[result] = '\0'; printf("%s", buf); } } EXIT: printf("there is no descriptor open, finishes...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- select fonksiyonu en çok okuma amaçlı izlemede kullanılmaktadır. Pekiyi yazma amaçlı izleme nedir? İşte bazı aygıtlar yazma sırasında da blokeye yol açabilmektedir. Örneğin biz borulara write fonksiyonu ile yazma yaptığımızda eğer boru tamamen doluysa boruda yer açılana kadar blokede bekleriz. Ancak bu bizim diğer borulara yazma yapmamızı engelleyebilir. O zaman biz select fonksiyonunu yazma izlemesi için de kullanabiliriz. Örneğin select fonksiyonuna biz üç boru betimleyicisini yazma izlemesi için vermiş olalım. select boruda yer açılınca blokeyi çözecek ve yazma yapılabilecek betimleyicileri yine okumadaki gibi set edecektir. Biz de okumadakine benzer bir biçimde hangi boruda boşluk oluştuysa ona yazma yaparız. (Borularda tüm bilgi boruya yazılana kadar bloke oluşmaktadır. select az bir byte yazabilme durumunda blokeyi çözmektedir. Tabii okuyan taraf da aynı miktar bilgiyi okuyorsa burada yine sorun çıkmayacaktır.) Benzer biçimde soketlere yazma yapılırken yazılan bilgiler önce network tamponuna yazılmaktadır. Eğer bu tampon doluysa yine bloke oluşmaktadır. Görüldüğü gibi okumadaki benzer bloke problemi yazmada da ortaya çıkabilmektedir. Borularda önce yazan tarafın boruyu kapatması gerekmektedir. Ancak okuyan taraf boruyu kapatırsa yazan taraf boruya yazma yaptığında SIGPIPE sinyalinin oluştuğunu anımsayınız. select fonksiyonunda okuyan taraf boruyu kapatırsa yazma takibinde sanki bir yazma olayı varmış gibi durum oluşmaktadır. Tabii bu durumda select geri dönünce yazma yapılırsa yine SIGPIPE sinyali oluşturulacaktır. Aynı durum aslında soketlerde de söz konusudur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- select fonksiyonunun dördüncü parametresi olan "errorfds" ne anlama gelmektedir? int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout); Bu parametre programcılar tarafından genellikle yanlış anlaşılmaktadır. İsminden dolayı sanki betimleyicide bir hata izlemesinin yapılacağı sanılmaktadır. Oysa bu parametre çok kısıtlı bir kullanıma sahiptir. Borularda bu parametrenin bir etkisi yoktur. Soketlerde ise "out of band data" oluştuğunda bir etkisi olmaktadır. Yani bu parametre bulunuyor olsa da önemli bir kullanıma sahip değildir. Dolayısıyla genellikle NULL geçilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- select fonksiyonunun son parametresi zaman aşımı belirtmektedir. Zaman aşımı "en kötü olasılıkla blokenin ne kadar süreceğini" belirtir. select eğer zaman aşımından dolayı sonlanırsa 0 değerine geri dönmektedir. Bu zaman aşımı parametresi NULL geçilirse bu durum herhangi bir zaman aşımının uygulanmayacağı anlamına gelir. Linux sistemlerinde bu zaman aşımı parametresi girilirse çıkışta bu elemana "kalan zaman" set edilmektedir. Ancak POSIX standartları bu davranışı garanti etmemektedir. select fonksiyonundaki zaman aşımı parametresi mikrosaniye duyarlılığındadır. Ancak pek çok işletim sistemi bu duyarlılıkta işlem yapamamaktadır. timeval yapısını yeniden anımsatmak istiyoruz: struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; Eskiden UNIX türevi sistemlerde yüksek çözünürlüklü bekleme yapan nanosleep ve clock_nanosleep fonksiyonları yoktu. Programcılar da bu amaçla select fonksiyonunu kullanabiliyordu. Eğer select fonksiyonunun izleme parametrelerinin hepsine NULL geçilirse ancak zaman aşımı parametresine belli bir süre girilirse fonksiyon sanki mikrosaniye duyarlılığına sahip sleep gibi çalışmaktadır. Tabii buradaki duyarlılık sistemlerde sağlanamayabiliyordu. Örneğin: struct timeval tv; ... tv.tv_sec = 3; tv.tv_usec = 500000; printf("waiting at sleep for 3.5 second...\n"); if (select(0, NULL, NULL, NULL, &tv) == -1) exit_sys("select"); ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include void exit_sys(const char *msg); int main(void) { struct timeval tv; tv.tv_sec = 3; tv.tv_usec = 500000; printf("waiting at sleep for 3.5 second...\n"); if (select(0, NULL, NULL, NULL, &tv) == -1) exit_sys("select"); printf("Ok\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- select fonksiyonunda kullanılan fd_set bit dizisi en fazla FD_SETSIZE kadar biti içermektedir. Linux sistemlerinde FD_SETSIZE 1024 olarak define edilmiştir. Dolayısıyla Linux sistemlerinde select fonksiyonu ile ancak ilk 1024 betimleyici izlenebilir. Biz setrlimit fonksiyonu ile dosya betimleyici tablomuzu büyütsek bile select fonksiyonu yalnızca ilk 1024 betimleyici ile çalışmaya devam edecektir. Bunun için çeşitli çözümler uyduruldaysa da bunların hiçbiri genel ve taşınabilir değildir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- select fonksiyonunun pselect isminde sigset_t parametreli bir biçimi de vardır: #include int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask); Fonksiyon sigmask parametresiyle aldığı sinyal kümesini thread'in sinyal bloke kümesi yapar. Böylece fonksiyon çalıştığı sürece bazı sinyaller bloke edilebilmekte ya da onların blokesi açılabilmektedir. Fonksiyon sonlandığında eski sinyal bloke kümesini yeniden thread'in sinyal bloke kümesi olarak set edilmektedir. Yani buraya girilecek sinyal bloke kümesi fonksiyon çalıştığı sürece etkili olmaktadır. Bu parametre NULL adres geçilirse fonksiyonun pselect fonksiyonundan bir farkı kalmaz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- poll fonksiyonu select fonksiyonun alternatifi olan bir fonksiyondur. select ile poll aynı amaçlarla kullanılmaktadır. Fakat bu iki fonksiyonun parametrik yapıları ve kullanılma biçimleri farklıdır. Daha önceden de belirttiğimiz gibi select fonksiyonu BSD sistemlerinde tasarlanmışken poll fonksiyonu klasik AT&T UNIX sistemlerinde tasarlanmıştır. Tabii bu iki fonksiyon da ilk zamandan beri POSIX standartlarında bulunmaktadır. poll fonksiyonu bazı bakımlardan select fonksiyonundan daha iyi gibi gözükmektedir. Ancak poll fonksiyonunun kullanımı biraz daha zordur. poll fonksiyonunun prototipi şöyledir: #include int poll(struct pollfd fds[], nfds_t nfds, int timeout); poll fonksiyonu izlenilecek betimleyicileri bit dizisi olarak değil, bir yapı dizisi olarak almaktadır. Fonksiyonun birinci parametresi pollfd türünden bir yapı dizisinin adresini, ikinci parametresi ise onun uzunluğunu almaktadır. Son parametre milisaniye cinsinden zaman aşımını belirtir. Bu parametre -1 girilirse zaman aşımı uygulanmaz, 0 girilirse fonksiyon betimleyicilerin durumuna bakıp hemen geri döner. pollfd yapısı şöyle bildirilmiştir: struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ }; Yapının fd elemanı izlenecek betimleyiciyi, events elemanı izleme biçimini belirtmektedir. Eğer bu betimleyici değeri negatif herhangi bir değer olarak girilirse o betimleyici için izleme yapılmamaktadır. Dolayısıyla pollfd dizisinden bir elemanı mantıksal olarak çıkartmak için bu betimleyici negatif bir değere çekilebilir. Programcı poll fonksiyonunu çağırmadan önce bu iki elemana değer yerleştirmelidir. Ancak yapının revents elemanı fonksiyon geri döndüğünde oluşan olay hakkında bilgi vermektedir. Bu eleman programcı tarafından events elemanında set edilmez, fonksiyon tarafından revents elemanında set edilir. (Bu sayede yapının elemanlarının bozulmadığına dikkat ediniz.) En önemli izleme olayları şunlardır: POLLIN: Okuma amaçlı izlemeyi belirtir. Boruda ya da sokette okunacak bilgi oluştuğunda fonksiyon tarafından bu bayrak set edilmektedir. Soketlerde accept yapan tarafta bir bağlantı isteği oluştuğunda da POLLIN bayrağı set edilmektedir. Aynı zamanda soketlerde karşı taraf soketi kapattığında da POLLIN bayrağı set edilmektedir. POLLOUT: Yazma amaçlı izlemeyi belirtir. Boruya ya da sokete yazma durumu oluştuğunda (yani boruda ya da network tamponunda yazma için yer açıldığında) fonksiyon tarafından bu bayrak set edilmektedir. Aynı zamanda soketlerde karşı taraf soketi kapattığında da POLLOUT bayrağı set edilmektedir. POLLERR: Hata amaçlı izlemeyi belirtir. Bu bayrak yapının events elemanında set edilmez, fonksiyon tarafından yapının revents elemanında set edilmektedir. Bu bayrak borularda okuma yapan tarafın boruyu kapatmasıyla yazma yapan tarafta set edilmektedir. (Normal olarak okuyan tarafın boruyu kapattığı durumda boruya yazma yapıldığında SIGPIPE sinyalinin oluştuğunu anımsayınız.) Eğer okuyan taraf boruyu kapattığında boruya yazma için yer varsa yazma yapan tarafta aynı zamanda POLLOUT bayrağı da set edilmektedir. POLLERR bayrağı soketlerde kullanılmamaktadır. POLLHUP: Boruya yazan tarafın boru betimleyicisini kapattığında okuma yapan tarafta bu bayrak set edilmektedir. Bu bayrak yapının events elemanında set edilmez, fonksiyon tarafından yapının revents elemanında set edilmektedir. (HUP, "hang up" anlamına gelmektedir.) Eğer boruya yazma yapan taraf boruyu kapattığında hala boruda okunacak bilgi varsa okuma yapan tarafta aynı zamanda POLLIN bayrağı da set edilmektedir. POLLHUP bayrağı soketlerde kullanılmamaktadır. POLLRDHUP: Soketlerde karşı taraf soketi kapattığında ya da shutdown fonksiyonu SHUT_WR argümanıyla çağrıldığında oluşur. POLLNVAL: Bu bayrak yapının events elemanında set edilmez. Fonksiyon tarafından eğer izlenen bir betimleyici kapalıysa yapının revents elemanında fonksiyon tarafından set edilmektedir. Bu bayraklar bit OR işlemine sokulabilmektedir. Örneğin hem okuma hem de yazma izlemesi için POLLIN|POLLOUT kullanılabilir. Bayrakların anlamları için "The Linux Programming Interface" kitabından şu tabloları da vermek istiyoruz: Boruda bilgi yok ve yazan tarafın betimleyicisi kapalı ===> Okuyan tarafta POLLHUP Boruda bilgi var ve yazan tarafın betimleyicisi kapalı ===> Okuyan tarafta POLLIN|POLLHUP Boruda bilgi var ve yazan tarafın betimleyicisi açık ===> Okuyan tarafta POLLIN Boruda yazma için yer yok ve okuyan tarafın betimleyicisi kapalı ===> Yazan tarafta POLLERR Boruda yazma için yer var ve okuyan tarafın betimleyicisi kapalı ===> Yazan tarafta POLLOUT|POLLERR Boruda yazma için yer var ve okuyan tarafın betimleyicisi açık ===> Yazan tarafta POLLOUT Sokette bilgi var ===> Okuyan tarafta POLLIN Network tamponunda yazacak yer var ===> Yazan tarafta POLLOUT accept yapan tarafta bağlantı isteği oluştuğunda ===> accept yapan tarafta POLLIN Karşı taraf soketi kapattığında ===> karşı tarafta POLLIN|POLLOUT|POLLRDHUP Geri döndürülen olayların birden fazlası birlikte gerçekleşmiş olabilir. Bu nedenle programcının kontrolü else-if ile değil, ayrık if deyimleriyle yapması gerekir. Okuyan taraf boruları ve soketleri kapatırsa bu durum poll fonksiyonunda POLLERR olayı biçiminde ele alınmaktadır. Programcının POLLHUP ve POLLERR bayraklarını yapının events elemanında set etmesi gerekmemektedir. Bu bayraklar gerektiğinde fonksiyon tarafından yapının revents elemanında set edilmektedir. Borular kapatıldığında seyrek de olsa hem POLLIN hem de POLLHUP olayları birlikte gerçekleşebilmektedir. Karşı taraf bir boruyu kapatıldığında POLLHUP oluştuktan sonra yeniden karşı tarafı kapalı olan boru poll işlemi uygulanırsa yine POLLHUP olayı gerçekleşmektedir. Yani karşı tarafın borusu kapalıysa artık her defasında POLLHUP olayı gerçekleşir. poll fonksiyonuna geçersiz bir betimleyici ya da açık olmayan bir betimleyici girilmişse poll fonksiyonu o betimleyici için POLLNVAL olayı oluşturmaktadır. Bu nedenle kapatılmış betimleyicilerin ya negatif bir değere çekilmesi ya da diziden çıkartılması gerekir. Server uygulamalarında programcı yine tipik olarak poll fonksiyonunu bir döngü içerisinde çağırır. Döngüden çıkışta dizinin tüm elemanlarının revents elemanını uygun olay için kontrol eder. Eğer bir betimleyici üzerinde hiçbir olay gerçekleşmemişse revents elemanı 0 değerinde olacaktır. Örneğin okuma amaçlı nfds kadar betimleyiciyi poll fonksiyonu ile izlemek isteyelim. İzlenecek betimleyici bilgilerinin pfds isimli dizi de olduğuna varsayalım. poll sonrasındaki kontrol şöyle yapılabilir (kontroller uygulanmamıştır): poll(pfds, nfds, -1); for (int i = 0; i < nfds; ++i) { if (pfds[i].revents & POLLIN) { read(pfds[i].fd, ...); ... } if (pfds[i].revents & POLLOUT) { read(pfds[i].fd, ...); ... } ... } Görüldüğü gibi dizinin hangi elemanlarında olay gerçekleştiği bilinmemektedir. (select fonksiyonunda da bizim tek tek betimleyicilere FD_ISSET makrosuyla baktığımızı anımsayınız.) poll fonksiyonu başarı durumunda bizim dizideki olay gerçekleşen eleman sayısına (olay sayısına değil) geri dönmektedir. Fonksiyon zaman aşımından dolayı sonlanmışsa 0 değerine geri dönmektedir. Zaman aşımı 0 verildiyse ve hiçbir olay gerçekleşmemişse fonksiyon yine 0 değerine geri dönmektedir. Fonksiyon başarısızlık durumunda -1 değerine geri döner ve errno değişkeni uygun biçimde set edilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 90. Ders 15/10/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda poll fonksiyonunun stdin üzerinde basit bir kullanımına örnek verilmiştir. poll fonksiyonu terminal ile kullanılırken Ctrl+d tuşlarına basıldığında POLLHUP olayı değil, POLLIN olayı gerçekleşmektedir. Dolayısıyla aşağıdaki kodda döngüden read fonksiyonu ile 0 byte okunduğunda çıkılmaktadır. Ctrl+d tuşlarına basıldığında EOF etkisi oluşturulmakta ve read fonksiyonu 0 byte okumaktadır. Ancak izleyen paragraflarda görüleceği gibi borular kapatıldığında poll fonksiyonu POLLHUP olayı oluşturmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { char buf[BUFFER_SIZE + 1]; ssize_t result; struct pollfd pfds[1]; pfds[0].fd = 0; pfds[0].events = POLLIN; for (;;) { if (poll(pfds, 1, -1) == -1) exit_sys("poll"); if (pfds[0].revents & POLLIN) { if ((result = read(pfds[0].fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); if (result == 0) break; buf[result] = '\0'; printf("%s", buf); } } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de gerçek bir server uygulamasına benzer biçimde bir grup isimli borudan poll fonksiyonu ile okuma yapmaya çalışalım. Aşağıdaki örnek daha önce select fonksiyonuyla yaptığımız örneğin poll versiyonudur. Yani bu örnekteki program yine bir grup isimli boru, komut satırı argümanı yapılarak çalıştırılmalıdır. Örneğin: ./sample x y z Örneğimizde önce borular açılıp pfds dizisi oluşturulmuştur: struct pollfd pfds[MAX_SIZE]; ... for (int i = 1; i < argc; ++i) { if (count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((pfds[i - 1].fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); pfds[i - 1].events = POLLIN; printf("%s opened...\n", argv[i]); ++count; } İzlemenin POLLIN ile okuma izlemesi olduğuna dikkat ediniz. Yukarıda da belirttiğimiz gibi izlemede POLLHUP belirtilmemektedir. Yine örneğimizde bir döngü içerisinde poll fonksiyonu çağrılmıştır: for (;;) { printf("waiting at poll...\n"); if (poll(pfds, count, -1) == -1) exit_sys("poll"); ... } Buradaki count, pfds dizisindeki eleman sayısını belirtmektedir. poll fonksiyonu blokeyi çözdüğünde bizim dizinin tüm elemanlarını kontrol edip POLLIN ve POLLHUP olaylarının gerçekleşip gerçekleşmediğine bakmamız gerekmektedir: for (int i = 0; i < count; ++i) { if (pfds[i].revents & POLLIN) { ... } else if (pfds[i].revents & POLLHUP) { ... } ... } Burada POLLIN ve POLLHUP olaylarının else-if biçiminde ele alındığına dikkat ediniz. Normalde örneğin okuma ve yazma izlemesi yapılırken bu olayların ayrık if deyimleriyle yapılması gerekir. Ancak POLLIN ve POLLHUP olaylarının else-if biçiminde ele alınması daha uygundur. Bunun nedenini şöyle açıklayabiliriz: Karşı taraf boruya (ya da sokete) bilgi yazıp hemen boruyu kapattığında POLLIN ve POLLHUP olayları birlikte oluşabilmektedir. Bu durumda POLLIN ve POLLHUP ayrık if deyimleriyle ele alınırsa ve POLLIN olayında borudakilerin hepsi okunmazsa arkadan POLLUP işlemi ele alınırken boru kapatılacağı için eksik yapılmış olacaktır. Halbuki else-if durumunda biz borudakilerin tamamını okumasak bile sonraki poll işleminde yeniden POLLIN ve POLLHUP oluşacak ve boruyu bitirdikten sonra artık yalnızca POLLHUP olayı oluşacaktır. Örneğimizde bir boru kapatıldığında onun dizi elemanındaki betimleyicisi negatif bir değere çekilmiş ve böylece mantıksal olarak diziden atılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 void exit_sys(const char *msg); int main(int argc, char *argv[]) { char buf[BUFFER_SIZE + 1]; struct pollfd pfds[MAX_SIZE]; ssize_t result; int tcount, count; if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); count = 0; for (int i = 1; i < argc; ++i) { if (count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((pfds[i - 1].fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); pfds[i - 1].events = POLLIN; printf("%s opened...\n", argv[i]); ++count; } tcount = count; for (;;) { printf("waiting at poll...\n"); if (poll(pfds, count, -1) == -1) exit_sys("poll"); for (int i = 0; i < count; ++i) { if (pfds[i].revents & POLLIN) { printf("POLLIN occured...\n"); if ((result = read(pfds[i].fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); } else if (pfds[i].revents & POLLHUP) { printf("POLLHUP occured...\n"); close(pfds[i].fd); pfds[i].fd = -1; --tcount; } } if (tcount == 0) break; } printf("there is no descriptor open, finishes...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi aslında kapanan betimleyicileri mantıksal olarak diziden atmak için onun betimleyicisi negatif bir değere çekilebilmektedir. Ancak istersek gerçekten kapanan betimleyicileri diziden atabiliriz. Tabii bu atma işlemi aslında dizinin sonundaki elemanın, atılacak elemanla yer değiştirilmesi yoluyla yapılabilmektedir. Aşağıda buna bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 void exit_sys(const char *msg); int main(int argc, char *argv[]) { char buf[BUFFER_SIZE + 1]; struct pollfd pfds[MAX_SIZE]; ssize_t result; int tcount, count; if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); count = 0; for (int i = 1; i < argc; ++i) { if (count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((pfds[i - 1].fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); pfds[i - 1].events = POLLIN; printf("%s opened...\n", argv[i]); ++count; } tcount = count; for (;;) { printf("waiting at poll...\n"); if (poll(pfds, count, -1) == -1) exit_sys("poll"); for (int i = 0; i < count; ++i) { if (pfds[i].revents & POLLIN) { printf("POLLIN occured...\n"); if ((result = read(pfds[i].fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); } else if (pfds[i].revents & POLLHUP) { printf("POLLHUP occured...\n"); close(pfds[i].fd); pfds[i] = pfds[tcount - 1]; --tcount; } } count = tcount; if (count == 0) break; } printf("there is no descriptor open, finishes...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Tıpkı select fonksiyonunda olduğu gibi poll fonksiyonun da ppoll isimli sigset_t parametreli bir biçimi de vardır: #define _GNU_SOURCE /* See feature_test_macros(7) */ #include int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *tmo_p, const sigset_t *sigmask); ppoll fonksiyonu POSIX standartlarında bulunmamaktadır. Linux sistemlerine özgüdür. Fonksiyon poll fonksiyonundan farklı olarak sinyal bloke kümesini parametre olarak alarak thread'in sinyal bloke kümesini set eder. Çıkışta da onu eski haline getirir. Yani buradaki sinyal bloke kümesi fonksiyon çalıştığı sürece etkili olmaktadır. Bu parametre NULL geçilirse fonksiyon poll fonksiyonu gibi çalışmaktadır. Ancak ppoll fonksiyonunun zaman aşımı parametresinin timespec türünden olduğuna dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi select ve poll fonksiyonlarını birbiriyle kıyaslarsak neler söyleyebiliriz? - poll fonksiyonunun kullanımı biraz daha kolay gibidir. - select fonksiyonu FD_SETSIZE (1024) kadar betimleyiciyi desteklemektedir. Oysa poll fonksiyonundaki dizi istenildiği kadar büyük olabilir. - select fonksiyonunda fonksiyona verdiğimiz kümeler fonksiyon tarafından güncellendiği için fonksiyonun her çağrılmasında eski kümeyi saklayarak yeniden kullanmamız gerekir. Halbuki poll fonksiyonunda yapının giriş ve çıkış elemanları birbirinden ayrılmıştır. - select fonksiyonundaki zaman aşımı duyarlılığı mikrosaniye, poll fonksiyonundaki zaman aşımı duyarlılığı milisaniye mertebesindendir. - Her iki fonksiyonda da betimleyici sayısı fazlalaştıkça performans düşme eğilimindedir. - poll fonksiyonunu kullanabilmek için pollfd türünden bir yapı dizisinin oluşturulması gerekmektedir. Halbuki select fonksiyonunda fd_set veri yapısı bitsel düzeyde olduğu için az yer kaplamaktadır. poll fonksiyonun select fonksiyonuna göre en önemli avantajı betimleyici sayısının istenildiği kadar çok olabilmesidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 91. Ders 22/10/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- select ve poll fonksiyonlarının en önemli sorunu betimleyici sayısı arttığında performansın ciddi biçimde düşmesidir. Yani bu fonksiyonlar iyi bir ölçeklenebilirliğe (scalability) sahip değildir. İşte bu nedenden dolayı multiplexed IO işlemleri için Linux sistemlerinde bu sistemlere özgü epoll fonksiyonları da bulundurulmuştur. epoll fonksiyonları, select ve poll fonksiyonlarına göre özellikle betimleyici sayısı arttığında çok daha iyi performans göstermektedir. Bu nedenle Linux sistemlerinde ölçeklenebilir server uygulamaları için tercih edilecek yöntem epoll yöntemi olmalıdır. Ancak epoll fonksiyonlarının taşınabilir olmadığına, yalnızca Linux sistemlerine özgü olduğuna dikkat ediniz. epoll arayüzünün kullanımı temelde üç fonksiyon ile yapılmaktadır. epoll_create (ya da epoll_create1), epoll_ctl ve epoll_wait. epoll arayüzünün kullanımı tipik olarak aşağıdaki adımlardan geçilerek yapılmaktadır: 1) Programcı önce epoll_create isimli fonksiyonla bir betimleyici elde eder. Bu betimleyicinin IO olaylarının izleneceği betimleyici ile bir ilgisi yoktur. Bu betimleyici diğer fonksiyonlara bir handle gibi geçirilmektedir. Fonksiyonun prototipi şöyledir: #include int epoll_create(int size); Fonksiyonun parametresi kaç betimleyicinin izlenileceğine yönelik bir ip ucu değeri alır. Programcı burada verdiği değerden daha fazla betimleyiciyi izleyebilir. Dolayısıyla bu parametre yalnızca bir ipucu niteliğindedir. Zaten daha sonra bu parametre tasarımcıları rahatsız etmiş ve epoll_create1 isimli fonksiyonla kaldırılmıştır: #include int epoll_create1(int flags); Buradaki flags şimdilik yalnızca FD_CLOEXEC değerini ya da 0 değerini alabilmektedir. Fonksiyonların geri dönüş değeri başarı durumunda handle görevinde olan bir betimleyicidir. epoll_create ve epoll_create1 fonksiyonları başarı durumunda epoll betimleyicisine, başarısızlık durumunda -1 değerine geri dönmektedir. 2) Artık programcı izleyeceği betimleyicileri epoll sistemine epoll_ctl fonksiyonuyla ekler. Örneğin programcı 3 boru betimleyicisini izleyecekse bu 3 betimleyici için de ayrı ayrı epoll_ctl çağrısı yapmalıdır. Fonksiyonun prototipi şöyledir: #include int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); Fonksiyonun birinci parametresi epoll_create ya da epoll_create1 fonksiyonundan elde edilen betimleyici değeridir. İkinci parametre EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL değerlerinden birini alır. EPOLL_CTL_ADD sisteme betimleyici eklemek için, EPOLL_CTL_DEL sistemden betimleyici çıkartmak için, EPOLL_CTL_MOD da mevcut eklenmiş betimleyicide izleme değişikliği yapmak için kullanılmaktadır. Üçüncü parametre izlenecek betimleyiciyi belirtir. Son parametre izlenecek olayı belirtmektedir. struct epoll_event yapısı şöyle bildirilmiştir: struct epoll_event { uint32_t events; epoll_data_t data; }; Yapının events elemanı tıpkı poll fonksiyonunda olduğu gibi izlenecek olayları belirten bayrak değerlerini almaktadır. Bu bayrak değerleri epoll_ctl fonksiyonunda izlenecek olayları belirtir. İzleyen paragraflarda göreceğimiz epoll_wait fonksiyonunda da gerçekleşen olayları belirtmektedir. İzleme amacıyla kullanılan tipik bayraklar şunlardır: EPOLLIN: Okuma amaçlı izlemeyi belirtir. Boruda ya da sokette okunacak bilgi oluştuğunda epoll_wait tarafından bu bayrak set edilir. Soketlerde accept yapan tarafta bir bağlantı isteği oluştuğunda da EPOLLIN bayrağı epoll_wait tarafından set edilmektedir. EPOLLIN bayrağı aynı zamanda karşı taraf soketi kapattığında da oluşmaktadır. EPOLLOUT: Yazma amaçlı izlemeyi belirtir. Boruya ya da sokete yazma durumu oluştuğunda (yani boruda ya da network tamponunda yazma için yer açıldığında) fonksiyon tarafından bu bayrak set edilmektedir. EPOLLOUT bayrağı aynı zamanda karşı taraf soketi kapattığında da oluşmaktadır. EPOLLERR: Hata amaçlı izlemeyi belirtir. Bu bayrak epoll_ctl fonksiyonunda set edilmez, epoll_wait fonksiyonu tarafından set edilmektedir. Bu bayrak borularda okuma yapan tarafın boruyu kapatmasıyla yazma yapan tarafta set edilmektedir. (Normal olarak okuyan tarafın boruyu kapattığı durumda boruya yazma yapıldığında SIGPIPE sinyalinin oluştuğunu anımsayınız.) Eğer okuyan taraf boruyu kapattığında boruya yazma için yer varsa yazma yapan tarafta aynı zamanda EPOLLOUT bayrağı da set edilmektedir. EPOLLERR bayrağı soketlerde kullanılmamaktadır. EPOLLHUP: Boruya yazan tarafın boru betimleyicisini kapattığında okuma yapan tarafta bu bayrak set edilmektedir. Bu bayrak epoll_ctl fonksiyonunda set edilmez, epoll_wait tarafından yapının set edilmektedir. (HUP, "hang up" anlamına gelmektedir.) Eğer boruya yazma yapan taraf boruyu kapattığında hala boruda okunacak bilgi varsa okuma yapan tarafta aynı zamanda EPOLLIN bayrağı da set edilmektedir. EPOLLHUP bayrağı soketlerde kullanılmamaktadır. EPOLLRDHUP: Bu olay soketlerde karşı taraf soketi kapattığında ya da shutdown fonksiyonu SHUT_WR argümanıyla çağrıldığında oluşur. Bayrak hem epoll_ctl fonksiyonunda set edilebilir hem de epoll_wait tarafından set edilebilir. Daha önce poll bayrakları için verdiğimiz tabloyu epoll bayrakları için de benzer biçimde vermek istiyoruz: Boruda bilgi yok ve yazan tarafın betimleyicisi kapalı ===> Okuyan tarafta EPOLLHUP Boruda bilgi var ve yazan tarafın betimleyicisi kapalı ===> Okuyan tarafta EPOLLIN|EPOLLHUP Boruda bilgi var ve yazan tarafın betimleyicisi açık ===> Okuyan tarafta EPOLLIN Boruda yazma için yer yok ve okuyan tarafın betimleyicisi kapalı ===> Yazan tarafta EPOLLERR Boruda yazma için yer var ve okuyan tarafın betimleyicisi kapalı ===> Yazan tarafta EPOLLOUT|EPOLLERR Boruda yazma için yer var ve okuyan tarafın betimleyicisi açık ===> Yazan tarafta EPOLLOUT Sokette bilgi var ===> Okuyan tarafta EPOLLIN Network tamponunda yazacak yer var ===> Yazan tarafta EPOLLOUT accept yapan tarafta bağlantı isteği oluştuğunda ===> accept yapan tarafta EPOLLIN Karşı taraf soketi kapattığında ===> karşı tarafta EPOLLIN|EPOLLOUT|EPOLLRDHUP epoll_event yapısının data elemanı aslında çekirdek tarafından saklanıp epoll_wait fonksiyonu yoluyla bize geri verilmektedir. Bu eleman bir birlik biçiminde bildirilmiştir (yani programcı tarafından bu birliğin yalnızca tek elemanı set edilmelidir): typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; Programcının tipik olarak olayın gerçekleştiği dosya betimleyicisinin hangisi olduğunu bilmesi gerekmektedir. Dolayısıyla genellikle birliğin fd elemanı set edilmektedir. Tabii programcı daha fazla bilgi set etmek istiyorsa bir yapı oluşturabilir. Betimleyiciyi ve diğer bilgileri bu yapının içerisine yerleştirebilir. Yapı nesnesinin adresini de birliğin ptr elemanına atayabilir. epoll_ctl fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. errno değişkeni uygun biçimde set edilmektedir. Kenar tetiklemeli (edge triggered) ve düzey tetiklemeli (level triggered) kavramları lojik elektronikte kullanılmaktadır. Kenar tetikleme belli bir olay ilk gerçekleştiğinde birtakım değişikliklerin yapıldığını, düzey tetikleme ise belli bir olay devam ettiği sürece değişiklerin sürekli yapıldığını anlatmaktadır. Her ne bu terimler elektronikten geçtiyse de yazılımda da anlatımları kolaylaştırmak için kullanılmaktadır. Örneğin select ve poll fonksiyonları "düzey tetiklemeli (level triggered)" olarak çalışmaktadır. Yani bir boruya bilgi geldiği zaman bu fonksiyonlar çağrıldığında durumu bize bildirirler. O bilgi borudan okunmadığı sürece bu fonksiyonları tekrar çağırdığımızda bu fonksiyonlar yine durumu bize bildirmektedir. epoll fonksiyonu ise default durumda yine düzey tetiklemeli çalışırken özel olarak epoll_event yapısının events elemanına EPOLLET eklenirse o betimleyici için "kenar tetiklemeli (edge triggered)" mod kullanılır. Yukarıda da belirtildiği gibi düzey tetiklemeli mod demek (select, poll'daki durum ve epoll'daki default durum) bir okuma ya da yazma olayı açılıp bloke çözüldüğünde programcı eğer okuma ya da yazma yapmayıp yeniden bu fonksiyonları çağırırsa bekleme yapılmayacak demektir. Yani örneğin biz select ya poll ile stdin dosyasını izliyorsak ve klavyeden bir giriş yapıldıysa bu fonksiyonlar blokeyi çözer. Fakat biz read ile okuma yapmazsak ve yeniden select ve poll fonksiyonlarını çağırırsak artık bloke oluşmaz. Halbuki kenar tetiklemeli modda biz okuma yapmasak bile yeni okuma eylemi oluşana kadar yine blokede kalırız. Biz buradaki örneklerimizde epoll fonksiyonunu düzey tetiklemeli olarak kullanacağız. Soketler konusunda epoll fonksiyonunun kenar tetiklemeli kullanımı üzerinde duracağız. Tabii programcının izleyeceği her betimleyici için epoll_ctl fonksiyonunu çağırması gerekir. Örneğin biz 3 farklı betimleyiciyi izleyeceksek bizim üç kere epoll_ctl fonksiyonunu çağırmamız gerekir. 3) Asıl bekleme ve izleme işlemi epoll_wait fonksiyonu tarafından yapılmaktadır. Bu fonksiyon select ve poll fonksiyonu gibi eğer izlenen betimleyicilerde hiçbir olay gerçekleşmemişse bloke oluşturur ve eğer en az bir betimleyicide izlenen olaylardan biri gerçekleşmişse blokeyi çözer. epoll_wait fonksiyonunun prototipi şöyledir: #include int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); Fonksiyonun birinci parametresi epoll_create ya da epoll_create1 fonksiyonundan elde edilmiş olan betimleyici değeridir. İkinci parametre oluşan olayların depolanacağı yapı dizisinin adresidir. Biz bu yapının events elemanından oluşan olayın ne olduğunu anlarız. Yapının data elemanı epoll_ctl sırasında verdiğimiz değeri belirtir. Bizim en azından epoll_ctl fonksiyonunda ilgili betimleyiciyi bu data elemanında girmiş olmamız gerekir. Fonksiyonun üçüncü parametresi, ikinci parametresiyle belirtilen dizinin uzunluğudur. Normal olarak bu dizinin eklenmiş olan betimleyici sayısı kadar olması gerekir. Ancak buradaki değer toplam izlenecek betimleyici sayısından az olabilir. Bu parametre tek hamlede en fazla kaç betimleyici hakkında bilgi verileceğini belirtmektedir. Örneğin biz fonksiyonun ikinci parametresine 5 elemanlı bir yapı dizisinin adresini, üçüncü parametresine de 5 değerini girebiliriz. Bu durumda epoll_wait fonksiyonunu çağırdığımızda fonksiyon bize en fazla 5 olay hakkında bilgi verecektir. Son parametre yine milisaniye cinsinden zaman aşımını belirtir. -1 değeri zaman aşımının kullanılmayacağını, 0 değeri hemen betimleyicilere bakılıp çıkılacağını belirtmektedir. Fonksiyon başarı durumunda diziye doldurduğu eleman sayısı ile, başarısızlık durumda -1 değeri ile geri dönmektedir. Örneğin fonksiyon 2 değerine geri dönmüş olsun. Bu durum fonksiyon tarafından verdiğimiz dizinin "ilk 2" elemanının doldurulduğu anlamına gelmektedir. Fonksiyon 0 değeri ile geri dönerse sonlanmanın zaman aşımından dolayı oluştuğu anlaşılmaktadır. Tabii epoll_wait fonksiyonunun yine bir döngü içerisinde çağrılması gerekmektedir. 4) İzleme işlemlerinin kapatılması için tek yapılacak şey epoll_create ya da epoll_create1 fonksiyonundan elde edilen betimleyicinin close fonksiyonuyla kapatılmasıdır. Belli bir betimleyiciyi izleme listesinden çıkartmak için normal olarak epoll_ctl fonksiyonu EPOLL_CTL_DEL parametresiyle çağrılmalıdır. Ancak epoll sisteminde çoğu kez buna gerek yoktur. Bir dosyaya ilişkin son betimleyici de kapatılmışsa o betimleyici otomatik olarak izleme listesinden çıkartılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda stdin dosyasından (0 numaralı betimleyiciden) epoll fonksiyonu ile okuma işlemine bir örnek verilmiştir. Örnekte sanki birden fazla betimleyici söz konusuymuş gibi işlem yapılmıştır. Bunun amacı kodun genel durum için bir şablon oluşturmasını sağlamaktır. Tıpkı poll fonksiyonunda olduğu gibi terminalden Ctrl+d tuşlarına basıldığında EPOLLIN olayı gerçekleştiğine dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #define MAX_EVENTS 1 #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(void) { int epfd; struct epoll_event ee; struct epoll_event ree[MAX_EVENTS]; int nevents; char buf[BUFFER_SIZE + 1]; ssize_t result; if ((epfd = epoll_create(1)) == -1) exit_sys("epoll_create"); ee.events = EPOLLIN; ee.data.fd = 0; /* yalnızca 0 numaralı betimleyici kullanıyoruz, aslında bu örnekte gerek yok */ if (epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee) == -1) exit_sys("epoll_ctl"); for (;;) { if ((nevents = epoll_wait(epfd, ree, MAX_EVENTS, -1)) == -1) exit_sys("epoll_wait"); for (int i = 0; i < nevents; ++i) { if (ree[i].events & EPOLLIN) { if ((result = read(ree[i].data.fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); if (result == 0) goto EXIT; buf[result] = '\0'; printf("%s", buf); } } } EXIT: close(epfd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de daha vermiş olduğumuz borulardan okuma örneğini aşağıda epoll fonksiyonu ile gerçekleştirelim. Örneğimizde yine program isimli boruları komut satırı argümanı olarak almaktadır. Örneğin: ./sample x y z Bu borular üzerinde okuma olayları izlenmektedir. Borular kapatıldığında EPOLLHUP olayı gerçekleşmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 #define MAX_EVENTS 5 void exit_sys(const char *msg); int main(int argc, char *argv[]) { char buf[BUFFER_SIZE + 1]; int epfd, fd; struct epoll_event ee; struct epoll_event ree[MAX_EVENTS]; int nevents; ssize_t result; int count; if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } if ((epfd = epoll_create(1)) == -1) exit_sys("epoll_create"); printf("opens named pipes... it may block...\n"); count = 0; for (int i = 1; i < argc; ++i) { if (count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); ee.events = EPOLLIN; ee.data.fd = fd; if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ee) == -1) exit_sys("epoll_ctl"); printf("%s opened...\n", argv[i]); ++count; } for (;;) { printf("waiting at epoll_wait...\n"); if ((nevents = epoll_wait(epfd, ree, MAX_EVENTS, -1)) == -1) exit_sys("epoll_wait"); for (int i = 0; i < nevents; ++i) { if (ree[i].events & EPOLLIN) { printf("EPOLLIN occured...\n"); if ((result = read(ree[i].data.fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); } else if (ree[i].events & EPOLLHUP) { printf("EPOLLHUP occured...\n"); close(ree[i].data.fd); --count; } } if (count == 0) break; } printf("there is no descriptor open, finishes...\n"); close(epfd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi Linux sistemlerinde epoll fonksiyonunun performansı select ve poll fonksiyonlarından çok daha iyidir. Bu nedenle Linux sistemlerinde ilk tercih edilecek multiplexed IO sistemi epoll olmalıdır. Tabii epoll sistemi POSIX uyumlu değildir. Yani epoll kullandığımız kodlar taşınabilir olmamaktadır. epoll performansı için Michael Kerrisk'in "The Linux Programming Environment" kitabında karşılaştırmalı olarak saniye cinsinden şu değerler verilmektedir: Number of descriptors poll() CPU time select() CPU time epoll CPU time 10 0.61 0.73 0.41 100 2.9 3.0 0.42 1000 35 35 0.53 10000 990 930 0.66 Burada görüldüğü gibi Linux sistemlerinde select ile poll fonksiyonlarının performansı birbirine çok yakındır. Ancak epoll sisteminin performansı açık ara çok daha iyidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- epoll fonksiyonu ile sistem genelinde izlenecek maksimum betimleyici sayısı default durumda sistem belleği ile ilgili bir biçimde ayarlanmaktadır. Bu bilgi /proc/sys/fs/epoll/max_user_watches dosyasında bulunmaktadır. Bu değer değiştirilebilmektedir. Kursun yapıldığı sanal makinede bu değer 858824 biçimindedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- epoll ile kenar tetiklemeli işlemler üzerinde burada örnek vermeyeceğiz. Bunun için ilgili dokümanlara başvurabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sinyal tabanlı (signal driven) IO işlemlerinde belli bir betimleyicide olay oluştuğunda prosese bir sinyal gönderilmektedir. Böylece sinyal oluştuğunda ilgili kaynaktan okuma/yazma işlemleri yapılabilmektedir. Bunun için SIGIO isimli bir sinyal bulundurulmuştur. Ancak izleyen paragraflarda da görüleceği üzere olay gerçekleştiğinde oluşturulacak olan bu sinyal değiştirilebilmektedir. Ancak bu model güncel POSIX standartlarında bulunmamaktadır. Bazı UNIX türevi sistemler ve Linux sistemleri bu modeli desteklemektedir. Sinyal tabanlı IO modeli tipik olarak şu aşamalardan geçilerek gerçekleştirilmektedir. 1) Betimleyici open fonksiyonuyla açılır. Eğer soketler söz konusu ise betimleyici socket fonksiyonuyla ya da accept fonksiyonuyla elde edilmektedir. 2) Oluşturulacak sinyal için (default durumda SIGIO sinyali) sinyal fonksiyonu set edilir. 3) İlgili betimleyicide olay oluştuğunda hangi prosese sinyal gönderileceği fcntl fonksiyonu ile set edilir. Tabii genel olarak programcı sinyalin kendi prosesine gönderilmesini ister. Bunun için fcntl fonksiyonunun ikinci parametresi olan fcntl komutu için F_SETOWN girilmelidir. fcntl fonksiyonunun üçüncü parametresine ise sinyalin gönderileceği prosesin id değeri girilir. Bu parametreye getpid() fonksiyonunun geri dönüş değeri girilirse ilgili olay gerçekleştiğinde kendi prosesimize sinyal gönderilir. Üçüncü parametre negatif bir proses id girilirse, bu değerin mutlak değeri proses grup belirtmektedir. 4) Betimleyici blokesiz moda sokulur ve aynı zamanda O_ASYNC bayrağı da set edilir. Bu işlem fcntl fonksiyonunda F_SETFL komut koduyla yapılabilmektedir. fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK | O_ASYNC); O_ASYNC bayrağı POSIX standartlarında bulunmamaktadır. Bu yöntemde ilgilenilen olay (yani okuma olayının mı yazma olayının mı izleneceği) gizlice open fonksiyonundaki açış modunda belirtilmektedir. Yani örneğin bizim open fonksiyonuyla dosyayı O_RDONLY modunda açmamız yalnızca okuma olayıyla ilgilendiğimizi, O_WRONLY modunda açmamız yalnızca yazma olayı ile ilgilendiğimizi, O_RDWR modunda açmamız da hem okuma hem de yazma ile ilgilendiğimizi belirtir. 5) Artık normal akış devam eder. İlgilenilen olay gerçekleştiğinde sinyal oluşturulmaktadır. Sinyal tabanlı IO işlemleri "kenar tetiklemeli (edge triggered)" bir biçimde oluşturulmaktadır. Yani yalnızca yeni bilgi geldiğinde sinyal oluşturulur. Bu nedenle programcının sinyal oluştuğunda bir döngü içerisinde başarısız olana kadar okuma/yazma yapması uygun olmaktadır. Örneğin boruya ya da sokete 100 byte gelmiş olsun. Bu durumda sinyal oluşturulur. Eğer biz sinyal oluştuğunda eksik bilgi okursak (örneğin 50 byte okuduğumuzu varsayalım) kenar tetikleme yüzünden artık boruya ya da sokete yeni bir bilgi gelene kadar sinyal oluşmayacaktır. Bu nedenle bizim o zamana kadar gelmiş olan tüm bilgileri bir döngü içerisinde okumamız uygun olur. Örneğin: for (;;) { result = read(STDIN_FILENO, buf, BUFFER_SIZE); if (result == -1) { if (errno == EAGAIN) break; exit_sys("read"); } if (result == 0) exit(EXIT_SUCCESS); buf[result] = '\0'; printf("%s", buf); // UNSAFE } Burada bir döngü içerisinde errno değeri EAGAIN olmayana kadar okuma yapılmıştır. Pekiyi sinyal geldiğinde okuma işlemi nasıl yapılmalıdır? İlk akla gelen yöntem okumanın sinyal fonksiyonun içerisinde yapılmasıdır. Ancak bu durum genellikle iyi bir teknik değildir. Bunun tipik nedenleri şunlardır: - Sinyal oluştuğunda sinyal fonksiyonunda uzun süre işlem yapmak iyi bir teknik değildir. Çünkü sinyal fonksiyonu çalıştığı sürece aynı sinyal blokede kalmaktadır. SIGIO sinyali gerçek zamanlı bir sinyal olmadığı için kuyruklanmamaktadır. Ancak mekanizmanın kenar tetiklemeli olduğunu anımsayınız. Bu nedenle sinyalin blokesi açıldığında o ana kadar gelmiş olan tüm bilgiler döngü içerisinde okunacaktır. - Sinyal fonksiyonu içerisinde ancak biz sinyal güvenli (signal safe) fonksiyonları çağırabiliriz. Ancak okuma ve sonrasında pek çok sinyal güvenli olmayan fonksiyonların çağrılması gerekebilmektedir. Pekiyi okuma/yazma işlemlerini sinyal geldiği zaman sinyal fonksiyonu içerisinde yapmayacaksak nerede ve nasıl yapmalıyız? İşte tipik olarak sinyal fonksiyonu içerisinde bir flag set edilip işlemler dışarıda yapılabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 92. Ders 28/10/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte stdin dosyasından sinyal tabanlı okuma yapılmıştır. Bu örnekte sinyal fonksiyonunda yalnızca bir flag set edilmiştir. Okumalar sinyal fonksiyonunun dışında bu flag değişkenine bakılarak gerçekleştirilmiştir. flag değişkeni sig_atomic_t türünden tanımlanmıştır. Anımsanacağı gibi bu türden nesnelere atama işlemleri tek makine komutuyla atomik yapılmaktadır. Her ne kadar sig_atomic_t türü "volatile" özelliğine de kapsıyor gibiyse de biz yine de flag değişkeninde volatile niteleyicisini kullandık. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #define BUFFER_SIZE 1024 void sigio_handler(int signo); void exit_sys(const char *msg); volatile sig_atomic_t g_sigio_flag; int main(void) { struct sigaction sa; int flags; char buf[BUFFER_SIZE + 1]; ssize_t result; sa.sa_handler = sigio_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGIO, &sa, NULL) == -1) exit_sys("sigaction"); if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); flags = fcntl(STDIN_FILENO, F_GETFL); if (fcntl(STDIN_FILENO, F_SETFL, flags|O_NONBLOCK|O_ASYNC) == -1) exit_sys("fcntl"); for (;;) { pause(); if (g_sigio_flag) { for (;;) { result = read(STDIN_FILENO, buf, BUFFER_SIZE); if (result == -1) { if (errno == EAGAIN) break; exit_sys("read"); } if (result == 0) exit(EXIT_SUCCESS); buf[result] = '\0'; printf("%s", buf); } g_sigio_flag = 0; } } return 0; } void sigio_handler(int signo) { g_sigio_flag = 1; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi sinyal tabanlı IO işlemlerinde birden fazla betimleyici ile nasıl işlem yapılacaktır? Örneğin biz 3 boru betimleyicisinden okuma yapmak isteyelim. SIGIO sinyali oluştuğunda hangi boruya bilgi geldiğini nasıl anlayacağız? Yukarıdaki örnekte yalnızca stdin dosyasından okuma yaptık. Bu örnekte böyle bir bilgiye ihtiyacımız yoktu. İşte bu yöntemin aslında etkin bir biçimde kullanılabilmesi için IO olayı olduğunda SIGIO sinyali yerine gerçek zamanlı bir sinyalin oluşturulması gerekmektedir. Gerçek zamanlı sinyaller hem kuyruklanmakta hem de bu sinyallere ek bir bilgi yerleştirilebilmektedir. Sinyal tabanlı IO işlemindeki default sinyalin SIGIO sinyali olduğunu belirtmiştik. Ancak SIGIO sinyalinin gerçek zamanlı olmaması bir handikap oluşturmaktadır. İşte default sinyal aslında fcntl fonksiyonu ile F_SETSIG komutu kullanılarak değiştirilebilmektedir. Örneğin: fcntl(fd, F_SETSIG, SIGRTMIN); Benzer biçimde oluşturulacak sinyalin numarası da fcntl fonksiyonunda F_GETSIG komutuyla elde edilebilmektedir. Biz F_SETSIG komutu ile gerçek zamanlı bir sinyal set ettiğimizde artık sinyal fonksiyonumuzun siginfo_t parametreli olması gerekmektedir. Gerçek zamanlı sinyallerin nasıl set edildiğini anımsatmak istiyoruz: void signal_handler(int signo, siginfo_t *info, void *context); ... struct sigaction sa; ... sa.sa_sigaction = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART|SA_SIGINFO; Gerçek zamanlı sinyallerin bu konudaki faydasını izleyen paragraflarda açıklayacağız. Ancak şimdi aşağıda sinyal tabanlı IO işlemleri için gerçek zamanlı sinyalin set edilmesine yönelik örnek vermek istiyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ #define _GNU_SOURCE #include #include #include #include #include #include #define BUFFER_SIZE 1024 void signal_handler(int signo, siginfo_t *info, void *context); void exit_sys(const char *msg); volatile sig_atomic_t g_sigio_flag; int main(void) { struct sigaction sa; int flags; char buf[BUFFER_SIZE + 1]; ssize_t result; sa.sa_sigaction = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART|SA_SIGINFO; if (sigaction(SIGRTMIN, &sa, NULL) == -1) exit_sys("sigaction"); if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); flags = fcntl(STDIN_FILENO, F_GETFL); if (fcntl(STDIN_FILENO, F_SETFL, flags|O_NONBLOCK|O_ASYNC) == -1) exit_sys("fcntl"); if (fcntl(STDIN_FILENO, F_SETSIG, SIGRTMIN) == -1) exit_sys("fcntl"); for (;;) { pause(); if (g_sigio_flag) { for (;;) { result = read(STDIN_FILENO, buf, BUFFER_SIZE); if (result == -1) { if (errno == EAGAIN) break; exit_sys("read"); } if (result == 0) exit(EXIT_SUCCESS); buf[result] = '\0'; printf("%s", buf); } g_sigio_flag = 0; } } return 0; } void signal_handler(int signo, siginfo_t *info, void *context) { g_sigio_flag = 1; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi gerçek zamanlı sinyallerde sinyal fonksiyonuna çekirdek tarafından içi doldurulan siginfo_t türünden bir yapı nesnesinin adresi geçiriliyordu. siginfo_y yapısını anımsayınız: siginfo_t { int si_signo; /* Signal number */ int si_errno; /* An errno value */ int si_code; /* Signal code */ int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */ pid_t si_pid; /* Sending process ID */ uid_t si_uid; /* Real user ID of sending process */ int si_status; /* Exit value or signal */ clock_t si_utime; /* User time consumed */ clock_t si_stime; /* System time consumed */ union sigval si_value; /* Signal value */ int si_int; /* POSIX.1b signal */ void *si_ptr; /* POSIX.1b signal */ int si_overrun; /* Timer overrun count; POSIX.1b timers */ int si_timerid; /* Timer ID; POSIX.1b timers */ void *si_addr; /* Memory location which caused fault */ long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */ int si_fd; /* File descriptor */ short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */ void *si_lower; /* Lower bound when address violation occurred (since Linux 3.19) */ void *si_upper; /* Upper bound when address violation occurred (since Linux 3.19) */ int si_pkey; /* Protection key on PTE that caused fault (since Linux 4.6) */ void *si_call_addr; /* Address of system call instruction (since Linux 3.5) */ int si_syscall; /* Number of attempted system call (since Linux 3.5) */ unsigned int si_arch; /* Architecture of attempted system call (since Linux 3.5) */ } Anımsanacağı gibi yapının si_signo elemanı oluşan sinyalin numarasını, si_pid elemanı sinyali oluşturan prosesin id değerini vermekteydi. Sinyal tabanlı IO işlemlerinde yapının si_fd elemanı sinyale yol açan betimleyicinin numarasını vermektedir. si_code elemanı ise sinyalin neden oluştuğuna yönelik bilgi vermektedir. Yapının bu si_code elemanında şu bitler set edilmiş olabilir: POLL_IN: Okuma ya da kapatma olayı POLL_OUT: Yazma olayı POLL_ERR: IO hatası Diğer bayraklar için dokümanlara başvurabilirsiniz. Aşağıda daha önce yaptığımız boru örneğinin sinyal tabanlı IO modeli ile gerçekleştirimini veriyoruz. Bu örnekte sinyaller senkron biçimde sigwaitinfo fonksiyonu ile işlenmiştir. Dolayısıyla bir sinyal fonksiyonu yazılmamıştır. sigwaitinfo uygulamadan önce beklenecek sinyalleri bloke etmeyi unutmayınız. Örneğimizde sigwaitinfo fonksiyonundan çıkıldığında oluşan olay siginfo_t yapısının si_code elemanından elde edilmiş ve yapının si_fd elemanından okuma yapılmıştır. Karşı taraf boruyu ya da soketi kapattığında yine POLL_IN olayının gerçekleşeceğini anımsatmak istiyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ #define _GNU_SOURCE #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 void exit_sys(const char *msg); int main(int argc, char *argv[]) { char buf[BUFFER_SIZE + 1]; int fd; ssize_t result; sigset_t sset; siginfo_t sinfo; int count; if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); count = 0; for (int i = 1; i < argc; ++i) { if (count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); if (fcntl(fd, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL)|O_NONBLOCK|O_ASYNC) == -1) exit_sys("fcntl"); if (fcntl(fd, F_SETSIG, SIGRTMIN) == -1) exit_sys("fcntl"); printf("%s opened...\n", argv[i]); ++count; } sigaddset(&sset, SIGRTMIN); if (sigprocmask(SIG_BLOCK, &sset, NULL) == -1) exit_sys("sigprocmask"); for (;;) { if ((sigwaitinfo(&sset, &sinfo)) == -1) exit_sys("sigwaitinfo"); if (sinfo.si_code & POLL_IN) { for (;;) { result = read(sinfo.si_fd, buf, BUFFER_SIZE); if (result == -1) { if (errno == EAGAIN) break; exit_sys("read"); } if (result == 0) { --count; close(sinfo.si_fd); break; } buf[result] = '\0'; printf("%s", buf); } if (count == 0) break; } } if (sigprocmask(SIG_UNBLOCK, &sset, NULL) == -1) exit_sys("sigprocmask"); printf("there is no descriptor open, finishes...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi multiplexed select ve poll modeli ile sinyal tabanlı IO modelini kıyaslarsak neler söylebiliriz? Linux sistemlerinde sinyal tabanlı IO modeli, select ve poll modeline göre daha yüksek performans sunmaktadır. Özellikle önceki örnekte yaptığımız senkron sinyal işlemesi yüksek miktarda betimleyici söz konusu olduğunda select ve poll modelinden daha iyi sonuçlar vermektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İleri IO modellerinden biri de "Asenkron IO" modelidir. Bu modelde okuma/yazma gibi işlemler başlatılır ancak akış devam eder. İşlemler bittiğinde durum programcıya bir sinyal ya da fonksiyon çağrısı ile bildirilir. Asenkron IO işlemleri POSIX standartları tarafından desteklenmektedir. Asenkron IO modelinde kullanılan fonksiyonlar aio_xxx biçiminde isimlendirilmiştir. Asenkron IO işlemleri için kullanılan fonksiyonlar ve yapılar dosyası içerisinde bildirilmiştir. Asenkron IO işlemleri tipik olarak şu aşamalardan geçilerek yürütülmektedir: 1) Önce içerisinde bildirilmiş olan struct aiocb (asychronous IO control block) isimli bir yapı türünden nesne tanımlanıp içinin doldurulması gerekir. Yapı şöyle bildirilmiştir. #include struct aiocb { int aio_fildes; off_t aio_offset; volatile void *aio_buf; size_t aio_nbytes; int aio_reqprio; struct sigevent aio_sigevent; int aio_lio_opcode; }; Yapının aio_fildes elemanına okuma/yazma yapılmak istenen dosyaya ilişkin dosya betimleyicisi yerleştirilir. Asenkron okuma/yazma işlemleri dosya göstericisinin gösterdiği yerden itibaren yapılmamaktadır. Okuma/yazmanın dosyanın neresinden yapılacağı yapının aio_offset elemanında belirtilir. (Seekable olmayan aygıtlar için bu elemana 0 girilebilir. Eğer yazma durumu söz konusuysa ve dosya O_APPEND modda açıldıysa bu durumda aio_offset elemanının değeri dikkate alınmaz. Her yazılan dosyaya eklenir.) Yapının aio_buf elemanı transferin yapılacağı bellek adresini belirtir. Bu adresteki dizinin işlem sonlanana kadar yaşıyor durumda olması gerekmektedir. Yapının aio_nbytes elemanı okunacak ya da yazılacak byte miktarını belirtmektedir. Tabii burada belirtilen byte miktarı aslında aio_buf dizisinin uzunluğunu belirtmektedir. Yoksa kesin olarak okunacak byte sayısını belirtmez. Yani örneğin asenkron biçimde bir borudan 100 byte okumak isteyelim. Bize "işlem bitti" bildirimi 100 byte okuduktan sonra gelmek zorunda değildir. En az 1 byte'lık okuma olayı gerçekleşmişse de "işlem bitti bildirimi" yapılır. Tabii hiçbir zaman burada belirtilen byte miktarından fazla okuma yazma yapılmayacaktır. Başka bir deyişle yapının bu aio_nbytes elemanı en fazla yapılacak okuma/yazma miktarını belirtmektedir. Yapının aio_reprio elemanı ise okuma/yazma için bir öncelik derecesi belirtmektedir. Yani bu değer yapılacak transferin önceliğine ilişkin bir ip ucu belirtir. Ancak işletim sisteminin bu ipucunu kullanıp kullanmayacağı isteğe bağlı bırakılmıştır. Bu elemana 0 geçilebilir. Yapının aio_sigevent elemanı işlem bittiğinde yapılacak bildirim hakkında bilgileri barındırmaktadır. Bu sigevent yapısını daha önce timer konusunda görmüştük. Burada yeniden anımsatmak istiyoruz: #include struct sigevent { int sigev_notify; int sigev_signo; union sigval sigev_value; void (*sigev_notify_function) (union sigval); void *sigev_notify_attributes; }; Bu yapının sigev_notify elemanı bildirimin türünü belirtir. Anımsanacağı gibi bu tür SIGEV_NONE, SIGEV_SIGNAL, SIGEV_THREAD biçiminde olabilmektedir. SIGEV_NONE IO olayı bittiğinde bir bildirimin yapılmayacağını, SIGEV_SIGNAL bir sinyal ile bildirimin yapılacağını, SIGEV_THREAD ise bildirimin kernel tarafından yaratılan bir thread yoluyla yapılacağını belirtmektedir. Yapının sigev_signo elemanı ise eğer sinyal yoluyla bildirimde bulunulacaksa sinyalin numarasını belirtmektedir. Yapının sigev_value elemanı sinyal fonksiyonuna ya da thread fonksiyonuna gönderilecek kullanıcı tanımlı bilgiyi temsil etmektedir. Buradaki birliğin aşağıdaki gibi bildirildiğini anımsayınız: #include union sigval { /* Data passed with notification */ int sival_int; /* Integer value */ void *sival_ptr; /* Pointer value */ }; Yapının sigev_notify_function elemanı eğer bildirim thread yoluyla yapılacaksa işletim sistemi tarafından yaratılan thread'in çağıracağı callback fonksiyonunu belirtmektedir. Yapının sigev_notify_attributes elemanı ise yaratılacak thread'in özelliklerini belirtir. Bu parametre NULL geçilebilir. Örneğin: struct aiocb cb; char buf[BUFFER_SIZE + 1]; ... cb.aio_fildes = STDIN_FILENO; cb.aio_offset = 0; cb.aio_buf = buf; cb.aio_nbytes = BUFFER_SIZE; cb.aio_reqprio = 0; cb.aio_sigevent.sigev_notify = SIGEV_THREAD; cb.aio_sigevent.sigev_value.sival_ptr = &cb; cb.aio_sigevent.sigev_notify_function = io_proc; cb.aio_sigevent.sigev_notify_attributes = NULL; 2) Şimdi okuma ya da yazma olayını aio_read ya da aio_write fonksiyonuyla başlatmak gerekir. Artık akış bu fonksiyonlarda bloke olmayacak fakat işlem bitince bize bildirimde bulunulacaktır. #include int aio_read(struct aiocb *aiocbp); int aio_write(struct aiocb *aiocbp); Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. İşlemlerin devam ettiğine yani henüz sonlanmadığına dikkat ediniz. Bu fonksiyonlara verdiğimiz aiocb yapılarının işlem tamamlanana kadar yaşıyor olması gerekir. Yani fonksiyon bizim verdiğimiz aiocb yapısını çalışırken kullanıyor olabilir. aio_read ve aio_write fonksiyonları yalnızca bir defalık okuma yazma için mekanizmayı kurmaktadır. Mekanizmanın nasıl devam ettirileceği izleyen maddede ele alınacaktır. Örneğin: if (aio_read(&cb) == -1) exit_sys("aio_read"); 3) Anımsanacağı gibi biz aiocb yapısının aio_nbytes elemanına maksimum okuma/yazma miktarını vermiştik. Halbuki bundan daha az okuma/yazma yapılması mümkündür. Pekiyi bize bildirimde bulunulduğunda ne kadar miktarda bilginin okunmuş ya da yazılmış olduğunu nasıl anlayacağız? İşte bunun için aio_return isimli fonksiyon kullanılmaktadır: #include ssize_t aio_return(struct aiocb *aiocbp); Fonksiyon başarı durumunda transfer edilen byte sayısına, başarısızlık durumunda -1 değerine geri dönmektedir. Eğer bildirim gelmeden bu fonksion çağrılırsa geri dönüş değeri anlamlı olmayabilir. aio_read ve aio_write fonksiyonları sinyal güvenli değildir, ancak aio_return ve aio_error fonksiyonları sinyal güvenlidir. Yukarıda aio_read ve aio_write işlemlerinin bir defalık okuma/yazma sağladığını belirtmiştik. İşlemin devamının sağlanması için her okuma/yazma olayı gerçekleştiğinde yeniden aio_read ve aio_write fonksiyonlarının çağrılması gerekmektedir. Yani IO işlemi gerçekleştiğinde, biz yeniden IO işlemi için bu fonksiyonların çağrılmasını sağlamalıyız. Aşağıda asenkron IO modelinin uygulanmasına ilişkin bir örnek verilmiştir. Örnekte stdin dosyasından sürekli okuma yapılmak istenmiştir. Burada bildirim SIGEV_THREAD ile kernel tarafından yaratılan thread yoluyla yapılmaktadır. Okuma olayı bittiğinde kernel tarafından yaratılmış olan thread, bizim belirlediğimiz fonksiyonu çağırmaktadır. Ancak bu işlemler için kaç thread'in yaratılacağı gibi özellikler sistemden sisteme değişebilmektedir. Örneğin Linux genellikle tek bir thread yaratıp tüm olayları bu thread'e yaptırmaktadır. Biz de bu fonksiyon içerisinde aio_return fonksiyonu ile kaç byte okunduğunu belirleyip işlemin devam etmesi için aio_read fonksiyonunu yeniden çağırmaktayız. Burada thread fonksiyonuna bizim aiocb yapısını nasıl geçirdiğimize dikkat ediniz: cb.aio_sigevent.sigev_value.sival_ptr = &cb; aio_sigevent yapısının sigev_value elemanı thread fonksiyonuna parametre olarak aktarılmaktadır. sigev_value elemanının bir birlik olduğunu anımsayınız. Biz de bu birliğin sival_ptr elemanına, aiocb yapı nesnesinin adresini yerleştirdik. Klavyeden Ctrl+d tuşlarına basıldığında bu da bir IO olayı olarak ele alınmaktadır. Ancak bu durumda aio_return fonksiyonu 0 değeri ile geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #define BUFFER_SIZE 4096 void io_proc(union sigval sval); void exit_sys(const char *msg); int main(void) { struct aiocb cb; char buf[BUFFER_SIZE + 1]; cb.aio_fildes = STDIN_FILENO; cb.aio_offset = 0; cb.aio_buf = buf; cb.aio_nbytes = BUFFER_SIZE; cb.aio_reqprio = 0; cb.aio_sigevent.sigev_notify = SIGEV_THREAD; cb.aio_sigevent.sigev_value.sival_ptr = &cb; cb.aio_sigevent.sigev_notify_function = io_proc; cb.aio_sigevent.sigev_notify_attributes = NULL; if (aio_read(&cb) == -1) exit_sys("aio_read"); printf("waiting at pause, press Ctrl+C to exit...\n"); pause(); return 0; } void io_proc(union sigval sval) { ssize_t result; struct aiocb *cb = (struct aiocb *)sval.sival_ptr; char *buf = (char *)cb->aio_buf; if ((result = aio_return(cb)) == -1) exit_sys("aio_return"); if (result == 0) { printf("Ctrl+d pressed...\n"); return; } buf[result] = '\0'; printf("%s", buf); if (aio_read(cb) == -1) exit_sys("aio_read"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 93. Ders 29/10/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte daha önce yapmış olduğumuz birden fazla borudan okuma örneğini asenkron IO modeli ile gerçekleştiriyoruz. Burada yine komut satırı argümanları ile alınan bir grup isimli boru açılmıştır. Her boru açıldığında bir aiocb yapısı dinamik bir biçimde tahsis edilerek içi doldurulmuştur. Tabii her asenkron IO işleminde farklı bir tamponun kullanılması gerekmektedir. Burada yine bildirim thread yoluyla yapılmaktadır. IO olayı bittiğinde belirlediğimiz fonksiyon kernel tarafından çağrılacaktır. Biz de bu fonksiyon içerisinde kaç byte okumanın yapıldığını belirleyip okunanları ekrana (stdout dosyasına) yazdırmaktayız. Tahsis edilen alanların betimleyici kapatıldıktan sonra free edildiğine dikkat ediniz. Ana akış bu sırada pause fonksiyonunda bekletilmektedir. Son boru betimleyicisi de kapatıldığında raise fonksiyonu ile kendi prosesimize SIGINT sinyalini göndererek işlemleri sonlandırmaktayız. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 void io_proc(union sigval sval); void exit_sys(const char *msg); volatile atomic_int g_count; int main(int argc, char *argv[]) { char buf[BUFFER_SIZE + 1]; int fd; struct aiocb *cb; if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); g_count = 0; for (int i = 1; i < argc; ++i) { if (g_count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); printf("%s opened...\n", argv[i]); if ((cb = (struct aiocb *)malloc(sizeof(struct aiocb))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } cb->aio_fildes = fd; cb->aio_offset = 0; if ((cb->aio_buf = malloc(BUFFER_SIZE + 1)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } cb->aio_nbytes = BUFFER_SIZE; cb->aio_reqprio = 0; cb->aio_sigevent.sigev_notify = SIGEV_THREAD; cb->aio_sigevent.sigev_value.sival_ptr = cb; cb->aio_sigevent.sigev_notify_function = io_proc; cb->aio_sigevent.sigev_notify_attributes = NULL; if (aio_read(cb) == -1) exit_sys("aio_read"); ++g_count; } printf("waiting at pause, press Ctrl+C to exit...\n"); pause(); free(cb->aio_buf); free(cb); return 0; } void io_proc(union sigval sval) { ssize_t result; struct aiocb *cb = (struct aiocb *)sval.sival_ptr; char *buf = (char *)cb->aio_buf; if ((result = aio_return(cb)) == -1) exit_sys("aio_return"); if (result == 0) { printf("pipe closed...\n"); close(cb->aio_fildes); free(buf); free(cb); --g_count; if (g_count == 0) if (raise(SIGINT) != 0) exit_sys("raise"); return; } buf[result] = '\0'; printf("%s", buf); if (aio_read(cb) == -1) exit_sys("aio_read"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki örneği biraz farklı bir biçimde de düzenleyebiliriz. Örneğin aiocb yapısı ile tampon, bizim oluşturduğumuz bir yapının içerisinde saklanabilir. Böylece tahsisat işlemleri ve kullanım işlemleri biraz daha kolaylaştırılabilir. Örneğin: typedef struct { struct aiocb cb; char buf[BUFFER_SIZE + 1]; } IOCB_BUF; Bu sayede biz aiocb yapısı ve tampon için iki ayrı tahsisat yapmak yerine tek bir tahsisat yapabiliriz. Aynı zamanda bu yapının içerisine başka bilgiler de yerleştirilebilmektedir. Gerçekten de özellikle TCP soket uygulamalarında okunan bilgilerin bir araya getirilmesi için tampona eşlik eden başka bilgilerinde bu yapıda tutulması gerekebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 typedef struct { struct aiocb cb; char buf[BUFFER_SIZE + 1]; } IOCB_INFO; void io_proc(union sigval sval); void exit_sys(const char *msg); volatile atomic_int g_count; int main(int argc, char *argv[]) { int fd; IOCB_INFO *ioinfo; if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); g_count = 0; for (int i = 1; i < argc; ++i) { if (g_count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); printf("%s opened...\n", argv[i]); if ((ioinfo = (IOCB_INFO *)malloc(sizeof(IOCB_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ioinfo->cb.aio_fildes = fd; ioinfo->cb.aio_offset = 0; ioinfo->cb.aio_buf = ioinfo->buf; ioinfo->cb.aio_nbytes = BUFFER_SIZE; ioinfo->cb.aio_reqprio = 0; ioinfo->cb.aio_sigevent.sigev_notify = SIGEV_THREAD; ioinfo->cb.aio_sigevent.sigev_value.sival_ptr = ioinfo; ioinfo->cb.aio_sigevent.sigev_notify_function = io_proc; ioinfo->cb.aio_sigevent.sigev_notify_attributes = NULL; if (aio_read(&ioinfo->cb) == -1) exit_sys("aio_read"); ++g_count; } printf("waiting at pause, press Ctrl+C to exit...\n"); pause(); free(ioinfo); return 0; } void io_proc(union sigval sval) { ssize_t result; IOCB_INFO *ioinfo = (IOCB_INFO *)sval.sival_ptr; if ((result = aio_return(&ioinfo->cb)) == -1) exit_sys("aio_return"); if (result == 0) { printf("pipe closed...\n"); close(ioinfo->cb.aio_fildes); free(ioinfo); --g_count; if (g_count == 0) if (raise(SIGINT) != 0) exit_sys("raise"); return; } ioinfo->buf[result] = '\0'; printf("%s", ioinfo->buf); if (aio_read(&ioinfo->cb) == -1) exit_sys("aio_read"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- aio_cancel fonksiyonu ise başlatılmış olan bir asenkron IO işlemini iptal etmek için kullanılmaktadır. #include int aio_cancel(int fd, struct aiocb *aiocbp); Fonksiyonun birinci parametresi iptal edilecek betimleyiciyi belirtir. Eğer iocb NULL geçilirse bu betimleyiciye ilişkin bütün asenkron işlemler iptal edilmektedir. Fonksiyon AIO_CANCELED değerine geri dönerse iptal başarılıdır. AIO_NOTCANCELED değerine geri dönerse işlem aktif biçimde devam etmekte olduğu için iptal başarısızdır. AIO_ALLDONE değeri ise işlemin zaten bittiğini belirtir. Fonksiyon başarısızlık durumunda -1 değerine geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- aio_error isimli fonksiyon herhangi bir durumda başlatılan işlemin akibeti konusunda bilgi almak için kullanılabilir. #include int aio_error(const struct aiocb *aiocbp); Fonksiyonun geri dönüş değeri bu asenkron işlemin o anda ne durumda olduğu hakkında bize bilgi vermektedir. Eğer fonksiyon EINPROGRESS biçiminde özel bir değere geri dönerse işlemin hala devam ettiği anlamı çıkar. Geri dönüş değeri ECANCELED ise bu durumda işlem aio_cancel fonksiyonuyla iptal edilmiştir (Bu geri dönüş değeri POSIX standartlarında bulunmamaktadır. Linux sistemlerinde bulunmaktadır.) Fonksiyon errno değerini set etmez. Geri dönüş değeri diğer pozitif değerlerden birisi ise hata ile ilgili errno değerini belirtir. Başlatılan IO işlemi başarılı bir biçimde sonlanmışsa fonksiyon 0 değerine geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Şimdiye kadar görmüş olduğumuz IO modellerinin kullanımları ve performansları hakkında şunları söyleyebiliriz: - select, poll modeli ve asenkron IO modeli POSIX standartlarında bulunan taşınabilir modellerdir. - epoll modeli ve sinyal tabanlı IO modeli Linux sistemlerine özgüdür. Yani taşınabilir değildir. - Linux sistemlerinde performansı en yüksek model epoll modelidir. Perfomans sıralaması iyiden kötüye doğru şöyledir: 1) epoll modeli 2) sinyal tabanlı IO modeli ve asenkron IO modeli 3) select ve poll modeli ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Üzerinde duracağımız son IO modeli İngilizce "scatter/gather" IO modeli denilen modeldir. ("scatter" saçmak, "gather" toplamak anlamına gelmektedir. Buna Türkçe "saçma/toplama IO modeli" diyebiliriz.) Pek çok uygulamada değişik adreslerdeki bilgilerin peşi sıra dosyaya yazılması ya da dosyadan okunanların değişik adreslere yazılması söz konusu olabilmektedir. Örneğin bir kaydı temsil eden aşağıdaki üç bilginin birbiri ardına dosyaya yazılmak istendiğini düşünelim: int record_len; char record[RECORD_SIZE]; int record_no; Bu bilgilerin dosyaya yazılması için normal olarak üç ayrı write işlemi yapmak gerekir: if (write(fd, &record_len, sizeof(int)) != sizeof(int)) { ... } if (write(fd, record, RECORD_SIZE) != RECORD_SIZE) { ... } if (write(fd, &record_no, sizeof(int)) != sizeof(int)) { ... } Burada farklı adreslerde bulunan üç farklı bilgi dosyaya peşi sıra yazılmak istenmiştir. Ancak bu write işlemi göreli bir zaman kaybı oluşturabilmektedir. Tabii zaman kaybı uygulamaların ancak çok azında bir önem oluşturur. Buradaki zaman kaybının en önemli nedeni her write çağrısının kernel mode'a geçiş yapmasıdır. Eğer bu zaman kaybını aşağı çekmek istiyorsak ilk akla gelen yöntem önce bu bilgileri başka bir tampona kopyalayıp tek bir write işlemi yapmaktır: char buf[BUFSIZE]; memcpy(buf, &recordlen, sizeof(int)); memcpy(buf + sizeof(int), record, RECORD_SIZE); memcpy(buf + sizeof(int) + RECORD_SIZE, &record_no, sizeof(int)); if (write(fd, buf, 2 * sizeof(int) + RECORD_SIZE) != 2 * sizeof(int) + RECORD_SIZE) { ... } Bu işlem üç ayrı write işlemine göre oldukça hızlıdır. işte readv ve writev isimli fonksiyonlar farklı adreslerdeki bilgileri yukarıdakine benzer biçimde dosyaya yazıp dosyadan okumaktadır. Bu işlemlere İngilizce "scatter/gather IO" denilmektedir. readv ve writev fonksiyonlarının prototipleri şöyledir: #include ssize_t readv(int fildes, const struct iovec *iov, int iovcnt); ssize_t writev(int fildes, const struct iovec *iov, int iovcnt); Fonksiyonların birinci parametreleri okuma ya da yazma işleminin yapılacağı dosya betimleyicisini, ikinci parametreleri kullanılacak tampon uzunluklarının ve adreslerinin belirtildiği yapı dizisinin adresini, üçüncü parametresi de bu yapı dizisinin uzunluğunu belirtir. Programcı struct iovec türünden bir yapı dizisi oluşturup onun içini doldurmalıdır. Fonksiyonlar başarısızlık durumunda -1 değerine, diğer durumlarda okunan yazılan toplam byte miktarına geri dönmektedir. Okuma ve yazma işlemleri tek parça halinde atomik biçimde yapılmaktadır. Yani bu okuma yazma işlemlerinin arasına başka bir dosya işlemi girememektedir. iovec yapısı şöyle bildirilmiştir: struct iovec { void *iov_base; size_t iov_len; }; Yapının iov_base elemanı yazılacak ya da okunacak bilginin bellek adresini, iov_len elemanı ise bunun uzunluğunu belirtmektedir. Aşağıdaki örnekte aslında üç farklı write işlemi ile yapılacak yazma işlemleri tek hamlede atomik olarak writev fonksiyonuyla yapılmıştır. Burada farklı adreslerdeki bilgilerin dosyaya dosya göstericisinin gösterdiği yerden itibaren peşi sıra yazıldığına dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #define BUFFER_SIZE 10 void exit_sys(const char *msg); int main(void) { int fd; char *buf1[BUFFER_SIZE]; char *buf2[BUFFER_SIZE]; char *buf3[BUFFER_SIZE]; struct iovec vec[3]; if ((fd = open("test.txt", O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("open"); memset(buf1, 'a', BUFFER_SIZE); memset(buf2, 'b', BUFFER_SIZE); memset(buf3, 'c', BUFFER_SIZE); vec[0].iov_base = buf1; vec[0].iov_len = BUFFER_SIZE; vec[1].iov_base = buf2; vec[1].iov_len = BUFFER_SIZE; vec[2].iov_base = buf3; vec[2].iov_len = BUFFER_SIZE; if (writev(fd, vec, 3) == -1) exit_sys("writev"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda readv fonksiyonun kullanımına bir örnek verilmiştir. Burada yukarıdaki örnekte oluşturulan dosya ters bir biçimde readv fonksiyonu ile farklı adreslere okunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #define BUFFER_SIZE 10 void exit_sys(const char *msg); int main(void) { int fd; char *buf1[BUFFER_SIZE]; char *buf2[BUFFER_SIZE]; char *buf3[BUFFER_SIZE]; struct iovec vec[3]; if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); vec[0].iov_base = buf1; vec[0].iov_len = BUFFER_SIZE; vec[1].iov_base = buf2; vec[1].iov_len = BUFFER_SIZE; vec[2].iov_base = buf3; vec[2].iov_len = BUFFER_SIZE; if (readv(fd, vec, 3) == -1) exit_sys("writev"); write(1, buf1, BUFFER_SIZE); write(1, buf2, BUFFER_SIZE); write(1, buf3, BUFFER_SIZE); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Arka planda sessiz sedasız çalışan bir kullanıcı arayüzü olmayan, kullanıcılarla terminal yoluyla etkileşmeyen programlara Windows dünyasında "servis (service)", UNIX/Linux dünyasında ise "daemon (di:mın biçiminde okunuyor)" denilmektedir. Servisler ya da daemon'lar tipik olarak boot süreci sırasında çalışmaya başlatılırlar ve yine tipik olarak makine reboot edilene kadar çalışmaya devam ederler. Tabii böyle bir zorunluluk yoktur. Yani servis ya da daemon programlar istenildiği zaman başlatılıp istenildiği zaman sonlandırılabilmektedir. "Servis" ya da "daemon" kernel mod bir kavram değildir. Yani servisler ve daemon'lar genellikle "user mode'da" çalışmak üzere yazılırlar. UNIX/Linux dünyasında geleneksel olarak daemon'lar "xxxxxd" biçiminde sonuna 'd' harfi getirilerek isimlendirilmektedir. Çekirdeğe ilişkin bazı thread'ler de servis benzeri işlemler yaptıkları için bunlar da çoğu kez sonu 'd' ile bitecek ancak başı da 'k' ile başlayacak biçimde isimlendirilmiştir. Bu kernel daemon'ların bizim şu andaki konumuz olan daemon'larla hiçbir ilgisi yoktur. Yalnızca işlev bakımından bir benzerlik söz konusudur. UNIX/Linux dünyasında daemon denildiğinde akla öncelikle "server programlar" gelmektedir. Örneğin ftp server programı (ftpd) ve http server programı (httpd) daemon programlar biçiminde yazılmışlardır. Daemon'lar genellikle arka planda önemli işlemler yaptıkları için uygun önceliklerle (yani sudo ile root hakkıyla) çalıştırılırlar. Daemon programlar pek çok modern UNIX/Linux sisteminde "init paketleri" içerisindeki özel utility'ler tarafından başlatılıp, sürdürülüp, sonlandırılmaktadır. Yani ilgili dağıtımın bu daemon'ları idare etmek için özel komutları bulunabilmektedir. Linux sistemlerinde init prosesi ve diğer proseslerin kodları ve boot süreci ile ilgili utility'ler "init paketleri" denilen paketler biçiminde farklı proje grupları tarafından oluşturulmuştur. Başka bir deyişle "servis yönetim (service management)" işlemleri organize bir biçimde bu "init paketleri" tarafından yapılmaktadır. Yaygın olarak kullanılan üç "init paketi" bulunmaktadır: 1) SysVinit: Klasik System5'teki işlevleri yapan init paketidir. Linux uzun bir süre bu paketi kullanmıştır. 2) Upstart: 2006 yılında oluşturulmuştur ve 2010'ların ortalarına kadar (bazı dağıtımlarda hala) kullanılmaya yaygın biçimde kullanılmıştır. 3) systemd: 2010 yılında oluşturulmuştur ve son yıllarda pek çok Linux dağıtımında kullanılmaya başlanmıştır. Bugün en yaygın kullanılan init paketi durumundadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 94. Ders 04/11/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir daemon programı yazabilmek için öncelikle onun terminal bağlantısının kesilmesi gerekmektedir. Bir programı komut satırından sonuna & getirerek "arka plan proses grubu" biçiminde çalıştırarak daemon oluşturamayız. Çünkü bu durumda terminal kapatıldığında bizim prosesimiz de SIGHUP sinyali yüzünden kapatılacaktır. Bir daemon programın yazılması tipik olarak şu aşamalardan geçilerek yapılmaktadır: 1) Daemon programlar bir dosya açmak istediklerinde tam olarak belirlenen haklarla bunu yapmalıdırlar. Bu nedenle bu proseslerin umask değerlerinin 0 yapılması uygun olur. Örneğin: umask(0); 2) Bir prosesin daemon etkisi yaratması için terminalle bir bağlantısının kalmaması gerekir. Bu da basit bir biçimde maalesef 0, 1, 2 numaralı terminal betimleyicilerinin kapatılmasıyla sağlanamaz. Bunu sağlamanın en temel yolu setsid fonksiyonunu çağırmaktır. Anımsanacağı gibi setsid fonksiyonu yeni bir oturum (session) ve yeni bir proses grubu oluşturup ilgili prosesi bu proses grubunun ve oturumun lideri yapmaktadır. Ayrıca setsid fonksiyonu prosesin terminal ilişkisini (controlling terminal) de ortadan kaldırmaktadır. Ancak setsid uygulayabilmek için prosesin herhangi bir proses grup lideri olmaması gerekir. Aksi takdirde setsid fonksiyonu başarısız olmaktadır. Yine anımsanacağı gibi kabuk programlar çalıştırdıkları programlar için bir proses grubu yaratıp o programı da proses grup lideri yapıyordu. İşte proses grup lideri olmaktan kurtulmak için bir kez fork yapıp üst prosesi sonlandırabiliriz. Aynı zamanda bu işlem kabuk programının hemen komut satırına yeniden düşmesine yol açacaktır. O halde 2'inci aşamada fork işlemi yapılıp üst proses sonlandırılmalıdır. Örneğin: if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); 3) Artık alt proses setsid fonksiyonunu uygulayarak yeni bir oturum yaratır ve terminal ilişkisini keser. Terminal ilişkisinin kesilmesi ile artık terminal kapatılsa bile programımız çalışmaya devam edecektir. Tabii setsid ile terminal bağlantısının kesilmiş olması programın terminale bir şey yazamayacağı anlamına gelmez. Hala 0, 1, 2 numaralı betimleyiciler açıktır. Terminal açık olduğu sürece oraya yazma yapılabilir. Örneğin: if (setsid() == -1) _exit(EXIT_FAILURE); 4) Daemon programların çalışma dizinlerinin (current working directory) sağlam bir dizin olması tavsiye edilir. Aksi takdirde o dizin silinirse arka plan programların çalışmaları bozulabilir. Bu nedenle daemon programlar çoğu kök dizini (silinemeyeceği için) çalışma dizini yapmaktadır. Tabii bu zorunlu değildir. Bunun yerine varlığı garanti edilmiş olan herhangi bir dizin de çalışma dizini yapılabilir. Örneğin: if (chdir("/") == -1) _exit(EXIT_FAILURE); 5) Daemon programın o ana kadar açılmış olan tüm betimleyicileri kapatması uygun olur. Örneğin 0, 1, 2 numaralı betimleyiciler ilgili terminale ilişkindir ve artık o terminal kapatılmış ya da kapatılacak olabilir. Program kendini daemon yaptığı sırada açmış olduğu diğer dosyaları da kapatmalıdır. Bunu sağlamanın basit bir yolu prosesin toplam dosya betimleyici tablosunun uzunluğunu elde edip her bir betimleyici için close işlemi uygulamaktır. Çünkü maalesef biz açık betimleyicileri pratik bir biçimde tespit edememekteyiz. Zaten kapalı bir betimleyiciye close uygulanırsa close başarısız olur, ancak program çökmez. Anımsanacağı gibi prosesin toplam betimleyici sayısı sysconf çağrısında _SC_OPEN_MAX argümanıyla ya da getrlimit fonksiyonunda RLIMIT_NOFILE argümanıyla elde edilebilir. İki fonksiyon da aynı değeri vermektedir. Örneğin: long maxfd; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); 6) Zorunlu olmamakla birlikte ilk üç betimleyiciyi "/dev/null" aygıtına yönlendirmek iyi bir fikirdir. Çünkü daemon içerisinde çağıracağımız bazı fonksiyonlar bu betimleyicileri kullanıyor olabilirler. Anımsanacağı gibi "/dev/null" aygıtına yazılanlar zaten kaybolmaktadır. Bu aygıttan okuma yapılmak istendiğinde ise EOF etkisi oluşmaktadır. Örneğin: int fd; if ((fd = open("/dev/null", O_RDONLY)) == -1) // fd is guaranteed to be 0 _exit(EXIT_FAILURE); if (dup(fd) == -1 || dup(fd) == -1) // now descriptor 1 and 2 redirected /dev/null _exit(EXIT_FAILURE); Burada bazı programcılar (örneğin Stevent & Rago ile Kerrisk'in kitaplarında bu biçimde) elde edilen betimleyicilerin 0, 1 ve 2 olduğunu doğrulamaya çalışmaktadır. Tüm betimleyiciler kapatıldığına göre ve open ile dup fonksiyonları en düşük betimleyicileri verdiğine göre böyle bir kontrolün yapılmasına aslında gerek yoktur. Bu kontrolün tek anlamı çok thread'li uygulamalarda o sırada başka bir thread'in bu noktada dosya açıyor olmasıdır. Böyle bir olasılığı değerlendirmek isterseniz kodu şöyle değiştirebilirsiniz: int fd0, fd1, fd2; if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); Pekiyi daemon'lar ne yaparlar? İşte daemon'lar arka planda genellikle sürekli bir biçimde birtakım işler yapmaktadır. Bu anlamda en tipik daemon örnekleri "server" programlardır. Örneğin http server aslında httpd isimli bir daemon'dan ibarettir. Bunun gibi UNIX/Linux sistemlerinde genellikle boot zamanında devreye giren onlarca daemon program vardır. Örneğin belli zamanlarda belli işlerin yapılması için kullanılan "cron" utility'si aslında bir daemon olarak çalışmaktadır. Yukarıda da belirttiğimiz gibi daemon'lar genellikle yetki gerektiren işlemler yaptıkları için uygun önceliğe sahip olacak biçimde (yani root olarak) çalıştırılmaktadırlar. Aşağıdaki örnekte tipik bir daemon program iskeleti oluşturulmuştur. Programın sonuna bir pause çağrısı yerleştirdik. Daemon'ı sonlandırmak için proses id'sini bulup kill işlemi uygulayabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* mydaemond.c */ #include #include #include #include #include #include #define DEF_FDT_SIZE 4096 void exit_sys(const char *msg); int main(void) { pid_t pid; long maxfd; int fd0, fd1, fd2; umask(0); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) _exit(EXIT_FAILURE); if (chdir("/") == -1) _exit(EXIT_FAILURE); errno = 0; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); pause(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aslında yukarıdaki işlemleri bir fonksiyona da yaptırabiliriz. Böylece o fonksiyonu çağırdığımızda proses bir daemon haline getirilir. ---------------------------------------------------------------------------------------------------------------------------*/ /* mydaemond.c */ #include #include #include #include #include #include void exit_sys(const char *msg); #define DEF_FDT_SIZE 4096 void make_daemon(void) { pid_t pid; long maxfd; int fd0, fd1, fd2; umask(0); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) _exit(EXIT_FAILURE); if (chdir("/") == -1) _exit(EXIT_FAILURE); errno = 0; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); } int main(void) { make_daemon(); pause(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 95. Ders 05/11/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi bir daemon ne zaman ve nasıl sonlandırılmalıdır? Daemon'lar arka planda sessiz sedasız çalıştığından onların sonlandırılması sinyaller yoluyla yapılabilir. Tipik olarak daemon'lar SIGTERM sinyali ile sonlandırılmaktadır. Anımsanacağı gibi SIGTERM sinyali ele alınabilir bir sinyaldir (SIGKILL sinyalinin ele alınamayacağını anımsayınız). İşte bir daemon birtakım son işlemler yapacaksa bunu SIGTERM sinyali oluştuğunda bu sinyali set ederek yapabilmektedir. Pekiyi daemon hiç sonlandırılmazsa ne olur? İşletim sistemi kapanırken bütün proseslere zaten önce SIGTERM sinyali göndermektedir. Dolayısıyla daemon'lar en kötü olasılıkla birtakım son işlemleri sistem kapatılırken SIGTERM sinyali yoluyla yapabilirler. Ancak sistemler genellikle SIGTERM sinyalinden sonra proseslere belli bir süre bekleyip SIGKILL sinyali de göndermektedir. Örneğin Linux kapanırken SIGTERM sinyalindan yaklaşık 5 saniye sonra her ihtimale karşı proseslere SIGKILL sinyali de göndermektedir. Bu durumda daemon programların son işlemlerini bu zaman aralığı içerisinde yavaş olmayacak biçimde yapması beklenir. Aşağıdaki daemon programının SIGTERM sinyalini işlemesi örneği verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* mydaemond.c */ #include #include #include #include #include #include #include void sigterm_handler(int signo); void exit_sys(const char *msg); #define DEF_FDT_SIZE 4096 void make_daemon(void) { pid_t pid; long maxfd; int fd0, fd1, fd2; umask(0); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) _exit(EXIT_FAILURE); if (chdir("/") == -1) _exit(EXIT_FAILURE); errno = 0; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); } int main(void) { struct sigaction sa; sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) exit_sys("sigaction"); make_daemon(); pause(); return 0; } void sigterm_handler(int signo) { /* cleanup processing */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Daemon programlar genellikle başlatılırken bazı yönergeleri kullanıcıların oluşturduğu konfigürasyon dosyalarından okumaktadır. Bu konfigürasyon dosyaları geleneksel olarak "/etc" dizininin altında uzantısı ".conf" olacak biçimde oluşturulmaktadır. Örneğin bizim daemon programımız "/etc/mydaemond.conf" dosyasını okuyup yönergeleri oradan alacak olabilir. Bu dosya henüz daemon'laştırma yapılmadan da okunabilir ya da daemon'laştırma yapıldıktan sonra da okunabilir. Daemon'laştırma yapıldıktan sonra konfigürasyon dosyasını açarken ve sonraki işlemlerde başarısızlıklara ilişkin mesajların artık terminale yazılamayacağına dikkat ediniz. Bunun için log mekanizmaları kullanılmaktadır. Örneğin: int main(void) { struct sigaction sa; make_daemon(); read_config(); sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) { // log mekanizması yoluyla hata mesajı oluşturulabilir _exit(EXIT_FAILURE); } // ... return 0; } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Daemon'ların çoğu tek bir proses olarak çalışmak zorundadır. Yani daemon'ın başka kopyasının da çalıştırılabilmesi problemlere yol açabilmektedir. Daemon yazarken onun tek kopyasının çalıştığına emin olmak isteyebilirsiniz. Bunun nasıl yapıldığını "dosya kilitleme işlemlerinde" açıklamıştık. Geleneksel olarak daemon'lar bu amaçla "/run" dizini içerisinde ".pid" uzantılı dosyalar oluşturmaktadır. Bu "/run" dizinine sıradan prosesler tarafından yazma hakkı verilmemektedir. Yani bu dizinde dosya yaratmak istiyorsanız daemon programınızı uygun önceliğe sahip olacak biçimde (yani root olarak)" çalıştırmalısınız. Aşağıda daemon'ın tek bir kopyasının çalıştırılmasının sağlanmasına yönelik bir örnek verilmiştir. Programı sudo ile çalıştırmayı unutmayınız. ---------------------------------------------------------------------------------------------------------------------------*/ /* mydaemond.c */ #include #include #include #include #include #include #include #include void read_config(void); void check_instance(void); void sigterm_handler(int signo); void exit_sys(const char *msg); #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" #define CONFIG_FILE "/etc/mydaemond.conf" void make_daemon(void) { pid_t pid; long maxfd; int fd0, fd1, fd2; umask(0); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) _exit(EXIT_FAILURE); if (chdir("/") == -1) _exit(EXIT_FAILURE); errno = 0; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); } int main(void) { struct sigaction sa; make_daemon(); check_instance(); read_config(); sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) { /* log mekanizması yoluyla hata mesajı oluşturulabilir */ _exit(EXIT_FAILURE); } pause(); return 0; } void read_config(void) { /* ... */ } void check_instance(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) { /* log mekanizması ile mesaj oluşturulabilir */ _exit(EXIT_FAILURE); } if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { /* log mekanizması ile mesaj oluşturulabilir */ } else { /* log mekanizması ile mesaj oluşturulabilir */ } _exit(EXIT_FAILURE); } } void sigterm_handler(int signo) { /* cleanup processing */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Daemon programların "reinitialize" edilebilmesi sık karşılaşılan bir uygulamadır. Daemon program konfigürasyon dosyasını okuyup çalıştıktan sonra onu durdurmadan adeta reset etmek isteyebiliriz. Örneğin daemon çalışırken konfigürasyon dosyasında bir değişiklik yapabiliriz. O değişiklikten sonra yeniden daemon programının o konfigürasyon dosyasını çalıştırmasını sağlamak isteyebiliriz. Genel olarak bu işlem için SIGHUP sinyalinden faydalanılmaktadır. SIGHUP sinyali terminal aygıt sürücüsü ve kabuk programları tarafından kullanılan bir sinyaldir. Ancak daemon programının terminal bağlantısı kesildiğinden SIGHUP sinyali daemon programlar için "boşa çıkmış ve kullanılabilir" bir sinyal durumundadır. Aşağıda SIGHUP sinyalinin işlenmesine yönelik bir örnek verilmiştir. Burada konfigürasyon dosyası sinyal fonksiyonu içerisinde okunmuştur. Bu durumda bu işlemlerin asenkron sinyal güvenli bir biçimde yapılması gerekir. Tabii buna alternatifler de söz konusu olabilir. Örneğin sinyal fonksiyonunda bir bayrak set edilip başka bir yerde bu bayrağa bakılabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /* mydaemond.c */ #include #include #include #include #include #include #include #include void read_config(void); void check_instance(void); void sigterm_handler(int signo); void sighup_handler(int signo); void exit_sys(const char *msg); #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" #define CONFIG_FILE "/etc/mydaemond.conf" void make_daemon(void) { pid_t pid; long maxfd; int fd0, fd1, fd2; umask(0); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) _exit(EXIT_FAILURE); if (chdir("/") == -1) _exit(EXIT_FAILURE); errno = 0; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); } int main(void) { struct sigaction sa; int fd; make_daemon(); check_instance(); read_config(); sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) { /* log mekanizması yoluyla hata mesajı oluşturulabilir */ _exit(EXIT_FAILURE); } sa.sa_handler = sighup_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGHUP, &sa, NULL) == -1) { /* log mekanizması yoluyla hata mesajı oluşturulabilir */ _exit(EXIT_FAILURE); } /* ... */ pause(); return 0; } void read_config(void) { /* ... */ } void check_instance(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) { /* log mekanizması ile mesaj oluşturulabilir */ _exit(EXIT_FAILURE); } if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { /* log mekanizması ile mesaj oluşturulabilir */ } else { /* log mekanizması ile mesaj oluşturulabilir */ } _exit(EXIT_FAILURE); } } void sigterm_handler(int signo) { /* cleanup processing */ } void sighup_handler(int signo) { read_config(); /* read_config asenkron sinyal güvenli bir fonksiyon olmalı */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Daemon gibi programlarda, kernel modüllerinde bir terminal bağlantısı olmadığı için hata mesajlarının ve diğer mesajların ekrana yazdırılması mümkün olmayabilir ya da uygun olmayabilir. İşte bu tür durumlarda birtakım mesajların bir log'lama mekanizması yoluyla oluşturulması gerekebilmektedir. UNIX/Linux sistemlerinde kernel tarafından desteklenen kapsamlı bir log mekanizması bulunmaktadır. Bu log mekaznizması değişik kaynaklardan gelen mesajların biriktirilip saklanmasını sağlamaktadır. Bu merkezi log mekanizması aslında ilk BSD sistemlerinde uygulanmıştır. Ancak daha sonra genelleştirilmiştir ve POSIX standartlarına yansıtılmıştır. Bu log mekanizması genel olarak "syslog" ismiyle belirtilmektedir. Öncelikle Linux sistemleri için bu merkezi syslog mekanizmasının çalışma biçimi hakkında bilgi vereceğiz. Bu çalışma açıklanırken genellikle şekillerden faydalanılmaktadır. Ancak burada text editörde çalıştığımızdan dolayı bu şekli çizemiyoruz. Bunun için "Linux Programming Environment" kitabının 775'inci sayfasına başvurabilirsiniz. Biz burada sözel anlatım uygulayacağız. Linux sistemlerindeki merkezi syslog mekanizması ile diğer sistemlerdeki syslog mekanizması bazı ayrıntılar dışında birbirine benzemektedir. Merkezi syslog mekanizmasının üst seviye önemli bir bileşeni "syslogd" isimli bir daemon programdır. Bu program user mode'da çalışmaktadır. syslogd daemon programı iki kaynaktan mesajları alarak hedefte oluşturmaktadır. Loglamanın hedefi değiştirilebilmektedir. syslogd daemon programının mesajları okuduğu iki kaynak şöyledir: 1) Yerel kullanımlar için /dev/log isimli UNIX domain datagram soket dosyası 2) Uzaktan kullanımlar için UDP 514 portu "/dev/log" isimli UDP UNIX domain soketine temelde iki aktör yazmaktadır. Bunlardan biri normal kullanıcılardır. Örneğin biz bir daemon yazarken mesajları syslog isimli POSIX fonksiyonu ile oluştururuz. Bu POSIX fonksiyonu da aslında bu sokete yazma yapmaktadır: user (syslog POSIX fonksiyonu) ---> /dev/log (UDP UNIX domain soket) <--- syslogd daemon okuyor /dev/log dosyasına yazan ikinci aktör ise aygıt sürücüler ve kernel modüllerdir. Bu kodlar kernel fonksiyonları ile "klogd" isimli kernel mode'daki daemon programa mesajları aktarmaktadır. Bu kernel mod daemon programı da /dev/log dosyasına yazma yapmaktadır: Aygıt sürücüler ve kernel modüller (printk) <--- klogd daemon'ı okuyor ---> /dev/log <--- syslogd daemon okuyor Loglama amacıyla user mod programlarda biz genellikle izleyen paragraflarda açıklayacağımız POSIX fonksiyonlarını kullanırız. Ancak kernel mod programlarda (kernel modüllerinde ve aygıt sürücülerinde) genellikle printk gibi bir kernel fonksiyonu kullanılmaktadır. Yukarıdaki şekillerden de gördüğünüz gibi her iki mekanizmada aynı loglama sistemine bilgileri aktarmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 96. Ders 11/11/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- User mode'da loglama işlemleri için üç POSIX fonksiyonu kullanılmaktadır: openlog syslog closelog openlog fonksiyonu loglama mekanizmasını başlatır, syslog fonksiyonu loglama işlemlerini yapar ve closelog fonksiyonu da sistemi kapatır. openlog fonksiyonu aslında mekanizma için zorunlu bir fonksiyon değildir. openlog fonksiyonunun prototipi şöyledir: #include void openlog(const char *ident, int logopt, int facility); Fonksiyonun birinci parametresi log mesajlarında görüntülenecek program ismini belirtmektedir. Genellikle programcılar bu parametre için program ismini argüman olarak verirler. Linux sistemlerinde bu parametreye NULL geçilebilmektedir. Bu durumda sanki bu parametre için program ismi yazılmış gibi işlem yapılır. Ancak POSIX standartlarında NULL geçme durumu belirtilmemiştir. İkinci parametre aşağıdaki sembolik sabitlerin bit or işlemine sokulmasıyla oluşturulabilir: LOG_PID LOG_CONS LOG_NDELAY LOG_ODELAY LOG_NOWAIT Burada LOG_PID log mesajında prosesin proses id'sinin de bulundurulacağını belirtir. LOG_CONS log mesajlarının aynı zamanda default consola ("/dev/console") da yazılacağını belirtmektedir. LOG_NDELAY bayrağı loglama sisteminin hemen açılması gerektiğini belirtir. Normal olarak bu sistem ilk loglama yapıldığında açılmaktadır. LOG_ODELAY zaten default durumdur. Loglama sistemi ilk log işlemi yapıldığında açılır. LOG_NOWAIT alt prosesler söz konusu olduğunda loglama için alt proseslerin yaratılmasının beklenmeyeceği anlamına gelmektedir. Bu parametre istenirse 0 olarak da geçilebilir. Ancak tipik uygulamalarda LOG_PID biçiminde geçilmektedir. Fonksiyonun üçüncü parametresi log mesajını yollayan prosesin kim olduğu hakkında temel bir bilgi vermek için düşünülmüştür. LOG_USER bir user proses tarafından bu loglamanın yapıldığını belirtmektedir. LOG_KERN mesajın kernel tarafından gönderildiğini belirtir. LOG_DAEMON (POSIX'te yok) mesajın bir sistem daemon programı tarafından gönderildiğini belirtmektedir. LOG_LOCAL0'dan LOG_LOCAL7'ye kadarki sembolik sabitler özel log kaynaklarını belirtmektedir. Bu parametre de 0 olarak geçilebilir. Bu durumda LOG_USER değeri girilmiş gibi işlem yapılmaktadır. Ancak tipik olarak LOG_USER biçiminde geçilmektedir. openlog fonksiyonunun başarısı kontrol edilememektedir. Çünkü fonksiyonun geri dönüş değeri void biçimdedir. Örneğin: openlog("sample", LOG_PID, LOG_USER); Log mesajlarının aktarımı için asıl fonksiyon syslog isimli fonksiyondur. Fonksiyonun prototipi şöyledir: #include void syslog(int priority, const char *format, ...); Fonksiyonun birinci parametresi mesajın öncelik derecesini (yani önemini) belirtir. Diğer parametreler tamamen printf fonksiyonundaki gibidir. Öncelik değerleri şunlardır: LOG_EMERG LOG_ALERT LOG_CRIT LOG_ERR LOG_WARNING LOG_NOTICE LOG_INFO LOG_DEBUG En çok kullanılanlar error mesajları için LOG_ERR, uyarı mesajları için LOG_WARNING ve genel bilgilendirme mesajları için LOG_INFO değerleridir. Buradaki değerler log mesajlarında görüntülenmektedir. Örneğin: syslog(LOG_ERR, "invalid operation"); Yukarıda openlog fonksiyonunun çağrılmasının zorunlu olmadığını belirtmiştik. Bu durumda openlog fonksiyonundaki belirlemeler için default değerler alınmaktadır. Ancak istenirse openlog fonksiyonunun üçüncü parametresi syslog fonksiyonunun birinci parametresiyle kombine edilebilir. Biz örneklerimizde openlog fonksiyonunu çağıracağız. syslog fonksiyonunun da geri dönüş değerinin void olduğuna dikkat ediniz. Nihayet loglama mekanizması eğer açılmışsa onu kapatmak için closelog fonksiyonu kullanılmaktadır. Bu fonksiyon eğer loglama mekanizması henüz açılmadıysa bir şey yapmamaktadır. Fonksiyonun prototipi şöyledir: #include void closelog(void); Pekiyi log mesajları nereye aktarılmaktadır? İşte aslında işletim sistemlerinde hedef çeşitli biçimlerde değiştirilebilmektedir. Ancak Linux'un ileri sürümlerinde default hedef "/var/log/syslog" dosyasıdır. Bu dosya sistemi disk tabanlı bir sistem değildir. Dolayısıyla buradaki bilgiler reboot işleminde kalıcı değildir. Bu dosya uzayabileceği için bir kuyruk sistemi gibi oluşturulmaktadır. Yani belli süre sonra önce yazılmış olan mesajlar kaybolabilmektedir. Dosyanın sonunu görebilmek için "tail" komutunu kullanabilirsiniz. tail komutu default olarak dosyanın sonundaki 10 satırı göstermektedir. Ancak -n seçeneği ile daha fazla satır görüntülenebilmektedir. Aşağıda loglama için basit bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include int main(void) { openlog("sample", LOG_PID, LOG_USER); syslog(LOG_INFO, "This is a test..."); closelog(); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi syslog log mesajlarını nereye yazmaktadır? Yukarıda da belirttiğimiz gibi aslında syslog fonksiyonu log mesajlarını "/dev/log" ismindeki UNIX domain datagram sokete yazmaktadır. Bu soketten okuma yapan "syslogd" isimli bir daemon vardır. Ancak yeni Linux sistemlerinde bu daemon'ın biraz daha gelişmiş biçimi olan "rsyslogd" daemon'ı da kullanılmaktadır. Kursun yapıldığı zamanlarda Linux sistemlerinde yaygın olarak "rsyslogd" isimli daemon kullanılmaktadır. İşte aslında log mesajlarının hangi dosyalara yazılacağına "syslogd" ya da "rsyslogd" daemon'ları karar vermektedir. Bu daemon'lar çalışmaya başladıklarında default durumda "/etc/syslog.conf" ya da "/etc/rsyslog.conf" dosyalarına bakmaktadır. İşte aslında bu daemon'ların hangi dosyalara yazacağı bu konfigürasyon dosyalarında sistem yöneticisi tarafından belirlenmektedir. Ancak bu dosyada da belirleme yapılmamışsa default olarak pek çok mesaj grubu (error, warning, info) "/var/log/syslog" dosyasına yazılmaktadır. O halde programcı syslog mesajları için default durumda bu dosyaya başvurmalıdır. Log dosyalarını incelemek için pek çok utility bulunmaktadır. Örneğin lnav, glogg, ksystemlog gibi. systemd init paketi içerisindeki servis programları olan systemctl ve journalctl komutları ile de görüntüleme yapılabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de log mekanizmasının bir daemon programda kullanımına bir örnek verelim. Merkezi log mekanizması daha çok terminal ilişkisi olmayan daemon'lar ve aygıt sürücüler gibi programlar tarafından kullanılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* mydaemond.c */ #include #include #include #include #include #include #include #include #include #include void read_config(void); void check_instance(void); void sigterm_handler(int signo); void sighup_handler(int signo); void exit_daemon(const char *msg); #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" #define CONFIG_FILE "/etc/mydaemond.conf" void make_daemon(void) { pid_t pid; long maxfd; int fd0, fd1, fd2; umask(0); if ((pid = fork()) == -1) exit_daemon("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) exit_daemon("setsid"); if (chdir("/") == -1) exit_daemon("chdir"); errno = 0; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else exit_daemon("sysconf"); for (long i = 0; i < maxfd; ++i) close(i); if ((fd0 = open("/dev/null", O_RDONLY)) == -1) exit_daemon("open"); if ((fd1 = dup(fd0)) == -1) exit_daemon("dup"); if ((fd2 = dup(fd0)) == -1) exit_daemon("dup"); if (fd0 != 0 || fd1 != 1 || fd2 != 2) { syslog(LOG_ERR, "invalid file descriptors"); _exit(EXIT_FAILURE); } } int main(void) { struct sigaction sa; int fd; openlog("mydaemond", LOG_PID, LOG_USER); make_daemon(); check_instance(); read_config(); sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) exit_daemon("sigaction"); sa.sa_handler = sighup_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGHUP, &sa, NULL) == -1) exit_daemon("sigaction"); syslog(LOG_INFO, "ok, daemon is running"); for (;;) pause(); closelog(); return 0; } void read_config(void) { syslog(LOG_INFO, "mydaemon is reading configuration file..."); } void check_instance(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) exit_daemon("open"); if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { syslog(LOG_ERR, "Only one instance of this daemon can be run..."); _exit(EXIT_FAILURE); } exit_daemon("flock"); } } void sigterm_handler(int signo) { syslog(LOG_INFO, "mydaemond is terminating..."); closelog(); _exit(EXIT_SUCCESS); } void sighup_handler(int signo) { syslog(LOG_INFO, "mydaemond got SIGHUP and read config file..."); read_config(); /* read_config asenkron sinyal güvenli bir fonksiyon olmalı */ } void exit_daemon(const char *msg) { syslog(LOG_ERR, "%s: %s", msg, strerror(errno)); closelog(); _exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux dünyasında boot işleminden sonra sistemin işler hale getirilebilmesi için ve servis yönetimlerinin yapılması için kullanılan programların bulunduğu paketlere "init paketleri" denilmektedir. Örneğin init prosesinin kodları da bu paketin içerisindedir. Yukarıda da belirtildiği gibi pek çok daemon aslında sistem boot edilirken çalıştırılmakta ve sistem kapatılana kadar çalışır durumda kalmaktadır. Fakat bazı daemon'lar ise gerektiğinde çalıştırılıp, gerekmediğinde durdurulabilmektedir. İşte UNIX/Linux sistemlerinde bu çalıştırma, durdurma gibi faaliyetler için "init paketleri" içerisinde daha yüksek seviyeli araçlar bulundurulmaktadır. Tarihsel süreç içerisinde Linux sistemlerinde boot sonrası işlemlerden ve servis işlemlerinden sorumlu üç önemli init paketi geliştirilmiştir: SysVinit (klasik) upstart systemd Kursun yapıldığı zaman diliminde ağırlıklı biçimde init paketi olarak "systemd" paketi kullanılmaktadır. Upstart paketinin de sürdürümü artık yapılmamaktadır. Tabii eskiden kurulmuş Linux sistemleri ve bazı dağıtımlar hala bu paketi kullanıyor olabilir. Biz kursumuzda "systemd" paketi hakkında temel bilgiler vereceğiz. Sisteminizde hangi init paketinin kurulu olduğunu çeşitli biçimlerde anlayabilirsiniz. Örneğin: $ sudo ls -l /proc/1/exe Buradan aşağıdakine benzer bir çıktı elde edilmiştir: lrwxrwxrwx 1 root root 0 Kas 11 11:45 /proc/1/exe -> /usr/lib/systemd/systemd Hangi init paketinin kullanıldığının anlaşılması için diğer bir yöntem de şöyle olabilir: $ cat /proc/1/status Name: systemd Umask: 0000 State: S (sleeping) Tgid: 1 Ngid: 0 Pid: 1 PPid: 0 ... Sisteminizde hangi init paketinin kullanıldığını anlamanın diğer bir yolu da doğrudan ps komutunu "-p 1" seçeneği ile kullanmaktır: $ ps -p 1 (ya da "ps -p 1 -o comm") ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- systemd paketi boot sonrasında devreye girecek programlardan ve daemon'lardan ve bazı faydalı komutlardan oluşmaktadır. Bu komutlar genel olarak xxxctl biçiminde isimlendirilmiştir. Paketin en önemli komutu şüphesiz "systemctl" isimli komuttur. Servis yönetimleri temel olarak bu komut yoluyla yapılmaktadır. Yani "systemctl" komutu adeta systemd paketinin ön yüzü (frontend) gibidir. systemd paketi çalışmaya başladığında çeşitli konfigürasyon dosyalarına başvurmaktadır. Bu konfigürasyon dosyaları "/etc/systemd" dizini içerisinde bulunmaktadır. Örneğin: $ ls -l /etc/systemd toplam 56 -rw-r--r-- 1 root root 615 Nis 1 2020 coredump.conf -rw-r--r-- 1 root root 1042 Nis 22 2020 journald.conf -rw-r--r-- 1 root root 1042 Nis 22 2020 logind.conf drwxr-xr-x 2 root root 4096 Nis 22 2020 network -rw-r--r-- 1 root root 584 Nis 1 2020 networkd.conf -rw-r--r-- 1 root root 529 Nis 1 2020 pstore.conf -rw-r--r-- 1 root root 642 Mar 18 2021 resolved.conf -rw-r--r-- 1 root root 790 Nis 1 2020 sleep.conf drwxr-xr-x 20 root root 4096 Ara 26 2021 system -rw-r--r-- 1 root root 1759 Nis 22 2020 system.conf drwxr-xr-x 2 root root 4096 Tem 3 2021 system.conf.d -rw-r--r-- 1 root root 604 Nis 22 2020 timesyncd.conf drwxr-xr-x 4 root root 4096 Tem 3 2021 user -rw-r--r-- 1 root root 1185 Nis 22 2020 user.conf Buradaki .conf dosyalarının içi genel olarak "değişken=değer" biçiminde satırlardan oluşmaktadır. Kolaylık olsun diye özellikler # ile yorum satırı haline getirilip dosyada bulundurulmuştur. Sistem yönticisi ilgili satırdaki #'i kaldırarak o özelliği değiştirebilir. systemd paketi içerisinde pek çok çalıştırılabilir program da bulunmaktadır. Bu programların bazıları "daemon" biçiminde yazılmıştır. systemd paketinin çalıştırılabilir (executable) program dosyaları "/lib/systemd" dizinindedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda systemd paketinin aynı zamanda bir "servis yöneticiliği" yaptığını da belirtmiştik. İşte bizim bir servisimizin (daemon'umuzun) systemd yönetiminin kontrolüne girebilmesi için ismine "unit dosyası (unit file)" denilen bir dosyanın hazırlanması gerekmektedir. Çeşitli amaçlar için çeşitli unit dosyaları kullanılmaktadır. Bu unit dosyalarının türlerine göre uzantıları farklı olabilmektedir. Önemli unit dosya türleri şunlardır: - Service unit dosyası (.service) - Socket unit dosyası (.socket) - Slice unit dosyası (.slice) - Mount ve Automount unit dosyası (.mount, .automount) - Target unit dosyası (.target) - Timer unit dosyası (.timer) - Path unit dosyası (.path) - Swap unit dosyası (.swap) Programcının kendi daemon'ları için "service unit dosyası" oluşturması gerekmektedir. Unit dosyaları genel olarak "/lib/systemd/system" dizini içerisindedir. Aslında "systemd" paketinin unit dosyaları için hangi dizinlere bakacağı konfigürasyon dosyalarında ayarlanabilmektedir. Default bakılan dizinler sırasıyla şunlardır: /lib/systemd/system /etc/systemd/system /run/systemd/system /usr/lib/systemd/system /usr/local/lib/systemd/system Sistem yöneticileri genellikle kendi unit dosyalarını "/etc/systemd/system" dizini içerisine yerleştirmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Sistem boot edildiğinde belli çalışma modellerinde belli servislerin aktive edilmesi ya da edilmemesi gerekebilmektedir. Örneğin sistem grafik modda açılmayacaksa XWindow sistemine ilişkin daemon'ların aktif hale getirilmesi anlamsızdır. Benzer biçimde sistemde bir network kartı yoksa veya wireless özelliği yoksa bunlara yönelik daemon'ların (inetd gibi) aktif hale getirilmesi gereksizdir. Eskiden klasik SystemVinit paketlerinde "çalışma düzeyi (run level)" denilen boot seçenekleri bulunuyordu. Sistem yöneticisi de hangi servisin hangi çalışma düzeyinde aktive edeceğini belirtiyordu. Böylece sistem bir çalışma düzeyinde boot edildiğinde yalnızca o çalışma düzeyinde aktif edilmesi gereken servisler (daemon'lar) aktive ediliyordu. Ancak bu çalışma düzeyi sistemi kısıtlı bir seçenek oluşturmaktaydı. systemd paketinde bu "çalışma düzeyi" kullanımı kaldırılmıştır. Bunun yerine "target unit dosyası" yöntemi kullanılmaya başlanmıştır. Bu sistemlerde "çalışma düzeyi" yerine ismine "target unit" denilen ve bir boot seçeneğini belirten bir unit dosyası bulundurulur. Servisler de hangi target unit için aktive edileceklerini kendileri belirtirler. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Unit dosyalarının içerikleri konusunda çeşitli ayrıntılar vardır. Ancak biz burada bu ayrıntıların üzerinde durmayacağız. Tipik bir "service unit dosyası" minimalist biçimde şöyle oluşturulmaktadır: # mydaemon.service [Unit] Description=Mydaemon Unit [Service] Type=forking ExecStart=/usr/bin/mydaemond [Install] WantedBy=multi-user.target Burada "Description" bizim unit dosyamızı temsil eden bir yazıdır. Type daemon'ın nasıl çalıştırılacağını belirtir. Burada "forking" normal fork mekanizmasıyla çalıştırma anlamına gelmektedir. ExecStart daemon dosyasının nerede olduğunu belirtmektedir. Daemon dosyaları tipik olarak "/usr/bin" dizinine ya da "/usr/local/bin" dizinlerine yerleştirilmelidir. WantedBy hangi target ile sistem boot edildiğinde bu daemon'ın yükleneceğini belirtir. Buradaki "multi-user.target" isimli target unit klasik ve tipik boot işlemini belirtmektedir. Biz burada minimalist bir service unit dosyası oluşturduk. Servis unit dosyalarının içeriğine yönelik pek çok ayrıntı vardır. Bunun resmi dokümanları için "man systemd.service" sayfasına başvurabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 97. Ders 12/11/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki adımları sırasıyla bir daha özetlemek istiyoruz. Daemon'ımızı systemd kontrolüne verebilmek için sırasıyla şu işlemlerin yapılması gerekmektedir. 1) Bir service unit dosyası oluşturulmalıdır. Bu dosyanın uzantısı ".service" biçiminde olmalıdır. Bu dosyada daemon programının yol ifadesi ExecStart ismiyle bulunmaktadır. 2) Bu service unit dosyası "/etc/systemd/system" dizinine kopyalanmalıdır. 3) Daemon programı "/usr/bin" ya da "usr/local/bin" dizinine kopyalanmalıdır. (Tabii bu dizin servis unit dosyasındaki ExecStart ile aynı olmalıdır.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki işlemler yapıldıktan sonra artık servis yönetimi "systemctl" komutuyla yapılabilir. Bu komutun çeşitli parametreleri vardır. Burada biz bazı parametreleri üzerinde açıklamalar yapacağız. - Daemon'ımızın boot zamanında devreye sokulması için "systemctl enable" komutunun kullanılması gerekmektedir. Komutun genel biçimi şöyledir: sudo systemctl enable Buradaki service unit dosyasının ismidir. Uzantı belirtilmeyebilir. Örneğin: $ sudo systemctl enable mydaemon Eğer daemon'ın o anda yüklenmesi isteniyorsa ayrıca komuta --now seçeneği de eklenmelidir. Örneğin: $ sudo systemctl enable mydaemon --now - Daemon'ımızın boot sırasında devreye sokulmasının kaldırılması için "systemctl disable" komutu kullanılmaktadır. Komutun genel biçimi şöyledir: $ sudo systemctl disable Örneğin: $ sudo systemctl disable mydaemon - Daemon'ı çalıştırmak için "systemctl start" komutu kullanılmaktadır. Komutun genel biçimi şöyledir: $ sudo systemctl start - Daemon'ı durdurmak için "systemctl stop" komutu kullanılmalıdır. Komutun genel biçimi şöyledir: $ sudo systemctl stop Daemon durdurulurken ona default SIGTERM sinyali gönderilmektedir. systemd SIGTERM sinyalini gönderdikten sonra bir süre bekler, daemon hala sonlanmamışsa bu kez ona SIGKILL sinyalini gönderir. - Daemon'ımızın durumunu anlamak için "systemctl status" komutu kullanılır. Komutun genel biçimi şöyledir: systemctl status [daemon_ismi] Örneğin: $ systemctl status mydaemon - Bazen daemon'ımızı "restart" etmek isteyebiliriz. restart önce durdurup sonra çalıştırmak anlamına gelmektedir. Komutun genel biçimi şöyledir: sudo systemctl restart Örneğin: $ sudo systemctl restart mydaemon - Daemon'ımızın boot zamanında devreye girip girmeyeceğini "systemctl status" komutunun yanı sıra "systemctl is-enabled" komutuyla da anlayabiliriz. Örneğin: $ systemctl is-enabled mydaemon - systemd tüm unit dosyalarını inceleyerek bir çalıştırma ağacı oluşturmaktadır. Biz bir unit dosyasını değiştirdiğimizde bu ağacın yeniden oluşturulması gerekmektedir. Bunun için "systemctl daemon-reload" komutu kullanılmaktadır. Komutun genel biçimi şöyledir: $ sudo systemctl daemon-reload Komutun parametresiz olduğuna dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz proseslerarası haberleşmeyi iki grubu ayırmıştık: 1) Aynı makinenin prosesleri arasında haberleşme 2) Farklı makinelerin prosesleri arasında haberleşme Kursumuzda aynı makinenin prosesleri arasındaki haberleşmeleri (boru haberleşmeleri, mesaj kuyrukları) görmüştük. Şimdi farklı makinelerin prosesleri arasındaki haberleşmeler üzerinde duracağı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) - 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 birimlere gö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ğlantının nasıl yapılacağına ilişkin belirlemeler - Ağ katmanından gelen paketlerin stream tabanlı organizasyonuna ilişkin belirlemeler - Veri aktarımını güvenli hale getirmek için akış kontrolüne ilişkin belirlemeler - Gönderilen bilgilerin hedefte ayrıştırı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 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 transport katmanı durumundadı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 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 protokolde 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 "well known ports" denilmektedir. Bu nedenle programcıların port numaralarını 1024'ten büyük olacak biçimde almaları 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 kullanarak 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 paket biçiminde yola çıkarılmaktadır. Ancak ana network protokolü IP'dir. O halde bu paketin aslında bir IP paketi olarak gönderilmesi gerekir. Bir IP paketi iki kısımdan oluşmaktadır: IP Header ve IP data +-------------------------+ | IP Header | +-------------------------+ | IP Data | +-------------------------+ 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 | | +-------------------------+ <---+ 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) | +----------------------------------------------------------------------------------------------+ ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 98. Ders 18/11/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- IP haberleşmesi (yani paketlerin, oluşturulması, gönderilmesi alınması vs.) işletim sistemlerinin çekirdekleri tarafından yapılmaktadır. Tabii User mod 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. Microsoft'un Windows sistemleri de bu API kütüphanesini desteklemektedir. Bu kütüphaneye "Winsock" ya da kısaca "WSA (Windows Socket API)" denilmektedir. Microsoft'un Winsock kütüphanesi hem klasik BSD soket API fonksiyonlarını hem de başı WSAXXX ile başlayan Windows'a özgü API fonksiyonlarını barındırmaktadır. Yani UNIX/Linux sistemlerinde yazdığımız soket programlarını küçük değişikliklerle Windows sistemlerine de port edebiliriz. 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. Berkeley soket kütüphanesi POSIX tarafından desteklenmektedir. Yani burada göreceğimiz soket fonksiyonları aynı zamanda birer POSIX fonksiyonudur. Soket fonksiyonlarının prototiplerinin önemli bir bölümü dosyası içerisinde bulunmaktadır. Ancak bu başlık dosyasının dışında bazı fonksiyonlar için başka başlık dosyalarının da include edilmesi gerekmektedir. Ağ protokollerinde "endian'lık" da önemli olmaktadır. IP ailesi "Big Endian" formata göre tasarlanmıştır. Buna protokolde "network byte ordering" denilmektedir. Dolayısıyla bizim soket API'lerine verdiğimiz birtakım değerlerin big endian formata dönüştürülmesi gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz kursumuzda önce soket kütüphanesi ile TCP/IP client-server programların oluşturulması konusunu ele alacağız. Sonra TCP/IP haberleşmesinin bazı protokol detaylarından bahsedeceğiz. Sonra da UDP/IP haberleşme üzerinde duracağız. Berkeley soket kütüphanesinin bazı fonksiyonları hem TCP/IP hem de UDP/IP haberleşmede ortak olarak kullanılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir TCP/IP uygulamasında server ve client olmak üzere iki ayrı program yazılır: "TCP Server Program" ve "TCP Client Program". Biz önce TCP server programın daha sonra da TCP client programın yazımı üzerinde duracağız. Tabii TCP server programın üzerinde dururken zaten bazı ortak soket fonksiyonlarını da göreceğiz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir TCP server program tipik olarak aşağıdaki soket API'lerinin sırayla çağrılmasıyla gerçekleştirilmektedir: socket ---> bind ---> listen ---> accept ---> send/recv ya da read/write ---> shutdown ---> close 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 bir dosya betimleyicisi verir. Biz diğer fonksiyonlarda soket biçiminde isimlendirilen bu betimleyiciyi kullanırız. socket fonksiyonunun prototipi şöyledir: #include int socket(int domain, int type, int protocol); Fonksiyonun birinci parametresi kullanılacak protokol ailesini belirtir. Bu parametre AF_XXX (Address Family) biçimindeki sembolik sabitlerden biri olarak girilir. IPv4 için bu parametreye AF_INET, IPv6 için AF_INET6 girilmelidir. 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ü stream tabanlı olduğu için TCP uygulamalarında bu parametre SOCK_STREAM olarak girilmelidir. Ancak UDP datagram tabanlı olduğu için UDP uygulamalarında bu parametre SOCK_DGRAM biçiminde girilmelidir. Fonksiyonun üçüncü parametresi transport katmanındaki protokolü belirtmektedir. Ancak zaten ikinci parametreden transport protokolü anlaşılıyorsa üçüncü parametre 0 olarak geçilebilmektedir. Örneğin IP 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 içerisindedir.) socket fonksiyonu başarı durumunda soket betimleyicisine, başarısızsa -1 değerine geri döner ve errno uygun biçimde set edilir. socket nesnesinin bir dosya gibi kullanıldığına dikkat ediniz. socket fonksiyonu bize open fonksiyonunda olduğu gibi dosya betimleyici tablosunda indeks belirten en düşük numaralı dosya betimleyicisini vermektedir. Örneğin: int server_sock; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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 prototipi şöyledir: #include int bind(int socket, const struct sockaddr *addr, socklen_t addrlen); Fonksiyonun birinci parametresi yaratılmış olan soket betimleyicisini alır. İkinci parametre her ne kadar sockaddr isimli yapı türündense de aslında her protokol için ayrı bir yapı adresini almaktadır. Yani sockaddr yapısı 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ı dosyası içerisinde aşağıdaki gibi bildirilmiştir: #include struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; }; 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ı in_port_t türündendir ve bu tür uint16_t olarak typedef edilmiştir. Bu eleman server'ın dinleyeceği port numarasını belirtir. Yapının sin_addr elemanı IP numarası belirten bir elemandır. Bu eleman in_addr isimli bir yapı türündendir. in_addr yapısı dosyası içerisinde şöyle bildirilmiştir: #include struct in_addr { in_addr_t s_addr; }; in_addr_t 4 byte'lık işaretsiz tamsayı türünü (uint32_t) belirtmektedir. Böylece s_addr 4 byte'lık IP adresini temsil eder. Eğer biz tüm network kartlarından gelen bağlantı isteklerini kabul etmek istiyorsak IP adresi olarak INADDR_ANY özel değerini geçmeliyiz. 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 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); Yukarıda da belirttiğimiz üzere IP adresi olarak INADDR_ANY özel değeri "tüm network kartlarından gelen bağlantı isteklerini kabul et" anlamına gelmektedir. Bu durumda sockaddr_in yapısı 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 -1 değerine geri dönmektedir. Örneğin: if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- server program bind işleminden sonra soketi aktif dinleme konumuna sokmak için listen fonksiyonunu çağırmalıdır. Fonksiyonun prototipi şöyledir: #include int listen(int socket, int backlog); Fonksiyonun birinci parametresi soket betimleyicini, 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 -1 değerine geri dönmektedir. Örneğin: 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.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bağlantıyı sağlayan asıl fonksiyon accept fonksiyonudur. accept fonksiyonu bağlantı kuyruğuna bakar. Eğer orada bir bağlantı isteği varsa onu alır ve hemen geri döner. Eğer orada bir bağlantı isteği yoksa default durumda blokede bekler. Fonksiyonun prototipi şöyledir: #include int accept(int socket, struct sockaddr *address, socklen_t *address_len); Fonksiyonun birinci parametresi 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 o host'taki 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ı 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 başarısızlık durumunda -1 değeri ile geri dönmektedir. Örneğin: 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. 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" denilen bir format ile yazı biçiminde görüntülenmektedir. Aşağıda accept işlemine kadar olan bir örnek server programı verilmiştir. Tabii programda sonraki paragraflarda göreceğimiz bazı eksiklikler vardır. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #define SERVER_PORT 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(SERVER_PORT); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 99. Ders 19/11/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- IP adresleri sockaddr_in yapısının içerisindeki in_addr yapısında belirtilmiştir. IPv4'te IP adreslerinin 4 byte uzunlukta olduğunu söylemiştik. sockaddr_in yapısını ve in_addr yapısını yeniden veriyoruz: #include struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; }; struct in_addr { in_addr_t s_addr; }; Buradaki in_addr_t türü uint32_t biçiminde typedef edilmiştir. Yani 4 byte'lık bir tamsayı türüdür. Elimizde sockaddr_in türünden bir nesne olsun: struct sockaddr_in sin_client; Biz bu nesne yoluyla 4 byte'lık IP adresini "sin_client.sin_addr.s_addr" ifadesi ile elde ederiz. IPv4 adresleri genellikle kullanıcılar tarafından "noktalı desimal format (dotted decimal format)" denilen bir formatla gösterilmektedir. Bu formatta IP adresinin her byte'ı arasına "." karakteri getirilir ve IP adresi bir yazı olarak gösterilir. Örneğin: "127.0.0.1" "192.168.1.1" "176.234.135.196" İşte 4 byte'lık işaretsiz bir tamsayı biçimindeki IP adresini noktalı desimal formata dönüştürmek için inet_ntoa isimli bir POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include char *inet_ntoa(struct in_addr in); Fonksiyon in_addr yapısını almaktadır (yapının tek bir elemanı olduğu için adresini almamaktadır). Fonksiyon başarı durumunda noktalı desimal formattaki IP adres yazısına geri dönmektedir. Fonksiyon başarısız olamamaktadır. Fonksiyonun geri döndürdüğü adres statik bir alanın adresidir. Dolayısıyla verilen adres thread güvenli değildir. inet_ntoa fonksiyonunun yaptığının tersini yapan inet_addr isimli bir POSIX fonksiyonu da vardır. Fonksiyonun prototipi şöyledir: #include in_addr_t inet_addr(const char *cp); Fonksiyon noktalı desimal formattaki yazıyı alarak onu 4 byte'lık IP adresine dönüştürmektedir. Başarı durumunda bu IP adresine, başarısızlık durumunda (in_addr_t)-1 değerine geri dönmektedir. (in_addr_t türü işaretsiz ise geri döndürülen değer -1 değil, en büyük pozitif sayı olmaktadır. Ancak işaretsiz tamsayı türünü -1 ile karşılaştırırsak zaten -1 değeri de o türe dönüştürüleceği için sorun ortaya çıkmayacaktır.) Başarısızlık durumunda herhangi bir errno değeri set edilmemektedir. inet_ntoa fonksiyonundaki ve bunun tersini yapan inet_addr fonksiyonundaki IP adresleri "big endian" formata göre yani "network byte ordering" formatına göre verilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bağlantı işleminden sonra artık bilgi gönderip alma işlemi yapılabilir. Sonra da aktif soket düzgün bir biçimde kapatılmalıdır. Ancak biz önce client programda da belli bir noktaya gelip bu ortak kısımları ondan sonra ele alacağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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: socket ---> bind (isteğe bağlı) ---> gethostbyname (isteğe bağlı) ---> connect ---> send/recv ya da read/write ---> shutdown ---> close Client taraf önce yine socket fonksiyonuyla bir soket yaratır. Örneğin: int client_sock; ... if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); 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 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 önce gethostbyname ve gethostbyaddr fonksiyonlarını göreceğiz. (Çünkü ana noktalar üzerinde durmak için vakit kaybetmemek istemiyoruz.) Belli süre sonra da getnameinfo ve getaddrinfo fonksiyonlarını açıklayacağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- gethostbyname fonksiyonunun prototipi şöyledir: #include struct hostent *gethostbyname(const char *name); Fonksiyon bizden host ismini alır ve DNS işlemi yaparak bize statik düzeyde tahsis edilmiş olan bir hostent yapı nesnesinin adresini verir. Fonksiyon errno değişkeni yerine h_errno isimli bir değişkeni set etmektedir. Bu değişkenin değerini yazıya dönüştürmek için strerror fonksiyonu değil, prototipi içerisinde olan hstrerror fonksiyonu kullanılmaktadır. hostent yapısı aşağıdaki gibi bildirilmiştir: struct hostent { char *h_name; /* official name of host */ char **h_aliases; /* alias list */ int h_addrtype; /* host address type */ int h_length; /* length of address */ char **h_addr_list; /* list of addresses */ }; Yapının h_name elemanı host'un asıl ismini vermektedir. Her host'un alternatif isimleri de olabilmektedir. Yapının h_aliases elemanı ise host'un diğer isimlerini belirtmektedir. Bu gösterici dizisinin her elemanı host'un bir ismini belirtir. Dizinin sonunda NULL adres verdır. Örneğin: h_aliases ----> adres -----> isim adres -----> isim adres -----> isim ... NULL Yapının h_addrtype elemanı adresin ilişkin olduğu protokol ailesini belirtmektedir. h_length elemanı söz konusu adresin byte uzunluğunu belirtir. Bu genel bir fonksiyon olduğundan ve adresler de değişik uzunluklarda ve türlerde olabileceğinden adresler char türden bir dizi içerisine byte byte kodlanmıştır. IPv4'te bu diziler 4 eleman uzunluğundadır. Buradaki adresler "big endian" formatta yani "network byte ordering" biçimindedir. Yine h_addr_list göstericinin gösterdiği dizinin son elemanı NULL adres içermektedir. Örneğin: h_addr_list ----> adres -----> byte byte byte byte adres -----> byte byte byte byte adres -----> byte byte byte byte ... NULL Biz h_addr_list elemanında belirtilen adreslerden ilkini (0'ıncı indekstekini alabiliriz) Tipik olarak client program önce server host isminin noktalı desimal formatta olup olmadığına bakar. Eğer bu host ismi noktalı desimal formatta ise onu inet_addr fonksiyonu ile IP numarasına dönüştürür. Eğer host ismi noktalı desimal formatta değilse, gethostbyname fonksiyonunu uygulayarak bu kez oradan IP adresini elde eder. Örneğin: #define SERVER_NAME "some_host_name or dotted decimal ip name" int client_sock; struct sockaddr_in sin_server; struct hostent *hent; ... sin_server.sin_family = AF_INET; sin_server.sin_port = htons(SERVER_PORT); if ((sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME)) == -1) { if ((hent = gethostbyname(SERVER_NAME)) == NULL) { fprintf(stderr, "gethostbyname: %s\n", hstrerror(h_errno)); exit(EXIT_FAILURE); } memcpy(&sin_server.sin_addr.s_addr, hent->h_addr_list[0], hent->h_length); } gethostbyname fonksiyonu isme karşı IP numaralarının elde edilmesi için kullanılmaktadır. Bunun ters olan gethostbyaddr isimli yine "deprecated" yapılmış bir fonksiyon daha vardır: #include struct hostent *gethostbyaddr(const void *addr, socklen_t len, int af); Fonksiyonun birinci parametresi "big endian" biçiminde byte dizilimine sahip adresi belirtmektedir. İkinci parametre bu adresin uzunluğunu, üçüncü parametre ise protokol ailesini belirtir. Yine fonksiyon başarı durumunda hostent nesnesinin adresine, başarısızlık durumunda NULL adrese geri dönmektedir. Bu fonksiyon da errno yerine h_errno değişkeninin set etmektedir. Yine bu h_errno değerine yazıya dönüştürmek için hstrerror fonksiyonu kullanılmaktadır. O anda çalışılan makinenin IPv4 adresi "127.0.0.1" ile temsil edilmektedir. Bu adrese "loopback address" de denilmektedir. Bazı işletim sistemlerinde (Windows, Linux ve macOS) "localhost" ismi de o anda çalışılan makinenin host ismi olarak kullanılabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Artık client program connect fonksiyonuyla TCP bağlantısını sağlayabilir. connect fonksiyonunun prototipi şö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 örnekte bağlantı için gereken minimum client program verilmiştir. Burada henüz görmediğimiz işlemleri hiç uygulamadık. ---------------------------------------------------------------------------------------------------------------------------*/ /* client.c */ #include #include #include #include #include #include #include #define SERVER_NAME "127.0.0.1" #define SERVER_PORT 55555 void exit_sys(const char *msg); int main(void) { int client_sock; struct sockaddr_in sin_server; struct hostent *hent; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); /* { struct sockaddr_in sin_client; sin_client.sin_family = AF_INET; sin_client.sin_port = htons(50000); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } */ sin_server.sin_family = AF_INET; sin_server.sin_port = htons(SERVER_PORT); if ((sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME)) == -1) { if ((hent = gethostbyname(SERVER_NAME)) == NULL) { fprintf(stderr, "gethostbyname: %s\n", hstrerror(h_errno)); exit(EXIT_FAILURE); } memcpy(&sin_server.sin_addr.s_addr, hent->h_addr_list[0], hent->h_length); } if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("connect"); printf("connected...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Tıpkı borularda olduğu gibi soketlerlerde de "blokeli" ve "blokesiz" çalışma söz konusudur. Soketlerde default çalışma biçimi "blokeli" moddur. Blokeli modda gönderme ve alma işlemlerinde aşağıda açıklayacağımız gibi belli koşullarda bloke oluşmaktadır. Blokesiz modda ise hiçbir zaman bloke oluşmaz. Biz de default mod olan blokeli modu ele alacağız. Genellikle kullanılan mod da zaten blokeli moddur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bağlantı sağlandıktan sonra artık recv ya da read ve write fonksiyonlarıyla gönderme alma işlemleri yapılabilir. recv fonksiyonunun prototipi şöyledir: #include ssize_t recv(int socket, void *buffer, size_t length, int flags); Fonksiyonun 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 -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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 100. Ders 25/11/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Soketten bilgi göndermek için send ya da write fonksiyonu kullanılmaktadır. send fonksiyonunun da parametik yapısı şöyledir: #include ssize_t send(int socket, const void *buffer, size_t length, int flags); Fonksiyonun 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 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, yazılmak istenen değerle aynı olur. send fonksiyonu başarısızlık durumunda -1 değeri ile geri döner ve errno uygun biçimde set edilir. Tıpkı borularda olduğu gibi 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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Haberleşmenin sonunda TCP soketi nasıl kapatılmalıdır? Mademki soketler UNIX/Linux sistemlerinde birer dosya betimleyicisi gibidir o halde soketi kapatma işlemi "close" ile yapılabilir. Tabii tıpkı dosyalarda olduğu gibi soketlerde de close işlemi yapılmazsa işletim sistemi proses normal ya da sinyal gibi nedenlerle sonlandığında otomatik olarak betimleyicileri kapatır yani close işlemini kendisi yapar. Soket betimleyicileri de "dup" işlemine sokulabilir. Bu durumda close işlemi uygulandığında soket nesnesi yok edilmez. Çünkü o nesneyi gören başka bir betimleyici daha vardır. Benzer biçimde fork işlemi sırasında da betimleyicilerin çiftlendiğine dikkat ediniz. Aktif soketlerin doğrudan close ile kapatılması iyi bir teknik değildir. Bu soketler önce "shutdown" ile haberleşmeden kesilmeli sonra close ile kapatılmalıdır. Bu biçimde soketlerin kapatılmasına İngilizce "graceful close (zarif kapatma)" denilmektedir. Pekiyi shutdown fonksiyonu ne yapmaktadır ve neden gerekmektedir? close işlemi ile bir soket kapatıldığında işletim sistemi sokete ilişkin tüm veri yapılarını ve bağlantı bilgilerini siler. Örneğin biz karşı tarafa send ile bir şey gönderdikten hemen sonraki satırda close yaparsak artık send ile gönderdiklerimizin karşı tarafa ulaşacağının hiçbir garantisi yoktur. Çünkü anımsanacağı gibi send aslında "gönderme tamponuna" bilgiyi yazıp geri dönmektedir. Hemen arkasından close işlemi uygulandığında artık bu sokete ilişkin gönderme ve alma tamponları da yok edileceğinden tamponda gönderilmeyi bekleyen bilgiler hiç gönderilmeyebilecektir. close işlemini bilgisayarımızı "power düğmesine basarak kapatmaya" benzetebiliriz. Bu durumda o anda çalışan tüm programlar ve işletim sistemi aniden yok edilmektedir. shutdown işlemini de "işletim sistemindeki shutdown" mekanizmasına benzetebiliriz. İşletim sistemini shutdown ettiğimizde tüm prosesler uygun biçimde sonlandırılıp sistem stabil olarak kapatılmaktadır. Tabii soketlerde doğrudan close işlemi çoğu kez önemli bir probleme yol açmayabilir. Ancak doğru teknik aktif soketlere önce shutdown uygulayıp sonra close etmektedir. shutdown fonksiyonunun üç işlevi vardır: 1) Haberleşmeyi TCP çerçevesinde el sıkışarak sonlandırmak (bu konu ileride ele alınacaktır). 2) Gönderme tamponuna yazılan bilgilerin gönderildiğine emin olmak. 3) Okuma ya da yazma işlemini sonlandırıp diğer işleme devam edebilmek (half close işlemi). shutdown fonksiyonunun prototipi şöyledir: #include int shutdown(int socket, int how); Fonksiyonun birinci parametresi sonlandırılacak soketin betimleyicisini, ikinci parametresi biçimini belirtmektedir. İkinci parametre şunlardan biri olarak girilebilir: SHUT_RD: Bu işlemden sonra artık soketten okuma yapılamaz. Fakat sokete yazma yapılabilir. Bu seçenek pek kullanılmamaktadır. SHUR_WR: Burada artık shutdown daha önce gönderme tamponuna yazılmış olan byte'ların gönderilmesine kadar bloke oluşturabilir. Bu işlemden sonra artık sokete yazma yapılamaz ancak okuma işlemi devam ettirilebilir. SHUT_RDWR: En çok kullanılan seçenektir. Burada da artık shutdown daha önce gönderme tamponuna yazılmış olan byte'ların gönderilmesine kadar bloke oluşturabilir. Artık bundan sonra soketten okuma ya da yazma yapılamamaktadır. shutdown başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. O halde aktif bir soketin kapatılması tipik olarak şöyle yapılmaktadır: shutdown(sock, SHUT_RDWR); close(sock); Karşı taraf soketi (peer socket) shutdown ile SHUT_WR ya da SHUT_RDWR ile sonlandırmışsa artık biz o soketten okuma yaptığımızda recv ya da read fonksiyonları 0 ile geri döner. Benzer biçimde karşı taraf doğrudan soketi close ile kapatmışsa yine recv ya da read fonksiyonları 0 ile geri döner. Karşı tarafın soketi kapatıp kapatmadığı tipik olarak recv fonksiyonunda anlaşılabilmektedir. Yukarıda da belirttiğimiz gibi karşı taraf soketi kapattıktan sonra biz sokete write ya da send ile bir şeyler yazmak istersek default durumda UNIX/Linux sistemlerinde SIGPIPE sinyali oluşmaktadır. Programcı send fonksiyonunun flags parametresine MSG_NOSIGNAL değerini girerse bu durumda send başarısız olmakta ve errno değişkeni EPIPE değeri ile set edilmektedir. Karşı taraf soketi kapatmamış ancak bağlantı kopmuş olabilir. Bu durumda send/write ve recv/read fonksiyonları başarısız olur ve -1 değeriyle ile geri döner. O halde recv ya da read işlemi yapılırken fonksiyonların geri dönüş değerleri -1 ve 0 ile kontrol edilmelidir. Örneğin: if ((result = recv(...)) == -1) exit_sys("recv"); if (result == 0) { // karşı taraf soketi kapatmış, gerekli işlemleri yap } Benzer biçimde send ya da write fonksiyonlarıyla yazma yapılırken fonksiyonların geri dönüş değerleri -1 ile kontrol edilmelidir. Örneğin: if (send(...) == -1) exit_sys("send"); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte client program, server programa bağlanarak stdin dosyasından okuduğu yazıları send fonksiyonu ile server programa göndermektedir. Server program da bu yazıları alarak stdout dosyasına basmaktadır. Haberleşme normal olarak client tarafın "quit" yazısını girmesiyle sonlandırılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #define SERVER_PORT 55555 #define BUFFER_SIZE 4096 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; char buf[BUFFER_SIZE + 1]; ssize_t result; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(SERVER_PORT); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port)); for (;;) { if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received: \"%s\"\n", (intmax_t)result, buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); close(server_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define SERVER_NAME "127.0.0.1" #define SERVER_PORT 55555 #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int client_sock; struct sockaddr_in sin_server; struct hostent *hent; char buf[BUFFER_SIZE]; char *str; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); /* { struct sockaddr_in sin_client; sin_client.sin_family = AF_INET; sin_client.sin_port = htons(50000); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } */ sin_server.sin_family = AF_INET; sin_server.sin_port = htons(SERVER_PORT); if ((sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME)) == -1) { if ((hent = gethostbyname(SERVER_NAME)) == NULL) { fprintf(stderr, "gethostbyname: %s\n", hstrerror(h_errno)); exit(EXIT_FAILURE); } memcpy(&sin_server.sin_addr.s_addr, hent->h_addr_list[0], hent->h_length); } if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("connect"); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bu noktada UNIX/Linux sistemlerinde yazılan soket programlarının Windows sistemlerine nasıl port edileceği hakkında bazı açıklamalarda bulunmak istiyoruz. Windows sistemlerindeki soket kütüphanesine "Winsock" denilmektedir. Şu anda bu kütüphanenin 2'inci versiyonu kullanılmaktadır. Winsock API fonksiyonları "UNIX/Linux uyumlu" fonksiyonlar ve Windows'a özgü fonksiyonlar olmak üzere iki biçimde kullanılabilmektedir. Ancak Winsock'un UNIX/Linux uyumlu fonksiyonlarında da birtakım değişiklikler söz konusudur. Bir UNIX/Linux ortamında yazılmış soket uygulamasının Windows sistemlerine aktarılması için şu düzeltmelerin yapılması gerekir: 1) POSIX'in soket sistemine ilişkin tüm başlık dosyaları kaldırılır. Onun yerine dosyası include edilir. 2) xxx_t'li typedef türleri silinir ve onların yerine (dokümanlara da bakabilirsiniz) int, short, unsigned int, unsigned short türleri kullanılır. (Örneğin ssize_t türü ve socklen_t türleri yerine int türleri kullanılmalıdır.) 3) Windows'ta soket sisteminin başlatılması için WSAStartup fonksiyonu işin başında çağrılır ve işin sonunda da bu işlem WSACleanup fonksiyonuyla geri alınır. Bu fonksiyonları şöyle kullanbilirsiniz: WSADATA wsadata; ... if ((result = WSAStartup(MAKEWORD(2, 2), &wsadata)) != 0) exit_sys("WSAStartup", EXIT_FAILURE, result); ... WSACleanup(); 4) Windows'ta dosya betimleyicisi kavramı yoktur. (Onun yerine "handle" kavramı vardır.) Dolayısıyla soket türü de int değil, SOCKET isimli bir typedef türüdür. 5) shutdown fonksiyonunun ikinci parametresi SD_RECEIVE, SD_SEND ve SD_BOTH biçimindedir. 6) close fonksiyonu yerine closesocket fonksiyonu ile soket kapatılır. 7) Windows'ta soket fonksiyonları başarısızlık durumunda -1 değerine geri dönmezler. socket fonksiyonu başarısızlık durumunda INVALID_SOCKET değerine, diğerleri ise SOCKET_ERROR değerine geri dönmektedir. 8) Visual Studio IDE'sinde default durumda "deprecated" durumlar "error"e yükseltilmiştir. Bunlar için bir makro define edilebilmektedir. Ancak proje ayarlarından "sdl check" disable da edilebilir. Benzer biçimde proje ayarlarından "Unicode" değeri "not set" yapılmalıdır. 9) Projenin linker ayarlarından Input/Additional Dependencies edit alanına Winsock kütüphanesi olan "Ws2_32.lib" import kütüphanesi eklenir. 10) Windows'ta son soket API fonksiyonlarının başarısızlık nedenleri WSAGetLastError fonksiyonuyla elde edilmektedir. Yani Windows sistemlerinde errno değişkeni set edilmemektedir. Belli bir hata kodunun yazıya dönüştürülmesi de biraz ayrıntılıdır. Bunun için aşağıdaki fonksiyonu kullanabilirsiniz: void ExitSys(LPCSTR lpszMsg, DWORD dwLastError) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Yukarıdaki programların Winsock'a dönüştürülmüş biçimleri aşağıda verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #define SERVER_PORT 55555 #define BUFFER_SIZE 4096 void ExitSys(LPCSTR lpszMsg, DWORD dwLastError); int main(void) { WSADATA wsadata; SOCKET server_sock, client_sock; struct sockaddr_in sin_server, sin_client; int sin_len; char buf[BUFFER_SIZE + 1]; int result; if ((result = WSAStartup(MAKEWORD(2, 2), &wsadata)) != 0) ExitSys("WSAStartup", result); if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) ExitSys("socket", WSAGetLastError()); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(SERVER_PORT); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); if (listen(server_sock, 8) == SOCKET_ERROR) ExitSys("listen", WSAGetLastError()); printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == SOCKET_ERROR) ExitSys("accept", WSAGetLastError()); printf("connected client ===> %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port)); for (;;) { if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == SOCKET_ERROR) ExitSys("recv", WSAGetLastError()); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received: \"%s\"\n", (intmax_t)result, buf); } shutdown(client_sock, SD_BOTH); closesocket(client_sock); closesocket(server_sock); WSACleanup(); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastError) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #define SERVER_NAME "192.168.153.131" #define SERVER_PORT 55555 #define BUFFER_SIZE 4096 void ExitSys(LPCSTR lpszMsg, DWORD dwLastError); int main(void) { WSADATA wsadata; SOCKET client_sock; struct sockaddr_in sin_server; struct hostent *hent; char buf[BUFFER_SIZE]; char *str; int result; if ((result = WSAStartup(MAKEWORD(2, 2), &wsadata)) != 0) ExitSys("WSAStartup", result); if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) ExitSys("socket", WSAGetLastError()); /* { struct sockaddr_in sin_client; sin_client.sin_family = AF_INET; sin_client.sin_port = htons(50000); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); } */ sin_server.sin_family = AF_INET; sin_server.sin_port = htons(SERVER_PORT); if ((sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME)) == SOCKET_ERROR) { if ((hent = gethostbyname(SERVER_NAME)) == NULL) ExitSys("gethostbyname", WSAGetLastError()); memcpy(&sin_server.sin_addr.s_addr, hent->h_addr_list[0], hent->h_length); } if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == SOCKET_ERROR) ExitSys("connect", WSAGetLastError()); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, (int)strlen(buf), 0) == SOCKET_ERROR) ExitSys("send", WSAGetLastError()); if (!strcmp(buf, "quit")) break; } shutdown(client_sock, SD_BOTH); closesocket(client_sock); WSACleanup(); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastError) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 101. Ders 26/11/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kurstaki denemeleri genellikle sanal makine üzerinden yapmaktayız. Sanal makine programları zaman içerisinde oldukça gelişmiştir. Bunlardan "docker" sistemleri de evrimleştirilmiştir. Sanal makine için bedava iki önemli alternatif "VMware Player" ve "VirtualBox" programlarıdır. Biz kursumuzda "VMware Player" kullanıyoruz. Ancak "VirtualBox" da problemsiz bir biçimde aynı kalitede işlev görmektedir. VMware Player donanım sanallaştırmasını tam olarak yapmaktadır. Yani oluşturulan sanal makine tamamen sanki aynı yerel ağa bağlı olan bağımsız bir makine gibi davranmaktadır. Bu sanal makineye biz istediğimiz kadar network kartı takabiliriz. TCP/IP denemelerinde client ve server programlar "host" ve "guest" sistemlerde konuşlandırılabilir. Bu durumda kullanılacak IP adreslerine dikkat etmek gerekir. VMWare'deki guest IP adresini öğrenebilmek için önce "Virtual Machine Settings/Network Adapter/Advanced" düğmelerinden sanal ethernet kartının MAC adresini görmelisiniz. Sonra host sistemde bu MAC adresine karşı gelen IP adresini bulmaya çalışabilirsiniz. Bunun için "arp -a" komutu kullanılabilir. Bu komutta aşağıdaki gibi bir çıktı göreceksiniz: Interface: 192.168.153.1 --- 0x10 Internet Address Physical Address Type 192.168.153.128 00-0c-29-76-3b-e8 dynamic 192.168.153.131 00-0c-29-76-3b-fc dynamic 192.168.153.255 ff-ff-ff-ff-ff-ff static 224.0.0.22 01-00-5e-00-00-16 static 224.0.0.251 01-00-5e-00-00-fb static 224.0.0.252 01-00-5e-00-00-fc static 239.255.255.250 01-00-5e-7f-ff-fa static 255.255.255.255 ff-ff-ff-ff-ff-ff static Burada MAC adresine karşı gelen IP adresini elde edebilirsiniz. Benzer biçimde guest sistemden host sisteme erişebilmek için host sistemin guest sistemde kullanılacak IP adresini elde etmeniz gerekir. Bu IP adresi host sistemin LAN üzerindeki yerel IP adresi değildir. Bu IP adresi "arp -a" komutuyla ya da Windows'taki "ipconfig" komutuyla elde edilebilir. Yukarıdaki çıktıdaki ilk satır zaten bunu belirtmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Client ve server programları yazarken onların ilgili olduğu server ve port numaralarını komut satırı argümanlarıyla almak iyi bir tekniktir. Örneğin biz client programı şöyle çalıştırabilmeliyiz: ./client -s -p -b Tabii buradaki -s ve -p seçeneklerinin default değerleri de söz konusu olacaktır. Benzer biçimde server program da aşağıdaki gibi çalıştırılabilir olmalıdır: ./server -p Buradaki port numarası server'ın dinlediği port numarasıdır. Bunun da bir default değeri olabilir. Aşağıda daha önce yazmış olduğumuz client ve server programların bu biçime getirilmiş hallerini veriyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; char buf[BUFFER_SIZE + 1]; ssize_t result; int option; int server_port; int p_flag, err_flag; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port)); for (;;) { if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received: \"%s\"\n", (intmax_t)result, buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); close(server_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_server, sin_client; struct hostent *hent; char buf[BUFFER_SIZE]; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int server_port, bind_port; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = atoi(optarg); break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); if ((sin_server.sin_addr.s_addr = inet_addr(server_name)) == -1) { if ((hent = gethostbyname(server_name)) == NULL) { fprintf(stderr, "gethostbyname: %s\n", hstrerror(h_errno)); exit(EXIT_FAILURE); } memcpy(&sin_server.sin_addr.s_addr, hent->h_addr_list[0], hent->h_length); } if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("connect"); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- TCP protokolünün gerçekleştirimlerinde tipik olarak işletim sistemleri her soket için "gönderme tamponu (sending buffer)" ve "alma tamponu (receiving buffer)" kullanmaktadır. TCP'de bir akış kontrolünün de uygulandığını anımsayınız. Bu akış kontrolünün nasıl uygulandığına izleyen paragraflarda değineceğiz. Bizim send ya da write fonksiyonlarıyla karşı tarafa göndermek istediğimiz bilgiler önce gönderme tamponuna yazılmakta sonra işletim sistemi tarafından TCP/IP paketi haline getirilip gönderilmektedir. send ve write fonksiyonları bilgiyi gönderme tamponuna yazıp hemen geri dönmektedir. Bu fonksiyonların geri dönmesi bilgilerin karşı tarafa iletildiği anlamına gelmemektedir. Benzer biçimde işletim sistemi network kartına gelen bilgileri onun ilişkin olduğu soketin alma tamponuna kendisi yerleştirmektedir. recv ve read fonksiyonları bu alma tamponuna bakmaktadır. Default blokeli modda bu alma tamponu boşsa bu fonksiyonların blokeye yol açtığını belirtmiştik. Benzer biçimde send ve write fonksiyonları da default blokeli modda gönderme tamponuna bilgi tam olarak yazılamıyorsa bilgi tam olarak tampona yazılana kadar blokeye yol açmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Soket programlamada bir tarafın tek bir send ya da write ile gönderdiklerini diğer tarafın tek bir recv ya da read ile okuması garanti değildir. Örneğin biz tek bir send ile 10000 byte göndermiş olalım. Karşı taraf da recv fonksiyonu ile bir döngüde sürekli okuma yapıyor olsun. Örneğin karşı taraf önce recv ile 4096 byte okuyabilir. Sonra diğer recv ile kalan byte'ları okuyabilir. Bu durum soket programlarının organizasyonunu biraz karışık hale getirmektedir. Bu nedenle örneğin biz 10000 byte gönderen bir tarafın gönderdiği 10000 byte'ı okuyabilmek için bir döngü kullanmamız gerekir. Soketten belli miktarda byte okuyana kadar okumayı devam ettiren bir fonksiyon aşağıdaki gibi yazılabilir: ssize_t read_socket(int sock, char *buf, size_t len) { size_t left, index; left = len; index = 0; while (left > 0) { if ((result = recv(sock, buf + index, left, 0)) == -1) return -1; if (result == 0) break; index += result; left -= result; } return (ssize_t) index; } Bu fonksiyonun çalışması oldukça basittir. recv ile her defasında left kadar byte okunmak istenmiştir. Ancak left kadar değil, result kadar byte okunmuş olabilir. Bu durumda left okunan miktar kadar azaltılmış index ise o miktar kadar artırılmıştır. Yukarıdaki fonksiyondan üç nedenle çıkılabilir: 1) Bağlantı kopmuştur ve recv başarısız olur. 2) Karşı taraf soketi kapatmıştır. recv 0 ile geri döner. 3) İstenen kadar miktar okunmuştur. Aslında recv fonksiyonunun talep edilen miktarda byte'ların hepsinin okunabilmesi için MSG_WAITALL biçiminde bir flags parametresi de vardır. Ancak MSG_WAITALL flags parametresi alma tamponundan daha yüksek miktarda verilerin okunması için uygun olmayabilmektedir. Bu konu ileride ele alınacaktır. Yeniden vurgulamak gerekirse bir soketten n byte okuma işlemi tek bir recv ile başarılmak zorunda değildir. Soket programlamaya yeni başlayanlar sanki bir disk dosyasından ya da borudan bilgi okunuyor gibi tek bir okuma çağrısı ile bunu yapma eğilimindedirler. Halbuki bu işlem yukarıdaki gibi bir döngüyle ya da recv fonksiyonuna MSG_WAITALL flags parametresi girilerek yapılmak zorundadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Zamanla bazı klasik soket fonksiyonları yerine onların işlevini yapabilecek daha yetenekli fonksiyonlar oluşturulmuştur. Eski fonksiyonlar IPv4 zamanlarında tasarlanmıştı. IPv6 ile birlikte bu IPv4 için tasarlanmış olan fonksiyonların IPv6'yı da destekleyecek daha genel versiyonları oluşturuldu. Bu eski fonksiyonların bir bölümü de "deprecated" hale getirildi. Biz yukarıdaki örneklerde bu eski fonksiyonları kullandık. Ancak artık yeni uygulamalarda IPv6'yı da destekleyen eski bazı fonksiyonların yeni biçimlerinin kullanılması daha uygundur. Biz de bu bölümde bu fonksiyonları ele alacağız bundan sonra bu fonksiyonları kullanacağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- inet_ntoa fonksiyonu bilindiği gibi 4 byte'lık IPv4 adresini noktalı desimal formata dönüştürüyordu. İşte bu fonksiyonun inet_ntop isimli IPv6'yı da kapsayan gelişmiş bir biçimi vardır: #include const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); Fonksiyonun birinci parametresi AF_INET (IPv4) ya da AF_INET6 (IPv6) olarak girilmelidir. İkinci parametre dönüştürülecek nümerik IPv4 ya da IPv6 adresinin bulunduğu nesnenin adresini belirtmektedir. Fonksiyon dönüştürme sonucunda elde edilecek yazısal noktalı desimal formatı üçüncü parametreyle belirtilen adresten itibaren yerleştirir. Son parametre üçüncü parametredeki dizinin uzunluğunu belirtir. Bu parametre INET_ADDRSTRLEN ya da INET6_ADDRSTRLEN biçiminde girilebilir. Fonksiyon başarı durumunda üçüncü parametreyle belirtilen adrese, başarısızlık durumunda NULL adrese geri döner. Örneğin bu fonksiyon server programda şöyle kullanılabilir: char ntopbuf[INET_ADDRSTRLEN]; ... printf("connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); inet_ntoa işleminin tersinin inet_addr ile yapıldığını belirtmiştik. İşte inet_addr fonksiyonunun yerine hem IPv4 hem de IPv6 ile çalışan inet_pton fonksiyonu kullanılabilmektedir: #include int inet_pton(int af, const char *src, void *dst); Fonksiyonun birinci parametresi yine AF_INET ya da AF_INET6 biçiminde geçilir. İkinci parametre noktalı desimal formatın bulunduğu yazının adresini, üçüncü parametre ise nümerik adresin yerleştirileceği adresi almaktadır. Bu parametreye IPv4 için 4 byte'lık, IPv6 için 16 byte'lık yerleştirme yapılmaktadır. Fonksiyon başarı durumunda 1 değerine, başarısızlık durumunda 0 ya da -1 değerine geri döner. Eğer başarısızlık birinci parametreden kaynaklanıyorsa -1, ikinci parametreden kaynaklanıyorsa 0 değerine geri dönmektedir. Bu durumda örneğin client programda inet_addr yerine inet_pton fonksiyonunu şöyle çağırabilirdik: if (inet_pton(AF_INET, server_name, &sin_server.sin_addr.s_addr) == 0) { ... } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 102. Ders 02/12/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- IPv6 ile birlikte yeni gelen diğer bir fonksiyon da getaddrinfo isimli fonksiyondur. Bu fonksiyon aslında 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 prototipi şöyledir: #include int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); 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ı struct 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 error koduna geri döner. Bu error kodları klasik errno değerlerinden farklı olduğu için strerror fonksiyonuyla değil, gai_strerror fonksiyonuyla yazıya dönüştürülmelidir. 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. getaddrinfo fonksiyonu sayesinde client program için önceki örneklerde yaptığımız işlemleri oldukça basitleştirmiş olmaktayız. Biz daha önce client programda önce inet_addr fonksiyonu ya da inet_pton fonksiyonu ile server adresinin noktalı formatta olup olmadığını anlayıp duruma göre gethostbyname fonksiyonu ile DNS işlemi yapmıştık. Oysa getaddrinfo fonksiyonu bu iki işlemi birlikte yapmaktadır. 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. Client programın bu fonksiyonu kullanarak bağlantı sağlaması aşağıdaki gibi bir kalıpla sağlanabilir: struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; ... if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); Burada server_name noktalı desimal formattaki server IP numarasını ya da server ismini belirtmektedir. server_port ise yazısal biçimde port numarasını belirtmektedir. sockaddr yapı nesnesinin oluşturulması için gereken bilgiler ise hints parametresinde girilmiştir. Fonksiyon DNS işlemi sonucunda elde edilen host bilgilerini bir bağlı liste biçiminde vermektedir. Bu örnek kodda bağlı listenin her elemanı connect fonksiyonuna sokulmuş ve bağlantı sağlanmaya çalışılmıştır. Örneğimizde biz hints parametresine AF_INET değerini girdik. Bu durumda DNS işlemi yapılırken fonksiyon yalnızca IPv4 adreslerini elde edecektir. IP bağlantısında bir taraf IPv4, diğer taraf IPv6 da olabilmektedir. hints parametresine eğer biz AF_UNSPEC geçseydik fonksiyon bize hem IPv4 hem de IPv6 adreslerini verecektir. Örnek kodda elde edilen adreslerden hiçbiri ile bağlantı sağlanamamışsa program sonlandırılmıştır. getaddrinfo fonksiyonunun tersini yapan getnameinfo isminde bir fonksiyon da sonraları soket kütüphanesine eklenmiştir. getnameinfo aslında inet_ntop, getserverbyname (biz görmedik) fonksiyonlarının birleşimi gibidir. Biz aşağıdaki örnekte bu fonksiyonu kullanmayacağız. #include int getnameinfo(const struct sockaddr *addr, socklen_t addrlen, char *host, socklen_t hostlen, char *serv, socklen_t servlen, int flags); Fonksiyonun birinci parametresi sockaddr_in ya da sockaddr_in6 yapısını almaktadır. İkinci parametre birinci parametredeki yapının uzunluğudur. Fonksiyonun sonraki dört parametresi sırasıyla noktalı hostun yazısal temsilin yerleştirileği dizinin adresi ve uzunluğu, port numarasına ilişkin yazının (servis ismi) yerleştirileceği dizinin adresi ve uzunluğudur. Son parametre 0 geçilebilir. Maksimum host ismi NI_MAXHOST ile maksimum servis ismi ise NI_MAXSERV ile belirtilmiştir. Yukarıda yazdığımız server ve client programlarının yeni fonksiyonlarla modern yazım biçimini de aşağıda veriyoruz. Bu server ve client programları birer şablon olarak kullanabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; char buf[BUFFER_SIZE + 1]; char ntopbuf[INET_ADDRSTRLEN]; ssize_t result; int option; int server_port; int p_flag, err_flag; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); for (;;) { if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received: \"%s\"\n", (intmax_t)result, buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); close(server_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE]; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Soket bağlantısında kullanılan diğer iki fonksiyon da getpeername ve getsockname fonksiyonlarıdır. getpeername fonksiyonu bağlı bir soketi parametre olarak alır ve karşı tarafın ip adresini ve port numarasını bize sockaddr_in ya da sockaddr_in6 biçiminde verir. Tabii aslında server bağlantıyı yaptığında karşı tarafın bilgisini zaten accept fonksiyonunda almaktadır. Bu bilgi saklanarak kullanılabilir. Ancak bu bilgi saklanmamışsa istenildiği zaman getpeername fonksiyonuyla alınabilmektedir. Fonksiyonun prototipi şöyledir: #include int getpeername(int sock, struct sockaddr *addr, socklen_t *addrlen); Fonksiyonun birinci parametresi soket betimleyicisidir. İkinci parametre duruma göre karşı tarafın bilgilerinin yerleştirileceği sockaddr_in ya da sockaddr_in6 yapı nesnesinin adresini alır. Son parametre ikinci parametredeki yapının uzunluğunu belirtmektedir. Eğer buraya az bir uzunluk girilirse kırpma yapılır ve gerçek uzunluk verdiğimiz adresteki nesneye yerleştirilir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. getpeername fonksiyonunun ters işlemini getsockname fonksiyonu yapmaktadır. Bu fonksiyon kendi bağlı soketimizin ip adresini ve port numarasını elde etmek için kullanılır. Genellikle bu fonksiyona gereksinim duyulmamaktadır. #include int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen); Fonksiyonun parametrik yapısı ve geri dönüş değeri getpeername fonksiyonundaki gibidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar TCP bağlantısının sağlanması ve send/recv fonksiyonlarının kullanımlarını gördük. Artık dikkatimizi bağlantı sonrasındaki haberleşmeye yönelteceğiz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- TCP/IP client-server programlamada en önemli konulardan biri "çok client'lı (multi-client)" server programların yazılmasıdır. Server program birden fazla client ile haberleşme yaparken bir client için recv ya da read fonksiyonunu kullandığında eğer o client'a bilgi gelmemişse bloke oluşacağından dolayı diğer client'lardan bilgi okuyamayacaktır. Bu durumda daha önce görmüş olduğumuz ileri IO tekniklerinin uygulanması gerekmektedir. Biz daha önce bu ileri IO tekniklerini borular üzerinde incelemiştik. Aslında bu tekniklerin borularda kullanılmasıyla soketlerde kullanılması benzer biçimdedir. Daha önce ele aldığımız ileri IO teknikleri şunlardı (scatter/getter IO tekniğini burada listelemiyoruz): 1) Multiplexed IO 2) Sinyal Tabanlı (Signal Driven) IO 3) Asenkron IO İşte biz bu bölümde bu IO tekniklerini de kullanarak çok client'lı TCP server uygulamalarının nasıl yazılabileceği üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Çok client'lı server uygulamalarında accept fonksiyonu bir kez çağrılmaz. Bir döngü içerisinde çağrılır. Çünkü server her client için accept uygulamak zorundadır. Örneğin: for (;;) { printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); } Tabii accept fonksiyonu default durumda blokeye yol açmaktadır. Pekiyi hem accept fonksiyonunda beklenip hem de bağlanılmış client'lar ile konuşma nasıl yapılacaktır? ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 103. Ders 03/12/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Multi-client programlar için en basit ancak en verimsiz yöntem/model "fork modeli"dir. Eğer az sayıda client söz konusu ise bu model basitliğinden dolayı tercih edilebilir. Bu modelde her accept işleminde bir fork yapılır. Alt proses bağlanılan client ile konuşur. Ancak her client için yeni bir prosesin yaratılması aslında verimsiz bir yöntemdir. Tabii az client söz konusu ise basitliğinden dolayı bu yöntem yine de kullanılabilir. Aşağıda fork modeliyle multi-client bir server örneği verilmiştir. Örnekte client programlar server ile bağlanıp ona yazı göndermekte, server program da bu yazıyı ters çevirerek client programlara geri yollamaktadır. Örneği test etmek için birden fazla terminal penceresi açmalısınız. Bu programda fork işlemi yapıldığında üst prosesin o andaki bellek alanının alt prosese kopyalandığına dolayısıyla alt prosesin son accept yapılan client sokete sahip olduğuna dikkat ediniz. Örneğimizde server program sonsuz döngüde çalışmaktadır. Server programı sonlandırmak için Ctrl+c tuşlarıyla ona SIGINT sinyalini gönderebilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 void client_proc(int sock, struct sockaddr_in *sin); char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; int p_flag, err_flag; pid_t pid; struct sigaction sa; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; sa.sa_handler = SIG_IGN; if (sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { client_proc(client_sock, &sin_client); exit(EXIT_SUCCESS); } } close(server_sock); return 0; } void client_proc(int sock, struct sockaddr_in *sin) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char ntopbuf[INET_ADDRSTRLEN]; unsigned port; ssize_t result; inet_ntop(AF_INET, &sin->sin_addr, ntopbuf, INET_ADDRSTRLEN); port = (unsigned)ntohs(sin->sin_port); for (;;) { if ((result = recv(sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, port, buf); revstr(buf); if (send(sock, buf, result, 0) == -1) exit_sys("send"); } printf("client disconnected %s:%u\n", ntopbuf, port); shutdown(sock, SHUT_RDWR); close(sock); } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Thread modeli fork modeline benzemektedir. Ancak thread yaratmak proses yaratmaktan daha kolay olduğu için proses modeline göre daha az maliyetlidir. Bu modelde her accept işleminde bir proses değil, thread yaratılmaktadır. Tabii thread'ler aynı adres alanını kullandığı için onlara gerekli parametreler uygun biçimde geçirilmelidir. Aşağıdaki örnekte fork işlemi ile proses yaratmak yerine pthread_create fonksiyonu ile thread yaratılmıştır. Yaratılan thread'e client bilgileri CLIENT_INFO yapısı eşliğinde geçirilmiştir. CLIENT_INFO yapı nesnesi dinamik olarak tahsis edilmiş ve thread fonksiyonu içerisinde free işlemi uygulanmıştır. UNIX/Linux sistemlerinde nasıl "zombie proses" oluyorsa "zombie thread" de oluşabilmektedir. Örneğimizde zombie thread oluşumunu engellemek için thread yaratılır yaratılmaz pthread_detach fonksiyonu ile thread detach moda sokulmuştur. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 void *client_thread_proc(void *param); char *revstr(char *str); void exit_sys(const char *msg); typedef struct tagCLIENT_INFO { int sock; struct sockaddr_in sin; } CLIENT_INFO; /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; int p_flag, err_flag; pthread_t tid; CLIENT_INFO *ci; int result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sin = sin_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } void *client_thread_proc(void *param) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char ntopbuf[INET_ADDRSTRLEN]; unsigned port; ssize_t result; CLIENT_INFO *ci = (CLIENT_INFO *)param; inet_ntop(AF_INET, &ci->sin.sin_addr, ntopbuf, INET_ADDRSTRLEN); port = (unsigned)ntohs(ci->sin.sin_port); for (;;) { if ((result = recv(ci->sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, port, buf); revstr(buf); if (send(ci->sock, buf, result, 0) == -1) exit_sys("send"); } printf("client disconnected %s:%u\n", ntopbuf, port); shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Multi-client server uygulamaları için diğer model de select modelidir. Biz daha önce bu modeli zaten görmüştük ve borular üzerinde bu modeli kullanmıştık. Soketler üzerinde de select modelinin kullanılması borulara çok benzerdir. Burada dikkat edilmesi gereken noktalar şunlardır: 1) Sokete bilgi geldiğinde, karşı taraf soketi kapattığında ve yeni bir bağlantı isteği oluştuğunda bu durum select tarafından bir "okuma olayı" olarak ele alınmaktadır. 2) İşin başında dinleme soketi (pasif soket) select fonksiyonunun okuma kümesine yerleştirilmelidir. select fonksiyonunun blokesi çözüldüğünde eğer söz konusu okuma olayı dinleme soketi üzerinde gerçekleşmişse bu durumda yeni bir bağlantı isteği söz konusudur. Bizim de accept fonksiyonunu çağırıp buradan elde ettiğimiz yeni soketi de select fonksiyonunun okuma kümesine dahil etmemiz gerekir. 3) Karşı taraf soketi kapattığında bu durum recv fonksiyonunda anlaşılmaktadır. Dolayısıyla programcının soketi kapatıp ilgili betimleyiciyi select fonksiyonunun okuma kümesinden çıkarması da gerekir. Aşağıda select modeli ile bir TCP server örneği verilmiştir. Bu örnekte select fonksiyonunun blokesi çözüldüğünde önce betimleyicinin dinleme soketine ilişkin betimleyici olup olmadığına bakılmıştır. Eğer betimleyici dinleme soketine ilişkinse accept işlemi uygulanmıştır. Değilse recv işlemi uygulanmıştır. Karşı taraf soketi kapattığında recv fonksiyonu 0 ile geri dönecektir. Bu durumda ilgili soket okuma kümesinden çıkartılmıştır. Örneğimizdeki server kodunda iç içe birkaç if deyimi kullanılmıştır. Kod aslında fonksiyonlar yoluyla daha anlaşılabilir biçimde de düzenlenebilirdi. Server program için diğer bir tasarım şöyle de olabilirdi: Bağlanan her client için yine bir CLIENT_INFO yapısı tahsis edilebilirdi. Client bilgileri bu yapının içinde saklanabilirdi. Sonra da dosya betimleyicisinden hareketle CLIENT_INFO nesnesine hızlı bir biçimde erişmek için "hash tablosu" oluşturulabilirdi. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sinaddr_len; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; int p_flag, err_flag; fd_set rset, tset; int maxfds; ssize_t result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); FD_ZERO(&rset); FD_SET(server_sock, &rset); maxfds = server_sock; printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); tset = rset; if (select(maxfds + 1, &tset, NULL, NULL, NULL) == -1) exit_sys("select"); for (int fd = 0; fd <= maxfds; ++fd) if (FD_ISSET(fd, &tset)) { if (fd == server_sock) { sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); FD_SET(client_sock, &rset); if (client_sock > maxfds) maxfds = client_sock; inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("connected client ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } else { sinaddr_len = sizeof(sin_client); if (getpeername(fd, (struct sockaddr *)&sin_client, &sinaddr_len) == -1) exit_sys("getpeername"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); if ((result = recv(fd, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result > 0) { buf[result] = '\0'; if (!strcmp(buf, "quit")) goto DISCONNECT; printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_client.sin_port), buf); revstr(buf); if (send(fd, buf, result, 0) == -1) exit_sys("send"); } else { /* result == 0 */ DISCONNECT: shutdown(fd, SHUT_RDWR); close(fd); FD_CLR(fd, &rset); printf("client disconnected ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } } } } close(server_sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- poll modeli de select modeline benzer biçimde uygulanabilir. Bu modelde dikkat edilmesi gereken noktalar şunlardır: 1) İşin başında yine dinleme soketi (pasif soket) pollfd dizisi içerisine yerleştirilmiş olmalıdır. 2) pollfd yapısının izlenecek olayı belirten events elemanı POLLIN olarak girilebilir. 3) poll geri döndüğünde pollfd dizisinin revents elemanlarına bakılmalı ve bu elemanlar üzerinde POLLIN olayının gerçekleşip gerçekleşmediği kontrol edilmelidir. 4) Yeni bağlantı isteği geldiğinde dinleme soketi üzerinde POLLIN olayı oluşmaktadır. Bu durumda programcının accept işlemini yapması gerekir. Yeni bağlantı kurulan client'ın pollfd bilgileri yine diziye eklenmelidir. 5) Karşı taraf soketi kapattığında yine POLLIN olayı gerçekleşmektedir. Bu durumda recv fonksiyonu ile 0 byte okunursa soketin kapatıldığı anlaşılmaktadır. Tabii bu durumda programcının bu pollfd dizisinden bu elemanı çıkarması gerekir. Aşağıda multi-client server için poll modeline bir örnek verilmiştir. Burada pfds isimli bir pollfd dizisini oluşturulmuştur. Bu dizinin maksimum uzunluğu MAX_CLIENT kadardır. Her bağlantı sağlandığında yeni client için bu pollfd dizisine bir eleman eklenmiştir. Bir client disconnect olduğunda bu diziden ilgili eleman silinmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 #define MAX_CLIENT 1000 char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sinaddr_len; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; int p_flag, err_flag; struct pollfd pfds[MAX_CLIENT]; int npfds, count; ssize_t result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); pfds[0].fd = server_sock; pfds[0].events = POLLIN; npfds = 1; printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); if (poll(pfds, npfds, -1) == -1) exit_sys("poll"); count = npfds; for (int i = 0; i < count; ++i) { if (pfds[i].revents & POLLIN) { if (pfds[i].fd == server_sock) { if (npfds >= MAX_CLIENT) { fprintf(stderr, "number of clints exceeds %d limit!...\n", MAX_CLIENT); continue; } sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); pfds[npfds].fd = client_sock; pfds[npfds].events = POLLIN; ++npfds; inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("connected client ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } else { sinaddr_len = sizeof(sin_client); if (getpeername(pfds[i].fd, (struct sockaddr *)&sin_client, &sinaddr_len) == -1) exit_sys("getpeername"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); if ((result = recv(pfds[i].fd, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result > 0) { buf[result] = '\0'; if (!strcmp(buf, "quit")) goto DISCONNECT; printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_client.sin_port), buf); revstr(buf); if (send(pfds[i].fd, buf, result, 0) == -1) exit_sys("send"); } else { DISCONNECT: shutdown(pfds[i].fd, SHUT_RDWR); close(pfds[i].fd); pfds[i] = pfds[npfds - 1]; --npfds; printf("client disconnected ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } } } } } close(server_sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 104. Ders 09/12/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi Linux sistemlerinde en etkin asenkron IO yöntemi epoll isimli yöntemdir. epoll yönteminin yalnızca Linux sistemlerine özgü olduğunu anımsayınız. Yine anımsayacağınız gibi bu yöntemde önce epoll_create ya da epoll_create1 fonksiyonlarıyla bir epoll betimleyicisinin yaratılması gerekiyordu. Daha sonra izlenecek betimleyiciler epoll_ctl fonksiyonu ile izlemeye dahil ediliyordu. Ancak olay beklemesi epoll_wait fonksiyonu ile yapılıyordu. Biz daha önce borularla epoll örneği yapmıştık. Soketlerle de işlemler benzer biçimde yürütülmektedir. epoll server modelinin anahtar noktalarını şöyle açıklayabiliriz: - Önce dinleme soketi epoll ile izlemeye alınmalıdır. Soketten okuma işlemleri için yine EPOLLIN olayı oluşmaktadır. - EPOLLIN olayı oluştuğunda olaya konu olan soket betimleyicisinin dinleme soketi olup olmadığına bakılmalıdır. Eğer olaya konu olan betimleyici dinleme soketiyse accept işlemi uygulanıp elde edilen betimleyicinin de izlenmesi sağlanmalıdır. - Karşı taraf soketi kapattığında hem EPOLLIN hem de EPOLLERR olayları oluşmaktadır. EPOLLIN olayı oluştuğunda recv uygulanıp 0 byte okunduğunda karşı tarafın soketi kapatmış olacağı düşünülmeli ve client soket kapatılmalıdır. - Client soketin kapatılması ile otomatik olarak izleme sona erdirilmektedir. Bunun için ayrıca epoll_ctl kullanılmasına gerek yoktur. - epoll modelinde karşı tarafın soketi kapatmasından dolayı oluşan EPOLLIN ve EPOLLERR olaylarında getpeername fonksiyonu kullanılmamalıdır. (Halbuki select ve poll fonksiyonlarında bu durumda getpeername fonksiyonu kullanılabilmektedir.) Anımsanacağı gibi epoll modelinde default izleme biçimi "düzey tetiklemeli (level triggered)" biçimdedir. Düzey tetiklemeli izlemede bir olay oluştuğunda o olayda olayın gereği yapılmadıktan sonra o olay yeniden oluşuyor gibi izleme yapılmaktadır. Örneğin düzey tetiklemeli izlemede sokete bilgi gelmiş olsun. Bu durumda epoll_wait yapıldığında EPOLLIN olayı gerçekleşecektir. Ancak eğer biz sokete gelen tüm bilgileri okumazsak epoll_wait fonksiyonunu bir daha çağırdığımızda yine EPOLLIN olayı gerçekleşecektir. Çünkü düzey tetiklemede sokette okunacak bilgi olduğu sürece epoll_wait hep bu olayı oluşturacaktır. Ancak kenar tetiklemede durum böyle değildir. Kenar tetiklemeli modda sokete bilgi gelmiş olsun. Bu durumda epoll_wait yapıldığında EPOLLIN olayı gerçekleşecektir. Biz bu olayda soketteki tüm bilgileri okumazsak bile artık epoll_wait fonksiyonunu çağırdığımızda EPOLLIN olayı oluşmayacaktır. EPOLLIN olayı bu modda yalnızca sokete yeni bir bilgi geldiğinde oluşmaktadır. Aşağıda daha önce yaptığımız client/server örneğinin epoll modeli ile düzey tetiklemeli gerçekleştirimi verilmiştir. Örneğimizde disconnect olan client'ın bilgilerini o sırada getpeername uygulayamadığımızdan dolayı yazdırmadık. (Tabii aslında bu tür uygulamalarda sürekli getpeername uygulamak iyi bir teknik değildir. Bağlanılan client'ın bilgilerini bir kere saklayıp oradan elde etmek daha iyi bir yöntemdir. Ancak biz buradaki uygulamalarda kodu karmaşık göstermemek için her defasında getpeername fonksiyonunu kullandık.) ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 #define MAX_EVENTS 1024 char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sinaddr_len; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; struct epoll_event ee; struct epoll_event ree[MAX_EVENTS]; int p_flag, err_flag; int epfd; int nevents; ssize_t result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); if ((epfd = epoll_create(1024)) == -1) exit_sys("epoll_create"); ee.events = EPOLLIN; ee.data.fd = server_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, server_sock, &ee) == -1) exit_sys("epoll_ctl"); printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); if ((nevents = epoll_wait(epfd, ree, MAX_EVENTS, -1)) == -1) exit_sys("epoll_wait"); for (int i = 0; i < nevents; ++i) { if (ree[i].events & EPOLLIN) { if (ree[i].data.fd == server_sock) { sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); ee.events = EPOLLIN; ee.data.fd = client_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &ee) == -1) exit_sys("epoll_ctl"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("connected client ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } else { if ((result = recv(ree[i].data.fd, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); buf[result] = '\0'; if (result > 0) { sinaddr_len = sizeof(sin_client); if (getpeername(ree[i].data.fd, (struct sockaddr *)&sin_client, &sinaddr_len) == -1) exit_sys("getpeername"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_client.sin_port), buf); revstr(buf); if (send(ree[i].data.fd, buf, result, 0) == -1) exit_sys("send"); } else { shutdown(ree[i].data.fd, SHUT_RDWR); close(ree[i].data.fd); printf("client disconnected\n"); } } } } } close(server_sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aslında bu tür client/server uygulamalarında epoll modelinin "kenar tetiklemeli (edge triggered)" modda kullanılmasının daha iyi bir performans sağladığı belirtilmektedir. Kenar tetiklemeli modu kullanırken göz önüne alınması gereken önemli anahtar noktalar şunlardır: - Kenar tetiklemeli modda sokete bilgi geldiğinde gelen bilgilerin hepsinin okunmasına gayret edilmelidir. Çünkü eğer biz sokete bilgi geldiğinde onların hepsini okumazsak bir daha EPOLLIN olayı ancak yeni bir bilgi geldiğinde oluşacağından gelmiş olan bilgilerin işleme sokulması gecikebilecektir. (Halbuki düzey tetiklemeli modda gelen bilgilerin hepsi okunmasa bile bir sonraki epoll_wait çağrımında yine EPOLLIN olayı gerçekleşeceği için böyle bir durum söz konusu olmayacaktır.) - Betimleyiciyi kenar tetiklemeli modda izlemek için epoll_event yapısının events elemanına EPOLLET bayrağının eklenmesi gerekmektedir. - Kenar tetiklemeli modda sokete gelen tüm bilgilerin okunması için betimleyicinin blokesiz modda olması gerekir. Aksi takdirde recv ya da read yaparken sokette bilgi kalmamışsa bloke oluşacaktır. Soket default olarak blokeli moddadır. Soketi daha önce görmüş olduğumuz fcntl fonksiyonu ile aşağıdaki gibi blokesiz moda sokabiliriz: if (fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); Blokesiz modda recv ya da read fonksiyonu ile başarısız olana kadar okuma da şöyle yapılabilir: for (;;) { if ((result = recv(...)) == -1) if (errno == EAGAIN) break; exit_sys("recv"); } // ... } - Aslında epoll modelinde bazı soket betimleyicileri düzey tetiklemeli bazıları kenar tetiklemeli modda olabilir. Örneğin pasif soketi düzey tetiklemeli modda tutup diğerlerini kenar tetiklemeli modda tutabilirsiniz. Aşağıda daha önce yazmış olduğumuz client/server programın epoll modeli ile kenar tetiklemeli biçimi verilmiştir. Bu örnekte dinleme soketi düzey tetiklemeli modda bırakılmış ancak client soketler kenar tetiklemeli moda sokulmuştur. Soket üzerinde EPOLLIN olayı gerçekleştiğinde bir döngü içerisinde recv fonksiyonu EAGAIN nedeniyle başarısız olana kadar okuma yapılmıştır. Tabii bu örnek aslında kenar tetiklemeli modele iyi bir örnek oluşturmamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 #define MAX_EVENTS 1024 char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sinaddr_len; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; struct epoll_event ee; struct epoll_event ree[MAX_EVENTS]; int p_flag, err_flag; int epfd; int nevents; ssize_t result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); if ((epfd = epoll_create(1024)) == -1) exit_sys("epoll_create"); ee.events = EPOLLIN; ee.data.fd = server_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, server_sock, &ee) == -1) exit_sys("epoll_ctl"); printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); if ((nevents = epoll_wait(epfd, ree, MAX_EVENTS, -1)) == -1) exit_sys("epoll_wait"); for (int i = 0; i < nevents; ++i) { if (ree[i].events & EPOLLIN) { if (ree[i].data.fd == server_sock) { sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); if (fcntl(client_sock, F_SETFL, fcntl(client_sock, F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); ee.events = EPOLLIN|EPOLLET; ee.data.fd = client_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &ee) == -1) exit_sys("epoll_ctl"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("connected client ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } else { for (;;) { if ((result = recv(ree[i].data.fd, buf, BUFFER_SIZE, 0)) == -1) { if (errno == EAGAIN) break; exit_sys("recv"); } buf[result] = '\0'; if (result > 0) { sinaddr_len = sizeof(sin_client); if (getpeername(ree[i].data.fd, (struct sockaddr *)&sin_client, &sinaddr_len) == -1) exit_sys("getpeername"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_client.sin_port), buf); revstr(buf); if (send(ree[i].data.fd, buf, result, 0) == -1) exit_sys("send"); } else { shutdown(ree[i].data.fd, SHUT_RDWR); close(ree[i].data.fd); printf("client disconnected\n"); break; } } } } } } close(server_sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi asenkron IO için başı aio_ ile başlayan bir grup asenkron IO fonksiyonları da bulunuyordu. Bu fonksiyonlarla okuma yazma yapılırken işlemler başlatılıyor ancak akış bloke olmadan arka planda devam ettiriliyordu. Olayın bittiği de bize bir sinyal ya da bir callback fonksiyonu yoluyla bildiriliyordu. Biz bu yöntemi daha önce incelemiş ve bununla ilgili bazı örnekler yapmıştık. Şimdi de bu yöntemi soketlerde kullanacağız. Asenkron IO modelinin soketlerde kullanılmasına ilişkin anahtar noktalar şunlardır: - accept işleminin bu mekanizmaya dahil edilmesi gerekmemektedir. Yani akış accept işleminde bloke olabilir. Tabii istenirse accept işlemi de bu mekanizmaya dahil edilebilir. Çünkü accept işlemi de bir okuma durumu oluşturmaktadır. - Bir okuma (ya da yazma) olayından sonra yeniden aynı mekanizmanın aio_read fonksiyonu çağrılarak kurulması gerekmektedir. Yani aio_read bir kez değil, her defasında yeniden çağrılmalıdır. Aşağıda daha önce yapmış olduğumuz server programını bu kez asenkron IO fonksiyonlarıyla gerçekleştiriyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 #define MAX_EVENTS 1024 void io_proc(union sigval sval); char *revstr(char *str); void exit_sys(const char *msg); typedef struct tagCLIENT_INFO { struct aiocb cb; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ char ntopbuf[INET_ADDRSTRLEN]; unsigned port; } CLIENT_INFO; /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; int option; int server_port; CLIENT_INFO *ci; int p_flag, err_flag; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); if ((ci = (CLIENT_INFO *)calloc(1, sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } inet_ntop(AF_INET, &sin_client.sin_addr, ci->ntopbuf, INET_ADDRSTRLEN); ci->port = ntohs(sin_client.sin_port); printf("connected client ===> %s:%u\n", ci->ntopbuf, ci->port); ci->cb.aio_fildes = client_sock; ci->cb.aio_offset = 0; ci->cb.aio_buf = ci->buf; ci->cb.aio_nbytes = BUFFER_SIZE; ci->cb.aio_reqprio = 0; ci->cb.aio_sigevent.sigev_notify = SIGEV_THREAD; ci->cb.aio_sigevent.sigev_value.sival_ptr = ci; ci->cb.aio_sigevent.sigev_notify_function = io_proc; ci->cb.aio_sigevent.sigev_notify_attributes = NULL; if (aio_read(&ci->cb) == -1) exit_sys("aio_read"); } close(server_sock); return 0; } void io_proc(union sigval sval) { CLIENT_INFO *ci = (CLIENT_INFO *)sval.sival_ptr; ssize_t result; if ((result = aio_return(&ci->cb)) == -1) exit_sys("aio_return"); ci->buf[result] = '\0'; if (result > 0) { printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ci->ntopbuf, (ci->port), ci->buf); revstr(ci->buf); if (send(ci->cb.aio_fildes, ci->buf, result, 0) == -1) exit_sys("send"); if (aio_read(&ci->cb) == -1) exit_sys("aio_read"); } else { shutdown(ci->cb.aio_fildes, SHUT_RDWR); close(ci->cb.aio_fildes); printf("client disconnected ===> %s:%u\n", ci->ntopbuf, (ci->port)); free(ci); } } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 105. Ders 10/12/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bu noktada dikkatimizi UDP protokolü üzerine yönelteceğiz. UDP protokolünü ele aldıktan sonra yine TCP protokolü ile ilgili bazı ayrıntılar üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Giriş kısmında da belirttiğimiz gibi UDP (User Datagram Protocol) bağlantılı olmayan bir protokoldür. Dolayısıyla bir taraf bir tarafa hiç bağlanmadan onun IP adresini ve port numarasını bilerek UDP paketlerini gönderebilir. Gönderen taraf alan tarafın paketi alıp almadığını bilmez. Yani UDP protokolünde bir akış kontrolü yoktur. Dolayısıyla alan taraf bilgi kaçırabilir. Protokol kaçırılan bilgilerin telafisini kendisi yapmamaktadır. Halbuki TCP protokülünde bir bağlantı oluşturulduğu için bir akış kontrolü uygulanarak karşı tarafa ulaşmamış TCP paketlerinin yeniden gönderilmesi sağlanmaktadır. UDP tabii ki TCP'ye göre daha hızlıdır. Zaten TCP bir bakıma UDP'nin organize edilmiş bağlantılı biçimidir. Pekiyi UDP protokolü ile ağ katmanı protokolü olan IP protokolü arasındaki fark nedir? Her iki protokolde aslında paketlerin iletimini yapmaktadır. Aslında UDP protokolünün gerçekten de IP protokolünden çok farkı yoktur. Ancak UDP bir aktarım (transport) katmanı protokolü olduğu için port numarası içermektedir. Halbuki IP protokolünde port numarası kavramı yoktur. Yani IP protokolünde biz bir host'a paket gönderebiliriz. Onun belli bir portuna paket gönderemeyiz. Bunun dışında UDP ile IP protokollerinin kullanımları konusunda yine bazı farklılıklar vardır. Aslında biz programcı olarak doğrudan IP paketleri de gönderebiliriz. Buna "raw socket" kullanımı denilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- TCP'de accept uygulayan tarafa "server", connect uygulayana tarafa "client" denilmektedir. Ancak UDP bağlantısız bir protokol olduğu için "client" ve "server" kavramları bu protokolde tam oturmamaktadır. Ancak yine de genellikle hizmet alan tarafa "client", hizmet veren tarafa "server" denilmektedir. UDP'de client daha çok gönderim yapan, server ise okuma yapan taraftır. UDP özellikle periyodik kısa bilgilerin gönderildiği ve alındığı durumlarda hız nedeniyle tercih edilmektedir. UDP haberleşmesinde bilgiyi alan tarafın (server) bilgi kaçırabilmesi söz konusu olabileceğinden dolayı böyle kaçırmalarda sistemde önemli bir aksamanın olmaması gerekir. Eğer bilgi kaçırma durumlarında sistemde önemli aksamalar oluşabiliyorsa UDP yerine TCP tercih edilmelidir. Örneğin bir televizyon yayınında görüntüye ilişkin bir frame karşı taraf tarafından alınmadığında önemli bir aksama söz konusu değildir. Belki görüntüde bir kasis olabilir ancak bu durum önemli kabul edilmemektedir. Örneğin birtakım makineler belli periyotlarda server'a "ben çalışıyorum" demek için periyodik UDP paketleri yollayabilir. Server da hangi makinenin çalışmakta olduğunu (bozulmamış olduğunu) bu sayede anlayabilir. Örneğin bir araba simülatörü arabanın durumunu UDP paketleriyle dış dünyaya verebilir. Bir UDP paketi 64K gibi bir sınıra sahiptir. TCP ve UDP protokollerinde bir uzunluk bilgisi yoktur. Uzunluk bilgisi IP protokolünde bulunmaktadır. IPv4 ve IPv6 protokollerinde bir IP paketi en fazla 64K uzunlukta olabilmektedir. Tabii TCP stream tabanlı olduğu için bu 64K uzunluğun TCP için bir önemi yoktur. Ancak UDP paket tabanlı olduğu için bir UDP paketi IP paketinin uzunluğunu aşamaz. Dolayısıyla bir UDP paketi en fazla 64K uzunlukta olabilmektedir. Büyük paketlerin UDP ile gönderilmesi için programcının paketlere kendisinin manuel numaralar vermesi gerekebilir. Zaten TCP protokolü bu şekilde bir numaralandırmayı kendi içerisinde yapmaktadır. UDP haberleşmesinin önemli bir farkı da "broadcasting" işlemidir. Broadcasting, yerel ağda belli bir host'un tüm host'lara UDP paketleri gönderebilmesine denilmektedir. TCP'de böyle bir broadcasting mekanizması yoktur. 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) | +----------------------------------------------------------------------------------------------+ ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UDP server programı tipik olarak şu adımlardan geçilerek oluşturulur: 1) Server SOCK_DGRAM parametresiyle bir socket yaratır. 2) Soketi bind fonksiyonuyla bağlar. 3) recvfrom fonksiyonuyla gelen paketleri alır ve sendto fonksiyonuyla UDP paketi gönderir. 4) Haberleşme bitince server soketi close fonksiyonuyla ile kapatır. Haberleşme bittiğinde shutdown gibi bir işlemin gerekmediğine dikkat ediniz. shutdown işlemi TCP'de bağlantıyı koparmak için kullanılmaktadır. Halbuki UDP protokolünde zaten bağlantı yoktur. Yukarıdaki adımları fonksiyon temelinde aşağıdaki gibi de özetleyebiliriz: socket (SOCK_DGRAM) ---> bind ---> recfrom/sendto ---> close ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UDP client program da şu adımlardan geçilerek oluşturulur: 1) Client soketi SOCK_DGRAM parametresiyle yaratır. 2) Client isteğe bağlı olarak soketi bind fonksiyonuyla bağlayabilir. 3) Client, server'ın host isminden hareketle server'ın IP adresini gethostbyname ya da getaddrinfo fonksiyonuyla elde edebilir. 4) Client sendto fonksiyonuyla UDP paketlerini gönderir ve recvfrom fonksiyonuyla UDP paketlerini alabilir. 5) Haberleşme bitince client close fonksiyonuyla soketi kapatır. Bu adımları fonksiyon isimleriyle şöyle özetleyebiliriz: socket (SOCK_DGRAM) ---> bind (isteğe bağlı) ---> gethostbyname/getaddrinfo (isteğe bağlı) ---> sendto/recvfrom ---> close ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UDP paketlerini okumak için kullanılan recvfrom prototipi şöyledir: #include ssize_t recvfrom(int socket, void *buffer, size_t length, int flags, struct sockaddr *address, socklen_t *address_len); Fonksiyonun birinci parametresi okuma işleminin yapılacağı soketi belirtir. İkinci parametre alınacak bilginin yerleştirileceği adresi belirtmektedir. Üçüncü parametre ikinci parametredeki alanın uzunluğunu belirtir. Eğer buradaki değer UDP paketindeki gönderilmiş olan byte sayısından daha az ise kırpılarak diziye yerleştirme yapılmaktadır. Fonksiyonun üçüncü parametresi (flags) birkaç seçeneğe sahiptir. Bu parametre için 0 girilebilir. Fonksiyonun dördüncü parametresi UDP paketini gönderen tarafın IP adresinin ve port numarasının yerleştirileceği sockaddr_in yapısının adresini alır. Son parametre ise bu yapının uzunluğunu tutan int nesnenin adresini almaktadır. Fonksiyon başarı durumunda UDP paketindeki byte sayısına, başarısızlık durumunda -1 değerine geri dönmektedir. recvfrom fonksiyonunun herhangi bir client'tan gelen paketi alabildiğine dikkat ediniz. Dolayısıyla her recvfrom ile alınan paket farklı bir client'a ilişkin olabilmektedir. recvfrom fonksiyonu, eğer soket blokeli moddaysa (default durum) UDP paketi gelene kadar blokeye yol açar. Blokesiz modda fonksiyon bekleme yapmaz, -1 değeriyle geri döner ve errno EAGAIN değeriyle set edilir. sendto fonksiyonunun prototipi de şöyledir: #include ssize_t sendto(int socket, const void *message, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t dest_len); Fonksiyonun parametreleri recvfrom da olduğu gibidir. Yani birinci parametre gönderim yapılacak soketi belirtir. İkinci ve üçüncü parametreler gönderilecek bilgilerin bulunduğu tamponu ve onun uzunluğunu belirtmektedir. Yine bu fonksiyonda da bir flags parametresi vardır. Dördüncü parametre bilginin gönderileceği IP adresini ve port numarasını belirtir. Son parametre ise dördüncü parametredeki yapının (sockaddr_in ya da sockaddr_in6) uzunluğunu alır. Fonksiyon blokeli modda paket network tamponuna yazılana kadar blokeye yol açmaktadır. sendto fonksiyonu da başarı durumunda network tamponuna yazılan byte sayısına, başarısızlık durumunda -1'e geri dönmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda tipik bir UDP client-server örneği verilmiştir. Bu örnekte client yine bir prompt'a düşerek kullanıcıdan bir yazı istemektedir. Bu yazıyı UDP paketi biçiminde server'a yollamaktadır. Server da bu yazıyı alıp görüntüledikten sonra yazıyı ters çevirip client'a geri yollamaktadır. Programların komut satırı argümanları diğer örneklerde olduğu gibidir. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; int server_port; char buf[BUFFER_SIZE + 1]; char ntopbuf[INET_ADDRSTRLEN]; ssize_t result; int option; int p_flag, err_flag; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); printf("waiting UDP packet...\n"); for (;;) { sin_len = sizeof(sin_client); if ((result = recvfrom(server_sock, buf, BUFFER_SIZE, 0, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("recvfrom"); buf[result] = '\0'; inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_client.sin_port), buf); revstr(buf); if (sendto(server_sock, buf, result, 0, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("sendto"); } close(server_sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "localhost" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client, sin_server; socklen_t sin_len; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res; int gai_result; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ char ntopbuf[INET_ADDRSTRLEN]; ssize_t result; char *str; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } freeaddrinfo(res); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if (sendto(client_sock, buf, strlen(buf), 0, res->ai_addr, sizeof(struct sockaddr_in)) == -1) exit_sys("send"); sin_len = sizeof(sin_server); if ((result = recvfrom(client_sock, buf, BUFFER_SIZE, 0, (struct sockaddr *)&sin_server, &sin_len)) == -1) exit_sys("recvfrom"); buf[result] = '\0'; inet_ntop(AF_INET, &sin_server.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("%jd byte(s) received from server %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_server.sin_port), buf); } close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 106. Ders 16/12/2023 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- TCP/IP ya da UDP/IP server uygulamalarında server'ın bir client'tan gelen isteği yerine getirmesi bir zaman kaybı oluşturabilmektedir. Server bir client ile uğraşırken diğer client'ların istekleri mecburen bekletilir. İşte bu durumu en aza indirmek için select, poll (ve epoll) modellerinde server bir client'ın isteğini bir thread ile yerine getirebilir. Böylece server birden fazla client'a aynı anda hizmet verebilecektir. Örneğin select modelinde bu işlem şöyle yapılabilir: for (;;) { select(...); if () { } } Benzer biçimde UDP server uygulamasında da işlem şöyle yapılabilir: for (;;) { recvfrom(...) } Tabii burada küçük bir işlem için yeni bir thread'in yaratılıp yok edilmesi etkin bir yöntem değildir. Çünkü bilindiği gibi thread'lerin yaratılıp yok edilmeleri de dikkate değer bir zaman kaybı oluşturmaktadır. Thread'ler konusunda da belirttiğimiz gibi bu tür durumlarda "thread havuzları (thread pools)" kullanılabilir. Thread havuzlarında zaten belli bir miktar thread yaratılmış ve bekler durumda (suspend durumda) tutulmaktadır. Böylece client'tan gelen isteğin bu thread'lerden biri tarafından gerçekleştirilmesi sağlanır. POSIX sistemlerinde C'de kullanılabilecek standart bir thread havuzu mekanizmasının olmadığını anımsayınız. Bu nedenle böyle bir thread havuzunu programcının kendisi yazabilir ya da C için yazılmış olan bir thread havuzu kütüphanesini kullanabilir. Windows sistemlerinde işletim sistemi düzeyinde thread havuzlarına ilişkin standart API fonksiyonları bulunmaktadır. C++'ta pek çok framework içerisinde (MFC gibi, Qt gibi) zaten thread havuzları sınıfsal biçimde bulunmaktadır. Pekiyi yoğun bir server düşünelim. Makinemizde de bir tane işlemci bulunuyor olsun. Böyle bir thread havuzunun kullanılması gerçek anlamda bir fayda sağlayabilir mi? Örneğin 5 tane client'ın isteğini thread yoluyla sağlamaya çalışalım. Her client'ın isteği için 3 quanta süresi gerekiyor olsun. İşlemler seri yapıldığında toplam 15 quanta zamanında tüm client'ların mesajları işlenmiş olacaktır. Thread'ler kullanıldığında işlemcinin çalışma kuyruğunda (run queue) 5 thread bulunacak ve bunlar zaman paylaşımlı biçimde çalışacaktır. Dolayısıyla yine bu client'ların hepsinin işlerini bitirmesi için 15 quanta süresi gerekecektir. Tabii sistemde başka proseslerin thread'leri de varsa çok thread'li çalışma toplamda bu server'ın diğer proseslere göre daha fazla işlemci zamanı kullanmasına yol açacaktır. Ancak makinemizde birden fazla işlemci varsa bu durumda yukarıdaki thread sistemi belirgin bir avantaj sağlayacaktır. Bu tür durumlarda işlemci sayısı kadar thread'in aynı anda çalışması sağlanabilir. Bu thread'ler farklı işlemcilerde eş zamanlı bir biçimde çalışabileceği için ciddi bir hızlanma sağlanacaktır. Tabii buradaki thread'lerin aynı anda farklı işlemciler tarafından çalıştırılması gerekir. İşletim sistemleri genellikle bu ayarlamayı kendi içlerinde yapabilmektedir. Örneğin Linux sistemlerinde kullanılan güncel çizelgeleme algoritmasında her işlemci için ayrı bir çalışma kuyruğu oluşturulmakta ve thread'ler bunlara dinamik bir biçimde dağıtılmaktadır. Ancak yine de bazı durumlarda thread'lerin belli işlemcilere programcı tarafından atanması (processor affinity) gerekebilmektedir. Örneğin makinemizde 8 işlemci ya da çekirdek olsun. Bu durumda biz 8 tane thread yaratıp bu 8 thread'in farklı işlemcilerde eş zamanlı olarak kendi içlerinde seri bir biçimde çalışmasını sağlayabiliriz. Bunun sağlanması iki biçimde yapılabilir. Birincisi her thread için yukarıdaki döngü yeniden oluşturulabilir. Örneğin: // 1'inci thread for (;;) { recvfrom(...) } // 2'inci thread for (;;) { recvfrom(...) } ... Soket fonksiyonları bu bağlamda thread güvenlidir. İkinci yöntemde server aldığı mesajları bir kuyruğa yazar. Thread'ler de aynı kuyruktan mesajları alarak işleme sokar. Örneğin: for (;;) { recvfrom(...) } ... // 1'inci thread for (;;) { } // 2'inci thread for (;;) { } ... Bu tür durumlarda işlemci ya da çekirdek sayısından daha fazla thread'in oluşturulması özel durumlar dışında önemli bir fayda sağlamamaktadır. Linux'un epoll modelinde thread'li kullanımda Linux genel olarak her işlemci ya da çekirdek için gerektiğinde kendisi thread oluşturmaktadır. Dolayısıyla epoll modeli yukarıdaki gibi bir organizasyon yapılmasa da daha iyi bir performans göstermektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi client istekleri için çok işlemcili ya da çok çekirdekli bir bilgisayar yetmiyorsa ne olacak? Bu tür durumlarda işleri birden fazla server makineye dağıtmak gerekir. Client'ın birden çok server olduğunu fark etmemesi dolayısıyla bu sürecin tamamen server tarafta otomatik hale getirilmesi uygun olur. Aynı makinenin klonundan çıkartılıp sisteme eklendiğinde yükü paylaşabilmesi sağlanmalıdır. Böylece "ölçeklenebilir (scalable)" bir sistem söz konusu olacaktır. Pekiyi yük bu server makinelere nasıl dağıtılabilir? İşte bunun için kullanılan mekanizmaya "load balancing", bu mekanizmayı sağlayan birime de "load balancer" denilmektedir. Server Makine Server Makine Server Makine Server Makine ... Load Balancer Bu tür dağıtık sistemlerde client aslında "load balancer" ile bağlantı sağlar. Load balancer, client'ı en az meşgul olan server'a iletir. Bugün kullanılan cloud sistemler de kendi içlerinde load balancer benzeri mekanizmalar içermektedir. Load Balencer'lar tamamen donanımsal olarak ya da tamamen yazılımsal olarak gerçekleştirilebilmektedir. Yazılımsal gerçekleştirim daha esnek olabilmektedir. Ancak donanımsal gerçekleştirimler bazı durumlarda daha etkin olabilmektedir. Donanmsal load balancer'larda client, server ile bağlantı kurmak istediğinde load balancer devreye girip sanki yalnızca en az meşgul olan server sistemde varmış gibi bağlantıyı onun kabul etmesini sağlamaktadır. Yazılımsal load balancer'larda load balancer bir "proxy" gibi çalışmaktadır. Client load balancer ile bağlantı sağlar. Load balancer bunu en az meşgul server'a yönlendirir. Bu kez client bu server ile bağlantı kurar. Buradaki load balancer görevini yapan "proxy" programınının server yüklerini sürekli izlemesi gerekmektedir. Bunun için genellikle UDP protokolü kullanılmaktadır. Yani UDP ile server makineler sürekli bir biçimde kendi durumlarını proxy'ye iletirler. Proxy'de bu bilgilerden hareketle en az meşgul server'ı tespit eder. Tabii server makineler eğer devre dışı kalırsa proxy'inin bunu fark etmesi ve artık ona yönlendirme yapmaması gerekir. Benzer biçimde yeni bir server makinesi sisteme eklendiğinde proxy hemen onu da sisteme otomatik olarak dahil etmelidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de TCP/IP ve UDP/IP protokollerinin ve bunun için kullanılan soket fonksiyonlarının ayrıntıları üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- TCP için kullandığımız recv ve send fonksiyonlarının, read ve write fonksiyonlarından tek farkı flags parametresidir. Biz yukarıdaki örneklerde bu parametreyi 0 geçtik. Dolayısıyla yukarıdaki örneklerde kullandığımız recv ve send fonksiyonlarının, read ve write fonksiyonlarından hiçbir farkı yoktur. Pekiyi bu flag değerleri neler olabilir? İşte POSIX standartlarında recv fonksiyonundaki flag değerleri şunlardan biri ya da birden fazlası olabilir: MSG_PEEK: Bu bayrak gelen bilginin tampondan okunacağını ancak tampondan atılmayacağını belirtmektedir. Yani biz MSG_PEEK flag değeri ile okuma yaparsak hem bilgiyi elde ederiz hem de sanki hiç okuma yapmamışız gibi bilgi network tamponunda kalır. Dolayısıyla bizim thread'imiz ya da başka bir thread recv yaparsa tampondakini okuyacaktır. Pekiyi bu bayrak hangi amaçla kullanılmaktadır. Bazen (çok seyrek olarak) mesajın ne olduğuna MSG_PEEK ile bakıp duruma göre onu kuyruktan almak isteyebiliriz. Eğer mesaj bizim beğenmediğimiz bir mesajsa onu almak istemeyebiliriz. Onun başka bir thread tarafından işlenmesini sağlayabiliriz. MSG_OOB: Out-of-band data (urgent data) denilen okumalar için kullanılmaktadır. Out-of-band data konusu ayrı bir paragrafta açıklanacaktır. MSG_WAITALL: Bu bayrak n byte okunmak istendiğinde bu n byte'ın hepsi okunana kadar bekleme sağlamaktadır. Fakat bu durumda bir sinyal geldiğinde yine recv -1 ile geri döner ve errno EINTR ile set edilir. Yine soket kapatıldığında ya da soket üzerinde bir hata oluştuğunda fonksiyon talep edilen kadar bilgiyi okuyamamış olabilir. Biz daha önce n byte okuma yapmak için aşağıdaki gibi bir fonksiyon önermiştik: ssize_t read_socket(int sock, char *buf, size_t len) { size_t left, index; left = len; index = 0; while (left > 0) { if ((result = recv(sock, buf + index, left, 0)) == -1) return -1; if (result == 0) break; index += result; left -= result; } return (ssize_t) index; } İşte aslında recv fonksiyonundaki MSG_WAITALL bayrağı adeta bunu sağlamaktadır. Ancak yine de bu bayrağın bazı sistemlerde bazı problemleri vardır. Örneğin Windows sistemlerinde ve Linux sistemlerinde bu bayrakla okunmak istenen miktar network alım tamponunun büyüklüğünden fazlaysa istenen miktarda byte okunamayabilmektedir. Ancak bu bayrak yüksek olmayan miktarlarda okumalar için yukarıdaki fonksiyonun yerine kullanılabilmektedir. send fonksiyonundaki POSIX bayrakları da şunlardır: MSG_EOR: Soket türü SOCK_SEQPACKET ise kaydı sonlandırmakta kullanılır. MSG_OOB: Out-of-band data gönderimi için kullanılmaktadır. Bu konu ayrı bir paragrafta ele alınacaktır. MSG_NOSIGNAL: Normal olarak send ya da write işlemi yapılırken karşı taraf soketi kapatmışsa bu fonksiyonların çağrıldığı tarafta SIGPIPE sinyali oluşmaktadır. Ancak bu bayrak kullanılırsa böylesi durumlarda SIGPIPE sinyali oluşmaz, send ya da write fonksiyonu -1 ile geri döner ve errno EPIPE değeri ile set edilir. Linux, POSIX'in bayraklarından daha fazlasını bulundurmaktadır. Örneğin recv ve send işleminde MSG_DONTWAIT bir çağrımlık "non-blocking" etki yaratmaktadır. Yani recv sırasında network tamponunda hiç bilgi yoksa recv bloke olmaz, -1 ile geri döner ve errno EAGAIN değeri ile set edilir. send işlemi sırasında da network tamponu dolu ise send bloke olmaz -1 ile geri döner ve errno yine EAGAIN değeri ile set edilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Normal olarak connect/accept işlemi TCP'de kullanılmaktadır. Ancak UDP soketlerde de seyrek biçimde connect/accept kullanılabilir. Eğer UDP bir soket connect ile UDP server'a bağlanırsa (server da bunu accept ile kabul etmelidir) bu durumda artık iki taraf recvfrom ve sendto fonksiyonlarının yerine recv ve send fonksiyonlarını kullanabilir. Tabii burada yine datagram haberleşmesi yapılmaktadır. Yalnızca her defasında gönderme ve alma işlemlerinde karşı tarafın soketine ilişkin bilgilerin belirtilmesine gerek kalmamaktadır. Bu biçimdeki connect/accept bağlantısında yine bir akış kontrolü uygulanmamaktadır. Aslında recv fonksiyonu yerine recvfrom fonksiyonu da kullanılabilir. Yani recvfrom fonksiyonunun son iki parametresi NULL geçilirse zaten bu işlem recv ile eşdeğer olmaktadır. Örneğin aşağıdaki iki çağrı eşdeğerdir: result = recv(sock, buf, len, flags); result = recvfrom(sock, buf, len, flags, NULL, NULL); Benzer biçimde send yerine sendto fonksiyonu da kullanılabilir. Bu durumda sendto fonksiyonunun son iki parametresi ihmal edilir. Örneğin aşağıdaki iki çağrı eşdeğerdir: result = send(sock, buf, len, flags); result = sendto(sock, buf, len, flags, any_value, any_value); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar client ile server programlar arasında ciddi bir mesaj alışverişi yapmadık. Verdiğimiz örneklerde client program, server programa bir yazı iletiyordu. Server program da client'a bu yazıyı ters çevirip gönderiyordu. Bu çok basit bir mesajlaşma işlemidir. Halbuki gerçek client-server uygulamalarında mesajlaşmalar çok daha çeşitli ve ayrıntılıdır. Gerçek uygulamalarda client program, server programdan çok çeşitli şeyleri yapmasını isteyebilir. Server'da client'ın isteğine uygun bir biçimde yanıtları iletir. Örneğin dört işlem yapan bir server program olsun. Client iki operand'ı server'a gönderip ondan dört işlemden birini yapmasını istesin. Bu durumda client'ın server'a gönderdiği mesajlar fazlalaşmaktadır. Örneğin: ADD op1 op2 SUB op1 op2 MUL op1 op2 DIV op1 op2 Bir chat programında client server'dan pek çok şey isteyebilir. Server da client'a çok çeşitli bilgiler iletebilir. Yani bu tür uygulamalarda mesajın bir içeriği ve parametreleri vardır. Pekiyi mesajlar karşı tarafa hangi formatta iletilecektir? İşte bunun için binary ve text olmak üzere iki mesajlaşma tekniği kullanılmaktadır. Binary mesajlaşmada kabaca her iki taraf birbirlerine bir yapı nesnesinin içeriğini binary biçimde gönderir. Karşı taraf da bu yapı nesnesini alarak işlemini yapar. Ancak farklı mesajlarda farklı yapılar kullanılacağı için mesajın başında mesajın türünü ve uzunluğunu belirten ortak bir başlık kısmı bulundurulur. Bunun için tipik olarak şöyle bir yol izlenir: Mesajı gönderecek taraf önce mesajın uzunluğunu sonra da mesajın ne mesajı olduğunu belirten mesaj kodunu (numarasını) sonra da mesajın içeriğini karşı tarafa yollar. Bu işlemi "pseudo code" olarak aşağıdaki gibi ifade edebiliriz: typedef struct tagMSG_HEADER { int len; int type; } MSG_HEADER; typedef struct tagMSG_XXX { // message info } MSG_XXX; typedef struct tagMSG_YYY { // message info } MSG_YYY; Örneğin MSG_XXX mesajı karşı tarafa gönderilecek olsun: MSG_HEADER header; MSG_XXX msg_xxx; header.len = sizeof(MSG_XXX); header.type = MSG_TYPE_XXX; send(sock, &header, sizeof(MSG_HEADER), 0); send(sock, &msg_xxx, sizeof(MSG_XXX), 0); Mesajı alan taraf da önce mesajın uzunluğunu ve kodunu elde eder, sonra da türünü elde eder ve soketten o uzunlukta okuma yapar. Örneğin: MSG_HEADER header; MSG_XXX msg_xxx; ... recv(sock, &header, sizeof(MSG_HEADER), MSG_WAITALL); switch (header.type) { case MSG_TYPE_XXX: recv(sock, &msg_xxx, sizeof(header.len), MSG_WAITALL); process_msg_xxx(&msg_xxx); break; ... } Burada aklınıza şöyle bir soru gelebilir: Okuyan taraf zaten mesajın kodunu (numarasını) elde edince o mesajın kaç byte uzunlukta olduğunu bilmeyecek mi? Bu durumda mesajın uzunluğunun karşı tarafa iletilmesine ne gerek var? İşte mesajlar sabit uzunlukta olmayabilir. Örneğin mesajın içerisinde bir metin bulunabilir. Bu durumda mesajın gerçek uzunluğu bu metnin uzunluğuna bağlı olarak değişebilir. Genel bir çözüm için mesajın uzunluğunun da karşı tarafa iletilmesi gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 107. Ders 17/12/2023 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki gibi binary tabanlı mesajlaşma daha hızlı ve etkin olma eğiliminde ise de pratikte daha çok text tabanlı mesajlaşmalar kullanılmaktadır. Çünkü text tabanlı mesajlaşmalar insanlar tarafından yazısal biçimde de oluşturulabilmektedir. IP protokol ailesinin uygulama katmanındaki POP3, Telnet, FTP gibi protokolleri text tabanlı mesajlaşmayı kullanmaktadır. Text tabanlı mesajlaşmada client'tan server'a ve server'dan client'a gönderilen mesajlar bir yazı olarak gönderilir. Karşı taraf bu yazıyı alır, parse eder ve gereğini yapar. Programlama dillerinde yazılarla işlem yapabilen pek çok standart araç bulunduğu için bu biçimde mesajların işlenmesi genel olarak daha kolaydır. Ancak mesaj tabanlı haberleşme genel olarak daha yavaştır. Çünkü birtakım bilgilerin yazısal olarak ifade edilmesi binary ifade edilmesinden genel olarak daha fazla yer kaplama eğilimindedir. Text tabanlı mesajlaşmada önemli bir sorun mesajın nerede bittiğinin tespit edilmesidir. Bunun için mesajın sonu özel bir karakterle sonlandırılabilir. Örneğin IP protokol ailesinin uygulama katmanındaki protokoller genel olarak mesajları CR/LF ('\r' ve '\n') çiftiyle bitirmektedir. Örneğin: "ADD op1 op2\r\n" "SUB op1 op2\r\n" "MUL op1 op2\r\n" "DIV op1 op2\r\n" Tabii bu biçimdeki mesajlaşmalarda soketten CR/LF çifti görülene kadar okuma yapılması gerekir. Bu işlem soketten byte byte okuma ile yapılmamalıdır. Çünkü her byte okuması için prosesin kernel mode'a geçmesi zaman kaybı oluşturmaktadır. Belli bir karakter ya da karakter kümesi görülene kadar soketten okuma işleminin nasıl yapılması gerektiği izleyen paragraflarda açıklanacaktır. Yazının sonunun tespit edilmesi için kullanılabilecek diğer bir yöntem de baştan yazının uzunluğunun iletilmesi olabilir. Örneğin: "ADD op1 op2" Yukarıdaki mesaj için önce bu yazının uzunluğu karşı tarafa iletilebilir. Sonra yazı gönderilir. Okuyan taraf da yazının hepsini belirtilen uzunlukta okuma yaparak elde edebilir. Yazının sonuna belli bir karakter yerleştirerek o karakteri görene kadar etkin okuma yapmak için şöyle bir teknik kullanılmaktadır: Karakterler soketten tek tek okunmaz. Blok blok okunurak bir tampona yerleştirilir. Sonra bu tampondan karakterler elde edilir. Tabii blok okuması yapıldığında birden fazla satır tamponda bulunabilecektir. Bu durumda okuma sırasında tamponda nerede kalındığının da tutulması gerekir. Bu işlemi yapan klasik bir algoritma "Effective TCP/IP Programming" kitabında verilmiştir. Aşağıda bu biçimde CR/LF çifti görülene kadar soketten etkin bir biçimde yukarıda belirttiğimiz gibi okuma yapan bir fonksiyon örneği veriyoruz: ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } Fonksiyonun birinci parametresi okuma yapılacak soketi, ikinci ve üçüncü parametreleri okunacak satırın yerleştirileceği dizinin adresini ve uzunluğunu almaktadır. Buradaki dizinin sonunda her zaman CR/LF ve null karakter bulunacaktır. Fonksiyon başarı durumunda diziye yerleştirilen karakter sayısı ile (CR/LF dahil) geri dönmektedir. Karşı taraf soketi kapatmışsa ve hiçbir okuma yapılamamışsa bu durumda fonksiyon 0 ile geri dönmektedir. Bu durumda programcının verdiği dizinin içeriği kullanılmamalıdır. Mesajın sonunda CR/LF çifti olmadıktan sonra fonksiyon başarılı okuma yapmamaktadır. Fonksiyon başarısızlık durumunda -1 değerine geri döner ve errno uygun biçimde set edilir. Burada yazmış olduğumuz sock_readline fonksiyonu bir satır okunana kadar blokeye yol açmaktadır. Dolayısıyla çok client'lı server uygulamalarında select, poll ve epoll gibi modellerde bu fonksiyon bu haliyle kullanılamaz. Örneğin biz select fonksiyonunda bir grup soketi bekliyor olalım. Bir sokete bir satırın yarısı gelmiş olabilir. Bu durumda biz read_line fonksiyonunu çağırırsak bloke oluşacaktır. Tabii gerçi satırın geri kalan kısmı zaten kısa bir süre sonra gelecek olsa da bu durum yine bir kusur oluşturacaktır. UNIX/Linux, macOS ve Windows sistemlerinde yalnızca CR ('\r) karakteri imleci bulunulan satırın başına geçirmektedir. Dolayısıyla bir yazının sonunda CR/LF çifti varsa yazının ekrana bastırılmasında bir sorun oluşmayacaktır. Çünkü önce CR karakteri imleci bulunulan satırın başına geçirecek sonra LF karakteri aşağı satırın başına geçirecektir. Böylece yazının sonunda LF karakterinin bulunmasıyla CR/LF karakterlerinin bulunması arasında bir fark oluşmayacaktır. Aşağıdaki örnekte client program, server programa CR/LF ile sonlandırılmış bir yazı göndermektedir. Server program da bu yazıyı yukarıdaki sock_readline fonksiyonunu kullanarak okuyup ekrana (stdout dosyasına) yazdırmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 ssize_t sock_readline(int sock, char *str, size_t size); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; char buf[BUFFER_SIZE + 1]; char ntopbuf[INET_ADDRSTRLEN]; ssize_t result; int option; int server_port; int p_flag, err_flag; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); for (;;) { if ((result = sock_readline(client_sock, buf, BUFFER_SIZE)) == -1) exit_sys("sock_readline"); if (result == 0) break; if (!strcmp(buf, "quit\r\n")) break; buf[strlen(buf) - 2] = '\0'; printf("%jd byte(s) received: \"%s\"\n", (intmax_t)result, buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); close(server_sock); return 0; } ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE]; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; strcat(str, "\r\n"); if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit\r\n")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 108. Ders 05/01/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 109. Ders 07/01/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi matematiksel işlemler yapan bir client/server uygulama yazmak isteyelim. Bu multi-client bir uygulama olsun. Bunun için thread modelini kullanalım. (Çünkü bu tür uygulamalarda diğer modeller kullanılırken dikkat etmemiz gereken başka noktalar vardır. Diğer modellerde soketten okunan bilgilerin biriktirilmesi gerekmektedir. Bu biriktirmenin nasıl yapılabileceğini izleyen paragraflarda açıklayacağız.) Uygulama katmanı protokollerinde genel olarak client'ın server'a her gönderdiği mesaj için server client'a bir yanıt verir. Bu yanıt olumlu ise istenen işlemin yanıtıdır. Olumsuz ise bir hata yanıtıdır. Bu tür protokollerde protokolü tasarlayan baştan client'ın server'a, server'ın da client'a göndereceği mesajları belirlemelidir. Matematiksel işlemler yapan client/server programımızda client'ın server'a göndereceği mesajlar şunlar olabilir: "LOGIN \r\n" "ADD op1 op2\r\n" "SUB op1 op2\r\n" "MUL op1 op2\r\n" "DIV op1 op2\r\n" "SQRT op1\r\n" "POW op1\r\n" "LOGOUT\r\n" Server'ın client'a gönderdiği mesajlar da şunlar olabilir: "LOGIN_ACCEPTED\r\n" "LOGOUT_ACCEPTED\r\n" "RESULT result\r\n" "ERROR message\r\n" Bu tür uygulama katmanı protokollerinde fiziksel bağlantı ile mantıksal bağlantının karıştırılmaması gerekir. Bir client'ın server'a connect fonksiyonuyla bağlanması onun hizmet alacağı anlamına gelmemektedir. Onun hizmet alabilmesi için mantıksal bir bağlantının da sağlanması gerekir. Bizim protokolümüzde bu mantıksal bağlantı "LOGIN" ve "LOGIN_ACCEPTED" mesajlarıyla yapılmaktadır. Client, TCP'den bağlandıktan sonra server'a kullanıcı adını ve parolayı yollar. Server doğrulamayı yaparsa client'a "LOGIN_ACCEPTED\r\n" mesajını iletir. Eğer server doğrulamayı yapamazsa bu durumda örneğin "ERROR LOGIN_FAILED\r\n" gibi bir hata mesajıyla geri dönüp soketi kapatacaktır. Aşağıda çok client'lı matematiksel işlem yapan bir client-server uygulama kodu verilmiştir. Bu uygulamada "calc-server" programı her client bağlantısında bir thread açıp o thread yoluyla ilgili client ile konuşmaktadır. Yani buradaki server IO modeli olarak thread modelini kullanmaktadır. Bir client, server'a "kullanıcı adı" ve "parola" ile bağlanmaktadır. Server program bir CSV dosyasına bakarak kullanıcı adı ve parola bilgisini doğrulamaktadır. Bir client bağlandığında server program CLIENT_INFO isimli bir yapı türünden bir nesne yaratıp client'ın bilgilerini orada saklamaktadır. Aslında bu tür programlarda tüm client'ların bilgileri bir dizi ya da bağlı liste içerisinde tutulmalıdır. Çünkü server tüm client'lara belli bir mesajı göndermek isteyebilir. Server programı aşağıdaki gibi derleyebilirsiniz: gcc -Wall -o calc-server calc-server.c -lm Prototipleri içerisinde olan standart C fonksiyonları libc kütüphanesinde değildir. libm isimli ayrı bir kütüphanededir. Maalesef gcc otomatik olarak bu kütüphaneyi link aşamasına dahil etmemektedir. Bu nedenle matematiksel fonksiyonları kullanırken linker için -lm seçeneğinin bulundurulması gerekmektedir. Client program (calc-client.c) server'a fiziksel olarak TCP'den bağlandıktan sonra ona kullanıcı adı ve parolayı mesaj olarak gönderir. Sonra bir komut satırına düşer. Komutlar komut satırından verilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /* calc-server.c */ #include #include #include #include #include #include #include #include #include #include #include #include #include #define CREDENTIALS_PATH "credentials.csv" #define DEF_SERVER_PORT 55555 #define MAX_MSG_SIZE 4096 #define MAX_MSG_PARAMS 32 #define MAX_USER_NAME 64 #define MAX_PASSWORD 64 #define MAX_CREDENTIALS 1024 typedef struct tagCREDENTIAL { char user_name[MAX_USER_NAME]; char password[MAX_PASSWORD]; } CREDENTIAL; typedef struct tagCLIENT_INFO { int sock; struct sockaddr_in sin; char buf[MAX_MSG_SIZE + 1]; // MAX_MSG_SIZE is enough CREDENTIAL credential; } CLIENT_INFO; typedef struct tagMSG { char *params[MAX_MSG_PARAMS]; int count; } MSG; typedef struct tagCLIENT_MSG_PROC { char *msg; bool (*proc)(CLIENT_INFO *, const MSG *); } CLIENT_MSG_PROC; int read_credentials(void); void *client_thread_proc(void *param); ssize_t sock_readline(int sock, char *str, size_t size); void receive_msg(CLIENT_INFO *ci); void send_msg(CLIENT_INFO *ci, const char *msg); int is_empty_line(const char *line); void exit_client_thread(CLIENT_INFO *ci); void parse_msg(char *msg, MSG *msgs); bool login_proc(CLIENT_INFO *ci); bool add_proc(CLIENT_INFO *ci, const MSG *msg); bool sub_proc(CLIENT_INFO *ci, const MSG *msg); bool mul_proc(CLIENT_INFO *ci, const MSG *msg); bool div_proc(CLIENT_INFO *ci, const MSG *msg); bool sqrt_proc(CLIENT_INFO *ci, const MSG *msg); bool pow_proc(CLIENT_INFO *ci, const MSG *msg); bool logout_proc(CLIENT_INFO *ci, const MSG *msg); char *revstr(char *str); void exit_sys(const char *msg); CLIENT_MSG_PROC g_client_msgs[] = { {"ADD", add_proc}, {"SUB", sub_proc}, {"MUL", mul_proc}, {"DIV", div_proc}, {"SQRT", sqrt_proc}, {"POW", pow_proc}, {"LOGOUT", logout_proc}, {NULL, NULL} }; CREDENTIAL g_credentials[MAX_CREDENTIALS]; int g_ncredentials; /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; int option; int server_port; int p_flag, err_flag; pthread_t tid; CLIENT_INFO *ci; int result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (read_credentials() == -1) { fprintf(stderr, "cannot read credentials...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); for (;;) { sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); // printf("connected client ===> %s : %u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sin = sin_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } int read_credentials(void) { char buf[MAX_USER_NAME + MAX_PASSWORD + 32]; FILE *f; char *str; if ((f = fopen(CREDENTIALS_PATH, "r")) == NULL) return -1; g_ncredentials = 0; while (fgets(buf, MAX_USER_NAME + MAX_PASSWORD + 32, f) != NULL) { if (is_empty_line(buf) == 0) continue; if ((str = strtok(buf, ",")) == NULL) return -1; strcpy(g_credentials[g_ncredentials].user_name, str); if ((str = strtok(NULL, "\n")) == NULL) return -1; strcpy(g_credentials[g_ncredentials].password, str); if ((str = strtok(NULL, "\n")) != NULL) return -1; ++g_ncredentials; } fclose(f); return 0; } int is_empty_line(const char *line) { while (*line != '\0') { if (!isspace(*line)) return -1; ++line; } return 0; } void *client_thread_proc(void *param) { char ntopbuf[INET_ADDRSTRLEN]; unsigned port; CLIENT_INFO *ci = (CLIENT_INFO *)param; MSG msg; int i; inet_ntop(AF_INET, &ci->sin.sin_addr, ntopbuf, INET_ADDRSTRLEN); port = (unsigned)ntohs(ci->sin.sin_port); if (!login_proc(ci)) { send_msg(ci, "ERROR incorrect user name or password\r\n"); exit_client_thread(ci); } send_msg(ci, "LOGIN_ACCEPTED\r\n"); printf("client connected with user name \"%s\"\n", ci->credential.user_name); for (;;) { receive_msg(ci); *strchr(ci->buf, '\r') = '\0'; printf("Message from \"%s\": \"%s\"\n", ci->credential.user_name, ci->buf); parse_msg(ci->buf, &msg); if (msg.count == 0) { send_msg(ci, "ERROR empty command\r\n"); continue; } for (i = 0; g_client_msgs[i].msg != NULL; ++i) if (!strcmp(g_client_msgs[i].msg, msg.params[0])) { if (!g_client_msgs[i].proc(ci, &msg)) goto EXIT; break; } if (g_client_msgs[i].msg == NULL) { send_msg(ci, "ERROR invalid command\r\n"); } } printf("client disconnected %s : %u\n", ntopbuf, port); EXIT: shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } void receive_msg(CLIENT_INFO *ci) { ssize_t result; if ((result = sock_readline(ci->sock, ci->buf, MAX_MSG_SIZE)) == -1) { fprintf(stderr, "sock_readline: %s\n", strerror(errno)); exit_client_thread(ci); } if (result == 0) { fprintf(stderr, "sock_readline: client unexpectedly down...\n"); exit_client_thread(ci); } } void send_msg(CLIENT_INFO *ci, const char *msg) { if (send(ci->sock, msg, strlen(msg), 0) == -1) exit_client_thread(ci); } void exit_client_thread(CLIENT_INFO *ci) { shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); pthread_exit(NULL); } void parse_msg(char *buf, MSG *msg) { char *str; msg->count = 0; for (str = strtok(buf, " \r\n\t"); str != NULL; str = strtok(NULL, " \r\n\t")) msg->params[msg->count++] = str; msg->params[msg->count] = NULL; } bool login_proc(CLIENT_INFO *ci) { MSG msg; char *user_name, *password; receive_msg(ci); parse_msg(ci->buf, &msg); user_name = msg.params[1]; password = msg.params[2]; if (msg.count != 3) return false; if (strcmp(msg.params[0], "LOGIN") != 0) return false; for (int i = 0; i < g_ncredentials; ++i) if (strcmp(user_name, g_credentials[i].user_name) == 0 && strcmp(password, g_credentials[i].password) == 0) { ci->credential = g_credentials[i]; return true; } return false; } bool add_proc(CLIENT_INFO *ci, const MSG *msg) { double op1, op2, result; char buf[MAX_MSG_SIZE]; if (msg->count != 3) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op1 = atof(msg->params[1]); op2 = atof(msg->params[2]); result = op1 + op2; sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool sub_proc(CLIENT_INFO *ci, const MSG *msg) { double op1, op2, result; char buf[MAX_MSG_SIZE]; if (msg->count != 3) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op1 = atof(msg->params[1]); op2 = atof(msg->params[2]); result = op1 - op2; sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool mul_proc(CLIENT_INFO *ci, const MSG *msg) { double op1, op2, result; char buf[MAX_MSG_SIZE]; if (msg->count != 3) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op1 = atof(msg->params[1]); op2 = atof(msg->params[2]); result = op1 * op2; sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool div_proc(CLIENT_INFO *ci, const MSG *msg) { double op1, op2, result; char buf[MAX_MSG_SIZE]; if (msg->count != 3) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op1 = atof(msg->params[1]); op2 = atof(msg->params[2]); result = op1 / op2; sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool sqrt_proc(CLIENT_INFO *ci, const MSG *msg) { double op, result; char buf[MAX_MSG_SIZE]; if (msg->count != 2) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op = atof(msg->params[1]); result = sqrt(op); sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool pow_proc(CLIENT_INFO *ci, const MSG *msg) { double op1, op2, result; char buf[MAX_MSG_SIZE]; if (msg->count != 3) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op1 = atof(msg->params[1]); op2 = atof(msg->params[2]); result = pow(op1, op2); sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool logout_proc(CLIENT_INFO *ci, const MSG *msg) { send_msg(ci, "LOGOUT_ACCEPTED\r\n"); printf("%s logging out...\n", ci->credential.user_name); return false; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* calc-client.c */ #include #include #include #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define MAX_MSG_SIZE 4096 #define MAX_MSG_PARAMS 32 typedef struct tagMSG { char *params[MAX_MSG_PARAMS]; int count; } MSG; typedef struct tagSERVER_MSG_PROC { char *msg; bool (*proc)(const MSG *); } SERVER_MSG_PROC; ssize_t sock_readline(int sock, char *str, size_t size); void receive_msg(int sock, char *msg); void send_msg(int sock, const char *msg); void parse_msg(char *buf, MSG *msg); int parse_error(char *buf, MSG *msg); int login_attempt(int sock, const char *user_name, const char *password); bool result_proc(const MSG *msg); bool error_proc(const MSG *msg); bool logout_accepted_proc(const MSG *msg); void exit_sys(const char *msg); SERVER_MSG_PROC g_server_msgs[] = { {"ERROR", error_proc}, {"RESULT", result_proc}, {"LOGOUT_ACCEPTED", logout_accepted_proc}, {NULL, NULL} }; /* ./calc-client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[MAX_MSG_SIZE]; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; const char *user_name, *password; MSG msg; int i; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } user_name = argv[optind + 0]; password = argv[optind + 1]; if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); if (login_attempt(client_sock, user_name, password) == -1) goto EXIT; printf("connection successful...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, MAX_MSG_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (buf[strspn(buf, " \t")] == '\0') /* check if buf contains white spaces */ continue; strcat(str, "\r\n"); send_msg(client_sock, buf); receive_msg(client_sock, buf); parse_msg(buf, &msg); for (i = 0; g_server_msgs[i].msg != NULL; ++i) if (!strcmp(g_server_msgs[i].msg, msg.params[0])) { if (!g_server_msgs[i].proc(&msg)) goto EXIT; break; } } EXIT: shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } void receive_msg(int sock, char *msg) { ssize_t result; if ((result = sock_readline(sock, msg, MAX_MSG_SIZE)) == -1) exit_sys("receive_msg"); if (result == 0) { fprintf(stderr, "receive_msg: unexpectedly down...\n"); exit(EXIT_FAILURE); } } void send_msg(int sock, const char *msg) { if (send(sock, msg, strlen(msg), 0) == -1) exit_sys("send_msg"); } void parse_msg(char *buf, MSG *msg) { char *str; if (parse_error(buf, msg) == 0) return; msg->count = 0; for (str = strtok(buf, " \r\n\t"); str != NULL; str = strtok(NULL, " \r\n\t")) msg->params[msg->count++] = str; msg->params[msg->count] = NULL; } int parse_error(char *buf, MSG *msg) { while (isspace(*buf)) ++buf; if (!strncmp(buf, "ERROR", 5)) { buf += 5; while (isspace(*buf)) ++buf; *strchr(buf, '\r') = '\0'; msg->count = 2; msg->params[0] = "ERROR"; msg->params[1] = buf; return 0; } return -1; } int login_attempt(int sock, const char *user_name, const char *password) { char buf[MAX_MSG_SIZE]; MSG msg; sprintf(buf, "LOGIN %s %s\r\n", user_name, password); send_msg(sock, buf); receive_msg(sock, buf); parse_msg(buf, &msg); if (!strcmp(msg.params[0], "ERROR")) { fprintf(stderr, "login error: %s\n", msg.params[1]); return -1; } if (strcmp(msg.params[0], "LOGIN_ACCEPTED") != 0) { fprintf(stderr, "unexpected server message!...\n"); return -1; } return 0; } bool result_proc(const MSG *msg) { printf("%s\n", msg->params[1]); return true; } bool error_proc(const MSG *msg) { printf("Error: %s\n", msg->params[1]); return true; } bool logout_accepted_proc(const MSG *msg) { return false; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 110. Ders 12/01/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de IRC stili bir chat programı yazacağımızı düşünelim. Programlardaki IO modeli üzerinde durmayacağız. Yalnızca mesajlaşmalar üzerinde duracağız. Bu tür chat programlarında server kendisine bağlanan tüm client'ların bilgilerini tutmaktadır. Bir client bir yazıyı tüm diğer client'ların görmesi için server'a iletir. Server'da bunu client'lara iletir. Böyle basit bir chat uygulamasında client'tan server'a gönderilecek mesajlar şunlar olabilir: "LOGIN \r\n" "SEND_MESSAGE \r\n "LOGOUT\r\n" Server'dan client'a gönderilecek mesajlar şunlar olabilir: "LOGIN_ACCEPTED\r\n" "ACTIVE_USERS\r\n" "NEW_USER_LOGGEDIN \r\n" "USER_LOGGEDOUT \r\n" "LOGOUT_ACCEPTED\r\n" "DISTRIBUTE_MESSAGE \r\n" "LOGOUT_ACCEPTED\r\n" "ERROR " Çalışma akışı şöyle olabilir: - Client önce LOGIN mesajı ile server'a mantıksal bakımdan bağlanır. Server da bağlantıyı kabul ederse client'a LOGIN_ACCEPTED mesajını gönderir. Tabii oturuma yeni kullanıcı katıldığı için aynı zamanda server diğer tüm client'lara NEW_USER_LOGGEDIN yollar. Bağlanan client'a ise oturumdakilerin hepsinin listesini ACTIVE_USERS mesajı ile iletmektedir. - Client bir mesajın oturumdaki herkes tarafından görülmesini sağlamak amacıyla server'a SEND_MESSAGE mesajını gönderir. Server da bu mesajı oturumdaki tüm client'lara DISTRIBUTE_MESSAGE mesajıyla iletir. - Bir kullanıcı logout olmak istediğinde server'a LOGOUT mesajını gönderir. Server'da bunu kabul ederse client'a LOGOUT_ACCEPTED mesajını gönderir. Ancak client'ın logout olduğu bilgisinin oturumdaki diğer client'lara da iletilmesi gerekmektedir. Bunun için server tüm client'lara USER_LOGGED mesajını göndermelidir. - Yine bir hata durumunda server client'lara ERROR mesajı gönderebilir. Aslında chat programları için IP protocol ailesinde IRC (Internet Relay Chat) protokolü bulunmaktadır. IRC server ve client programlar Linux sistemlerinde zaten bulunmaktadır. Siz de bu protokolün dokümanlarını inceleyerek bu protokol için client ve/veya server programları yazabilirsiniz. IRC protokolü "RFC 1459" olarak dokümante edilmiştir. Başka kurumların da chat protokollerinin bazıları artık dokümante edilmiştir. Microsoft'un MSN Chat protokülünün dokümanlarına aşağıdaki adresten erişebilirsiniz: http://www.hypothetic.org/docs/msn/ ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Komut satırı tabanlı bir telnet, ssh benzeri client-server bir program yazmak isteyelim. Amacımız bir client'ın server makinede komut satırından işlem yapmasını sağlamak olsun. Yani client server'a bağlanacak ona shell komutları yollayacak, komutların çıktısını da görecek. Böyle bir programın server tarafının çatısı şöyle oluşturulabilir: - Client program bağlandığında server iki boru yaratır ve fork işlemi yapar. - fork işleminden sonra henüz exec işlemi yapmadan alt prosesin stdin betimleyicisini borunun birine, stdout betimleyicisini de diğerine yönlendirir. Böylece üst proses boruya yazma yaptığında aslında alt proses bunu stdin betimleyicisinden okuyacaktır. Benzer biçimde alt proses diğer boruya yazma yaptığında üst proses de bunu diğer borudan okuyabilecektir. - Bu yönlendirmelerden sonra server exec yaparak shell programını çalıştırır. - Client, server'a shell komutunu gönderdiğinde server komutu shell programına işletir, çıktısını elde eder ve client'a yollar. Aslında bu işlemi yapan iki standart protokol vardır: telnet ve ssh protokolleri. telnet protokolü güvenlik bakımından zayıf olduğu için günümüzde daha çok ssh kullanılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 111. Ders 14/01/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Telnet ya da ssh benzeri programı yazarken üst prosesin /bin/bash programını çalıştırıp ona komutlar yollaması ve ondan komutlar alması gerekmektedir. İşin bu kısmını yapan örnek bir program aşağıda verilmiştir. Aşağıdaki programda üst proses iki boru yaratmıştır. Sonra alt prosesi yaratarak exec uygulamıştır. Ancak üst proses henüz exec yapmadan alt prosesin stdin betimleyicisini ve stdout betimleyicisini boruya yönlendirmiştir. Böylece şöyle bir mekanizma oluşturulmuştur: Üst proses borulardan birine yazdığında sanki alt prosesin stdin dosyasına yazmış gibi olmaktadır. Üst proses diğer borudan okuma yaptığında alt prosesin stdout dosyasına yazılanları okumuş gibi olmaktadır. Aşağıdaki programda bazı kusurlar vardır. Örneğin: - Üst proses kabuğa komutu ilettikten sonra onun stdout ya da stdin dosyasına yazdıklarını okumaya çalışmaktadır. Ancak ne kadar bilginin okunacağı belli değildir. - Üst proses alt prosesin yazdıklarını okuyabilmek için biraz gecikme uygulamıştır. Ancak alt proses bu gecikmeden uzun süre çalışıyorsa program hatalı çalışır. ---------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include #include #include #define BUFFER_SIZE 65536 void exit_sys(const char *msg); int main(void) { pid_t pid; int fdsout[2]; int fdsin[2]; char buf[BUFFER_SIZE + 1]; ssize_t result; int status; if (pipe(fdsin) == -1) exit_sys("pipe"); if (pipe(fdsout) == -1) exit_sys("pipe"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { /* child */ close(fdsin[0]); close(fdsout[1]); if (dup2(fdsin[1], 1) == -1) _exit(EXIT_FAILURE); if (dup2(fdsin[1], 2) == -1) _exit(EXIT_FAILURE); if (dup2(fdsout[0], 0) == -1) _exit(EXIT_FAILURE); close(fdsin[1]); close(fdsout[0]); if (execl("/bin/bash", "/bin/bash", (char *)NULL) == -1) _exit(EXIT_FAILURE); /* unreachable code */ } /* parent process */ close(fdsin[1]); close(fdsout[0]); /* parent writes fdsout[1] and read fdsin[0] */ if (fcntl(fdsin[0], F_SETFL, fcntl(fdsin[0], F_GETFL)|O_NONBLOCK) == -1) exit_sys("fcntl"); for (;;) { printf("Command: "); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if (write(fdsout[1], buf, strlen(buf)) == -1) exit_sys("write"); if (!strcmp(buf, "exit\n")) break; usleep(300000); if ((result = read(fdsin[0], buf, BUFFER_SIZE)) == -1) { if (errno == EAGAIN) continue; exit_sys("read"); } buf[result] = '\0'; printf("%s", buf); } if (wait(&status) == -1) exit_sys("wait"); if (WIFEXITED(status)) printf("Shell exits normally with exit code: %d\n", WEXITSTATUS(status)); else printf("shell exits abnormally!...\n"); close(fdsin[0]); close(fdsout[1]); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 112. Ders 19/01/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- IP ailesinin uygulama katmanındaki Telnet, SSH, HTTP, POP3, SMTP gibi protokoller daha önceden de belirttiğimiz gibi hep yazısal işlem yapmaktadır. Yani bu protokollerde mesajlar birer yazı biçiminde sonu \r\n ile bitecek biçimde gönderilip alınmaktadır. Burada e-posta almak için kullanılan POP3 (Post Office Protocol Version 3) protokolü üzerinde kısaca duracağız. E-posta almak için yaygın kullanılan diğer protokol IMAP protokolüdür. IMAP protokolü, POP3 protokolünden daha güvenli ve ayrıntılı tasarlanmıştır. E-posta okuyucuları iki protokolü de kullanmaktadır. POP3 ve IMAP protokollerinin client program olduğuna dikkat ediniz. Client program, POP3 ve IMAP server'lara bağlanarak onlardan e-postaları almaktadır. Yani e-postaları tutan sunucu tarafıdır ve client onlara bağlanıp e-postaları yerel makineye çekmektedir. Tipik olarak e-posta gönderip alma işlemleri şöyle gerçekleştirilmektedir. 1) Bunun için bir e-posta sunucu programının bulunuyor olması gerekir. Eğer tüm sistemi siz kuruyorsanız bu sunucuyu (server) da sizin kurmanız gerekmektedir. Zaten Windows sistemlerinde, UNIX/Linux sistemlerinde bu sunucular hazır biçimde bulunmaktadır. Tabii eğer domain hizmetini aldığınız bir kurum varsa onlar da zaten e-posta hizmeti vermek için hazır e-posta sunucuları bulundurmaktadır. E-posta gönderebilmek için ya da e-posta alabilmek için bizim e-posta sunucusunun adresini biliyor olmamız gerekir. Gönderme işleminde kullanılacak sunucu ile alma işleminde kullanılacak sunucu farklı olabilmektedir. Örneğin CSD'nin e-posta sunucusuna "mail.csystem.org" adresiyle erişilebilmektedir. Bu sunucu hem gönderme hem de alma işlemini yapmaktadır. E-posta gönderebilmek için client program ile server program, "SMTP (Simple Mail Transfer Protocol)" denilen bir protokolle haberleşmektedir. O halde gönderim için bizim e-posta sunucusuna bağlanarak SMTP protokolü ile göndereceğimiz e-postayı ona iletmemiz gerekir. 2) Biz göndereceğimiz e-postayı SMTP protokolü ile e-posta sunucumuza ilettikten sonra bu sunucu hedef e-posta sunucusuna bu e-postayı yine SMTP protokolü ile iletmektedir. E-postayı alan sunucu bunu bir posta kutusu (mail box) içerisinde saklar. 3) Karşı taraftaki client program POP3 ya da IMAP protokolü ile kendi e-posta sunucuna bağlanarak posta kutusundaki e-postayı yerel makineye indirir. client ---SMTP---> e-posta sunucusu ---SMTP--> e-posta sunucusu ---POP3/IMAP---> client Görüldüğü gibi POP3 ve IMAP protokolleri e-posta sunucusunun posta kutusundaki zaten gelmiş ve saklanmış olan e-postaları yerel makineye indirmek için kullanılmaktadır. POP3 protokolü RFC 1939 dokümanlarında açıklanmıştır. Protokol kabaca şöyle işlemektedir: 1) Client program 110 numaralı (ya da 995 numaralı) porttan server'a TCP ile fiziksel olarak bağlanır. 2) Protokolde mesajlaşma tamamen text tabanlı ve satırsal biçimde yapılmaktadır. Satırlar CR/LF karakterleriyle sonlandırılmaktadır. Protokolde client'ın gönderdiği her komuta karşı server bir yanıt göndermektedir. (Fiziksel bağlantı sağlandığında da server bir onay mesajı gönderir.) Eğer yanıt olumluysa mesaj "+OK" ile, eğer yanıt olumsuzsa mesaj "-ERR" ile başlatılmaktadır. Yani server'ın client'a gönderdiği mesajın genel biçimi şöyledir: +OK [diğer bilgiler] CR/LF -ERR [diğer bilgiler] CR/LF 3) Fiziksel bağlantıdan sonra client program mantıksal olarak server'a login olmalıdır. Login olmak için önce "user name" sonra da "password" gönderilmektedir. User name ve password gönderme işlemi aşağıdaki iki komutla yapılmaktadır. "USER CR/LF" "PASS CR/LF" Kullanıcı adı e-posta adresiyle aynıdır. Örneğin biz "test@csystem.org" için e-posta sunucusuna bağlanıyorsak buradaki kullanıcı ismi "test@csystem.org" olacaktır. Parola e-postalarınızı okumak için kullandığınız paroladır. Sisteme başarılı bir biçimde login olduğumuzu varsayıyoruz. Tipik olarak server bize şu mesajı iletecektir: +OK Logged in. Eğer password yanlış girilmişse yeniden önce user name ve sonra password gönderilmelidir. 4) Client program LIST komutunu göndererek e-posta kutusundaki mesaj bilgilerini elde eder. LIST komutuna karşılık server önce aşağıdaki gibi bir satır gönderir: +OK 6 messages: Burada server e-posta kutusunda kaç e-posta olduğunu belirtmektedir. Sonra her e-postaya bir numara vererek onların byte uzunluklarını satır satır iletir. Komut yalnızca '.' içeren bir satırla son bulmaktadır. Örneğin: +OK 6 messages: 1 1565 2 5912 3 11890 4 4920 5 9714 6 4932 . 5) Belli bir e-posta RETR komutuyla elde edilmektedir. Bu komuta elde edilecek e-postanın index numarası girilir. Örneğin: "RETR 2 CR/LF" RETR komutuna karşı server önce aşağıdaki gibi bir satır gönderir: +OK 5912 octets Burada programcı bu satırı parse ederek burada belirtilen miktarda byte kadar soketten okuma yapmalıdır. Anımsanacağı gibi porttan tam olarak n byte okumak TCP'de tek bir recv ile yapılamamaktadır. 6) Mesajı silmek için DELE komutu kullanılır. Komuta parametre olarak silinecek mesajın indeks numarası girilmektedir. Örneğin: "DELE 3 CR/LF" Bu komut uygulandığında server henüz e-postayı posta kutusundan silmez. Yalnızca onu "silinecek" biçiminde işaretler. Silme işlemi QUIT komutuyla oturum sonlandırıldığında yapılmaktadır. Eğer client silme eyleminden pişmanlık duyarsa RSET komutuyla ilk duruma gelir. RSET komutu logout yapmaz. Yalnızca silinmiş olarak işaretlenenlerin işaretlerini kaldırır. 7) STAT komutu o anda e-posta kutusundaki e-posta sayısını bize vermektedir. Bu komut gönderildiğinde aşağıdaki gibi bir yanıt alınacaktır: +OK 5 27043 Burada server e-posta kutusunda toplam 5 e-postanın bulunduğunu ve bunların byte uzunluklarının da 27043 olduğunu söylemektedir. 8) Protocol client programın QUIT komutunu göndermesiyle sonlandırılmaktadır. Örneğin: "QUIT CR/LF" 9) POP3 protokolününde client belli bir süre server'a hiç mesaj göndermezse, server client'ın soketini kapatıp bağlantıyı koparmaktadır. Her ne kadar RFC 1939'da server'ın en azından 10 dakika beklemesi gerektiği söylenmişse de server'ların çoğu çok daha az bir süre beklemektedir. POP3 protokolünde client programın gönderdiği yazısal komutlar için server programın gönderdiği yanıtlar parse edilerek tam gerektiği kadar okuma yapılabilir. Ancak aşağıdaki programda biz basitlik sağlamak amacıyla server'dan gelen mesajları başka bir thread ile ele aldık. ---------------------------------------------------------------------------------------------------------------------------*/ /* pop3.c */ #include #include #include #include #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "mail.csystem.org" #define DEF_SERVER_PORT "110" #define BUFFER_SIZE 4096 ssize_t sock_readline(int sock, char *str, size_t size); void *thread_proc(void *param); void send_msg(int sock, const char *msg); void exit_sys_thread(const char *msg, int err); void exit_sys(const char *msg); /* ./pop3 [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; char buf[BUFFER_SIZE + 1]; pthread_t tid; int result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind != 0) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); if ((result = pthread_create(&tid, NULL, thread_proc, (void *)client_sock)) != 0) exit_sys_thread("pthread_create", result); usleep(500000); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (buf[strspn(buf, " \t")] == '\0') /* check if buf contains white spaces */ continue; strcat(str, "\r\n"); send_msg(client_sock, buf); sleep(1); if (!strcmp(buf, "QUIT\r\n")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_thread("pthread_join", result); return 0; } void *thread_proc(void *param) { int sock = (int)param; char buf[BUFFER_SIZE]; int result; for (;;) { if ((result = sock_readline(sock, buf, BUFFER_SIZE)) == -1) exit_sys("receive_msg"); if (result == 0) { printf("closing connection...\n"); break; } printf("%s", buf); } exit(EXIT_SUCCESS); return NULL; } ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } void send_msg(int sock, const char *msg) { if (send(sock, msg, strlen(msg), 0) == -1) exit_sys("send_msg"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_thread(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 113. Ders 21/01/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında POP3 client programda her mesaja karşılık server'ın nasıl bir yazı gönderdiği bilindiğine göre buradan hareketle hiç thread oluşturmadan gönderilen komut için yanıt elde edilebilir. Aşağıda bu fikre bir örnek verilmiştir. Örneğimizde LIST ve RETR komutları özel olarak ele alınmıştır. LIST komutunda server'ın listeyi ilettikten sonra son satırda "." gönderdiğini anımsayınız. Biz de aşağıda programda satırda "." görene kadar okuma yaptık. RETR komutunda +OK yazısından sonra mesajdaki byte sayısının da gönderildiğini anımsayınız. Biz de bundan faydalanarak soketten o kadar byte okuduk. ---------------------------------------------------------------------------------------------------------------------------*/ /* pop3.c */ #include #include #include #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "mail.csystem.org" #define DEF_SERVER_PORT "110" #define BUFFER_SIZE 4096 ssize_t sock_readline(int sock, char *str, size_t size); void send_msg(int sock, const char *msg); void receive_msg(int sock, char *msg); void getcmd(const char *buf, char *cmd); void proc_list(int sock); void proc_retr(int sock); void exit_sys(const char *msg); /* ./pop3 [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; char buf[BUFFER_SIZE + 1]; char cmd[BUFFER_SIZE]; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind != 0) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); receive_msg(client_sock, buf); printf("%s", buf); freeaddrinfo(res); usleep(500000); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (buf[strspn(buf, " \t")] == '\0') /* check if buf contains white spaces */ continue; strcat(str, "\r\n"); send_msg(client_sock, buf); getcmd(buf, cmd); if (!strcmp(cmd, "LIST")) proc_list(client_sock); else if (!strcmp(cmd, "RETR")) proc_retr(client_sock); else { receive_msg(client_sock, buf); printf("%s", buf); } if (!strcmp(cmd, "QUIT")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } void send_msg(int sock, const char *msg) { if (send(sock, msg, strlen(msg), 0) == -1) exit_sys("send_msg"); } void receive_msg(int sock, char *msg) { ssize_t result; if ((result = sock_readline(sock, msg, BUFFER_SIZE)) == -1) exit_sys("receive_msg"); if (result == 0) { fprintf(stderr, "receive_msg: unexpectedly down...\n"); exit(EXIT_FAILURE); } } void getcmd(const char *buf, char *cmd) { int i; for (i = 0; buf[i] != '\0' && !isspace(buf[i]); ++i) cmd[i] = buf[i]; cmd[i] = '\0'; } void proc_retr(int sock) { ssize_t result; char bufrecv[BUFFER_SIZE + 1]; ssize_t n; int i, ch; for (i = 0;; ++i) { if ((result = recv(sock, &ch, 1, 0)) == -1) exit_sys("sock_readline"); if (result == 0) return; if ((bufrecv[i] = ch) == '\n') break; } bufrecv[i] = '\0'; printf("%s\n", bufrecv); n = (ssize_t)strtol(bufrecv + 3, NULL, 10); while (n > 0) { if ((result = recv(sock, bufrecv, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; bufrecv[result] = '\0'; printf("%s", bufrecv); fflush(stdout); n -= result; } } void proc_list(int sock) { ssize_t result; char bufrecv[BUFFER_SIZE]; do { if ((result = sock_readline(sock, bufrecv, BUFFER_SIZE)) == -1) exit_sys("sock_readline"); if (result == 0) break; printf("%s", bufrecv); } while (*bufrecv != '.'); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi biz bir e-postaya bir resim ya da dosya iliştirirsek ne olacaktır? POP3 protokolü çok eski bir protokoldür. Internet'in uygulama katmanındaki ilk protokollerden biridir. Bu protokolde her şey yazı gibi gönderilip alınmaktadır. Dolayısıyla kullanıcı e-postasına bir resim ya da dosya iliştirdiğinde onun içeriği yazıya dönüştürülerek sanki bir yazıymış gibi gönderilmektedir. Pekiyi e-postanın bu gibi farklı içerikleri posta okuyan client tarafından nasıl ayrıştırılacaktır? İşte bir yazı içerisinde değişik içerikler MIME denilen sistemle başlıklandırılmaktadır. E-postaları içeriklerine ayrıştırabilmek için ilgili içeriklerin nasıl yazıya dönüştürüldüğünü ve nasıl geri dönüşüm yapıldığını bilmeniz gerekmektedir. Bunun için Base 64 denilen yöntem kullanılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Soketler yaratıldıktan sonra onların bazı özellikleri setsockopt isimli fonksiyonla değiştirilebilir ve getsockopt isimli fonksiyonla da elde edilebilir. setsockopt fonksiyonunun prototipi şöyledir: #include int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len); Fonksiyonun birinci parametresi özelliği değiştirilecek soketi belirtir. İkinci parametresi değişimin hangi düzeyde yapılacağını belirten bir sembolik sabit biçiminde girilir. Soket düzeyi için tipik olarak SOL_SOCKET girilmelidir. Üçüncü parametre hangi özelliğin değiştirileceğini belirtmektedir. Dördüncü parametre değiştirilecek özelliğin değerinin bulunduğu nesnenin adresini almaktadır. Son parametre dördüncü parametredeki nesnenin uzunluğunu belirtmektedir. Fonksiyon başarı durumunda 0, başarısızlık durumunda -1 değerine geri döner. Soket seçeneğini elde etmek için de getsockopt fonksiyonu kullanılmaktadır: #include int getsockopt(int socket, int level, int option_name, void *restrict option_value, socklen_t *restrict option_len); Parametreler setsockopt'ta olduğu gibidir. Yalnızca dördüncü parametrenin yönü değişiktir ve beşinci parametre gösterici almaktadır. Tipik soket seçenekleri (üçüncü parametre) şunlardan biri olabilir: SO_ACCEPTCONN SO_BROADCAST SO_DEBUG SO_DONTROUTE SO_ERROR SO_KEEPALIVE SO_LINGER SO_OOBINLINE SO_RCVBUF SO_RCVLOWAT SO_RCVTIMEO SO_REUSEADDR SO_SNDBUF SO_SNDLOWAT SO_SNDTIMEO SO_TYPE Burada bizim için şimdilik önemli olan birkaç seçenek vardır: SO_BROADCAST, SO_OOBLINE, SO_SNDBUF, SO_RECVBUF, SO_REUSEADDR. Soket seçeneğinin değiştirilmesi aşağıdaki örnekte olduğu gibi yapılabilir. Örneğin: int buflen; int optsize = sizeof(int) ... if (getsockopt(sock_client, SOL_SOCKET, SO_RCVBUF, &buflen, &optsize) == -1) exit_sys("getsockopt"); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 114. Ders 26/01/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- SO_REUSEADDR seçeneği belli bir port için bind işlemi yapmış bir server'ın client bağlantısı sağladıktan sonra sonlanması sonucunda bu server'ın yeniden çalıştırılıp aynı portu bind edebilmesi için kullanılmaktadır. Bir portu bind eden server, bir client ile bağlandıktan sonra çökerse, ya da herhangi bir biçimde sonlanırsa işletim sistemleri o portun yeniden belli bir süre bind edilmesini engellemektedir. Bunun nedeni eski çalışan server ile yeni çalışacak olan server'ın göndereceği ve alacağı paketlerin karışabilme olasılığıdır. Eski bağlantıda yollanmış olan paketlerin ağda maksimum bir geçerlilik süresi vardır. İşletim sistemi de bunun iki katı kadar bir süre (2 dakika civarı, neden iki katı olduğu protokolün aşağı seviyeli çalışması ile ilgilidir) bu portun yeniden bind edilmesini engellemektedir. İşte eğer SO_REUSEADDR soket seçeneği kullanılırsa artık sonlanan ya da çöken bir server hemen yeniden çalıştırıldığında bind işlemi sırasında "Address already in use" biçiminde bir hata ile karşılaşılmayacaktır. Bu soket seçeneğini aşağıdaki gibi setsockopt fonksiyonu ile set edebiliriz: int sockopt = 1; ... if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt)) == -1) exit_sys("setsockopt"); SO_REUSEADDR seçeneğini set etmek için int bir nesne alıp onun içerisine sıfır dışı bir değer yerleştirip, onun adresini setsockopt fonksiyonunun dördüncü parametresine girmek gerekir. Bu nesneye 0 girip fonksiyonu çağırırsak bu özelliği kapatmış oluruz. SO_REUSEADDR bayrağı daha önce bir program tarafından bind edilmiş soketin ikinci kez diğer bir program tarafından bind edilmesi için kullanılmamaktadır. Eğer böyle bir ihtiyaç varsa (nadiren olabilir) Linux'ta (fakat POSIX'te değil) SO_REUSEPORT soket seçeneği kullanılmalıdır. Bu soket seçeneği benzer biçimde Windows sistemlerinde SO_EXCLUSIVEADDRUSE biçimindedir. Yani bu soket seçenekleri kullanıldığında aynı port birden fazla server tarafından bind edilip aynı anda kullanılabilir. Bu bayraklarla birden fazla proses aynı portu bind ettiğinde bir client'tan bu porta connect işlemi yapıldığı zaman işletim sistemi belli bir load balancing yaparak bağlantının server'lardan biri tarafından kabul edilmesini sağlayacaktır. Aşağıdaki server programını client ile bağlandıktan sonra Ctrl+C ile sonlandırınız. Sonra yeniden çalıştırmaya çalışınız. SO_REUSEADDR seçeneği kullanıldığından dolayı bir sorun ile karşılaşılmayacaktır. Daha sonra server programdan o kısmı silerek yeniden denemeyi yapınız. Örneğimizdeki client program server adresini ve port numarasını, server program ise yalnızca port numarasını komut satırı argümanı olarak almaktadır. Programları farklı terminallerde aşağıdaki gibi çalıştırabilirsiniz: ./server 55555 ./client localhost 555555 ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock, sock_client; struct sockaddr_in sinaddr, sinaddr_client; socklen_t sinaddr_len; char ntopbuf[INET_ADDRSTRLEN]; in_port_t port; ssize_t result; char buf[BUFFER_SIZE + 1]; int sockopt = 1; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } port = (in_port_t)strtoul(argv[1], NULL, 10); if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt)) == -1) exit_sys("setsockopt"); sinaddr.sin_family = AF_INET; sinaddr.sin_port = htons(port); sinaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(sock, (struct sockaddr *)&sinaddr, sizeof(sinaddr)) == -1) exit_sys("bind"); if (listen(sock, 8) == -1) exit_sys("listen"); printf("Waiting for connection...\n"); sinaddr_len = sizeof(sinaddr_client); if ((sock_client = accept(sock, (struct sockaddr *)&sinaddr_client, &sinaddr_len)) == -1) exit_sys("accept"); printf("Connected: %s : %u\n", inet_ntop(AF_INET, &sinaddr_client, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sinaddr_client.sin_port)); for (;;) { if ((result = recv(sock_client, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%ld bytes received from %s (%u): %s\n", (long)result, ntopbuf, (unsigned)ntohs(sinaddr_client.sin_port), buf); } shutdown(sock_client, SHUT_RDWR); close(sock_client); close(sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock; struct addrinfo *ai, *ri; struct addrinfo hints = {0}; char buf[BUFFER_SIZE]; char *str; int result; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; if ((result = getaddrinfo(argv[1], argv[2], &hints, &ai)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(result)); exit(EXIT_FAILURE); } for (ri = ai; ri != NULL; ri = ri->ai_next) if (connect(sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(ai); printf("Connected...\n"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((send(sock, buf, strlen(buf), 0)) == -1) exit_sys("send"); } shutdown(sock, SHUT_RDWR); close(sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- OOB Verisi (Out-Of-Band Data) bazı stream protokollerinde olan bir özelliktir. Örneğin TCP protokolü OOB verisini 1 byte olarak desteklemektedir. OOB verisine TCP'de "Acil (Urgent)" veri de denilmektedir. Bunun amacı OOB verisinin normal stream sırasında değil daha önce gönderilenlerin -eğer hedef host'ta henüz onlar okunmamışsa- önünde ele alınabilmesidir. Yani biz TCP'de birtakım verileri gönderdikten sonra OOB verisini gönderirsek bu veri önce göndermiş olduklarımızdan daha önde işleme sokulabilir. Böylece OOB verisi uygulamalarda önce gönderilen birtakım bilgilerin iptal edilmesi gibi gerekçelerle kullanılabilmektedir. OOB verisini gönderebilmek için send fonksiyonunun flags parametresine MSG_OOB bayrağını girmek gerekir. Tabii TCP yalnızca 1 byte uzunluğunda OOB verisinin gönderilmesine izin vermektedir. Bu durumda eğer send ile birden fazla byte MSG_OOB bayrağı ile gönderilmek istenirse gönderilenlerin yalnızca son byte'ı OOB olarak gönderilir. Son byte'tan önceki tüm byte'lar normal veri olarak gönderilmektedir. Normal olarak OOB verisi recv fonksiyonunda MSG_OOB bayrağı ile alınmaktadır. Ancak bu bayrak kullanılarak recv çağrıldığında eğer bir OOB verisi sırada yoksa recv başarısız olmaktadır. recv fonksiyonunun MSG_OOB bayraklı çağrısında başarılı olabilmesi için o anda bir OOB verisinin gelmiş olması gerekir. Pekiyi OOB verisinin geldiğini nasıl anlarız? İşte tipik yöntem SIGURG sinyalinin kullanılmasıdır. Çünkü sokete bir OOB verisi geldiğinde işletim sistemi SIGURG sinyali oluşturabilmektedir. Bu sinyalin default durumu IGNORE biçimindedir. (Yani ilgili proses eğer bu sinyali set etmemişse sanki sinyal oluşmamış gibi bir davranış gözükür.) Ancak default olarak OOB verisi geldiğinde sinyal oluşmamaktadır. Bunu mümkün hale getirmek için soket üzerinde fcntl fonksiyonu ile F_SETOWN komut kodunu kullanarak set işlemi yapmak gerekir. fcntl fonksiyonunun son parametresi bu durumda sinyalin gönderileceği prosesin id değeri olarak girilmelidir. Eğer bu parametre negatif bir proses grup id'si olarak girilirse bu durumda işletim sistemi bu proses grubunun bütün üyelerine bu sinyali gönderir. Tabii tipik olarak sinyalin soket betimleyicisine sahip olan prosese gönderilmesi istenir. Bu işlem şöyle yapılabilir: if (fcntl(sock_client, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); Aşağıdaki server programda bir OOB verisi geldiğinde SIGURG sinyali oluşturulmaktadır. Bu sinyalin içerisinde recv fonksiyonu MSG_OOB bayrağı ile çağrılmıştır. OOB verisinin okunması için MSG_OOB bayrağı gerekir. Ancak OOB verisinin olmadığı bir durumda bu bayrak kullanılırsa recv başarısız olmaktadır. O halde SIGURG sinyali geldiğinde recv fonksiyonu MSG_OOB bayrağı ile çağrılmalıdır. Bu durumda TCP'de her zaman yalnızca 1 byte okunabilmektedir. Ayrıca server programda SIGURG sinyali set edilirken sigaction yapısının flags parametresinin SA_RESTART biçiminde geçildiğine dikkat ediniz. Bu recv üzerinde beklerken oluşabilecek SIGURG sinyalinden sonra recv'in otomatik yeniden başlatılması için kullanılmıştır. Yine buradaki server program port numarasını, client program ise server adresini ve port numarasını komut satırı argümanı olarak almaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* oobserver.c */ #include #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void sigurg_handler(int sno); void exit_sys(const char *msg); int sock_client; int main(int argc, char *argv[]) { int sock; struct sockaddr_in sinaddr, sinaddr_client; socklen_t sinaddr_len; char ntopbuf[INET_ADDRSTRLEN]; in_port_t port; ssize_t result; char buf[BUFFER_SIZE + 1]; struct sigaction sa; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } sa.sa_handler = sigurg_handler; sa.sa_flags = SA_RESTART; sigemptyset(&sa.sa_mask); if (sigaction(SIGURG, &sa, NULL) == -1) exit_sys("sigaction"); port = (in_port_t)strtoul(argv[1], NULL, 10); if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sinaddr.sin_family = AF_INET; sinaddr.sin_port = htons(port); sinaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(sock, (struct sockaddr *)&sinaddr, sizeof(sinaddr)) == -1) exit_sys("bind"); if (listen(sock, 8) == -1) exit_sys("listen"); printf("Waiting for connection...\n"); sinaddr_len = sizeof(sinaddr_client); if ((sock_client = accept(sock, (struct sockaddr *)&sinaddr_client, &sinaddr_len)) == -1) exit_sys("accept"); if (fcntl(sock_client, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); printf("Connected: %s : %u\n", inet_ntop(AF_INET, &sinaddr_client, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sinaddr_client.sin_port)); for (;;) { if ((result = recv(sock_client, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%ld bytes received from %s (%u): %s\n", (long)result, ntopbuf, (unsigned)ntohs(sinaddr_client.sin_port), buf); } shutdown(sock_client, SHUT_RDWR); close(sock_client); close(sock); return 0; } void sigurg_handler(int sno) { char oob; if (recv(sock_client, &oob, 1, MSG_OOB) == -1) exit_sys("recv"); printf("OOB Data received: %c\n", oob); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* oobclient.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock; struct addrinfo *ai, *ri; struct addrinfo hints = {0}; char buf[BUFFER_SIZE]; char *str; int result; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; if ((result = getaddrinfo(argv[1], argv[2], &hints, &ai)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(result)); exit(EXIT_FAILURE); } for (ri = ai; ri != NULL; ri = ri->ai_next) if (connect(sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(ai); printf("Connected...\n"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((send(sock, buf, strlen(buf), buf[0] == 'u' ? MSG_OOB : 0)) == -1) exit_sys("send"); } shutdown(sock, SHUT_RDWR); close(sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Soket fonksiyonlarını kullanarak aynı makinenin prosesleri arasında haberleşme yapabiliriz. Ancak bunun için IP protokol ailesinin kullanılması oldukça yavaş bir haberleşme sağlamaktadır. İşte aynı makinenin prosesleri arasında soket fonksiyonlarını kullanarak hızlı bir biçimde haberleşmenin sağlanabilmesi için ismine "UNIX Domain Socket" denilen bir soket haberleşmesi oluşturulmuştur. Her ne kadar bu soket haberleşmesinin isminde UNIX geçiyorsa da bu soketler Windows sistemleri ve macOS sistemleri tarafından da desteklenmektedir. UNIX domain soket yaratabilmek için socket fonksiyonunun birinci parametresi (protocol family) AF_UNIX geçilmelidir. UNIX domain soketlerin TCP/IP ya da UDP/IP soketlerle bir ilgisi yoktur. Bu soketler UNIX/Linux sistemlerinde oldukça etkin bir biçimde gerçekleştirilmektedir. Dolayısıyla aynı makinenin prosesleri arasında haberleşmede borulara, mesaj kuyruklarına, paylaşılan bellek alanlarına bir seçenek olarak kullanılabilmektedir. Hatta bazı UNIX türevi sistemlerde (ama Linux'ta böyle değil) aslında çekirdek tarafından önce bu protokol gerçekleştirilip daha sonra boru mekanizması bu protokol kullanılarak gerçekleştirilmektedir. Böylece örneğin aynı makinedeki iki prosesin haberleşmesi için UNIX domain soketler TCP/IP ve UDP/IP soketlerine göre çok daha hızlı çalışmaktadır. Aynı makine üzerinde çok client'lı uygulamalar için UNIX domain soketler boru haberleşmesine ve mesaj kuyruklarına göre organizasyonel avantaj bakımından tercih edilebilmektedir. Çünkü çok client'lı boru uygulamalarını ve mesaj kuyruğu uygulamalarını yazmak daha zahmetlidir. Programcılar TCP/IP ve UDP/IP soket haberleşmesi yaparken kullandıkları fonksiyonların aynısını UNIX domain soketlerde de kullanabilmektedir. Böylece örneğin elimizde bir TCP/IP ya da UDP/IP client-server program varsa bu programı kolaylıkla UNIX domain soket kullanılacak biçimde değiştirebiliriz. UNIX domain soketlerin kullanımı en çok boru kullanımına benzemektedir. Ancak UNIX domain soketlerin borulara olan bir üstünlüğü "full duplex" haberleşme sunmasıdır. Bilindiği gibi borular "half duplex" bir haberleşme sunmaktadır. Ancak genel olarak boru haberleşmeleri UNIX domain soket haberleşmelere göre daha hızlı olma eğilimindedir. UNIX domain soketler kullanım olarak daha önce görmüş olduğumuz TCP/IP ve UDP/IP soketlerine çok benzemektedir. Yani işlemler sanki TCP/IP ya da UDP/IP client server program yazılıyormuş gibi yapılır. Başka bir deyişle UNIX domain soketlerinde client ve server programların genel yazım adımları TCP/IP ve UDP/IP ile aynıdır. UNIX domain soketlerde client'ın server'a bağlanması için gereken adres bir dosya ismi yani yol ifadesi biçimindedir. Kullanılacak yapı sockaddr_in değil, sockaddr_un yapısıdır. Bu yapı dosyası içerisinde bildirilmiştir ve en azından şu elemanlara sahip olmak zorundadır: #include struct sockaddr_un { sa_family_t sun_family; char sun_path[108]; }; Yapının sun_family elemanı AF_UNIX biçiminde, sun_path elemanı da soketi temsil eden dosyanın yol ifadesi biçiminde girilmelidir. Burada yol ifadesiyle belirtilen dosya bind işlemi tarafından yaratılmaktadır. Yaratılan bu dosyanın türü "ls -l komutunda" "(s)ocket" biçiminde görüntülenmektedir. Eğer bu dosya zaten varsa bind fonksiyonu başarısız olur. Dolayısıyla bu dosyanın varsa silinmesi gerekmektedir. O halde client ve server programlar işin başında bir isim altında anlaşmalıdır. Önemli bir nokta da şudur: sockaddr_un yapısının kullanılmadan önce sıfırlanması gerekmektedir. bind tarafından yaratılan bu soket dosyaları normal bir dosya değildir. Yani open fonksiyonuyla açılamamaktadır. UNIX domain soketlerde port numarası biçiminde bir kavramın olmadığına dikkat ediniz. Port numarası kavramı IP ailesinin aktarım katmanına ilişkin bir kavramdır. UNIX domain soketler AF_UNIX protokol ailesi ismiyle oluşturulan başka bir ailenin soketleridir. Pekiyi stream tabanlı UNIX domain soketlerde server accept uyguladığında client'a ilişkin sockaddr_un yapısından ne almaktadır? Aslında bu protokolde bir port kavramı olmadığına göre server bağlantıdan bir bilgi elde etmeyecektir. Fakat yine de client program da bind uygulayıp ondan sonra sokete bağlanabilir. Bu durumda server, client bağlantısından sonra sockaddr_un yapısından client'ın bind ettiği soket dosyasının yol ifadesini elde eder. Aşağıdaki örnekte çok client'lı bir UNIX domain soket örneği verilmiştir. Örneğimizdeki server programda thread modeli kullanılmıştır. Yani server her client bağlantısında bir thread yaratmaktadır. Bu programların her ikisinde de komut satırı argümanı olarak soket dosyasının yol ifadesi alınmaktadır. Bir soket dosyası zaten var ise bind işleminin başarısız olacağını anımsayınız. ---------------------------------------------------------------------------------------------------------------------------*/ /* uds-server.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 typedef struct tagCLIENT_INFO { int sock; struct sockaddr_un sun; } CLIENT_INFO; void *client_thread_proc(void *param); char *revstr(char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_un sun_server, sun_client; socklen_t sun_len; CLIENT_INFO *ci; ssize_t result; pthread_t tid; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((server_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (bind(server_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); for (;;) { printf("waiting for connection...\n"); sun_len = sizeof(sun_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sun_client, &sun_len)) == -1) exit_sys("accept"); printf("Connected new client\n"); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sun = sun_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } void *client_thread_proc(void *param) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough CLIENT_INFO *ci = (CLIENT_INFO *)param; ssize_t result; for (;;) { if ((result = recv(ci->sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received: %s\n", (intmax_t)result, buf); revstr(buf); if (send(ci->sock, buf, result, 0) == -1) exit_sys("send"); } printf("client disconnected...\n"); shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* uds-client.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int client_sock; struct sockaddr_un sun_server; ssize_t result; char buf[BUFFER_SIZE + 1]; char *str; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((client_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (connect(client_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("connect"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if ((send(client_sock, buf, strlen(buf), 0)) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%ld bytes received: %s\n", (long)result, buf); } close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 115. Ders 28/01/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte client program da bind işlemi uygulamaktadır. Böylece server client'ın soket ismini accept fonksiyonundan elde edebilmektedir. Ancak uygulamada client'ın bu biçimde bind yapması genellikle tercih edilmemektedir. Eğer client'a bir isim verilecekse sonraki paragrafta açıklanacağı gibi "soyut bir isim" verilmelidir. Buradaki örneğimizde yine server program socket dosyasının yol ifadesi ile çalıştırılmalıdır. Client program da hem server soketin hem de client soketin yol ifadesi ile çalıştırılmalıdır. Örneğin: ./uds-server serversock ./uds-client serversock clientsock ---------------------------------------------------------------------------------------------------------------------------*/ /* uds-server.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 typedef struct tagCLIENT_INFO { int sock; struct sockaddr_un sun; } CLIENT_INFO; void *client_thread_proc(void *param); char *revstr(char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_un sun_server, sun_client; socklen_t sun_len; CLIENT_INFO *ci; ssize_t result; pthread_t tid; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((server_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (bind(server_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); for (;;) { printf("waiting for connection...\n"); sun_len = sizeof(sun_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sun_client, &sun_len)) == -1) exit_sys("accept"); printf("Connected new client: %s\n", sun_client.sun_path); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sun = sun_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } void *client_thread_proc(void *param) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough CLIENT_INFO *ci = (CLIENT_INFO *)param; ssize_t result; for (;;) { if ((result = recv(ci->sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received from \"%s\": %s\n", (intmax_t)result, ci->sun.sun_path, buf); revstr(buf); if (send(ci->sock, buf, result, 0) == -1) exit_sys("send"); } printf("\"%s\" client disconnected...\n", ci->sun.sun_path); shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* uds-client.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int client_sock; struct sockaddr_un sun_server, sun_client; ssize_t result; char buf[BUFFER_SIZE + 1]; char *str; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((client_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_client, 0, sizeof(sun_client)); sun_client.sun_family = AF_UNIX; strcpy(sun_client.sun_path, argv[2]); if (bind(client_sock, (struct sockaddr *)&sun_client, sizeof(sun_client)) == -1) exit_sys("bind"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (connect(client_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("connect"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if ((send(client_sock, buf, strlen(buf), 0)) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%ld bytes received: %s\n", (long)result, buf); } close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aslında Linux sistemlerinde client'ın server'a kendini tanıtması için soyut (abstract) bir adres de oluşturulabilmektedir. Client program sockaddr_un yapısındaki sun_path elemanının ilk byte'ını null karakter olarak geçip diğer byte'larına bir bilgi girebilir. Client bu biçimde bind işlemi yaptığında artık soket dosyası yaratılmaz. Ancak bu isim accept ile karşı tarafa iletilir. Dolayısıyla client'ın girmiş olduğu yol ifadesi aslında soyut bir yol ifadesi olarak client'ı tespit etmek amacıyla kullanılabilir. Bu özelliğin POSIX standartlarında bulunmadığını, yalnızca Linux sistemlerine özgü olduğunu bir kez daha anımsatmak istiyoruz. Aşağıdaki örnekte client program bind işlemini yukarıda açıkladığımız gibi yapmaktadır. Dolayısıyla client program bir soket yaratmamış olacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /* uds-server.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 typedef struct tagCLIENT_INFO { int sock; struct sockaddr_un sun; } CLIENT_INFO; void *client_thread_proc(void *param); char *revstr(char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_un sun_server, sun_client; socklen_t sun_len; CLIENT_INFO *ci; ssize_t result; pthread_t tid; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((server_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (bind(server_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); for (;;) { printf("waiting for connection...\n"); sun_len = sizeof(sun_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sun_client, &sun_len)) == -1) exit_sys("accept"); printf("Connected new client: %s\n", sun_client.sun_path + 1); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sun = sun_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } void *client_thread_proc(void *param) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough CLIENT_INFO *ci = (CLIENT_INFO *)param; ssize_t result; for (;;) { if ((result = recv(ci->sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received from \"%s\": %s\n", (intmax_t)result, ci->sun.sun_path + 1, buf); revstr(buf); if (send(ci->sock, buf, result, 0) == -1) exit_sys("send"); } printf("\"%s\" client disconnected...\n", ci->sun.sun_path + 1); shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* uds-client.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int client_sock; struct sockaddr_un sun_server, sun_client; ssize_t result; char buf[BUFFER_SIZE + 1]; char *str; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((client_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_client, 0, sizeof(sun_client)); sun_client.sun_family = AF_UNIX; strcpy(sun_client.sun_path + 1, argv[2]); if (bind(client_sock, (struct sockaddr *)&sun_client, sizeof(sun_client)) == -1) exit_sys("bind"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (connect(client_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("connect"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if ((send(client_sock, buf, strlen(buf), 0)) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%ld bytes received: %s\n", (long)result, buf); } close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aslında Linux sistemlerinde server program da bind işlemini yaparken soyut isim kullanabilir. Yani server program da aslında sockaddr_un yapısındaki sun_path elemanının ilk karakterini null karakter yapıp diğer karakterlerine soketin ismini yerleştirebilir. Bu durumda haberleşme sırasında gerçekte hiçbir soket dosyası yaratılmayacaktır. Tabii soket dosyalarının önemli bir işlevi erişim haklarına sahip olmasıdır. Soyut isimler kullanıldığında böyle bir erişim hakkı kontrolü yapılmamaktadır. Aşağıdaki örnekte server program da soyut bir isim kullanmaktadır. Buradaki haberleşmede hiç soket dosyasının yaratılmayacağına dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* uds-server.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 typedef struct tagCLIENT_INFO { int sock; struct sockaddr_un sun; } CLIENT_INFO; void *client_thread_proc(void *param); char *revstr(char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_un sun_server, sun_client; socklen_t sun_len; CLIENT_INFO *ci; ssize_t result; pthread_t tid; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((server_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path + 1, argv[1]); if (bind(server_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); for (;;) { printf("waiting for connection...\n"); sun_len = sizeof(sun_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sun_client, &sun_len)) == -1) exit_sys("accept"); printf("Connected new client: %s\n", sun_client.sun_path + 1); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sun = sun_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } void *client_thread_proc(void *param) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough CLIENT_INFO *ci = (CLIENT_INFO *)param; ssize_t result; for (;;) { if ((result = recv(ci->sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received from \"%s\": %s\n", (intmax_t)result, ci->sun.sun_path + 1, buf); revstr(buf); if (send(ci->sock, buf, result, 0) == -1) exit_sys("send"); } printf("\"%s\" client disconnected...\n", ci->sun.sun_path + 1); shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* uds-client.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int client_sock; struct sockaddr_un sun_server, sun_client; ssize_t result; char buf[BUFFER_SIZE + 1]; char *str; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((client_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_client, 0, sizeof(sun_client)); sun_client.sun_family = AF_UNIX; strcpy(sun_client.sun_path + 1, argv[2]); if (bind(client_sock, (struct sockaddr *)&sun_client, sizeof(sun_client)) == -1) exit_sys("bind"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path + 1, argv[1]); if (connect(client_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("connect"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if ((send(client_sock, buf, strlen(buf), 0)) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%ld bytes received: %s\n", (long)result, buf); } close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- UNIX domain soketler aynı makinenin prosesleri arasında haberleşme sağladığına göre bunlarda send işlemi ile gönderilen bilginin tek bir recv işlemi ile alınması beklenir. Gerçekten de Linux sistemlerinde tasarım bu biçimde yapılmıştır. Ancak POSIX standartları bu konuda bir garanti vermemektedir. Ancak Linux sistemlerinde tıpkı borularda olduğu gibi bu işlem için ayrılan tampon büyüklüğünden fazla miktarda byte send (ya da write) ile gönderildiğinde parçalı okuma gerçekleşebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX domain soketlerde datagram haberleşme de yapılabilir. Bu haberleşme mesaj kuyruklarına bir seçenek oluşturmaktadır. UNIX domain soketlerde datagram haberleşmede gönderilen datagram'ların aynı sırada alınması garanti edilmiştir. Yani gönderim UDP/IP'de olduğu gibi güvensiz değil, güvenlidir. Anımsanacağı gibi UDP/IP'de gönderilen datagram'lar hedefe farklı sıralarda ulaşabiliyordu. Aynı zamanda bir datagram ağda kaybolursa bunun bir telafisi söz konusu değildi. UNIX domain soketlerde her şey aynı makinede ve işletim sisteminin kontrolü altında gerçekleştirildiği için böylesi bir durum söz konusu olmayacaktır. Aşağıda UNIX domain soketler kullanılarak bir datagram haberleşme örneği verilmiştir. Burada server hiç bağlantı sağlamadan herhangi bir client'tan paketi alır, oradaki yazıyı ters çevirip ona geri gönderir. Hem client hem de server ayrı ayrı iki dosya ismi ile bind işlemi yapmaktadır. Server program komut satırı argümanı olarak kendi bind edeceği soket dosyasının yol ifadesini, client program ise hem kendi bind edeceği soket dosyasının yol ifadesini hem de server soketin yol ifadesini almaktadır. Bu örnekte server ve client önce remove fonksiyonu ile daha önce yaratılan soket dosyasını aynı zamanda silmektedir. Aşağıda UNIX domain datagram sokete ilişkin bir örnek verilmiştir. Örnekte client server'a bir datagram mesaj göndermekte ve server da onu ters çevirip client'a geri yollamaktadır. Server program, server soketin yol ifadesini komut satırı argümanı olarak almaktadır. Client program da hem server soketin yol ifadesini hem de client soketin yol ifadesini komut satırı argümanı olarak almaktadır. Programları şöyle çalıştırabilirsiniz: ./uds-dg-server serversock ./uds-dg-client serversock clientsock ---------------------------------------------------------------------------------------------------------------------------*/ /* uds-dg-server.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 char *revstr(char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock; struct sockaddr_un sun_server, sun_client; socklen_t sun_len; ssize_t result; char buf[BUFFER_SIZE + 1]; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((sock = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (remove(argv[1]) == -1 && errno != ENOENT) exit_sys("remove"); if (bind(sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("bind"); printf("Waiting for client data...\n"); for (;;) { sun_len = sizeof(sun_client); if ((result = recvfrom(sock, buf, BUFFER_SIZE, 0, (struct sockaddr *)&sun_client, &sun_len)) == -1) exit_sys("recvfrom"); buf[result] = '\0'; printf("%ld bytes received from \"%s\": %s\n", (long)result, sun_client.sun_path, buf); revstr(buf); if (sendto(sock, buf, strlen(buf), 0, (struct sockaddr *)&sun_client, sizeof(sun_client)) == -1) exit_sys("sendto"); } close(sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* uds-dg-client.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock; struct sockaddr_un sun_client, sun_server, sun_response; socklen_t sun_len; char buf[BUFFER_SIZE]; char *str; ssize_t result; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((sock = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) exit_sys("socket"); memset(&sun_client, 0, sizeof(sun_client)); sun_client.sun_family = AF_UNIX; strcpy(sun_client.sun_path, argv[2]); if (remove(argv[2]) == -1 && errno != ENOENT) exit_sys("remove"); if (bind(sock, (struct sockaddr *)&sun_client, sizeof(sun_client)) == -1) exit_sys("bind"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); for (;;) { printf("Yazı giriniz:"); fgets(buf, BUFFER_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if (sendto(sock, buf, strlen(buf), 0, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("sendto"); sun_len = sizeof(sun_server); if ((result = recvfrom(sock, buf, BUFFER_SIZE, 0, (struct sockaddr *)&sun_response, &sun_len)) == -1) exit_sys("recvfrom"); buf[result] = '\0'; printf("%ld bytes received from \"%s\": %s\n", (long)result, sun_response.sun_path, buf); } close(sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 116. Ders 02/02/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX domain soketler "isimsiz boru haberleşmesine" benzer biçimde de kullanılabilmektedir. Anımsanacağı gibi isimsiz borularla yalnızca üst ve alt proseslerer arasında haberleşme yapılabiliyordu. Yine anımsayacağınız gibi pipe fonksiyonu bize iki betimleyici veriyordu. Biz de fork işlemi ile bu betimleyicileri alt prosese geçiriyorduk. İşte isimsiz borularla yapılan şeylerin benzeri soketlerle de yapılabilmektedir. İsimsiz soketlere İngilizce "unbound sockets" de denilmektedir. İsimsiz (unbound) soket yaratımı socketpair isimli fonksiyonla yapılmaktadır. Fonksiyonun prototipi şöyledir: #include int socketpair(int domain, int type, int protocol, int sv[2]); Fonksiyonun birinci parametresi protokol ailesinin ismini alır. Her ne kadar fonksiyon genel olsa da pek çok işletim sistemi bu fonksiyonu yalnızca UNIX domain soketler için gerçekleştirmektedir. (Gerçekten de üst ve alt prosesler arasında UNIX domain soketler varken örneğin TCP/IP soketleriyle haberleşmenin zarardan başka bir faydası olmayacaktır.) Linux sistemleri isimsiz soket olarak yalnızca UNIX domain soketlerini desteklemektedir. Dolayısıyla bu birinci parametre Linux sistemlerinde AF_UNIX biçiminde geçilmelidir. Fonksiyonun ikinci parametresi kullanılacak soketin türünü belirtir. Bu parametre yine SOCK_STREAM ya da SOCK_DGRAM biçiminde girilmelidir. Üçüncü parametre kullanılacak transport katmanını belirtmektedir. Bu parametre 0 olarak geçilebilir. Son parametre bir çift soket betimleyicisinin yerleştirileceği iki elemanlı int türden dizinin başlangıç adresini almaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Örneğin: int socks[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, socks) == -1) exit_sys("socketpair"); socketpair fonksiyonu SOCK_STREAM soketler için zaten bağlantı sağlanmış iki soketi bize vermektedir. Yani bu fonksiyon çağrıldıktan sonra listen, accept, connect gibi fonksiyonların çağrılması gereksizdir. Dolayısıyla tipik haberleşme şöyle gerçekleştirilmektedir: 1) socketpair fonksiyonu ile soket çifti yaratılır. 2) Soket çifçi yaratıldıktan sonra fork ile alt proses yaratılır. 3) İki taraf da kullanmayacakları soketleri kapatırlar. Hangi prosesin socketpair fonksiyonunun son parametresine yerleştirilen hangi soket betimleyicisini kullanacağının bir önemi yoktur. 4) Haberleşme soket fonksiyonlarıyla gerçekleştirilir. Pekiyi isimsiz borularla socketpair fonksiyonuyla oluşturulan isimsiz UNIX domain soketler arasında ne fark vardır? Aslında bu iki kullanım benzer etkilere sahiptir. Ancak en önemli farklılık UNIX domain soketlerin çift yönlü (full duplex) bir haberleşme sağlamasıdır. Normalde isimsiz mesaj kuyrukları olmadığına dikkat ediniz. Halbuki isimsiz UNIX domain soketler sanki isimsiz mesaj kuyrukları gibi de kullanılabilmektedir. Aşağıdaki programda tıpkı isimsiz boru haberleşmesinde olduğu gibi üst ve alt prosesler birbirleri arasında isimsiz UNIX domain soketler yoluyla haberleşmektedir. Buradaki soketlerin çift yönlü haberleşmeye olanak verdiğini anımsayınız. ---------------------------------------------------------------------------------------------------------------------------*/ /* uds-socketpair.c */ #include #include #include #include #include #include #define BUFFER_SIZE 1024 char *revstr(char *str); void exit_sys(const char *msg); int main(void) { int socks[2]; char buf[BUFFER_SIZE + 1]; char *str; ssize_t result; pid_t pid; if (socketpair(AF_UNIX, SOCK_STREAM, 0, socks) == -1) exit_sys("socketpair"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent */ close(socks[1]); for (;;) { if ((result = recv(socks[0], buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; revstr(buf); if (send(socks[0], buf, strlen(buf), 0) == -1) exit_sys("send"); } if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); close(socks[0]); exit(EXIT_SUCCESS); } else { /* child */ close(socks[0]); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if ((send(socks[1], buf, strlen(buf), 0)) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(socks[1], buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%ld bytes received: %s\n", (long)result, buf); } close(socks[1]); exit(EXIT_SUCCESS); } return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Bu bölümde TCP ve UDP protokollerinin aşağı seviyeli çalışma mekanizması üzerinde durulacaktır. Ancak bu protokollerin aşağı seviyeli çalışma biçimleri biraz karmaşıktır. Biz burada çok derine inmeden bu çalışma biçimini açıklayacağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- TCP protokolü 1981 yılında RFC 793 dokümanı ile tanımlanmıştır (https://tools.ietf.org/html/rfc793). Sonradan protokole bazı revizyonlar ve eklemeler de yapılmıştır. Protokolün son güncel versiyonu 2022'de RFC 9293 dokümanında tanımlanmıştır. Paket tabanlı protokollerin hepsinde gönderilip alınan veriler paket biçimindedir (yani bir grup byte biçimindedir). Bu paketlerin "başlık (header) ve veri (data)" kısımları vardır. Örneğin "Ethernet paketinin başlık ve veri kısmı", "IP paketinin başlık ve veri kısmı", "TCP paketinin başlık ve veri kısmı" bulunmaktadır. Öte yandan TCP protokolü aslında IP protokolünün üzerine oturtulmuştur. Yani aslında TCP paketleri IP paketleri gibi gönderilip alınmaktadır. Nihayet aslında bu paketler bilgisayarımıza Ethernet ya da Wireless paketi olarak gelmektedir. Paketlerin başlık kısımlarında önemli "meta data" bilgileri bulunmaktadır. O halde örneğin aslında bizim network kartımıza bilgiler Ethernet paketi gibi gelmektedir. Aslında IP paketi Ethernet paketinin veri kısmında, TCP paketi de aslında IP paketinin veri kısmında konuşlandırılmaktadır. Yani aslında bize gelen Ethernet paketinin veri kısmında IP paketi, IP paketinin veri kısmında da TCP paketi bulunmaktadır. TCP'de gönderdiğimiz veriler aslında IP paketinin veri kısmını oluşturmaktadır. Örneğin bir host'tan diğerine bir TCP paketinin gönderildiğini düşünelim. TCP paketi "TCP Header" ve "TCP Data" kısmından oluşmaktadır: +-------------------------+ | TCP Header | +-------------------------+ | TCP Data | +-------------------------+ Ancak TCP paketi aslında IP paketi gibi gönderilmektedir. IP paketi de "IP Header" ve "IP Data" kısımlarından oluşmaktadır. İşte aslında TCP paketi IP paketinin Data kısmında bulundurulur. Yani yolculuk eden TCP paketinin görünümü şöyledir: +-------------------------+ | IP Header | +-------------------------+ <---+ | TCP Header | | +-------------------------+ IP Data | TCP Data | | +-------------------------+ <---+ TCP paketi de bilgisayarımızın Ethernet kartına sanki Ethernat paketi gibi gelmektedir. Ethernet paketi de "Ethernet Header" ve "Ethernet Data" kısımların oluşmaktadır. İşte bütün TCP paketi aslında IP paketi gibi IP paketi de Ethernet paketi gibi gönderilip alınmaktadır: +-------------------------+ | Ethernet Header | +-------------------------+ <----------------+ | IP Header | | +-------------------------+ <---+ | | TCP Header | | Ethernet Data +-------------------------+ IP Data | | TCP Data | | | +-------------------------+ <---+------------+ Bu durumu aşağıdaki gibi de gösterebiliriz: +-------------------------+ | Ethernet Header | +----+--------------------+----+ | IP Header | +----+--------------------+----+ | TCP Header | +----+--------------------+----+ | TCP Data | +-------------------------+ Örneğin biz TCP'de send fonksiyonuyla "ankara" yazısını gönderiyor olalım. Bu "ankara" yazısını oluşturan byte'lar aslında TCP paketinin veri kısmındadır. +-------------------------+ | Ethernet Header | +----+--------------------+----+ | IP Header | +----+--------------------+----+ | TCP Header | +----+--------------------+----+ | "ankara" | +-------------------------+ Ethernet protokolu (IEEE 802.3) OSI katmanına göre fiziksel ve veri bağlantı katmanının işlevlerini yerine getirmektedir. Wireless haberleşme için kullanılan Wi-Fi protokolü (IEEE 802.11) Ethernet protokolünün telsiz (wireless) biçimi gibi düşünülebilir. Tabii IP paketleri aslında yalnızca bilgisayarımıza gelirken Ethernet paketi ya da Wi-Fi paketi olarak gelir. Dışarıda rotalanırken Ethernet paketi söz konusu değildir. IP protokolünün IPv4 ve IPv6 biçiminde iki versiyonunun olduğunu anımsayınız. Ancak TCP ve UDP protokollerinin böyle bir versiyon numarası yoktur. TCP paketi IPv4 paketinin veri kısmında da IPv6 paketinin veri kısmında da konuşlandırılmış olabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yerel ağımızda aslında router tarafından gönderilip alınan paketlerin hepsi yerel ağdaki tüm bilgisayarlara ulaşmaktadır. Ethernet ve Wireless kartları yalnızca kendilerini ilgilendiren paketleri alıp işletim sistemini haberdar edebilmektedir. Ancak bu kartlar için yazılmış özel programlar sayesinde bilgisayarımıza ulaşan tüm paketler incelenebilmektedir. Bu tür yardımcı programlara "network sniffer" da denilmektedir. En yaygın kullanılan "network sniffer" program "wireshark" isimli open source programdır. Bu programın eskiden ismi "Ethereal" biçimindeydi. Aslında wireshark programı "libpcap" isimli open source kütüphane kullanılarak yazılmıştır. Yani asıl işlevsellik bu kütüphanededir. Wireshark adeta libpcap kütüphanesinin bir önyüzü (frontend) gibidir. Bu kütüphanenin Windows versiyonuna "npcap" denilmektedir. Linux Debian türevi sistemlerde kütüphane aşağıdaki gibi indirilebilir: $ sudo apt-get install libpcap-dev Benzer biçimde Linux'ta wireshark programını da GUI arayüzü yazılım yöneticisinden yüklenebileceği gibi komut satırından Debian türevi sistemlerde aşağıdaki gibi yüklenebilir: $ sudo apt-get install wireshark Wireshark programının kullanımına ilişkin pek çok "tutorial" bulunmaktadır. Kursumuzun EBooks klasöründe de birkaç kitap bulunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- IPv4 protokolünün başlık (header) kısmı şöyledir (her satırda 4 byte bulunmaktadır). Toplam başlık uzunluğu 20 byte'dır. <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +-----------+-----------+----------------------+-----------------------------------------------+ ^ | Version | IHL | Type of Service | Total Length | (4 bytes) | | (4 bits) | (4 bits) | (8 bits) | (16 bits) | | +-----------+-----------+----------------------+-----------+-----------------------------------+ | | Identification | Flags | Fragment Offset | (4 bytes) | | (16 bits) | (3 bits) | (13 bits) | | +-----------------------+----------------------+-----------+-----------------------------------+ | | Time to Live (TTL) | Protocol | Header Checksum | (4 bytes) | 20 bytes | (8 bits) | (8 bits) | (16 bits) | | +-----------------------+----------------------+-----------------------------------------------+ | | Source IP Address (32 bits) | (4 bytes) | +----------------------------------------------------------------------------------------------+ | | Destination IP Address (32 bits) | (4 bytes) | +----------------------------------------------------------------------------------------------+ v | Segment (L4 protocol (TCP/UDP) + Data) | +----------------------------------------------------------------------------------------------+ Version : IP versiyonunu içerir. IPv4 ya da IPv6 değerlerinden birini içermektedir. IPv4 için 4 değeri kullanılmaktadır. (0100) IHL : Internet Header Length bilgisini içermektedir. Genelde 20 byte değerini içerir. Fakat farklı değerler aldığı durumlar da söz konusu olabilmektedir. Type of Service : DS / DSCP / ECN alanlarını içermektedir. Paket önceliği konusunda kullanılmaktadır. Total Length : Header ve data'nın toplam uzunluk bilgisini içermektedir. Identification, Flags, Frament Offset : Paketin ikinci 4 byte'lık kısmı fragmentation için kullanılmaktadır. Time to Live (TTL) : Paketin yaşam ömrünün bilgisini içermektedir. Yaşam ömrü her router geçildiğinde bir azalmaktadır. Eğer TTL değeri 0 olursa paket router tarafından çöpe atılmaktadır. TTL genel olarak yolunu şaşırmış paketlerin network'lerde sonsuza kadar dolaşmasını önlemek için kullanılmaktadır. Bazen de paket router'lar arasında bir loop (routing loop) içerisinde takılıp kalmaktadır. TTL, bu gibi durumları engellemek için kullanılmaktadır. Dünya'da en uzak noktaya bile data gönderirken maksimum 15-20 router geçilmektedir. Protocol : L4 protokol bilgisini içermektedir. Örneğin, TCP için 6, UDP için 17, ICMP için 1 değerlerini içermektedir. Header Checksum : Router'lar IP paket header'ının yolda bozulup bozulmadığını bu değeri kontrol ederek sağlayabilmektedir. Source IP Address : Kaynak IP adresinin unicast IP adresi olması gerekmektedir. Destination IP Address : Hedef IP adresi unicast, broadcast ve multicast IP adresi olabilir. Buradan da gördüğünüz gibi IP başlığında kaynak ve hedef IP adresleri ve IP paketinin toplam uzunluğu bulunmaktadır. Port kavramının IP protokolünde olmadığını anımsayınız. IPv6 header kısmı aşağıda verilmiştir. Toplam başlık uzunluğu 40 byte'a sabitlenmiştir. <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +-----------+-----------------+----------------------------------------------------------------+ ^ | Version | Traffic Class | Flow Label | (4 bytes) | | (4 bits) | (4 bits) | | | +-----------+-----------------+----------------+-----------------------+-----------------------+ | | Payload Length | Next Header | Hop Limit | (4 bytes) | | (16 bits) | | | | +----------------------------------------------+-----------------------+-----------------------+ | | | | | | | | | | | | | | Source IP Address (128 bits) | (16 bytes) | | | | 40 bytes | | | | | | +----------------------------------------------------------------------------------------------+ | | | | | | | | | | | | | | Destination IP Address (128 bits) | (16 bytes) | | | | | | | | | | +----------------------------------------------------------------------------------------------+ v Version : IP versiyonunu içerir. IPv6 için 6 değeri kullanılmaktadır. (0110) Traffic Class : Paket önceliği konusunda kullanılmaktadır. Flow Label : Bir sunucuyla yapılan haberleşme için bir numara belirlenmektedir ve haberleşme boyunca bütün paketlerde bu numara kullanılarak iletişim sağlanmaktadır. Farklı amaçlar için de kullanıldığı durumlar vardır. Payload Length : Datanın boyutunu içermektedir. Next Header : L4 protokol bilgisini içermektedir. Hop Limit : IPv4'teki TTL alanıyla aynıdır. TCP paketi yukarıda da belirttiğimiz gibi IP paketinin veri (data) kısmındadır. TCP paketi de "TCP Header" ve "TCP Data" kısımlarından oluşmaktadır. TCP başlık kısmı şöyledir. (her satırda 4 byte bulunmaktadır): <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +----------------------------------------------+-----------------------------------------------+ ^ | Source Port | Destination Port | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ | | Sequence Number | (4 bytes) | | (32 bits) | | +----------------------------------------------------------------------------------------------+ | | Acknowledgement Number | (4 bytes) | | (32 bits) | | 20 bytes +-----------+----------------+-----------------+-----------------------------------------------+ | |Header Len.| Reserved | Control Bits | Window Size | (4 bytes) | | (4 bits) | (6 bits) | (6 bits) | (16 bits) | | +-----------+----------------+-----------------+-----------------------------------------------+ | | Checksum | Urgent | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ v | Options | | (0 or 32 bits) | +----------------------------------------------------------------------------------------------+ | Application Layer Data | | (Size Varies) | +----------------------------------------------------------------------------------------------+ Burada her satır 32 bit yani 4 byte yer kaplamaktadır. TCP başlığı 20 byte'tan 60 byte'a kadar değişen uzunlukta olabilir. Başlıktaki Header Length TCP data'sının hangi offset'ten başladığını dolayısıyla TCP başlığının DWORD (4 byte olarak) uzunluğunu belirtir. Yani başlığın byte uzunluğu için buradaki değer 4 ile çarpılmalıdır. Böylece Header Length kısmında en az 5 (toplam 20 byte) en fazla 15 (toplam 60 byte) değeri bulunabilir. Bu başlıkta kaynak ve hedef IP adreslerinin ve TCP data kısmının uzunluğunun bulunmadığına dikkat ediniz. Çünkü bu bilgiler zaten IP başlığında doğrudan ya da dolaylı biçimde bulunmaktadır. TCP paketi her zaman IP paketinin data kısmında konuşlandırılmaktadır. Başlıktaki Control Bits alanı 6 bitten oluşmaktadır. Her bit bir özelliği temsil eder. Buradaki belli bitler set edildiğinde başlıktaki belli alanlar da anlamlı hale gelebilmektedir. Buradaki bitler yani flag'ler şunlardır: URG, ACK, PSH, RST, SYN, FIN. Flags alanındaki birden fazla bit 1 olabilir. Yani birden fazla flag set edilmiş olabilir. Bir TCP paketi (TCP segment) yalnızca başlık içerebilir. Yani hiç data içermeyebilir. Başka bir deyişle TCP paketinin data kısmı 0 byte uzunluğunda olabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 117. Ders 04/02/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- TCP protokolünde o anda iki tarafın da bulunduğu bir "durum (state)" vardır. Taraflar belli eylemler sonucunda durumdan duruma geçiş yaparlar. Bu nedenle TCP'nin çalışması bir "sonlu durum makinesi (finite state machine)" biçiminde ele alınıp açıklanabilir. Henüz bağlantı yoksa iki taraf da CLOSED denilen durumdadır. TCP'de tarafların hangi olaylar sonucunda hangi durumda olduklarına ilişkin diyagrama "durum diyagramı (state diagram)" denilmektedir. (TCP durum diyagramı için Google'da "TCP state diagram" araması ile görsellerden çizilmiş diyagramları görebilirsiniz.) TCP bağlantısının kurulması için client ile server data kısmı boş olan (yani yalnızca başlık kısmı bulunan) paketleri gönderip almaktadır. Buna el sıkışma (hand shaking) denilmektedir. TCP'de bağlantı kurulması için yapılan el sıkışma 4'lü (four way) ya da 3'lü (three way) olabilir. Burada 3'lü demekle bağlantı için toplam 3 paketin yolculuk etmesi, 4'lü demekle toplam 4 paketin yolculuk etmesi kastedilmektedir. Uygulamada daha çok 3'lü el sıkışma kullanılmaktadır. TCP'de bağlantının kurulabilmesi için "iki tarafın da birbirlerine SYN biti set edilmiş data kısmı olmayan TCP paketi (20 byte) gönderip karşı taraftan ACK biti set edilmiş data'sı olmayan TCP paketi alması" gerekir. Yukarıda da belirttiğimiz gibi bunun iki yolu olabilir: Client Server +-----------------+ +-----------------+ | CLOSED | | LISTEN | +-----------------+ +-----------------+ ------- SYN -----> +-----------------+ | SYN-SENT | +-----------------+ <------ ACK ------ <------ SYN ------ +-----------------+ +-----------------+ | ESTABLISHED | | SYN-RECEIVED | +-----------------+ +-----------------+ ------- ACK -----> +-----------------+ | ESTABLISHED | +-----------------+ Burada 4 paket kullanıldığı için buna 4'lü el sıkışma denilmektedir. Yukarıdaki bağlantı kurulurken iki tarafın TCP durumunu (state) da belirttik. Server bağlantı sırasında ACK ile SYN bitini tek bir paket olarak da gönderilebilir. (Yani paketin Flags kısmında hem SYN hem de ACK biti set edilmiş olabilir.) Buna 3'lü el sıkışma denilmektedir: Client Server +-----------------+ +-----------------+ | CLOSED | | LISTEN | +-----------------+ +-----------------+ ------- SYN -----> +-----------------+ | SYN-SENT | +-----------------+ <--- SYN + ACK --- +-----------------+ | SYN-RECEIVED | +-----------------+ ------- ACK -----> +-----------------+ +-----------------+ | ESTABLISHED | | ESTABLISHED | +-----------------+ +-----------------+ ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bağlantının kopartılması için iki tarafın da birbirlerine FIN biti set edilmiş paketler gönderip ACK biti set edilmiş paketleri alması gerekir. Bağlantının kopartılması da tipik olarak 3'lü ya da 4'lü el sıkışma yoluyla yapılmaktadır. FIN ve ACK paketleri ayrı ayrı gönderilirse 4'lü el sıkışma tek bir paket olarak gönderilirse 3'lü el sıkışma gerçekleşir. Bağlantının kopartılması talebini herhangi bir taraf başlatabilir. 4'lü el sıkışma ile bağlantının kopartılması şöyle yapılmaktadır: Peer - 1 Peer - 2 +-----------------+ +-----------------+ | ESTABLISHED | | ESTABLISHED | +-----------------+ +-----------------+ ------- FIN -----> +-----------------+ +-----------------+ | FIN-WAIT-1 | | CLOSE_WAIT | +-----------------+ +-----------------+ <------ ACK ------ +-----------------+ | FIN-WAIT-2 | +-----------------+ <------ FIN ------ +-----------------+ | LAST-ACK | +-----------------+ ------- ACK -----> +-----------------+ +-----------------+ | TIME-WAIT | | CLOSED | +-----------------+ +-----------------+ +-----------------+ | CLOSED | +-----------------+ Burada iki taraf da birbirlerine FIN biti set edilmiş data kısmı olmayan TCP paketleri gönderip ACK biti set edilmiş data kısmı olmayan TCP paketleri almıştır. 3'lü el sıkışma ile bağlantının kopartılmasında bir taraf tek bir pakette hem FIN biti set edilmiş hem de ACK biti set edilmiş paket göndermektedir: Peer - 1 Peer - 2 +-----------------+ +-----------------+ | ESTABLISHED | | ESTABLISHED | +-----------------+ +-----------------+ ------- FIN -----> +-----------------+ +-----------------+ | FIN-WAIT-1 | | CLOSE_WAIT | +-----------------+ +-----------------+ <--- FIN + ACK --- +-----------------+ +-----------------+ | FIN-WAIT-2 | | LAST-ACK | +-----------------+ +-----------------+ ------- ACK -----> +-----------------+ +-----------------+ | TIME-WAIT | | CLOSED | +-----------------+ +-----------------+ +-----------------+ | CLOSED | +-----------------+ Burada özetle bir taraf önce karşı tarafa FIN paketi yollamıştır. Karşı taraf buna ACK+FIN ile karşılık vermiştir. Diğer taraf da son olarak karşı tarafa ACK yollamıştır. Ancak bağlantıyı kopartmak isteyen taraf bu ACK yollama işinden sonra MSL (Maximum Segment Life) denilen bir zaman aralığının iki katı kadar beklemektedir (Tipik olarak 2 dakika). MSL bir paketin kaybolduğuna karar verilmesi için gereken zamanı belirtmektedir. (Eğer alıcı taraf beklemeden hemen CLOSED duruma geçseydi bu durumda gönderici taraf yeniden bağlantı kurduğunda henüz alıcı taraf paketi almamışsa sanki eski bağlantı devam ettiriliyormuş gibi bir durum olabilirdi.) Soket programlamada bağlantı shutdown ile SHUT_RD kullanılarak kopartıldığında yukarıdaki 3'lü el sıkışma gerçekleşmektedir. Bağlantının koparılması "yarım biçimde de (half close") yapılabilir. Bu durumda bir taraf diğer tarafa FIN paketi gönderir. Karşı taraf da buna ACK paketi ile karşılık verir. Bundan sonra artık FIN gönderen taraf veri gönderemez ama alabilir, ACK gönderen taraf ise veri alamaz ama gönderebilir: Peer - 1 Peer - 2 +-----------------+ +-----------------+ | ESTABLISHED | | ESTABLISHED | +-----------------+ +-----------------+ ------- FIN -----> +-----------------+ +-----------------+ | FIN-WAIT-1 | | CLOSE_WAIT | +-----------------+ +-----------------+ <------ ACK ------ +-----------------+ | CLOSING | +-----------------+ +-----------------+ | TIME_WAIT | +-----------------+ Bunun tersi de şöyle söz konusu olabilir: Peer - 1 Peer - 2 <------ FIN ------ +-----------------+ +-----------------+ | CLOSE_WAIT | | FIN-WAIT-1 | +-----------------+ +-----------------+ ------- ACK -----> +-----------------+ | CLOSING | +-----------------+ +-----------------+ | TIME_WAIT | +-----------------+ Burada da artık Peer-2 veri gönderemez ama alabilir, Peer-1 ise veri veri alamaz fakat gönderebilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 118. Ders 09/02/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi shutdown fonksiyonunun "half close" işlemindeki etkisi nasıldır? Aslında shutdown fonksiyonunun ikinci parametresinde belirtilen SHUT_WR, SHUT_RD ve SHUT_RDWR değerlerinin protokoldeki bağlantının kopartılması süreciyle bir ilgisi yoktur. shutdown fonksiyonu her durumda "half close" uygulamaktadır. Yani shutdown fonksiyonunun ikinci parametresi ne olursa olsun bu fonksiyonu çağıran taraf karşı tarafa FIN paketi yollar, karşı taraf da bu tarafa ACK paketi yollar. Zaten protokolün kendisinde "half close" işlemi SHUT_WR, SHUT_RD ya da SHUT_RDWR biçiminde bir bilgi taşımamaktadır. TCP protokolü tasarlandığında "half close" işleminin "bir tarafı göndermeye kapatıp diğer tarafı almaya kapatmak" gibi bir işlev göreceği düşünülmüştür. Ancak bu "half close" işleminin işletim sistemleri tarafından tam olarak nasıl ele alınacağı TCP/IP soket gerçekleştirimini yapanlar tarafından belirlenmektedir. Şimdi Linux sistemlerinde shutdown fonksiyonunun muhtemel gerçekleştirimi ve arka planda gerçekleşen muhtemel işlemler konusunda bilgi verelim. Örneğin bir taraf shutdown fonksiyonunu SHUT_WR parametresiyle aşağıdaki gibi çağırmış olsun. Ancak karşı taraf shutdown fonksiyonunu çağırmamış olsun: shutdown(sock, SHUT_WR); Burada SHUT_WR uygulayan taraf diğer tarafa FIN paketi gönderir, diğer taraf da buna ACK ile yanıt verir ve "half close" işlemi gerçekleşir. Artık SHUT_WR uygulayan taraf bundan sonra diğer tarafa veri göndermemeli diğer taraf da karşı taraftan veri almamalıdır. Bir taraf SHUT_WR ile "half close" uyguladığında karşı taraf recv işlemi yaparsa sanki soket kapatılmış gibi recv fonksiyonu 0 ile geri dönecektir. SHUT_WR yapan taraf send fonksiyonunu kullandığında ise SIGPIPE sinyali oluşacaktır. Şimdi de bir taraf shutdown fonksiyonunu SHUT_RD ile çağırmış olsun. shutdown(sock, SHUT_RD); Bu durumda yine SHUT_RD uygulayan taraf karşı tarafa FIN paketi gönderir ve karşı taraftan ACK paketi alır. Böylece "half close" işlemi gerçekleşir. Artık SHUT_RD uygulayan taraf veri almayacak fakat veri gönderebilecektir. Karşı taraf ise veri alabilecek ancak veri gönderemeyecektir. Tabii aslında karşı taraf shutdown fonksiyonunun aslında hangi parametreyle çağrıldığını bilmemektedir. Dolayısıyla aslında soket fonksiyonlarıyla veri göndermeye devam edebilecektir. Karşı taraf eğer send işlemi yaparsa burada işletim sistemi değişik davranışlar gösterebilmektedir. Karşı tarafın gönderdiği paketler karşı tarafa ulaştığında SHUT_RD yapan taraftaki işletim sistemi bu paketleri hiç dikkate almayabilir. Böylece SHUT_RD yapan taraf recv fonksiyonunu çağırsa bile recv 0 ile geri döner. Ya da işletim sistemi böylesi bir durumda karşı taraf veri gönderdiğinde ona RST bayrağı set edilmiş paket gönderip (buna "connection reset" denilmektedir) karşı tarafın artık send işlemlerinde SIGPIPE sinyali üretmesini sağlayabilir. Şimdi de shutdown fonksiyonunun SHUT_RDWR parametresi ile çağrıldığını düşünelim. Bu en çok kullanılan parametredir. Bu durumda yine fonksiyonu çağıran taraf karşı tarafa FIN paketi gönderir, karşı taraftan ACK paketi alır. Yine "half close" işlemi gerçekleşir. Ancak artık SHUT_RDWR uygulayan taraf recv ve send işlemlerini yapamayacaktır. SHUT_RDWR uygulayan taraf recv fonksiyonunu çağırırsa fonksiyon 0 ile geri dönecek, send fonksiyonunu çağırırsa doğrudan SIGPIPE sinyali oluşacaktır. Bu durumda SHUT_RDWR uygulayan tarafın karşı tarafı, artık send işlemi yaparsa yine davranış yukarıda SHUT_RD fonksiyonunda belirtildiği gibi gerçekleşecektir. Tabii normal olarak iki tarafın da aslında ayrı ayrı shutdown fonksiyonunu çağırması gerekir. Bu durumda 4'lü el sıkışma gerçekleşecektir. TCP/IP soket programlamada önce bir taraf shutdown uygulayıp "half close" oluşturabilir. Diğer taraf da bunu anlayıp o da shutdown uygulayarak 4'lü el sıkışma oluşturabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- TCP'de akış kontrolü için "acknowledgement" yani "alındı" bildirimi kullanılmaktadır. Bir taraf bir tarafa bir paket veri gönderirken verinin yanı sıra aynı zamanda paketin Flags kısmındaki PSH bitini 1 yapar. Karşı taraf da paketi aldığını diğer tarafa haber vermek için diğer tarafa ACK biti set edilmiş bir paket gönderir. TCP'de her gönderilen paket için bir "alındı" bilgisinin alınması gerekir. Eğer paketi gönderen taraf bu paketi içeren bir ACK paketi alamazsa bu durumda "paketin karşı tarafa ulaşmadığından" şüphelenmektedir. Bu durumda paketi gönderen taraf belli bir algoritma ile zaman aralıklarıyla aynı paketi yeniden göndermektedir. Böylesi bir durumda paketi alan taraf aynı paketi birden fazla kez de alabilir. Bunun yalnızca tek bir kopyasını işleme sokmak alan tarafın sorumluluğundadır. Tabii gönderen tarafın paketi yolda kaybolabileceği gibi alan tarafın ACK paketi de yolda kaybolabilir. Bu durumda yine gönderen taraf ACK alamadığına göre göndermeye devam edecektir. Bu durumda alıcı taraf bunun için yine ACK gönderecektir. Yukarıda da belirttiğimiz gibi paketin "data" kısmı dolu olmak zorunda değildir. Bağlantı sağlanırken ve sonlandırırken gönderilen SYN ve FIN paketleri "data" içermemektedir. ACK paketi ayrı bir paket olarak gönderilmek zorunda değildir. Bilgiyi alan taraf hem bilgi gönderirken hem de ACK işlemi yapabilir. Örneğin: +---------+ | PSH | +---------+ --- data ---> +---------------+ | PSH + ACK | +---------------+ <--- data --- +---------+ | ACK | +---------+ ------------> Aslında izleyen paragraflarda da ele alınacağı gibi ACK biti yalnızca "alındığını bildirme için değil" pencere genişliklerinin ayarlanması için de kullanılmaktadır. Yani bir taraf karşı taraftan bilgi almadığı halde yine ACK gönderebilir. TCP'de kümülatif bir "acknowledgement" sistemi kullanılmaktadır. Yani paketi gönderen taraf bu paket için ACK almadan başka paketleri gönderebilir. Paketleri alan taraf birden fazla paket için tek bir ACK yollayabilir. Kümülatif ACK işlemi için "sıra numarası (sequence number)" denilen bir değerden faydalanılmaktadır. Sıra numarası (sequence number) gönderilen paketin bütün içerisindeki kaçıncı byte'tan başladığını belirten bir değerdir. Bunu dosyalardaki dosya göstericisine benzetebiliriz. Sıra numarası TCP başlığında 32 bitlik bir alanda tutulmaktadır. Sıra numarası 32 bitlik değerin sonuna geldiğinde yeniden başa dönmektedir (wrapping). Sıra numarası bağlantı kurulduğunda sıfırdan başlatılmaz, rastgele bir değerden başlatılmaktadır. Örneğin belli bir anda bir tarafın sıra numarası 1552 olsun. Şimdi bu taraf karşı tarafa 300 byte göndersin. Artık bu gönderimden sonra sıra numarası 1852 olacaktır. Yani bir sonraki gönderimde bu taraf sıra numarası olarak 1852'yi kullanacaktır. Sıra numarası her bilgi gönderiminde bulundurulmak zorundadır. Bilgiyi alan taraf ACK paketini gönderirken paketteki sıra numarasını "talep ettiği sonraki sıra numarası" olarak paketin sıra numarasını belirten kısmına yerleştirir. Örneğin: Peer-1 Peer-2 300 byte (sequence Number: 3560) -----> 100 byte (sequence Number: 3860) -----> <---- ACK (Acknowledgement Number: 3960) 50 byte (sequence Number: 3960) ------> <---- ACK (Acknowledgement Number: 4010) 10 byte (sequence Number: 4010) ------> Buradaki örnek gönderimde gönderen taraf önce 300 byte'lık bir paketi sonra 100 byte'lık bir paketi karşı tarafa göndermiştir. Karşı taraf ise bu iki paket için tek bir ACK göndermiştir. Karşı tarafın gönderdiği ACK aslında diğer taraftan yeni talep edeceği sıra numarasındaki bilgiyi belirtmektedir. İki paketi gönderen taraf karşı taraftan gelen ACK içerisindeki bu sıra numarasına baktığında bu iki paketinde alındığını anlamaktadır. Görüldüğü gibi her paket için ayrı bir ACK yollanmak zorunda değildir. Buna "kümülatif alındı (cumulative acknowledgment)" bildirimi denilmektedir. Örneğin bir tarafın karşı tarafa peş peşe 5 paket gönderdiğini düşünelim. Karşı taraftan bir ACK gelmiş olsun. Gönderen taraf bu ACK paketine bakarak gönderdiği bilginin ne kadarının karşı taraf tarafından alındığını anlayabilmektedir. Pekiyi bir TCP paketi (TCP segment) gönderici (sender) tarafından gönderildikten sonra alıcı (receiver) bunu alamamışsa ne olacaktır? Çünkü TCP'nin güvenli bir protokol olması demek bir biçimde böyle bir durumda bir telafinin yapılması demektir. İşte yukarıda da belirttiğimiz gibi TCP protokolü şöyle yöntem izlemektedir: Gönderen taraf her gönderdiği paket (TCP segment) için bir zamanlayıcı kurar. Bu zamanlayıcıya "retransmission timer" denilmektedir. Eğer belli süre içerisinde gönderilen TCP paketini kapsayan bir ACK gelmediyse gönderici taraf aynı paketi yeniden göndermektedir. Böylece aslında gönderilen paket henüz onun için ACK gelmedikçe gönderme tamponundan atılmaz. Retransmission timer bazı değerlere göre dinamik bir biçimde oluşturulmaktadır. Bunun detayları için önerilen kaynaklara bakılabilir. Tabii böyle bir sistemde alıcı taraf aynı paketi birden fazla kez alabilmektedir. Yukarıda da belirttiğimiz gibi bu durumda bu paketlerin yalnızca tek bir kopyasını alıp diğerlerini atmak alıcı tarafın sorumluluğundadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 119. Ders 11/02/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- TCP protokolünün bir "akış kontrolü (flow control)" oluşturduğunu belirtmiştik. Akış kontrolünün amacı tampon taşmasının engellenmesidir. Bağlantı sağlandıktan sonra bir tarafın diğer tarafa sürekli bilgi gönderdiğini düşünelim. Bu bilgileri işletim sistemi alacak ve bekletecektir. Pekiyi ya ilgili proses soketten okuma yapmazsa? Bu durumda hala karşı taraf bilgi gönderirse işletim sisteminin ayırdığı tampon taşabilir. Tipik olarak işletim sistemleri bağlantı yapılmış her soket için iki tampon bulundurmaktadır: Gönderme tamponu (send buffer) ve alma tamponu (receive buffer). Biz send fonksiyonunu kullandığımızda göndermek istediğimiz bilgiler gönderme tamponuna yazılır ve hemen send fonksiyonu geri döner. Gönderme tamponundaki bilgilerin paketlenerek gönderilmesi belli bir zaman sonra işletim sistemi tarafından yapılmaktadır. Eğer send işlemi sırasında zaten gönderme tamponu doluysa send fonksiyonu gönderilecek olanları tamamen tampona yazana kadar blokede beklemektedir. Alma tamponu (receive buffer) karşı tarafın gönderdiği bilgilerin alınması için kullanılan tampondur. Karşı tarafın gönderdiği bilgiler alındığında işletim sistemi bu bilgileri alma tamponuna yerleştirir. Aslında recv fonksiyonu bu tampondan bilgileri almaktadır. send ---> [gönderme tamponu] ---> işletim sistemi gönderiyor ---> ||||| <--- işletim sistemi alıyor ---> [alma tamponu] <--- recv Akış kontrolünün en önemli unsurlarından biri alma tamponunun taşmasını engellemektir. Örneğin gönderici taraf sürekli bilgi gönderirse fakat alıcı taraftaki proses recv işlemiyle hiç okuma yapmazsa alıcı taraftaki işletim sisteminin alıcı tamponu dolabilir ve sistem çökebilir. İşte akış kontrolü sayesinde alıcı taraf gönderici tarafa "artık gönderme, benim tamponum doldu" diyebilmektedir. Şimdi bir taraftaki prosesin diğer tarafa bir döngü içerisinde send fonksiyonuyla bilgi gönderdiğini ancak diğer taraftaki prosesin bu bilgiyi almadığını varsayalım. Akış kontrolünün uygulandığı durumda ne olacaktır? İşte önce send ile gönderilenler karşı tarafa iletilecektir. Karşı tamponu dolduğunda karşı taraf gönderen tarafa "artık gönderme" diyecektir. Bu durumda göndermeyi kesen taraftaki proses hala send işlemi yapacağına göre o tarafın gönderme tamponu dolacak ve send fonksiyonu blokeye yol açacaktır. Linux sistemlerinde tek bir send ile gönderme tamponundan daha büyük bir bilgiyi göndermek istediğimizde tüm bilgi yine tampona yerleştirilene kadar bloke oluşmaktadır. Aşağıdaki örnekte client program bağlantı kurduktan sonra bir döngü içerisinde server programa send fonksiyonu ile bilgi göndermektedir. Ancak server program bu bilgiyi recv ile okumamaktadır. Yukarıda da belirttiğimiz gibi bu durumda server programın tamponu dolacak ve server program karşı tarafa "artık gönderme" diyecek. Bu kez de karşı tarafın gönderme tamponu dolacak dolayısıyla send fonksiyonu da bir süre sonra blokede bekleyecektir. Bu programları farklı terminallerden aşağıdaki gibi çalıştırabilirsiniz: ./server 55555 ./client localhost 55555 ---------------------------------------------------------------------------------------------------------------------------*/ /* server.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock_server, sock_client; struct sockaddr_in sinaddr, sinaddr_client; socklen_t sinaddr_len; char ntopbuf[INET_ADDRSTRLEN]; in_port_t port; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } port = (in_port_t)strtoul(argv[1], NULL, 10); if ((sock_server = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sinaddr.sin_family = AF_INET; sinaddr.sin_port = htons(port); sinaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(sock_server, (struct sockaddr *)&sinaddr, sizeof(sinaddr)) == -1) exit_sys("bind"); if (listen(sock_server, 8) == -1) exit_sys("listen"); printf("Waiting for connection...\n"); sinaddr_len = sizeof(sinaddr_client); if ((sock_client = accept(sock_server, (struct sockaddr *)&sinaddr_client, &sinaddr_len)) == -1) exit_sys("accept"); printf("Connected: %s : %u\n", inet_ntop(AF_INET, &sinaddr_client, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sinaddr_client.sin_port)); printf("Press any key to EXIT...\n"); getchar(); shutdown(sock_client, SHUT_WR); close(sock_client); close(sock_server); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock; struct addrinfo *ai, *ri; struct addrinfo hints = {0}; char buf[BUFFER_SIZE] = {0}; int result; ssize_t sresult; ssize_t stotal; if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; if ((result = getaddrinfo(argv[1], argv[2], &hints, &ai)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(result)); exit(EXIT_FAILURE); } for (ri = ai; ri != NULL; ri = ri->ai_next) if (connect(sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(ai); printf("Connected...\n"); stotal = 0; for (;;) { printf("send calls...\n"); if ((sresult = send(sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("send"); stotal += sresult; printf("bytes sent: %jd, total bytes sent: %jd\n", sresult, stotal); } close(sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda basit olarak açıkladığımız akış kontrolünün TCP'de bazı detayları vardır. TCP'de bunun için "pencere (window)" kavramı kullanılmaktadır. Pencerenin bir büyüklüğü (window size) vardır. Pencere büyüklüğü TCP başlığında belirtilmektedir. Pencere büyüklüğü demek "hiç ACK gelmediği durumda göndericinin en fazla gönderebileceği byte sayısı" demektir. Örneğin pencere genişliğinin 8K olması demek "alıcı ACK göndermedikten sonra göndericinin en fazla 8K gönderebilmesi" demektir. Pencere genişliği alıcı taraf tarafından gönderici tarafa bildirilir. Örneğin pencere genişliği alıcı taraf için 8K olsun. Bu durumda gönderici taraf sırasıyla 1K + 1K + 1K + 1K + 1K uzunluğunda toplam 5K'lık bilgiyi karşı tarafa göndermiş olsun. Eğer henüz ACK gelmemişse gönderici taraf en fazla 3K kadar daha bilgi gönderebilir. TCP'de her ACK sırasında yeni pencere genişliği de karşı tarafa gönderilmek zorundadır. Yani ACK paketi gönderilirken aynı zamanda yeni pencere genişliği de gönderilmektedir. ACK paketi yalnızca alındı bilgisini göndermek için değil pencere genişliğini ayarlamak için de gönderilebilmektedir. Başka bir deyişle bir taraf "yalnızca bilgi aldığı için" ACK göndermek zorunda değildir. Hiç bilgi almadığı halde yeni pencere genişliğini karşı tarafa bildirmek için de ACK gönderebilir. Pencere genişliği en fazla 64K olabilir. Çünkü bunun için TCP başlığında 16 bit yer ayrılmıştır. Şimdi bir tarafın diğer tarafa send fonksiyonu ile sürekli bilgi gönderdiğini ancak diğer tarafın bilgiyi recv ile okumadığını düşünelim. İşletim sisteminin alıcı taraf için oluşturduğu alma tamponunun 1 MB olduğunu düşünelim. Alıcı taraf muhtemelen bilgi geldikçe ACK yaparken 64K'lık pencere genişliğini karşı tarafa bildirecektir. Ancak zamanla alma tamponu dolduğu için bu pencere genişliğini düşürecek en sonunda ACK ile pencere genişliğini 0 yapacak ve karşı tarafa "artık gönderme" diyecektir. Pencere genişliği ile alma tamponunun genişliği birbirine karıştırılmamalıdır. Alma tamponu gelen bilgilerin yerleştirildiği tampondur. Pencere genişliği karşı tarafın ACK almadıktan sonra gönderebileceği maksimum byte sayısıdır. Pekiyi pencere genişlikleri ve sıra numaraları bağlantı sırasında nasıl karşı tarafa bildirilmektedir? İşte bağlantı kurulurken client taraf SYN paketi içerisinde kendi başlangıç sıra numarasını karşı tarafa iletmektedir. Server da bağlantıyı kabul ederken yine SYN (ya da SYN + ACK) paketinde kendi sıra numarasını karşı tarafa bildirmektedir. Pencere genişliği de aslında ilk kez bağlantı yapılırken ACK paketlerinde belirtilmektedir. TCP/IP stack gerçekleştirimleri ACK stratejisi için bazı yöntemler uygulamaktadır. Örneğin eğer gönderilecek paket varsa bununla birlikte ACK paketinin gönderilmesi, ACK'ların iki paket biriktirildikten sonra gönderilmesi gibi. Benzer biçimde pencere genişliklerinin ayarlanması için de bazı stratejiler izlenebilmektedir. Bunun için "TCP/IP Protocol Suite" kitabının 466'ıncı sayfasına başvurabilirsiniz. TCP paketindeki önemli Flag'lerden birisi de "RST" bitidir. Buna "reset isteği" denilmektedir. Bir taraf RST bayrağı set edilmiş paket alırsa artık karşı tarafın "abnormal" bir biçimde bağlantıyı kopartıp yeniden bağlanma talep ettiği anlaşılır. Normal sonlanma el sıkışarak başarılı bir biçimde yapılırken RST işlemi anormal sonlanmaları temsil eder. Örneğin soket kütüphanelerinde hiç shutdown yapmadan soket close edilirse close eden taraf karşı tarafa RST paketi göndermektedir. Halbuki önce shutdown yapılırsa el sıkışmalı sonlanma gerçekleştirilir. O halde her zaman aktif soketler shutdown yapıldıktan sonra close edilmelidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 120. Ders 16/02/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UDP protokolü aslında saf IP protokolüne çok benzerdir. UDP'yi IP'den ayıran iki önemli farklılık şudur: 1) UDP port numarası kavramına sahiptir. 2) UDP'nin hata için bir checksum mekanizması vardır. Yani bir taraf diğer tarafa UDP paketi gönderirken gönderdiği veri için checksum bilgisini de UDP başlık kısmına iliştirmektedir. Bir UDP paketi yine aslında IP paketinin data kısmında bulunmaktadır. 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 | Length | Checksum | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ v | Application Layer Data | | (Size Varies) | +----------------------------------------------------------------------------------------------+ Burada UDP paketinin toplam uzunluğunun bulunması aslında gereksizdir. Çünkü uzunluk TCP'de olduğu gibi aslında IP paketinin başlığına bakılarak tespit edilebilmektedir. Ancak hesaplama kolaylığı oluşturmak için bu uzunluk UDP başlığında ayrıca bulundurulmuştur. Ayrıca checksum UDP paketlerinde bulunmak zorunda değildir. Eğer gönderici checksum kontrolü istemiyorsa burayı 0 bitleriyle doldurur. (Eğer zaten checksum 0 ise burayı 1 bitleriyle doldurmaktadır.) Alan taraf checksum hatasıyla karşılaşırsa TCP'de olduğu gibi paketi yeniden talep etmez. Yalnızca onu atar. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bazen TCP ve UDP yerine doğrudan IP protokolünü kullanmak isteyebiliriz. Buna soket programlamada "raw socket" denilmektedir. Diğer protokol ailelerinde de "raw socket" ağ katmanı protokolünü belirtmektedir. Biz kursumuzda "raw socket" işlemleri üzerinde durmayacağız. Ancak daha aşağı seviyeli çalışmalar için ya da örneğin aktarım katmanını gerçekleştirmek (implemente etmek) için "raw socket" kullanımını bilmek gerekir. Genel bir "raw soket" oluşturmak için soket nesnesi yaratılırken protokol ailesi için AF_PACKET girilir. Soket türü için de SOCK_RAW girilmelidir. IP protokolü için protokol ailesi yine AF_INET ya da AF_INET6 girilip soket türü SOCK_RAW olarak girilebilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bu bölümde UNIX/Linux sistemlerinde kullanılan kütüphane dosyaları ve onların ayrıntıları üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kütüphane "hazır kodların bulunduğu topluluklar" için kullanılan bir terimdir. Ancak aşağı seviyeli dünyada kütüphane kavramı daha farklı bir biçimde kullanılmaktadır. Aşağı seviyeli dünyada "içerisinde derlenmiş bir biçimde fonksiyonların bulunduğu dosyalara kütüphane (library)" denilmektedir. Aslında kütüphaneler yalnızca fonksiyon değil, global nesneler de içerebilmektedir. Kütüphaneler "statik" ve "dinamik" olmak üzere ikiye ayrılmaktadır. Statik kütüphane dosyalarının uzantıları UNIX/Linux sistemlerinde ".a (archive)" biçiminde, Windows sistemlerinde ".lib (library)" biçimindedir. Dinamik kütüphane dosyalarının uzantıları ise UNIX/Linux sistemlerinde ".so (shared object), Windows sistemlerinde ".dll (dynamic link library)" biçimindedir. UNIX/Linux dünyasında kütüphane dosyaları geleneksel olarak başında "lib" öneki olacak biçimde isimlendirilmektedir. (Örneğin "x" isimli bir statik kütüphane dosyası UNIX/Linux sistemlerinde genellikle "libx.a" biçiminde, "x" isimli bir dinamik kütüphane dosyası ise UNIX/Linux sistemlerinde genellikle "libx.so" biçiminde isimlendirilmektedir.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Statik kütüphaneler aslında "object modülleri (yani .o dosyalarını)" tutan birer kap gibidir. Yani statik kütüphaneler object modüllerden oluşmaktadır. Statik kütüphanelere link aşamasında linker tarafından bakılır. Bir program statik kütüphane dosyasından bir çağırma yaptıysa (ya da o kütüphaneden bir global değişkeni kullandıysa) linker o statik kütüphane içerisinde ilgili fonksiyonun bulunduğu object modülü link aşamasında statik kütüphane dosyasından çekerek çalıştırılabilir dosyaya yazar. (Yani statik kütüphaneden bir tek fonksiyon çağırsak bile aslında o fonksiyonun bulunduğu object modülün tamamı çalıştırılabilen dosyaya yazılmaktadır.) Statik kütüphaneleri kullanan programlar artık o statik kütüphaneler olmadan çalıştırılabilirler. Statik kütüphane kullanımının şu dezavantajları vardır: 1) Kütüphaneyi kullanan farklı programlar aynı fonksiyonun (onun bulunduğu object modülün) bir kopyasını çalıştırılabilir dosya içerisinde bulundururlar. Yani örneğin printf fonksiyonu statik kütüphanede ise her printf kullanan C programı aslında printf fonksiyonunun bir kopyasını da barındırıyor durumda olur. Bu da programların diskte fazla yer kaplamasına yol açacaktır. 2) Aynı statik kütüphaneyi kullanan programlar belleğe yüklenirken işletim sistemi aynı kütüphane kodlarınını yeniden fiziksel belleğe yükleyecektir. İşletim sistemi bu kodların ortak olarak kullanıldığını anlayamamaktadır. 3) Statik kütüphanede bir değişiklik yapıldığında onu kullanan programların yeniden link edilmesi gerekir. Statik kütüphane kullanımının şu avantajları vardır: 1) Kolay konuşlandırılabilirler. Statik kütüphane kullanan bir programın yüklenmesi için başka dosyalara gereksinim duyulmamaktadır. 2) Statik kütüphanelerin kullanımları kolaydır, statik kütüphane kullanan programlar için daha kolay build ya da make işlemi yapılabilmektedir. 3) Statik kütüphane kullanan programların yüklenmesi dinamik kütüphane kullanan programların yüklenmesinden çoğu kez daha hızlı yapılmaktadır. Ancak bu durum çeşitli koşullara göre tam ters bir hale de gelebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde statik kütüphane dosyaları üzerinde işlemler "ar" isimli utility program yoluyla yapılmaktadır. "ar" programına önce bir seçenek, sonra statik kütüphane dosyasının ismi, sonra da bir ya da birden fazla object modül ismi komut satırı argümanı olarak verilir. Örneğin: $ ar r libmyutil.a x.o y.o Burada "r" seçeneği belirtmektedir. ar eski bir komut olduğu için burada seçenekler '-' ile başlatılarak verilmeyebilir. Ancak POSIX standartlarında seçenekler yine "-" ile belirtilmektedir. Komuttaki "libmyutil.a" işlemden etkilenecek statik kütüphane dosyasını "x.o" ve "y.o" argümanları ise object modülleri belirtmektedir. Biz buradaki seçeneği "-" ile de belirtebiliriz: $ ar -r libmyutil.a x.o y.o Tipik ar seçenekleri ve yaptıkları işler şunlardır: -r (replace) seçeneği ilgili object modüllerin kütüphaneye yerleştirilmesini sağlar. Eğer kütüphane dosyası yoksa komut aynı zamanda onu yaratmaktadır. Örneğin: $ ar -r libmyutil.a x.o y.o Burada "libmyutil.a" statik kütüphane dosyasına "x.o" ve "y.o" object modülleri yerleştirilmiştir. Eğer "libmyutil.a" dosyası yoksa aynı zamanda bu dosya yaratılacaktır. Bu seçenekte eğer kütüphaneye yerleştirilmek istenen amaç dosya zaten kütüphane içerisinde varsa değiştirilmektedir ("replace" zaten buradan geliyor). -t seçeneği kütüphane içerisindeki object modüllerin listesini almakta kullanılır. Örneğin: $ ar -t libsample.a -d (delete) seçeneği kütüphaneden bir object modülü silmekte kullanılır. Örneğin: $ ar -d libmyutil.a x.o -x (extract) seçeneği kütüphane içerisindeki object modülü bir dosya biçiminde diske save etmekte kullanılır. Ancak bu object modül kütüphane dosyasından silinmeyecektir. Örneğin: $ ar -x libmyutil.a x.o -m (modify) seçeneği de bir object modülün yeni versiyonunu eski versiyonla değiştirmekte kullanılır. O halde "x.c" ve "y.c" dosyalarının içerisindeki fonksiyonları statik kütüphane dosyasına eklemek için sırasıyla şunlar yapılmalıdır: $ gcc -c x.c $ gcc -c y.c $ ar r libmyutil.a x.o y.o ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Statik kütüphane kullanan programlar derlenirken statik kütüphane dosyaları komut satırında belirtilebilir. Bu durumda gcc ve clang derleyicileri o dosyayı bağlama (link) işleminde kullanmaktadır. Örneğin: $ gcc -o app app.c libmyutil.a Burada "libmyutil.a" dosyasına C derleyicisi bakmamaktadır. gcc aslında bu dosyayı bağlayıcıya (linker) iletmektedir. Biz bu işlemi iki adımda da yapabilirdik: $ gcc -c app.c $ gcc -o app app.o libmyutil.a Her ne kadar GNU'nun bağlayıcı programı aslında "ld" isimli programsa da genellikle programcılar bu ld bağlayıcısını doğrudan değil yukarıdaki gibi gcc yoluyla kullanırlar. Çünkü ld bağlayıcısını kullanılırken "libc" kütüphanesinin start-up amaç dosyaların (start-up object modules) programcı tarafından ld bağlayıcısına verilmesi gerekmektedir. Bu da oldukça sıkıcı bir işlemdir. Halbuki biz ld bağlayıcısını gcc yoluyla çalıştırdığımızda libc kütüphanesi ve bu start-up amaç dosyalar ld bağlayıcısına gcc tarafından verilmektedir. gcc eskiden C derleyicisi anlamına geliyordu (GNU C Compiler). Ancak zamanla derleyicileri çalıştıran bir önyüz (frontend) program haline getirildi ve ismi de "GNU Compiler Collection" biçiminde değiştirildi. Yani aslında uzunca bir süredir gcc programı ile yalnızca C programlarını değil diğer programlama dillerinde yazılmış olan programları da derleyebilmekteyiz. Komut satırında kütüphane dosyalarının komut satırı argümanlarının sonunda belirtilmesi uygundur. Çünkü gcc programı kütüphane dosyalarını yalnızca onların solunda belirtilen dosyaların bağlanmasında kullanmaktadır. Örneğin: $ gcc -o app app1.o libmyutil.a app2.o Böylesi bir kullanımda "libmyutil.a" kütüphanesinin solunda yalnızca "app1.o" dosyası vardır. Dolayısıyla bağlayıcı yalnızca bu modül için bu kütüphaneye bakacaktır, "app2.o" için bu kütüphaneye bakılmayacaktır. Şüphesiz statik kütüphane kullanmak yerine aslında amaç dosyaları da doğrudan bağlama işlemine sokabiliriz. Örneğin: $ gcc -o sample sample.c x.o y.o Ancak çok sayıda object modül söz konusu olduğunda bu işlemin zorlaşacağına dikkat ediniz. Yani amaç dosyalar (object modules) dosyalara benzetilirse statik kütüphane dosyaları dizinler gibi düşünülebilir. Derleme işlemi sırasında kütüphane dosyası -l biçiminde de belirtilebilir. Bu durumda arama sırasında "lib" öneki ve ".a" uzantısı aramaya dahil edilmektedir. Yani örneğin: $ gcc -o sample sample.c -lmyutil İşleminde aslında "libmyutil.a" (ya da "libmyutil.so") dosyaları aranmaktadır. Arama işlemi sırasıyla bazı dizinlerde yapılmaktadır. Örneğin "/lib" dizini, "/usr/lib dizini", "/usr/local/lib" dizini gibi dizinlere bakılmaktadır. Ancak "bulunulan dizine (current working directory)" bakılmamaktadır. -l seçeneği ile belli bir dizine de bakılması isteniyorsa "-L" seçeneği ile ilgili dizin belirtilebilir. Örneğin: $ gcc -o sample sample.c -lmyutil -L. Buradaki '.' çalışma dizinini temsil etmektedir. Artık "libmyutil.a" kütüphanesi için bulunulan dizine de (current working directory) bakılacaktır. Birden fazla dizin için -L seçeneğinin yinelenmesi gerekmektedir. Örneğin: $ gcc -o sample sample.c -lmyutil -L. -L/home/csd Geleneksel olarak "-l" ve "-L" seçeneklerinden sonra boşluk bırakılmamaktadır. Ancak boşluk bırakılmasında bir sakınca yoktur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir statik kütüphane başka bir statik kütüphaneye bağımlı olabilir. Örneğin biz "liby.a" kütüphanesindeki kodda "libx.a" kütüphanesindeki fonksiyonları kullanmış olabiliriz. Bu durumda "liby.a" kütüphanesini kullanan program "libx.a" kütüphanesini de komut satırında belirtmek zorundadır. Örneğin: $ gcc -o sample sample.c libx.a liby.a ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 121. Ders 18/02/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dinamik kütüphane dosyalarının UNIX/Linux sistemlerinde uzantıları ".so" (shared object'ten kısaltma), Windows sistemlerinde ise ".dll" (Dynamic Link Library) biçimindedir. Bir dinamik kütüphaneden bir fonksiyon çağrıldığında linker statik kütüphanede olduğu gibi gidip fonksiyonun kodunu (fonksiyonun bulunduğu amaç dosyayı) çalıştırılabilen dosyaya yazmaz. Bunun yerine çalıştırılabilen dosyaya çağrılan fonksiyonun hangi dinamik kütüphanede olduğu bilgisini yazar. Çalıştırılabilen dosyayı yükleyen işletim sistemi o dosyanın çalışması için gerekli olan dinamik kütüphaneleri çalıştırılabilen dosyayla birlikte bütünsel olarak prosesin sanal bellek alanına yüklemektedir. Böylece birtakım ayarlamalar yapıldıktan sonra artık çağrılan fonksiyon için gerçekten o anda sanal belleğe yüklü olan dinamik kütüphane kodlarına gidilmektedir. Örneğin biz "app" programımızda "libmyutil.so" dinamik kütüphanesinden foo isimli fonksiyonu çağırmış olalım. Bu foo fonksiyonunun kodları dinamik kütüphaneden alınıp "app" dosyasına yazılmayacaktır. Bu "app" dosyası çalıştırıldığında işletim sistemi bu "app" dosyası ile birlikte "libmyutil.so" dosyasını da sanal belleğe yükleyecektir. Programın akışı foo çağrısına geldiğinde akış "libmyutil.so" dosyası içerisindeki foo fonksiyonunun kodlarına aktarılacaktır. Dinamik kütüphane dosyalarının bir kısmının değil hepsinin prosesin adres alanına yüklendiğine dikkat ediniz. (Tabii işletim sisteminin sanal bellek mekanizması aslında yalnızca bazı sayfaları fiziksel belleğe yükleyebilecektir.) Dinamik kütüphane kullanımının avantajları şunlardır: 1) Çalıştırılabilen dosyalar fonksiyon kodlarını içermezler. Dolayısıyla önemli bir disk alanı kazanılmış olur. Oysa statik kütüphanelerde statik kütüphanelerden çağrılan fonksiyonlar çalıştırılabilen dosyalara yazılmaktadır. 2) Dinamik kütüphaneler birden fazla proses tarafından fiziksel belleğe tekrar tekrar yüklenmeden kullanılabilmektedir. Yani işletim sistemi arka planda aslında aynı dinamik kütüphaneyi kullanan programlarda bu kütüphaneyi tekrar tekrar fiziksel belleğe yüklememektedir. Bu da statik kütüphanelere göre önemli bir bellek kullanım avantaj oluşturmaktadır. Bu durumda eğer dinamik kütüphanenin ilgili kısmı daha önce fiziksel belleğe yüklenmişse bu durum dinamik kütüphane kullanan programın daha hızlı yüklemesine de yol açabilmektedir. Prog1 ve Prog2 biçiminde iki programın çalıştığını düşünelim. Bunlar aynı dinamik kütüphaneyi kullanıyor olsun. İşletim sistemi bu dinamik kütüphaneyi bu proseslerin sanal bellek alanlarının farklı yerlerine yükleyebilir. Ancak aslında işletim sistemi sayfa tablolarını kullanarak mümkün olduğunca bu iki dinamik kütüphaneyi aynı fiziksel sayfaya eşlemeye çalışacaktır. Tabii bu durumda proseslerden biri dinamik kütüphane içerisindeki bir statik nesneyi değiştirdiğinde artık "copy on write" mekanizması devreye girecek ve dinamik kütüphanenin o sayfasının yeni bir kopyası oluşturulacaktır. Aslında bu durum fork fonksiyonu ile yeni bir prosesin yaratılması durumuna çok benzemektedir. Burada anlatılan unsurların ayrıntıları "sayfalama ve sanal bellek" kullanımın açıklandığı paragraflarda ele alınmıştır. 3) Dinamik kütüphaneleri kullanan programlar bu dinamik kütüphanelerdeki değişikliklerden etkilenmezler. Yani biz dinamik kütüphanenin yeni bir versiyonunu oluşturduğumuzda bunu kullanan programları yeniden derlemek ya da bağlamak zorunda kalmayız. Örneğin bir dinamik kütüphaneden foo fonksiyonunu çağırmış olalım. Bu foo fonksiyonunun kodları bizim çalıştırılabilir dosyamızın içerisinde değil de dinamik kütüphanede olduğuna göre dinamik kütüphanedeki foo fonksiyonu değiştirildiğinde bizim programımız artık değişmiş olan foo fonksiyonunu çağıracaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dinamik kütüphanelerin gerçekleştiriminde ve kullanımında önemli bir sorun vardır. Dinamik kütüphanelerin tam olarak sanal belleğin neresine yükleneceği baştan belli değildir. Halbuki çalıştırılabilen dosyanın sanal belleğin neresine yükleneceği baştan bilinebilmektedir. Yani çalıştırılabilen dosyanın tüm kodları aslında derleyici ve bağlayıcı tarafından zaten "onun sanal bellekte yükleneceği yere göre" oluşturulmaktadır. Fakat dinamik kütüphanelerin birden fazlası prosesin sanal adres alanına yüklenebildiğinden bunlar için yükleme adresinin önceden tespit edilmesi mümkün değildir. İşte bu sorunu giderebilmek için işletim sistemlerinde değişik teknikler kullanılmaktadır. Windows sistemlerinde "import-export tablosu ve "load time relocation" yöntemleri tercih edilmiştir. Bu sistemlerde dinamik kütüphane belli bir adrese yüklendiğinde işletim sistemi o dinamik kütüphanenin "relocation" tablosuna bakarak gerekli makine komutlarını düzeltmektedir. Dinamik kütüphane fonksiyonlarının çağrımı için de "import tablosu" ve "export tablosu" denilen tablolar kullanılmaktadır. UNIX/Linux dünyasında dinamik kütüphanelerin herhangi bir yere yüklenebilmesi ve minimal düzeyde relocation uygulanabilmesi için "Konumdan Bağımsız Kod (Position Independent Code - PIC)" denilen teknik kullanılmaktadır. Konumdan bağımsız kod "nereye yüklenirse yüklenilsin çalışabilen kod" anlamına gelmektedir. Konumdan bağımsız kod oluşturabilmek derleyicinin yapabileceği bir işlemdir. Konumdan bağımsız kod oluşturabilmek için gcc ve clang derleyicilerinde derleme sırasında "-fPIC" seçeneğinin bulundurulması gerekmektedir. Biz kursumuzda konumdan bağımsız kod oluşturmanın ayrıntıları üzerinde durmayacağız. Pekiyi Windows sistemlerinin kullandığı "relocation" tekniği ile UNIX/Linux sistemlerinde kullanılan "konumdan bağımsız kod tekniği" arasında performans bakımından ne farklılıklar vardır? İşte bu tekniklerin kendi aralarında bazı avantaj ve dezavantajları bulunmaktadır. Windows'taki teknikte "relocation" işlemi bir zaman kaybı oluşturabilmektedir. Ancak bir "relocation" işlemi yapıldığında kodlar daha hızlı çalışma eğilimindedir. Konumdan bağımsız kod tekniğinde ise "relocation" işlemine minimal düzeyde gereksinim duyulmaktadır. Ancak dinamik kütüphanelerdeki fonksiyonlar çağrılırken göreli biçimde daha fazla zaman kaybedilmektedir. Aynı zamanda bu teknikte kodlar biraz daha fazla yer kaplamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux sistemlerinde aslında dinamik kütüphaneler ismine "dinamik linker (dynamic linker)" denilen bir dinamik kütüphane tarafından yüklenmektedir. Bu dinamik kütüphane "ld.so" ya da "ld-linux.so" ismiyle bulunmaktadır. Programın yüklenmesinin execve sistem fonksiyonu tarafından yapıldığını anımsayınız. Bu sistem fonksiyonu ayrıntılı birtakım işlemler yaparak tüm yüklemeyi gerçekleştirmektedir. Bu sürecin ayrıntıları olmakla birlikte kabaca execve süreci bu bağlamda şöyle yürütülmektedir: execve fonksiyonu önce işletim sistemi için gereken çeşitli veri yapılarını oluşturur sonra çalıştırılabilen dosyayı belleğe yükler. Sonra da dinamik linker kütüphanesini belleğe yükler. Bundan sonra akış dinamik linker'daki koda aktarılır. Dinamik linker da çalıştırılabilir dosyada belirtilen dinamik kütüphaneleri yükler. Sonra da akışı çalıştırılabilen dosyada belirtilen gerçek başlangıç adresine (entry point) aktarır. Dinamik linker kodları aslında user mode'da mmap sistem fonksiyonunu kullanarak diğer dinamik kütüphaneleri yüklemektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi neden dinamik kütüphane dosyaları linker tarafından tıpkı çalıştırılabilir dosyalarda olduğu gibi sanal bellekte belli bir yere yüklenince sorunsuz çalışacak biçimde oluşturulmuyor? Çalıştırılabilir dosyalar sanal bellek boşken yüklendiğinden onların belli bir yere yüklenmesinde bir sorun oluşmamaktadır. Ancak bir program çok fazla dinamik kütüphane kullanabileceğine göre bu dinamik kütüphanelerin baştan yerinin belirlenmesi olanaksızdır. Pekiyi dinamik kütüphaneler içerisindeki global değişkenlerin ve fonksiyonların yükleme yerinden bağımsız bir biçimde dinamik kütüphane içerisinden kullanılması nasıl sağlanabilir? Dinamik kütüphane içerisinde aşağıdaki gibi bir kod parçası bulunuyor olsun: int g_a; ... g_a = 10; Burada derleyicinin yukarıdaki ifadeye ilişkin makine kodlarını üretebilmesi için g_a değişkeninin tüm bellekteki adresini (yani tepeden itibaren adresini) bilmesi gerekir. Bir nesnenin belleğin tepesinden itibarenki adresine "mutlak adres (absolute address) de denilmektedir. Örneğin Intel işlemcilerinde yukarıdaki ifade aşağıdaki gibi makine komutlarına dönüştürülmektedir: MOV EAX, 10 MOV [g_a'nın mutlak adresi], EAX İşte sorun buradaki g_a değişkeninin mutlak adresinin program yüklenene kadar bilinmemesidir. Bu sorunu çözmenin de iki yolu vardır: 1) Derleyici ve linker g_a'nın mutlak adresinin bulunduğu yeri boş bırakır. Yükleyicinin bu yeri yükleme adresine göre doldurmasını ister. İşte bu işlem yükleyicinin yaptığı "relocation" işlemidir. Bu tür relocation işlemlerine "load time relocation" da denilmektedir. Windows sistemleri bu yöntemi kullanmaktadır. 2) Derleyici makine komutunu o anda komutun yürütüldüğü yerin adresini barındıran ve ismine "Instruction Pointer" denilen yazmaca dayalı olarak oluşturabilir. Çünkü linker komutun bulunduğu yerden g_a'ya kadar kaç byte'lık bir açıklık olduğunu bilmektedir. İşte buna "konumdan bağımsız kod (position independent code)" denilmektedir. Yukarıda da belirttiğimiz gibi birinci teknik (Windows sistemlerinin kullandığı teknik) relocation yapıldıktan sonra kodun hızlı çalışmasını sağlamaktadır. Ancak bu teknikte relocation zamanı yüklemeyi uzatabilmektedir. İkinci teknikte ise relocation minimal düzeyde tutulmaktadır. Ancak bu global değişkenlere erişim birkaç makine komutu ile daha yavaş yapılmaktadır. UNIX/Linux sistemleri genel olarak bu tekniği kullanmaktadır. Ayrıca birinci teknikte kod üzerinde relocation uygulandığı için mecburen "copy on write" mekanizması devreye sokulmaktadır. Bu da fiziksel belleğin kullanım verimini düşürebilmektedir. Bu noktada ek olarak işlemcilerde bazı makine komutlarının (MOV, LOAD, STORE gibi) mutlak adres kullandığını ancak CALL ve JMP gibi bazı makine komutlarının hem mutlak hem de göreli adres kullanabildiğini belirtelim. Aslında işlemcileri tasarlayanlar relocation işlemi gerekmesin diye CALL ve JMP komutlarının göreli (relative) versiyonlarını da oluşturmuşlardır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde dinamik kütüphaneler şöyle oluşturulmaktadır: 1) Önce dinamik kütüphaneye yerleştirilecek amaç dosyaların (object files) -fPIC seçeneği ile "Konumdan Bağımsız Kod (Position Independent Code)" tekniği kullanılarak derlenmesi gerekir. (-fPIC seçeneğinde -f'ten sonra boşluk bırakılmamalıdır.) 2) Bağlama işleminde "çalıştırılabilir (executable)" değil de "dinamik kütüphane" dosyasının oluşturulması için -shared seçeneğinin kullanılması gerekir. "-shared" seçeneği kullanılmazsa bağlayıcı dinamik kütüphane değil, normal çalıştırılabilir dosya oluşturmaya çalışmaktadır. (Zaten bu durumda main fonksiyonu olmadığı için linker hata mesajı verecektir.) Örneğin: $ gcc -fPIC a.c b.c c.c $ gcc -shared -o libmyutil.so a.o b.o c.o Dinamik kütüphanelere daha sonra dosya eklenip çıkartılamaz. Onların her defasında yeniden bütünsel biçimde oluşturulmaları gerekmektedir. Yukarıdaki işlem aslında tek hamlede de aşağıdaki gibi yapılabilmektedir: $ gcc -shared -o libmyutil.so -fPIC a.c b.c c.c ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi biz -fPIC seçeneğini kullanmadan yani "konumdan bağımsız kod" üretmeden dinamik kütüphane oluşturmaya çalışırsak ne olur? Mevcut GNU linker programları "-shared" seçeneği kullanıldığında global değişkenler için relocation işlemi söz konusu ise bir mesaj vererek link işlemini yapmamaktadır. Yani bu durumda mevcut GNU linker programları kodun "-fPIC" seçeneği ile derlenmesini zorunlu tutmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 122. Ders 23/02/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz yukarıda dinamik kütüphanelerin nasıl oluşturulduğunu gördük. Pekiyi dinamik kütüphaneler nasıl kullanılmaktadır? Dinamik kütüphane kullanan bir program bağlanırken kullanılan dinamik kütüphanenin komut satırında belirtilmesi gerekir. Örneğin: $ gcc -o app app.c libmyutil.so Tabii bu işlem yine -l seçeneği ile de yapılabilirdi: $ gcc -o app app.c -lmyutil -L. Bu biçimde çalıştırılabilir dosya oluşturulduğunda linker bu çalıştırılabilir dosyanın çalıştırılabilmesi için hangi dinamik kütüphanelerin yüklenmesi gerektiğini ELF formatının ".dynamic" isimli bölümüne yazmaktadır. Böylece yükleyici bu programı yüklerken onun kullandığı dinamik kütüphaneleri de yükleyecektir. Ancak linker bu ".dynamic" bölümüne çalıştırılabilir dosyanın kullandığı dinamik kütüphanelerin yol ifadesini (yani tam olarak nerede olduğunu) yazmaz. Yalnızca isimlerini yazmaktadır. İşte yükleyici (dinamik linler) bu nedenle dinamik kütüphaneleri önceden belirlenen bazı yerlerde aramaktadır. Bu yere çalıştırılabilir dosyanın yüklendiği dizin (current working directory) dahil değildir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İster statik kütüphane isterse dinamik kütüphane yazacak olalım yazdığımız kütüphaneler için bir başlık dosyası oluşturmak iyi bir tekniktir. Örneğin içerisinde çeşitli fonksiyonların bulunduğu "libmyutil.so" dinamik kütüphanesini "libmyutil.c" dosyasından hareketle oluşturmak isteyelim. İşte "libmyutil.c" dosyasındaki fonksiyonların prototipleri, gerekli olan sembolik sabitler, makrolar, inline fonksiyonlar, yapı bildirimleri gibi "nesne yaratmayan bildirimler" bir başlık dosyasına yerleştirilmelidir. Böylece bu kütüphaneyi kullanacak kişiler bu dosyayı include ederek gerekli bildirimlerin kodlarını oluşturmuş olurlar. Başlık dosyaları oluşturulurken iki önemli noktaya dikkat edilmelidir: 1) Başlık dosyalarına yalnızca "nesne yaratmayan bildirimler (declarations)" yerleştirilmelidir. 2) Başlık dosyalarının başına "include koruması (include guard)" yerleştirilmelidir. Include koruması aşağıdaki gibi yapılabilir: #ifndef SOME_NAME #define SOME_NAME #endif Buradaki SOME_NAME dosya isminden hareketle uydurulmuş olan herhangi bir isim olabilir. Örneğin: #ifndef MYUTIL_H_ #define MYUTIL_H_ #endif Örneğin "myutil.so" dinamik kütüphanesinde foo ve bar isimli iki fonksiyon bulunuyor olsun. Bunun için "myutil.h" isimli başlık dosyası aşağıdaki gibi oluşturulabilir: #ifndef MYUTIL_H_ #define MYUTIL_H_ void foo(void); void bar(void); #endif ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Standart C fonksiyonlarının ve POSIX fonksiyonlarının bulunduğu "libc" kütüphanesi gcc ve clang programlarıyla derleme yapıldığında otomatik olarak bağlama aşamasında devreye sokulmaktadır. Yani biz standart fonksiyonları ve POSIX fonksiyonları için bağlama aşamasında kütüphane belirtmek zorunda değiliz. Default durumda gcc ve clang programları standart C fonksiyonlarını ve POSIX fonksiyonlarını dinamik kütüphaneden alarak kullanır. Ancak programcı isterse "-static" seçeneği ile statik bağlama işlemi de yapabilir. Bu durumda bu fonksiyonlar statik kütüphanelerden alınarak çalıştırılabilen dosyalara yazılacaktır. Örneğin: $ gcc -o app -static app.c "-static" seçeneği ile bağlama işlemi yapıldığında artık üretilen çalıştırılabilir dosyanın dinamik kütüphanelerle hiçbir ilgisi kalmamaktadır. Zaten "-static" seçeneği belirtildiğinde artık dinamik kütüphaneler bağlama aşamasına programcı tarafından da dahil edilememektedir. Tabii bu biçimde statik bağlama işlemi yapıldığında çalıştırılabilen dosyanın boyutu çok büyüyecektir. Eğer "libc" kütüphanesinin default olarak bağlama aşamasında devreye sokulması istenmiyorsa "-nodefaultlibs" seçeneğinin kullanılması gerekir. Örneğin: $ gcc -nodefaultlibs -o app app.c Burada glibc kütüphanesi devreye sokulmadığı için bağlama aşamasında hata oluşacaktır. Tabii bu durumda da kütüphane açıkça belirtilebilir: $ gcc -nodefaultlibs -o app app.c -lc Bir kütüphanenin statik ve dinamik biçimi aynı anda bulunuyorsa ve biz bu kütüphaneyi "-l" seçeneği ile belirtiyorsak bu durumda default olarak kütüphanenin dinamik versiyonu devreye sokulmaktadır. Eğer bu durumda kütüphanelerin statik versiyonlarının devreye sokulması isteniyorsa "-static" seçeneğinin kullanılması ya da komut satırında açıkça statik kütüphaneye referans edilmesi gerekir. Örneğin: $ gcc -o app app.c -lmyutil -L. Burada eğer hem "libmyutil.so" hem de "libmyutil.a" dosyaları varsa "libmyutil.so" dosyası kullanılacaktır. Yani dinamik bağlama yapılacaktır. Tabii biz açıkça statik kütüphanenin ya da dinamik kütüphanenin kullanılmasını sağlayabiliriz: $ gcc -o app app.c libmyutil.a Aynı etkiyi şöyle de sağlayabilirdik: $ gcc -static -o app app.c -lmyutil -L. Burada "libc" kütüphanesinin dinamik biçimi devreye sokulacaktır. Ancak "libmyutil" kütüphanesi statik biçimde bağlanmıştır. Eğer "-static" seçeneği kullanılırsa bu durumda tüm kütüphanelerin statik versiyonları devreye sokulmaktadır. Tabii bu durumda biz açıkça dinamik kütüphanelerin bağlama işlemine sokulmasını isteyemeyiz. Örneğin: $ gcc -static -o app app.c libmyutil.so Bu işlem başarısız olacaktır. Çünkü "-static" seçeneği zaten "tüm kütüphanelerin statik olarak bağlanacağı" anlamına gelmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir programın kullandığı dinamik kütüphaneler ldd isimli utility program ile basit bir biçimde görüntülenebilir. Örneğin: $ ldd sample linux-vdso.so.1 (0x00007fff38162000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7ec0b5c000) /lib64/ld-linux-x86-64.so.2 (0x00007f7ec114f000 ldd programı dinamik kütüphanelerin kullandığı dinamik kütüphaneleri de görüntülemektedir. Programın doğrudan kullandığı dinamik kütüphanelerin listesi readelf komutuyla aşağıdaki gibi de elde edilebilir: $ readelf -d sample | grep "NEEDED" 0x0000000000000001 (NEEDED) Shared library: [libc.so.6] ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux sistemlerinde dinamik kütüphane kullanan programların yüklenmesi süreci biraz ilginçtir. Anımsanacağı gibi aslında her türlü program exec fonksiyonları tarafından yüklenip çalıştırılmaktadır. Bu exec fonksiyonlarının taban olanı "execve" isimli fonksiyondur. (Yani diğer exec fonksiyonları bunu çağırmaktadır.) execve fonksiyonu da bir sistem fonksiyonu olarak yazılmıştır. Dinamik kütüphane kullanan programların kullandığı dinamik kütüphaneler ismine "dinamik linker (dynamic linker)" denilen özel bir program tarafından yüklenmektedir. exec fonksiyonları aslında sıra dinamik kütüphanelerin yüklenmesine geldiğinde dinamik linker denilen bu programı çalıştırmaktadır. Dinamik linker "ld.so" ismiyle temsil edilmektedir. Programın kullandığı dinamik kütüphanelerin başka bir program tarafından yüklenmesi esneklik sağlamaktadır. Bu sayede sistem programcısı isterse (genellikle istemez) bu dinamik linker programını değiştirerek yükleme sürecinde özel işlemler yapabilir. Dinamik linker tamamen user mode'da çalışmaktadır. Programın dinamik kütüphanelerinin yüklenmesinde kullanılacak olan dinamik linker'ın yol ifadesi ELF formatında "Program Başlık Tablosu'nda" INTERP türüyle belirtilmektedir. INTERP türüne ilişkin Program Başlığı'nda dinamik bağlayıcının yol ifadesinin bulunduğu dosya offset'i belirtilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi bizim programımız örneğin "libmyutil.so" isimli bir dinamik kütüphaneden çağrı yapıyor olsun. Bu "libmyutil.so" dosyasının program çalıştırılırken nerede bulundurulması gerekir? İşte program çalıştırılırken ilgili dinamik kütüphane dosyasının özel bazı dizinlerde bulunuyor olması gerekmektedir. Dinamik kütüphanelerin dinamik bağlayıcı tarafından yüklendiğini ve dinamik bağlayıcının da "ld.so" ismiyle temsil edildiğini anımsayınız. "ld.so" ismiyle temsil edilen dinamik bağlayıcı akkında "man ld.so" komutuyla bilgi alabilirsiniz. "ld.so" için hazırlanan "man" sayfasında dinamik kütüphaneleri dinamik bağlayıcının nasıl ve nerelerde aradığı maddeler halinde açıklanmıştır. Bu maddeleri tek tek ele almak istiyoruz: 1) Dinamik bağlayıcı önce çalıştırılabilen dosyanın ".dynamic" bölümündeki DT_RPATH tag'ına bakar. Bu tag'ın değeri tek bir dizin ya da ':' karakterleriyle ayrılmış olan birden fazla dizin belirten bir yazı olabilir. Bu durumda dinamik bağlayıcı bu dizinlere sırasıyla bakmaktadır. Ancak birinci aşamada bu tag'a bakılmasının bir tasarım kusuru olduğu anlaşılmıştır. Bu nedenle ".dynamic" bölümüne DT_RPATH tag'ının yerleştirilmesi "deprecated" yapılmıştır. 2) Dinamik bağlayıcı yüklenmekte olan program dosyasına ilişkin prosesin LD_LIBRARY_PATH çevre değişkenine bakar. Eğer böyle bir çevre değişkeni varsa dinamik kütüphaneleri bu çevre değişkeninde belirtilen dizinlerde sırasıyla arar. Bu çevre değişkeni ':' karakterleriyle ayrılmış yol ifadelerinden oluşmaktadır. Biz programı genellikle kabuk üzerinden çalıştırdığımıza göre kabukta bu çevre değişkenini aşağıdaki örnekte olduğu gibi set edebiliriz: $ export LD_LIBRARY_PATH=/home/kaan:/home/kaan/Study/UnixLinux-SysProg:. Burada artık dinamik kütüphaneler sırasıyla "/home/kaan" dizininde, "/home/kaan/Study/UnixLinux-SysProg" dizininde ve prosesin çalışma dizininde (current working directory) aranacaktır. Çevre değişkeninin sonundaki "." karakterinin exec uygulayan prosesin o andaki çalışma dizinini temsil ettiğine dikkat ediniz. Tabii biz kabuk programının değil çalıştırılacak programın çevre değişken listesine ekleme yaparak da programı aşağıdaki gibi çalıştırabiliriz: $ LD_LIBRARY_PATH=:. ./app 3) Dinamik bağlayıcı çalıştırılabilen dosyanın ".dynamic" bölümündeki DT_RUNPATH tag'ına bakar. Birinci aşamada biz DT_RPATH tag'ının "deprecated" yapıldığını belirtmiştik. İşte bu tag yerine artık DT_RUNPATH tag'ı kullanılmalıdır. Bu tag'ın değeri de yine ':' karakterleriyle ayrılmış olan dizin listesinden oluşmaktadır. Dinamik bağlayıcı bu dizinlerde sırasıyla arama yapmaktadır. DT_RPATH ile DT_RUNPATH arasındaki tek fark DT_RUNPATH tag'ına LD_LIBRARY_PATH çevre değişkeninden daha sonra bakılmasıdır. 4) Dinamik bağlayıcı daha sonra "/etc/ld.so.cache" isimli cache dosyasına bakar. Bu cache dosyası her bir dinamik kütüphanenin hangi dizinlerde olduğunu belirtmektedir. Bu konu izleyen paragraflarda ele alınacaktır. 5) Nihayet dinamik bağlayıcı dinamik kütüphaneleri sırasıyla "/lib", /usr/lib" dizinlerinde de aramaktadır. 64 bit Linux sistemlerininin bir bölümünde 64 bit dinamik kütüphaneler için "/lib64" ve "/usr/lib64" dizinlerine de bakılabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki üçüncü maddede aranacak yol ifadesini çalıştırılabilir dosyanın DT_RUNPATH tag'ına yerleştirmek için ld bağlayıcısında "-rpath " bağlayıcı seçeneği kullanılmalıdır. Buradaki yol ifadelerinin mutlak olması zorunlu değilse de şiddetle tavsiye edilmektedir. gcc ve clang derleyicilerinde "-rpath" seçeneğini bağlayıcıya geçirebilmek için "-Wl" seçeneği kullanılabilir. "-Wl" seçeneği bitişik yazılan virgüllü alanlardan oluşmalıdır. gcc ve clang bu komut satırı argümanını "ld" bağlayıcısına virgüller yerine boşluklar (SPACE) koyarak geçirmektedir. Örneğin: $ gcc -o app app.c -Wl,-rpath,/home/kaan/Study/UnixLinux-SysProg libmyutil.so Burada ELF formatının DT_RUNPATH tag'ına yerleştirme yapılmaktadır. Çalıştırılabilir dosyaya iliştirilen DT_RUNPATH bilgisi "readelf" programı ile aşağıdaki gibi görüntülenebilir: $ readelf -d app | grep "RUNPATH" 0x000000000000001d (RUNPATH) Library runpath: [/home/csd/Study/UnixLinux-SysProg Biz bu tag'a birden fazla dizin de yerleştirebiliriz. Bu durumda yine dizinleri ':' ile ayırmamız gerekir. Örneğin: $ gcc -o app app.c -Wl,-rpath,/home/csd/Study/UnixLinux-SysProg:/home/kaan libmyutil.so Birden fazla kez "-rpath" seçeneği kullanıldığında bu seçenekler tek bir DT_RUNPATH tag'ına aralarına ':' karakteri getirilerek yerleştirilmektedir. Yani aşağıdaki işlem yukarıdaki ile eşdeğerdir: $ gcc -o app app.c -Wl,-rpath,/home/csd/Study/UnixLinux-SysProg,-rpath,/home/kaan libmyutil.so "-rpath" bağlayıcı seçeneğinde default durumda DT_RUNPATH tag'ına yerleştirme yapıldığına dikkat ediniz. Eğer DT_RPATH tag'ına yerleştirme yapılmak isteniyorsa bağlayıcı seçeneklerine ayrıca "--disable-new-dtags" seçeneğinin de girilmesi gerekmektedir. Örneğin: $ gcc -o app app.c -Wl,-rpath,/home/csd/Study/UnixLinux-SysProg,--disable-new-dtags libmyutil.so DT_RUNPATH tag'ını da aşağıdaki gibi görüntüleyebiliriz: $ readelf -d app | grep "RUNPATH" 0x000000000000001d (RUNPATH) Library runpath: [/home/kaan/Study/UnixLinux-SysProg] Çalıştırılabilir dosyaya DT_RUNPATH tag'ının mutlak ya da göreli yol ifadesi biçiminde girilmesi bazı kullanım sorunlarına yol açabilmektedir. Çünkü bu durumda dinamik kütüphaneler uygulamanın kurulduğu dizine göreli biçimde konuşlandırılacağı zaman uygulamanın kurulum yeri değiştirildiğinde sorunlar oluşabilmektedir. Örneğin biz çalıştırılabilir dosyanın DT_RUNPATH tag'ına "home/kaan/test" isimli yol ifadesini yazmış olalım. Programımızı ve dinamik kütüphanemizi bu dizine yerleştirirsek bir sorun oluşmayacaktır. Ancak başka bir dizine yerleştirirsek dinamik kütüphanemiz bulunamayacaktır. İşte bunu engellemek için "-rpath" seçeneğinde '$ORIGIN' argümanı kullanılmaktadır. Buradaki '$ORIGIN' argümanı "o anda çalıştırılabilen dosyanın bulunduğu dizini" temsil etmektedir. Örneğin: $ gcc -o app app.c -Wl,-rpath,'$ORIGIN'/. libmyutil.so Burada artık çalıştırılabilen dosya nereye yerleştirilirse yerleştirilsin ve nereden çalıştırılırsa çalıştırılsın dinamik kütüphaneler çalıştırılabilen dosyanın yerleştirildiği dizinde aranacaktır. Yukarıda da belirttiğimiz gibi aslında arama sırası bakımından DT_RPATH tag'ının en yukarıda olması (LD_LIBRARY_PATH'in yukarısında olması) yanlış bir tasarımdır. Geriye doğru uyumu koruyarak bu yanlış tasarım DT_RUNPATH tag'ı ile telafi edilmiştir. DT_RUNPATH tag'ına LD_LIBRARY_PATH çevre değişkeninden sonra başvurulmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dinamik kütüphanelerin aranması sırasında "/lib" ve "/usr/lib" dizinlerine bakılmadan önce özel bir dosyaya da bakılmaktadır. Bu dosya "/etc/ld.so.cache" isimli dosyadır. "/etc/ld.so.cache" dosyası aslında binary bir dosyadır. Bu dosya hızlı aramanın yapılabilmesi için "sözlük (dictionary)" tarzı yani algoritmik aramaya izin verecek biçimde bir içeriğe sahiptir. Bu dosya ilgili dinamik kütüphane dosyalarının hangi dizinler içerisinde olduğunu gösteren bir yapıdadır. (Yani bu dosya ".so" dosyalarının hangi dizinlerde olduğunu belirten binary bir dosyadır.) Başka bir deyişle bu dosyanın içerisinde "falanca .so dosyası filanca dizinde" biçiminde bilgiler vardır. İlgili ".so" dosyasının yerinin bu dosyada aranması dizinlerde aranmasından çok daha hızlı yapılabilmektedir. Ayrıca dinamik kütüphaneler değişik dizinlerde bulunabilmektedir. Bunların LD_LIBRARY_PATH çevre değişkeninde belirtilen dizinlerde tek tek aranması bir yavaşlık oluşturabilmektedir. Pekiyi bu "/etc/ld.so.cache" dosyasının içerisinde hangi ".so" dosyaları vardır? Aslında bu dosyanın içerisinde "/lib" ve "/usr/lib" dizinindeki ".so" dosyalarının hepsi bulunmaktadır. Ama programcı isterse kendi dosyalarını da bu cache dosyasının içerisine yerleştirebilir. Burada dikkat edilmesi gereken nokta bu cache dosyasına "/lib" ve "/usr/lib" dizinlerinden daha önce bakıldığı ve bu dizinlerin içeriğinin de zaten bu cache dosyasının içerisinde olduğudur. O halde aslında "/lib" ve "/usr/lib" dizinlerinde arama çok nadir olarak yapılmaktadır. Ayrıca bu cache dosyasına LD_LIBRARY_PATH çevre değişkeninden daha sonra bakıldığına dikkat ediniz. O halde programcının kendi ".so" dosyalarını da -eğer uzun süreliğine konuşlandıracaksa- bu cache dosyasının içerisine yerleştirmesi tavsiye edilmektedir. Pekiyi "/etc/ld.so.cache" dosyasına biz nasıl bir dosya ekleriz? Aslında programcı bunu dolaylı olarak yapmaktadır. Şöyle ki: "/sbin/ldconfig" isimli bir program vardır. Bu program "/etc/ld.so.conf" isimli bir text dosyasına bakar. Bu dosya dizinlerden oluşmaktadır. Bu "ldconfig" programı bu dizinlerin içerisindeki "so" dosyalarını "/etc/ld.so.cache" dosyasına eklemektedir. Şimdilerde "/etc/ld.so.conf" dosyasının içeriği şöyledir: include /etc/ld.so.conf.d/*.conf Bu satır "/etc/ld.so.conf.d" dizinindeki tüm ".conf" uzantılı dosyaların bu işleme dahil edileceğini belirtmektedir. Biz "ldconfig" programını çalıştırdığımızda bu program "/lib", "/usr/lib" ve "/etc/ld.so.conf" (dolayısıyla "/etc/ld.so.conf.d" dizinindeki ".conf" dosyalarına) bakarak "/etc/ld.so.cache" dosyasını yeniden oluşturmaktadır. O halde bizim bu cache'e ekleme yapmak için tek yapacağımız şey "/etc/ld.so.conf.d" dizinindeki bir ".conf" dosyasına yeni bir satır olarak bir dizinin yol ifadesini girmektir. (".conf" dosyaları her satırda bir dizinin yol ifadesinden oluşmaktadır.) Tabii programcı isterse bu dizine yeni bir ".conf" dosyası da ekleyebilir. İşte programcı bu işlemi yaptıktan sonra "/sbin/ldconfig" programını çalıştırınca artık onun eklediği dizinin içerisindeki ".so" dosyaları da "/etc/ld.so.cache" dosyasının içerisine eklenmiş olacaktır. Daha açık bir anlatımla programcı bu cache dosyasına ekleme işini adım adım şöyle yapar: 1) Önce ".so" dosyasını bir dizine yerleştirir. 2) Bu dizinin ismini "/etc/ld.so.conf.d" dizinindeki bir dosyanın sonuna ekler. Ya da bu dizinde yeni ".conf" dosyası oluşturarak dizini bu dosyanın içerisine yazar. 3) "/sbin/ldconfig" programını çalıştırır. "ldconfig" programının "sudo" ile çalıştırılması gerektiğine dikkat ediniz. Zaten "/sbin" dizinindeki tüm programlar "super user" için bulundurulmuştur. Programcı "/etc/ld.so.conf.d" dizinindeki herhangi bir dosyaya değil de "-f" seçeneği sayesinde kendi belirlediği bir dosyaya da ilgili dizinleri yazabilmektedir. Başka bir deyişle "-f" seçeneği "şu config dosyasına da bak" anlamına gelmektedir. "ldconfig" her çalıştırıldığında sıfırdan yeniden cache dosyasını oluşturmaktadır. Programcı "/lib" ya da "/usr/lib" dizinine bir ".so" dosyası eklediğinde "ldconfig" programını çalıştırması -zorunlu olmasa da- iyi bir tekniktir. Çünkü o dosya da cache dosyasına yazılacak ve daha hızlı bulunacaktır. ldconfig programında "-p" seçeneği ile cache dosyası içerisindeki tüm dosyalar görüntülenebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kütüphane dosyalarının "so" isimleri denilen bir isimleri de bulunabilmektedir. Kütüphane dosyalarının "so" isimleri linker tarafından kullanılan isimleridir. Kütüphane dosyası oluşturulurken "so" isimleri verilmeyebilir. Yani bir kütüphane dosyasının "so" ismi olmak zorunda değildir. Kütüphane dosyalarına "so" isimlerini vermek için "-soname " linker seçeneği kullanılmaktadır. Kütüphanelere verilen "so" isimleri ELF formatının dinamik bölümündeki (dynamic section) SONAME isimli bir tag'ına yerleştirilmektedir. "-soname" komut satırı argümanı linker'a ilişkin olduğu için "-Wl" seçeneği ile kullanılmalıdır. Örneğin biz libx.so isimli bir dinamik kütüphaneyi "so" ismi vererek oluşturmak isteyelim. Bu işlemi şöyle yapabiliriz: $ gcc -o libx.so -fPIC -shared -Wl,-soname,liby.so libx.c Burada "libx.so" kütüphane dosyasına "liby.so" "so" ismi verilmiştir. Kütüphane dosyalarına iliştirilen "so" isimleri readelf ile aşağıdaki gibi görüntülenebilir: $ readelf -d libx.so | grep "SONAME" 0x000000000000000e (SONAME) Kitaplık so_adı: [liby.so] Aynı işlem objdump programıyla da şöyle yapılabilir: objdump -x libx.so | grep "SONAME" SONAME liby.so Tabii yukarıda da belirttiğimiz gibi biz dinamik kütüphanelere "so" ismi vermek zorunda değiliz. "so" ismi içeren bir kütüphaneyi kullanan bir program link edilirken linker çalıştırılabilen dosyaya "so" ismini içeren kütüphanenin ismini değil "so" ismini yazmaktadır. Yukarıdaki örneğimizde "libx.so" kütüphanesi "so" ismi olarak "liby.so" ismini içermektedir. Şimdi libx.so dosyasını kullanan "app.c" dosyasını derleyip link edelim: $ gcc -o app app.c libx.so Burada link işleminde "libx.so" dosya ismi kullanılmıştır. Ancak oluşturulan "app" dosyasının içerisine linker bu ismi değil, "so" ismi olan "liby.so" ismini yazacaktır. Örneğin: $ readelf -d app | grep "NEEDED" 0x0000000000000001 (NEEDED) Paylaşımlı kitaplık: [liby.so] 0x0000000000000001 (NEEDED) Paylaşımlı kitaplık: [libc.so.6] O halde biz buradaki "app" dosyasını çalıştırmak istediğimizde yükleyici (yani dinamik linker) artık "libx.so" dosyasını değil, "liby.so" dosyasını yüklemeye çalışacaktır. Örneğin. $ export LD_LIBRARY_PATH=. $ ./app ./app: error while loading shared libraries: liby.so: cannot open shared object file: No such file or directory Tabii yukarıda belirttiğimiz gibi eğer kütüphaneyi oluştururken ona "so" ismi vermeseydik bu durumda linker "app" dosyasına "libx.so" dosyasını yazacaktı ve yükleyici de (dynamic linker) bu dosyası yükleyecekti. Pekiyi yukarıdaki örnekte "app" programı artık "liby.so" dosyasını kullanıyor gibi olduğuna göre ve böyle de bir dosya olmadığına göre bu işlemlerin ne anlamı vardır? İşte biz bu örnekte "so" ismine ilişkin dosyayı bir sembolik link dosyası haline getirirsek ve bu sembolik link dosyası da "libx.so" dosyasını gösterir hale gelirse sorunu ortadan kaldırabiliriz. Örneğin: $ ln -s libx.so liby.so $ ls -l liby.so lrwxrwxrwx 1 kaan study 7 Şub 25 16:44 liby.so -> libx.so Şimdi artık "app" dosyasını çalıştırmak istediğimizde yükleyici "liby.so" dosyasını yüklemek isteyecektir. Ancak "liby.so" dosyası da zaten "libx.so" dosyasını belirttiği için yine "libx.so" dosyası yüklenecektir. Yani artık "app" dosyasını çalıştırabiliriz. Tabii burada tüm bunları neden yapmış olduğumuza bir anlam verememiş olabilirsiniz. İşte bunun anlamını izleyen paragraflarda dinamik kütüphanelerin versiyonlanması konusunda açıklayacağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- UNIX/Linux sistemlerinde dinamik kütüphane dosyalarına isteğe bağlı olarak birer versiyon numarası verilebilmektedir. Bu versiyon numarası dosya isminin bir parçası durumundadır. Linux sistemlerinde izlenen tipik numaralandırma (convention) şöyledir: .so... Örneğin: libmyutil.so.2.4.6 Majör numaralar büyük değişiklikleri, minör numaralar ise küçük değişiklikleri anlatmaktadır. Majör numara değişirse yeni dinamik kütüphane eskisiyle uyumlu olmaz. Burada "uyumlu değildir" lafı eski dinamik kütüphaneyi kullanan programların yenisini kullanamayacağı anlamına gelmektedir. Çünkü muhtemelen bu yeni versiyonda fonksiyonların isimlerinde, parametrik yapılarında değişiklikler söz konusu olmuş olabilir ya da bazı fonksiyonlar silinmiş olabilir. Fakat majör numarası aynı ancak minör numaraları farklı olan kütüphaneler birbirleriyle uyumludur. Yani alçak minör numarayı kullanan program yüksek minör numarayı kullanırsa sorun olmayacaktır. Bu durumda tabii yüksek minör numaralı kütüphanede hiçbir fonksiyonun ismi, parametrik yapısı değişmemiş ve hiçbir fonksiyon silinmemiş olmalıdır. Örneğin yüksek minör numaralarda fonksiyonlarda daha hızlı çalışacak biçimde optimizasyonlar yapılmış olabilir. Ya da örneğin yüksek minör numaralarda yeni birtakım fonksiyonlar da eklenmiş olabilir. Çünkü yeni birtakım fonksiyonlar eklendiğinde eski fonksiyonlar varlığını devam ettirmektedir. Tabii yine de bu durum dinamik kütüphanenin eski versiyonunu kullanan programların düzgün çalışacağı anlamına gelmemektedir. Çünkü programcılar kodlarına yeni birtakım şeyler eklerken istemeden eski kodların çalışmasını da bozabilmektedir. (Bu tür problemler Windows sistemlerinde eskiden ciddi sıkıntılara yol açmaktaydı. Bu probleme Windows sistemlerinde "DLL cehennemi (DLL Hell)" deniyordu.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux sistemlerinde versiyonlama bakımından bir dinamik kütüphanenin üç ismi bulunmaktadır: 1) Gerçek ismi (real name) 2) so ismi (so name) 3) Linker ismi (linker name) Kütüphanenin majör ve çift minör versiyonlu ismine gerçek ismi denilmektedir. Örneğin: libmyutil.so.2.4.6 "so" ismi ise yalnızca majör numara içeren ismidir. Örneğin yukarıdaki gerçek ismin "so" ismi şöyledir: libmyutil.so.2 Linker ismi ise hiç versiyon numarası içermeyen ismidir. Örneğin yukarıdaki kütüphanelerin linker ismi ise şöyledir: libmyutil.so İşte tipik olarak "so" ismi gerçek isme sembolik link, linker ismi de en yüksek numaralı "so" ismine sembolik link yapılır. linker ismi ---> so ismi ---> gerçek ismi Örneğin: $ gcc -o libmyutil.so.1.0.0 -shared -fPIC libmyutil.c (gerçek isimli kütüphane dosyası oluşturuldu) $ ln -s libmyutil.so.1.0.0 libmyutil.so.1 (so ismi oluşturuldu) $ ln -s libmyutil.so.1 libmyutil.so (linker ismi oluşturuldu) Burada oluşturulan üç dosyayı "ls -l" komutu ile görüntüleyelim: lrwxrwxrwx 1 kaan study 14 Şub 25 15:45 libmyutil.so -> libmyutil.so.1 lrwxrwxrwx 1 kaan study 18 Şub 25 15:45 libmyutil.so.1 -> libmyutil.so.1.0.0 -rwxr-xr-x 1 kaan study 15736 Şub 25 15:45 libmyutil.so.1.0.0 ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 123. Ders 25/02/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dinamik kütüphanelerin linker isimleri o kütüphaneyi kullanan programlar link edilirlen link aşamasında (link ederken) kullanılan isimlerdir. Bu sayede link işlemini yapan programcıların daha az tuşa basarak genel bir isim kullanması sağlanmıştır. Bu durumda örneğin biz libmyutil isimli kütüphaneyi kullanan programı link etmek istersek şöyle yapabiliriz: $ gcc -o app app.c libmyutil.so Ya da şöyle yapabiliriz: $ gcc -o app app.c -lmyutil -L. Burada aslında "libmyutil.so" dosyası "so ismine" "so" ismi de "gerçek isme link yapılmış" durumdadır. Yani bu komutun aslında eşdeğeri şöyledir: $ gcc -o app app.c libmyutil.so.1.0.0 ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 124. Ders 01/03/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda anlattıklarımızı özetlersek geldiğimiz noktayı daha iyi kavrayabiliriz: 1) Bir dinamik kütüphane oluştururken ona bir versiyon numarası da atanabilmektedir. Örneğin biz oluşturduğumuz "myutil" dinamik kütüphanesine "1.0.0" versiyon numarası atamış olalım. Bu durumda kütüphanemizin gerçek ismi "libmyutil.so.1.0.0" olacaktır. Kütüphanemizi aşağıdaki gibi derlemiş olalım: $ gcc -fPIC -shared -o libmyutil.so.1.0.0 -Wl,-soname,libmyutil.so.1 libmyutil.c 2) Dinamik kütüphanelerin "so ismi" kütüphanelerin içerisine yazılan ismidir. Yukarıdaki gibi bir derlemede biz "libmyutil.so.1.0.0" kütüphanesinin içerisine "so ismi" olarak "libmyutil.so" ismini yerleştirdik. "so isimleri" genel olarak yalnızca majör numara içeren isimlerdir. Bizim bu aşamada tipik olarak bir sembolik link oluşturarak "so" ismine ilişkin dosyanın gerçek kütüphane dosyasını göstermesini sağlamamız gerekir. Bunu şöyle yapabiliriz: $ ln -s libmyutil.so.1.0.0 libmyutil.so.1 Şimdi her iki dosyayı da görüntüleyelim: lrwxrwxrwx 1 kaan study 18 Mar 1 20:02 libmyutil.so.1 -> libmyutil.so.1.0.0 -rwxr-xr-x 1 kaan study 15736 Mar 1 19:57 libmyutil.so.1.0.0 3) Dinamik kütüphanenin link aşamasında kullanılmasını kolaylaştırmak için sonunda versiyon uzuntısı olmayan bir "linker ismi" oluşturabiliriz. Tabii bu linker ismi aslında gerçek kütüphaneye referans edecektir. Ancak bu referansın doğrudan değil de "so ismi" üzerinden yapılması daha esnek bir kullanıma yol açacaktır. Örneğin: $ ln -s libmyutil.so.1 libmyutil.so Artık kütüphanenin "linker ismi" "so ismine", "so ismi" de gerçek ismine sembolik link yapılmış durumdadır. Bu üç dosyayı aşağıda yeniden görüntüleyelim: lrwxrwxrwx 1 kaan study 14 Mar 1 20:07 libmyutil.so -> libmyutil.so.1 lrwxrwxrwx 1 kaan study 18 Mar 1 20:02 libmyutil.so.1 -> libmyutil.so.1.0.0 -rwxr-xr-x 1 kaan study 15736 Mar 1 19:57 libmyutil.so.1.0.0 Aşağıdaki gibi bir durum elde ettiğimize dikkat ediniz: Linker ismi ---> so ismi ---> gerçek isim 4) Şimdi kütüphaneyi kullanan bir "app" programını derleyip link edelim: $ gcc -o app app.c libmyutil.so Şimdi LD_LIBRARY_PATH çevre değişkenini belirleyip programı çalıştıralım: $ LD_LIBRARY_PATH=. ./app 30.000000 -10.000000 200.000000 0.500000 Burada app programının kullandığı kütüphane ismi app dosyasının içerisinde kütüphanenin "so ismi" olarak set edilecektir. Yani burada "app" dosyası sanki "libmyutil.so.1" dosyasını kullanıyor gibi olacaktır. Örneğin: $ readelf -d app | grep "NEEDED" 0x0000000000000001 (NEEDED) Paylaşımlı kitaplık: [libmyutil.so.1] 0x0000000000000001 (NEEDED) Paylaşımlı kitaplık: [libc.so.6] İşte "app" programını yükleyecek olan dinamik linker aslında "libmyutil.so.1" dosyasını yüklemeye çalışacaktır. Bu dosyann kütüphanenin gerçek ismine sembolik link yapıldığını anımsayınız. Bu durumda gerçekte yüklenecek olan dosya "libmyutil.so.1" dosyası değil, "libmyutil.so.1.0.0" dosyası olacaktır. Yani çalışmada bir sorun ortaya çıkmayacaktır. Pekiyi tüm bunların amacı nedir? Bunu şöyle açıklayabiliriz: 1) Örneğin kütüphanemizin libmyutil.so.1.1.0 biçiminde majör numarası aynı, minör numarası farklı öncekiyle uyumlu yeni bir versiyonunun daha oluşturulduğunu düşünelim. Şimdi biz uygulamamızı çektiğimiz dizin içerisindeki "libmyutil.so" dosyasını bu yeni versiyonu referans edecek biçimde değiştirebiliriz. Bu durumda dinamik linker "app" programını yüklemeye çalışırken aslında artık "libmyutil.so.1.1.0" kütüphanesini yükleyecektir. Burada biz hiç "app" dosyasının içini değiştirmeden artık "app" dosyasının kütüphanenin yeni minör versiyonunu kullamasını sağlamış olduk. 2) Şimdi de kütüphanemizin "libmyutil.so.2.0.0" biçiminde yeni bir majör versiyonunun oluşturulduğunu varsayalım. 1 numaralı majör versiyonla 2 numaralı majör versiyon birbirleriyle uyumlu değildir. Biz bu "libmyutil.so.2.0.0" yeni versiyonu derlerken ona "so ismi" olarak artık "libmyutil.so.2" ismini vermeliyiz. Tabii bu durumda biz yine "libmyutil.so.2" sembolik bağlantı dosyasının "libmyutil.so.2.0.0" dosyasını göstermesini sağlamalıyız. Artık kütüphanenin 2'inci versiyonunu kullanan programlarda yüklenecek kütüphane "libmyutil.so.2" kütüphanesi olacaktır. Bu kütüphanede 2'inci versiyonunun gerçek kütüphane ismine sembolik link yapılmış durumdadır. "so ismine" ilişkin sembolik link çıkartma ve "/etc/ld.so.cache" dosyasının güncellenmesi işlemi ldconfig tarafından otomatik yapılabilmektedir. Yani aslında örneğin biz kütüphanenin gerçek isimli dosyasını "/lib" ya da "/usr/lib" içerisine yerleştirip "ldconfig" programını çalıştırdığımızda bu program zaten "so ismine" ilişkin sembolik linki de oluşturmaktadır. Örneğin biz "libmyutil.so.1.0.0" dosyasını "/usr/lib" dizinine kopyalayalım ve "ldconfig" programını çalıştıralım. "ldconfig" programı "libmyutil.so.1" sembolik link dosyasını oluşturup bu sembolik link dosyasının "libmyutil.so.1.0.0" dosyasına referans etmesini sağlayacaktır. Tabii cache'e de "libmyutil.so.1" dosyasını yerleştirecektir. Örneğin: $ ldconfig -p | grep "libmyutil" libmyutil.so.1 (libc6,x86-64) => /lib/libmyutil.so.1 $ ls -l /usr/lib | grep "libmyutil" lrwxrwxrwx 1 root root 18 Mar 1 21:02 libmyutil.so.1 -> libmyutil.so.1.0.0 -rwxr-xr-x 1 root root 15736 Mar 1 21:01 libmyutil.so.1.0. Özetle Dinamik kütüphane kullanırken şu konvansiyona uymak iyi bir tekniktir: - Kütüphane ismini "lib" ile başlatarak vermek - Kütüphane ismine majör ve minör numara vermek - Gerçek isimli kütüphane dosyasını oluştururken "so ismi" olarak "-Wl,-soname" seçeneği ile kütüphanenin "so ismini" yazmak - Kütüphane için "linker ismi" ve "so ismini" sembolik link biçiminde oluşturmak - Kütüphane paylaşılacaksa onu "/lib" ya da tercihen "/usr/lib" dizinine yerleştirmek ve ldconfig programı çalıştırarak /etc/ld.so.cache dosyasının güncellenmesini sağlamak ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dinamik kütüphane dosyaları program çalıştırıldıktan sonra çalışma zamanı sırasında çalışmanın belli bir aşamasında da yüklenebilir. Buna "dinamik kütüphane dosyalarının dinamik yüklenmesi" de denilmektedir. Dinamik kütüphane dosyalarının baştan "dinamik linker" tarafından değil de programın çalışma zamanı sırasında yüklenmesinin bazı avantajları şunlar olabilmektedir: 1) Dinamik kütüphaneler baştan yüklenmediği için program başlangıçta daha hızlı yüklenebilir. 2) Programın sanal bellek alanı gereksiz bir biçimde doldurulmayabilir. Örneğin nadiren çalışacak bir fonksiyon dinamik kütüphanede olabilir. Bu durumda o dinamik kütüphanenin işin başında yüklenmesi gereksiz bir yükleme zamanı ve bellek israfına yol açabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Dinamik kütüphanelerin dinamik yüklenmesi dlopen, dlsym, dlerror ve dlclose fonksiyonlarıyla yapılmaktadır. Bu fonksiyonlar "libdl" kütüphanesi içerisindedir. Dolayısıyla link işlemi için -ldl seçeneğinin bulundurulması gerekir. Dinamik kütüphanelerin dinamik yüklenmesi için önce "dlopen" fonksiyonu ile dinamik kütüphanenin yüklenmesinin sağlanması gerekir. dlopen fonksiyonunun prototipi şöyledir: #include void *dlopen(const char *filename, int flag); Fonksiyonun birinci parametresi yüklenecek dinamik kütüphanenin yol ifadesini, ikinci parametresi seçenek belirten bayrakları almaktadır. Fonksiyon başarı durumunda kütüphaneyi temsil eden bir handle değerine, başarısızlık durumunda NULL adrese geri dönmektedir. Başarısızlık durumunda fonksiyon errno değişkenini set etmez. Başarısızlığa ilişkin yazı doğrudan dlerror fonksiyonuyla elde edilmektedir: char *dlerror(void); dlopen fonksiyonunun birinci parametresindeki dinamik kütüphane isminde eğer hiç / karakteri yoksa bu durumda kütüphanenin aranması daha önce ele aldığımız prosedüre göre yapılmaktadır. Eğer dosya isminde en az bir / karakteri varsa dosya yalnızca bu mutlak ya da göreli yol ifadesinde aranmaktadır. Dinamik yükleme sırasında yüklenecek kütüphanenin SONAME alanında yazılan isme hiç bakılmamaktadır. (Bu SONAME alanındaki isim yalnızca link aşamasında linker tarafından kullanılmaktadır.) Örneğin: void *dlh; if ((dlh = dlopen("libmyutil.so.1.0.0", RTLD_NOW)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } Burada dlopen fonksiyonunun ikinci parametresine RTLD_NOW bayrağı geçilmiştir. Bu bayrağın etkisi izleyen paragraflarda ele alınacaktır. Kütüphanenin adres alanından boşaltılması ise dlclose fonksiyonuyla yapılmaktadır: #include int dlclose(void *handle); Aynı kütüphane dlopen fonksiyonu ile ikinci kez yüklenebilir. Bu durumda gerçek bir yükleme yapılmaz. Ancak yüklenen sayıda close işleminin yapılması gerekmektedir. Kütüphanenin içerisindeki fonksiyonlar ya da global nesneler adresleri elde edilerek kullanılırlar. Bunların adreslerini elde edebilmek için dlsym isimli fonksiyon kullanılmaktadır: #include void *dlsym(void *handle, const char *symbol); Fonksiyon başarı durumunda ilgili sembolün adresine, başarısızlık durumunda NULL adrese geri döner. Örneğin: double (*padd)(double, double); ... if ((padd = (double (*)(double, double))(dlsym(dlh, "add")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = padd(10, 20); printf("%f\n", result); Ancak burada C standartları bağlamında bir pürüz vardır. C'de (ve tabii C++'ta) fonksiyon adresleri ile data adresleri tür dönüştürme operatörü ile bile dönüştürülememektedir. Yani yukarıdaki tür dönüştürmesi ile atama geçersizdir. Ayrıca void * türü data adresi için anlamlıdır. Yani biz C'de de C++'ta da void bir adresi fonksiyon göstericisine, fonksiyon adresini de void bir göstericiye atayamayız. Ancak pek çok derleyici default durumda bu biçimdeki dönüştürmeleri kabul etmektedir. Yani yukarıdaki kod aslında C'de geçersiz olmasına karşın gcc ve clang derleyicilerinde sorunsuz derlenecektir. (Derleme sırasında -pedantic-errors seçeneği kullanılırsa derleyiciler standartlara uyumu daha katı bir biçimde ele almaktadır. Dolayısıyla yukarıdaki kod bu seçenek kullanılarak derlenirse error oluşacaktır.) Pekiyi bu durumda ne yapabiliriz? İşte bunun için bir hile vardır. Fonksiyon göstericisinin adresini alırsak artık o bir data göstericisi haline gelir. Bir daha * kullanırsak data göstericisi gibi aslında fonksiyon göstericisinin içerisine değer atayabiliriz. Örneğin: if ((*(void **)&padd = dlsym(dlh, "add")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } Sembol isimleri konusunda dikkat etmek gerekir. Çünkü bazı derleyiciler bazı koşullar altında isimleri farklı isim gibi object dosyaya yazabilmektedir. Buna "name decoration" ya da "name mangling" denilmektedir. Örneğin C++ derleyicileri fonksiyon isimlerini parametrik yapıyla kombine ederek başka bir isimle object dosyaya yazar. Halbuki dlsym fonksiyonunda sembolün dinamik kütüphanedeki dekore edilmiş isminin kullanılması gerekmektedir. Sembollerin dekore edilmiş isimlerini elde edebilmek için "nm" utility'sini kullanabilirsiniz. Örneğin: nm libmyutil.so.1.0.0 nm utility'si ELF formatının string tablosunu görüntülemektedir. Aynı işlem readelf programında -s ile de yapılabilir: readelf -s libmyutil.so.1.0.0 Aşağıda bir dinamik kütüphane dinamik olarak yüklenmiş ve oradan bir fonksiyon ve data adresi alınarak kullanılmıştır. Buradaki dinamik kütüphaneyi daha önce yaptığımız gibi derleyebilirsiniz: $ gcc -fPIC -shared -o libmyutil.so.1.0.0 -Wl,-soname,libmyutil.so.1 libmyutil.c ---------------------------------------------------------------------------------------------------------------------------*/ /* libmyutil.c */ #include double add(double a, double b) { return a + b; } double sub(double a, double b) { return a - b; } double multiply(double a, double b) { return a * b; } double divide(double a, double b) { return a / b; } /* app.c */ #include #include #include typedef double (*PROC)(double, double); int main(void) { void *dlh; PROC padd, psub, pmul, pdiv; double result; if ((dlh = dlopen("libmyutil.so.1.0.0", RTLD_NOW)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } if ((*(void **)&padd = dlsym(dlh, "add")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = padd(10, 20); printf("%f\n", result); if ((*(void **)&psub = dlsym(dlh, "sub")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = psub(10, 20); printf("%f\n", result); if ((*(void **)&pmul = dlsym(dlh, "multiply")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = pmul(10, 20); printf("%f\n", result); if ((*(void **)&pdiv = dlsym(dlh, "divide")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = pdiv(10, 20); printf("%f\n", result); dlclose(dlh); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Dinamik kütüphane dlopen fonksiyonuyla yüklenirken global değişkenlerin ve fonksiyonların nihai yükleme adresleri bu dlopen işlemi sırasında hesaplanabilir ya da onlar kullanıldıklarında hesaplanabilir. İkisi arasında kullanıcı açısından bir fark olmamakla birlikte tüm sembollerin adreslerinin yükleme sırasında hesaplanması bazen yükleme işlemini (eğer çok sembol varsa) uzatabilmektedir. Bu durumu ayarlamak için dlopen fonksiyonunun ikinci parametresi olan flags parametresi kullanılır. Bu flags parametresi RTLD_NOW olarak girilirse (yukarıdaki örnekte böyle yaptık) tüm sembollerin adresleri dlopen sırasında, RTLD_LAZY girilirse kullanıldıkları noktada hesaplanmaktadır. İki biçim arasında çoğu kez programcı için bir farklılık oluşmamaktadır. Ancak aşağıdaki örnekte bu iki biçimin ne anlama geldiği gösterilmektedir. Aşağıdaki örnekte "libmyutil.so.1.0.0" kütüphanesindeki foo fonksiyonu gerçekte olmayan bir bar fonksiyonunu çağırmıştır. Bu fonksiyonun gerçekte olmadığı foo fonksiyonunun sembol çözümlemesi yapıldığında anlaşılacaktır. İşte eğer bu kütüphaneyi kullanan "app.c" programı kütüphaneyi RTLD_NOW ile yüklerse tüm semboller o anda çözülmeye çalışılacağından dolayı bar fonksiyonunun bulunmuyor olması hatası da dlopen sırasında oluşacaktır. Eğer kütüphane RTLD_LAZY ile yüklenirse bu durumda sembol çözümlemesi foo'nun kullanıldığı noktada (yani dlsym fonksiyonunda) gerçekleşecektir. Dolayısıyla hata da o noktada oluşacaktır. Bu programı RTLD_NOW ve RTLD_LAZY bayraklarıyla ayrı ayrı derleyip çalıştırınız. ---------------------------------------------------------------------------------------------------------------------------*/ /* libmyutil.c */ #include void bar(void); void foo(void) { bar(); } /* app.c */ #include #include #include int main(void) { void *dlh; void (*pfoo)(void); double result; if ((dlh = dlopen("libmyutil.so.1.0.0", RTLD_LAZY)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } printf("dlopen called\n"); if ((*(void **)&pfoo = dlsym(dlh, "foo")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } pfoo(); dlclose(dlh); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- 125. Ders 03/03/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bazen bir dinamik kütüphane içerisindeki sembollerin o dinamik kütüphaneyi kullanan kodlar tarafından kullanılması istenmeyebilir. Örneğin dinamik kütüphanede "bar" isimli bir fonksiyon vardır. Bu fonksiyon bu dinamik kütüphanenin kendi içerisinden başka fonksiyonlar tarafından kullanılıyor olabilir. Ancak bu fonksiyonun dinamik kütüphanenin dışından kullanılması istenmeyebilir. (Bunun çeşitli nedenleri olabilir. Örneğin kapsülleme sağlamak için, dışarıdaki sembol çakışmalarını ortadan kaldırmak için vs.) İşte bunu sağlamak amacıyla gcc ve clang derleyicilerine özgü "__attribute__((...))" eklentisindan faydalanılmaktadır. "__attribute__((...))" eklentisi pek çok seçeneğe sahip platform spesifik bazı işlemlere yol açmaktadır. Bu eklentinin seçeneklerini gcc dokümanlarından elde edebilirsiniz. Bizim bu amaçla kullanacağımız "__attribute__((...))" seçeneği "visibility" isimli seçenektir. Aşağıdaki örnekte bar fonksiyonu foo fonksiyonu tarafından kullanılmaktadır. Ancak kütüphanenin dışından bu fonksiyonun kullanılması istenmemiştir. Eğer fonksiyon isminin soluna "__attribute__((visibility("hidden")))" yazılırsa bu durumda bu fonksiyon dinamik kütüphanenin dışından herhangi bir biçimde kullanılamaz. Örneğin: void __attribute__((visibility("hidden"))) bar(void) { // ... } Burada fonksiyon özelliğinin (yani __attribute__ sentaksının) fonksiyon isminin hemen soluna getirildiğine ve çift parantez kullanıldığına dikkat ediniz. Burada kullanılan özellik "visibility" isimli özelliktir ve bu özelliğin değeri "hidden" biçiminde verilmiştir. Aşağıdaki örnekte "libmyutil.so.1.0.0" kütüphanesindeki foo fonksiyonu dışarıdan çağrılabildiği halde bar fonksiyonu dışarıdan çağrılamayacaktır. Tabii kütüphane içerisindeki foo fonksiyonu bar fonksiyonunu çağırabilmektedir. Dosyaları aşağıdaki gibi derleyebilirsiniz: $ gcc -shared -fPIC -Wl,-soname,libmyutil.so.1 -o libmyutil.so.1.0.0 libmyutil.c $ gcc -o app app.c libmyutil.so.1.0.0 -ldl ---------------------------------------------------------------------------------------------------------------------------*/ /* libmyutil.c */ #include void __attribute__((visibility("hidden"))) bar(void) { printf("bar\n"); } void foo(void) { printf("foo\n"); bar(); } /* app.c */ #include #include #include int main(void) { void *dlh; void (*pfoo)(void); void (*pbar)(void); double result; if ((dlh = dlopen("./libmyutil.so.1.0.0", RTLD_NOW)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } if ((*(void **)&pfoo = dlsym(dlh, "foo")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } pfoo(); if ((*(void **)&pbar = dlsym(dlh, "bar")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } pbar(); dlclose(dlh); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi dinamik kütüphaneyi dinamik yüklemeyip normal yöntemle kullansaydık ne olacaktı? İşte bu durumda hata, programı link ederken oluşmaktadır. Örneğin: $ gcc -shared -fPIC -Wl,-soname,libmyutil.so.1 -o libmyutil.so.1.0.0 libmyutil.c $ gcc -o app app.c libmyutil.so.1.0.0 -ldl /usr/bin/ld: /tmp/ccK2cCXC.o: in function `main': app.c:(.text+0xe): undefined reference to `bar' collect2: error: ld returned 1 exit status Bu testi yapabilmek için app.c programı şöyle olabilir: #include void __attribute__((visibility("hidden"))) bar(void) { printf("bar\n"); } void foo(void) { printf("foo\n"); bar(); } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir dinamik kütüphane normal olarak ya da dinamik olarak yüklendiğinde birtakım ilk işlerin yapılması gerekebilir. (Örneğin kütüphane thread güvenli olma iddiasındadır ve birtakım senkronizasyon nesnelerinin ve thread'e özgü alanların yaratılması gerekebilir.) Bunun için gcc ve clang derleyicilerine özgü olan __attribute__((constructor)) fonksiyon özelliği (function attribute) kullanılmaktadır. Benzer biçimde dinamik kütüphane programın adres alanından boşaltılırken de birtakım son işlemler için __attribute__((destructor)) ile belirtilen fonksiyon çağrılmaktadır. (Aslında bu "constructor" ve "destructor" fonksiyonları normal programlarda da kullanılabilir. Bu durumda ilgili fonksiyonlar main fonksiyonundan önce ve main fonksiyonundan sonra çağrılmaktadır.) Dinamik kütüphane birden fazla kez yüklendiğinde yalnızca ilk yüklemede toplamda bir kez constructor fonksiyonu çağrılmaktadır. Benzer biçimde destructor fonksiyonu da yalnızca bir kez çağrılır. Aşağıda normal bir programda __attribute__((constructor)) ve __attribute__((destructor)) fonksiyon özelliklerinin kullanımına bir örnek verilmiştir. Ekranda şunları göreceksiniz: constructor foo begins... constructor foo ends... main begins... main ends... destructor bar begins... destructor bar ends... ---------------------------------------------------------------------------------------------------------------------------*/ #include void __attribute__((constructor)) foo(void) { printf("constructor foo begins...\n"); printf("constructor foo ends...\n"); } void __attribute__((destructor)) bar(void) { printf("destructor bar begins...\n"); printf("destructor bar ends...\n"); } int main(void) { printf("main begins...\n"); printf("main ends...\n"); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda da dinamik kütüphane içerisinde __attribute__((constructor)) ve __attribute__((destructor)) fonksiyon özelliklerinin kullanımına bir örnek verilmiştir. Derlemeyi aşağıdaki gibi yapabilirsiniz: $ gcc -shared -fPIC -Wl,-soname,libmyutil.so.1 -o libmyutil.so.1.0.0 libmyutil.c $ gcc -o app app.c libmyutil.so.1.0.0 -ldl Kütüphaneye "so ismi" verdiğimiz için sembolik link oluşturmayı unutmayınız: $ ln -s libmyutil.so.1.0.0 libmyutil.so.1 Programı çalıştırmadan önce LD_LIBRARY_PATH çevre değişkenini de ayarlayınız: $ export LD_LIBRARY_PATH=. ---------------------------------------------------------------------------------------------------------------------------*/ /* libmyutil.c */ #include void __attribute__((constructor)) constructor(void) { printf("constructor begins...\n"); printf("constructor ends...\n"); } void __attribute__((destructor)) destructor(void) { printf("destructor begins...\n"); printf("destructor ends...\n"); } void foo(void) { printf("foo\n"); } /* app.c */ #include void foo(void); int main(void) { foo(); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Bir kütüphane oluşturmak isteyen kişi kütüphanesi için en azından bir başlık dosyasını kendisi oluşturmalıdır. Çünkü kütüphane içerisindeki fonksiyonları kullanacak kişiler en azından onların prototiplerini bulundurmak zorunda kalacaklardır. Kütüphaneler için oluşturulacak başlık dosyalarında kütüphane için anlamlı sembolik sabitler, fonksiyon prototipleri, inline fonksiyon tanımlamaları, typedef bildirimleri gibi "nesne yaratmayan" bildirimler bulunmalıdır. Başlık dosyalarında include korumasının yapılması unutulmamalıdır. Aşağıda kütüphane için bir başlık dosyası oluşturma örneği verilmiştir. Örneği aşağıdaki gibi derleyebilirsiniz: $ gcc -shared -fPIC -Wl,-soname,libmyutil.so.1 -o libmyutil.so.1.0.0 libmyutil.c $ gcc -o app app.c libmyutil.so.1.0.0 Sembolik bağlantı yoksa aşağıdaki gibi yaratabilirsiniz: ln -s libmyutil.so.1.0.0 libmyutil.so.1 ---------------------------------------------------------------------------------------------------------------------------*/ /* util.h */ #ifndef UTIL_H_ #define UTIL_H_ /* Function prototypes */ void foo(void); void bar(void); #endif /* libmyutil.c */ #include #include "util.h" void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } /* app.c */ #include #include "util.h" int main(void) { foo(); bar(); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- C++'ta yazılmış kodların da kütüphane biçimine getirilmesinde farklı bir durum yoktur. Sınıfların bildirimleri başlık dosyalarında bulundurulur. Bunlar yine C++ derleyicisi ile (g++ ya da clang++) derlenir. Aynı biçimde kullanılır. Aşağıda C++'ta yazılmış olan bir sınıfın dinamik kütüphaneye yerleştirilmesi ve oradan kullanılmasına bir örnek verilmiştir. Derleme işlemlerini şöyle yapabilirsiniz: $ g++ -shared -fPIC -Wl,-soname,libmyutil.so.1 -o libmyutil.so.1.0.0 libmyutil.cpp $ g++ -o app app.cpp libmyutil.so.1.0.0 ---------------------------------------------------------------------------------------------------------------------------*/ /* util.hpp */ #ifndef UTIL_HPP_ #define UTIL_HPP_ /* Function prototypes */ namespace CSD { class Date { public: Date() = default; Date(int day, int month, int year); void disp() const; private: int m_day; int m_month; int m_year; }; } #endif /* libmyutil.cpp */ #include #include "util.hpp" namespace CSD { Date::Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } void Date::disp() const { std::cout << m_day << '/' << m_month << '/' << m_year << std::endl; } } /* app.cpp */ #include #include "util.hpp" using namespace CSD; int main() { Date d{10, 12, 2009}; d.disp(); return 0; } /*-------------------------------------------------------------------------------------------------------------------------- Bir projeyi tek bir kaynak dosya biçiminde organize etmek iyi bir teknik değildir. Böylesi bir durumda dosyada küçük bir değişiklik yapıldığında bile tüm kaynak dosyanın yeniden derlenmesi gerekmektedir. Aynı zamanda bu biçim kodun güncellenmesini de zorlaştırmaktadır. Proje tek bir kaynak dosyada olduğu için bu durum grup çalışmasını da olumsuz yönde etkilemektedir. Bu nedenle projeler birden fazla "C ya da C++" kaynak dosyası biçiminde organize edilir. Örneğin 10000 satırlık bir proje app1.c, app2.c, app3.c, ..., app10.c biçiminde 10 farklı kaynak dosya biçiminde oluşturulmuş olsun. Pekiyi build işlemi bu durumda nasıl yapılacaktır. Build işlemi için önce her dosya bağımsız olarak "-c" seçeneği ile derlenip ".o" uzantılı "amaç dosya (object module)" haline getirilir. Sonra bu dosyalar link aşamasında birleştirilir. Örneğin: $ gcc -c app1.c $ gcc -c app2.c $ gcc -c app3.c ... $ gcc -c app10.c $ gcc -o app app1.o app2.o app3.o ... app10.o Bu çalışma biçiminde bir kaynak dosyada değişiklik yapıldığında yalnızca değişikliğin yapılmış olduğu kaynak dosya yeniden derlenir ancak link işlemine yine tüm amaç dosyalar dahil edilir. Örneğin app3.c üzerinde bir değişilik yapmış olalım: $ gcc -c app3.c $ gcc -o app app1.o app2.o app3.o ... app10.o İşte bu sıkıcı işlemi ortadan kaldırmak ve build işlemini otomatize etmek için "build otomasyon araçları (build automation tools)" denilen araçlar geliştirilmiştir. Bunların en eskisi ve yaygın olanı "make" isimli araçtır. make aracının yanı sıra "cmake" gibi "qmake" gibi daha yüksek seviyeli build araçları da zamanla geliştirilmiştir. make aracı pek çok sistemde benzer biçimde bulunmaktadır. Bugün UNIX/Linux sistemlerinde "GNU make" aracı kullanılmaktadır. Microsoft klasik make aracının "nmake" ismiyle başka versiyonunu geliştirmiştir. Ancak Microsoft uzun bir süredir "msbuild" denilen başka bir build sistemini kullanmaktadır. Örneğin Microsoft'un Visual Studio IDE'si arka planda bu "msbuild" aracını kullanmaktadır. Qt Framework'ünde "qmake" isimli üst düzey make aracı kullanılmaktadır. Bazı IDE'ler "cmake" kullanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 126. Ders 10/03/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- En fazla kullanılan build otomasyon aracı "make" isimli araçtır. make aracını kullanmak için ismine "make dosyası" denilen bir dosya oluşturulur. Sonra bu dosya "make" isimli program ile işletilir. Dolayısıyla make aracının kullanılması için make dosyalarının nasıl oluşturulduğunun bilinmesi gerekir. Make dosyaları aslında kendine özgü bir dil ile oluşturulmaktadır. Bu make dilinin kendi sentaksı ve semantiği vardır. make aracı için çeşitli kitaplar ve öğretici dokümanlar (tutorials) oluşturulmuştur. Orijinal dokümanlarına aşağıdaki bağlantıdan erişilebilir: https://www.gnu.org/software/make/manual/ Yukarıda da belirttiğimiz gibi "make" aracı değişik sistemlerde birbirine benzer biçimde bulunmaktadır. Microsoft'un make aracına "nmake" denilmektedir. GNU Projesi kapsamında bu make aracı yeniden yazılmıştır. Bugün ağırlıklı olarak GNU projesindeki make aracı kullanılmaktadır. Bu araca "GNU Make" de denilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir make dosyası "kurallardan (rules)" oluşmaktadır. Bir kuralın (rule) genel biçimi şöyledir: hedef (target) : ön_koşullar (prerequisites) işlemler (recipes) Örneğin: app: a.o b.o c.o gcc -o app a.o b.o c.o Burada "app" hedefi, "a.o b.o c.o" ön koşulları ve "gcc -o app a.o b.o c.o" satırı da "işlemleri (recipes)" belirtmektedir. Hedef genellikle bir tane olur. Ancak ön koşullar birden fazla olabilir. İşlemler tek bir satırdan oluşmak zorunda değildir. Eğer birden fazla satırdan oluşacaksa satırlar alt alta yazılır. İşlemler belirtilirken yukarıdaki satırdan bir TAB içeriye girinti verilmek zorundadır. Örneğin: app: a.o b.o c.o gcc -o app a.o b.o c.o Kuraldaki hedef ve ön koşullar tipik olarak birer dosyadır. Kuralın anlamı şöyledir: Ön koşullarda belirtilen dosyaların herhangi birinin tarih ve zamanı, hedefte belirtilen dosyanın tarih ve zamanından ileri ise (yani bunlar güncellenmişse) bu durumda belirtilen işlemler yapılır. Yukarıdaki kuralı yeniden inceleyiniz: app: a.o b.o c.o gcc -o app a.o b.o c.o Burada eğer "a.o" ya da "b.o" ya da "c.o" dosyalarının tarih ve zamanı "app" dosyasının tarih ve zamanından ilerideyse aşağıdaki kabuk komutu çalıştırılacaktır: $ gcc -o app a.o b.o c.o Bu link işlemi anlamına gelir. Link işleminden sonra artık "app" dısyasının tarih ve zamanı ön koşul dosyalarından daha ileride olacağı için kural "güncel (up to date)" hale gelir. Artık bu kural işletildiğinde bu link işlemi yapılmayacaktır. Bu link işleminin yeniden yapılabilmesi için "a.o" ya da "b.o" ya da "c.o" dosyalarında güncelleme yapılmış olması gerekir. Bu dosyalar derleme işlem sonucunda oluşacağına göre bu dosyaların güncellenmesi aslında bunlara ilişkin ".c" dosyalarının derlenmesiyle olabilir. Şimdi aşağıdaki kuralları yazalım: a.o: a.c gcc -c a.c b.o: b.c gcc -c b.c c.o: c.c gcc -c c.c Bu kurallar "ilgili .c dosyalarında bir değişiklik olduğunda onları yeniden derle" anlamına gelmektedir. Şimdi önceki kuralla bu kuralları bir araya getirelim: app: a.o b.o c.o gcc -o app a.o b.o c.o a.o: a.c gcc -c a.c b.o: b.c gcc -c b.c c.o: c.c gcc -c c.c make programı çalıştırıldığında önce program make dosyasından hareketle bir "bağımlılık grafı (dependency graph)" oluşturmaktadır. Bağımlılık grafı "hangi dosya hangi dosyanın durumuna bağlı" biçiminde oluşturulan bir graftır. Yukarıdaki örnekte "a.o", "b.o" ve "c.o" dosyaları aşağıdaki kurallara bağımlıdır. Daha sonra make programı sırasıyla bu grafa uygun olarak aşağıdan yukarıya kuralları işletmektedir. Yukarıdaki örnekte birinci kural ikinci, üçüncü ve dördüncü kurallara bağımlıdır. Dolayısıyla önce bu kurallar işletilip daha sonra birinci kural işletilir. Böylece bu make dosyasından şöyle sonuç çıkmaktadır: "Herhangi bir .c dosya değiştirildiğinde onu derle ve hep birlikte link işlemi yap". Kuralın hedefindeki dosya yoksa koşulun sağlandığı kabul edilmektedir. Yani bu durumda ilgili işlemler yapılacaktır. Yukarıdaki örnekte "object dosyalarını silersek" bu durumda derleme işlemlerinin hepsi yapılacaktır. Normal olarak her ön koşul dosyasının bir hedefle ilişkili olması beklenir. Yani ön koşulda belirtilen dosyaların var olması gerekmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- make dosyası hazırlandıktan sonra make programı ile dosya işletilir. make programı işletilecek dosyayı "-f" ya da "--file" seçeneği ile komut satırı argümanından almaktadır. Örneğin: $ make -f project.mak Ancak -f seçeneği kullanılmazsa make programı sırasıyla "GNUmakefile", "makefile" ve "Makefile" dosyalarını aramaktadır. GNU dünyasındaki genel eğilim projenin make dosyasının "Makefile" biçiminde isimlendirilmesidir. Açık kaynak kodlu bir yazılımda projenin make dosyasının da verilmiş olması beklenir. Böylece kaynak kodları elde eden kişiler yeniden derlemeyi komut satırında "make" yazarak yapabilirler. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında make programı çalıştırılırken program belli bir hedefi gerçekleştirmek için işlem yapar. Gerçekleştirilecek hedef make programında komut satırı argümanı olarak verilmektedir. Eğer hedef belirtilmezse ilk hedef gerçekleştirilmeye çalıştırılır. Örneğin: # Makefile app: a.o b.o c.o gcc -o app a.o b.o c.o a.o: a.c gcc -c a.c b.o: b.c gcc -c b.c c.o: c.c gcc -c c.c project: project.c gcc -o project project.c Burada birbirinden bağımsız iki hedef vardır: app ve project. Biz make programını hedef belirtmeden çalıştırırsak ilk hedef gerçekleştirilmeye çalışılır. Ancak belli bir hedefin de gerçekleştirilmesini sağlayabiliriz. Örneğin: $ make project ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir kuralda ön koşul yoksa kuralın sağlandığı varsayılmaktadır. Yani bu durumda doğrudan belirtilen işlemler (recipes) yapılır. Örneğin: clean: rm -f *.o Burada make programını aşağıdaki gibi çalıştırmış olalım: $ make clean Bu durumda tüm ".o" dosyaları silinecektir. Örneğin: # Makefile app: a.o b.o c.o gcc -o app a.o b.o c.o a.o: a.c gcc -c a.c b.o: b.c gcc -c b.c c.o: c.c gcc -c c.c clean: rm -f *.o install: sudo cp app /usr/local/bin Burada "clean" hedefi rebuild işlemi için object dosyaları silmektedir. "install" hedefi ise elde edilen programı belli bir yere kopyalamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir kaynak dosya bir başlık dosyasını kullanıyorsa bağımlılıkta bu başlık dosyasının da belirtilmesi uygun olur. Çünkü bu başlık dosyasında bir güncelleme yapıldığında bu kaynak dosyanın da yeniden derlenmesi beklenir. Örneğin: a.o: a.c app.h gcc -c app.c Burada artık app.h dosyası üzerinde bir değişiklik yapıldığında derleme işlemi yeniden yapılacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 127. Ders 15/03/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- make dosyasına dışarıdan parametre aktarabiliriz. Bunun için komut satırında "değişken=değer" sentaksı kullanılmaktadır. Burada geçirilen değer ${değişken} ifadesi ile make dosyasının içerisinden kullanılabilir. Örneğin: # Makefile ${executable}: a.o b.o c.o gcc -o app a.o b.o c.o a.o: a.c gcc -c a.c b.o: b.c gcc -c b.c c.o: c.c app.h gcc -c c.c clean: rm -f *.o install: sudo cp app /usr/local/bin Burada executable dosyanın hedefi komut satırından elde edilmektedir. Örneğin biz make programını şöyle çalıştırabiliriz: $ make executable=app Bu durumda "app" dosyası hedef olarak ele alınacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Make dosyası içerisinde değişkenler kullanılabilmektedir. Bir değişken "değişken = değer" sentaksıyla oluşturulur ve make dosyasının herhangi bir yerinde ${değişken} biçiminde kullanılır. Örneğin: # Makefile CC = gcc OBJECTS = a.o b.o c.o INSTALL_DIR = /usr/local/bin APP_NAME = app ${APP_NAME}: ${OBJECTS} ${CC} -o app a.o b.o c.o a.o: a.c ${CC} -c a.c b.o: b.c ${CC} -c b.c c.o: c.c app.h gcc -c c.c clean: rm -f ${OBJECTS} install: sudo cp ${APP_NAME} ${INSTALL_DIR} ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir C programını derlediğimizde link işlemi için bir main fonksiyonunun bulunması gerekmektedir. Aslında GNU'nun linker programının ismi "ld" isimli programdır. gcc zaten bu ld linker'ını çalıştırmaktadır. Bir programın link edilebilmesi için aslında main fonksiyonunun bulunması gerekmez. main fonksiyonu assembly düzeyinde anlamlı bir fonksiyon değildir. C için anlamlı bir fonksiyondur. Yani örneğin biz bir assembly programı yazarsak onu istediğimiz yerden çalışmaya başlatabiliriz. Bir dosya "executable" olarak link edilirken tek gerekli olan şey "entry point" denilen akışın başlatılacağı noktadır. Entry point ld linker'ında "--entry" seçeneği ile belirtilmektedir. Biz bir C programını gcc ile derlediğimizde gcc aslında ld linker'ını çağırırken ismine "start-up modüller" denilen bir grup modülü de link işlemine gizlice dahil etmektedir. Programın gerçek entry point'i bu start-up modül içerisinde bir yerdedir. Aslında main fonksiyonunu bu start-up modül çağırmaktadır. Bu start-up modülün görevi birtakım hazırlık işlemlerini yapıp komut satırı argümanlarıyla main fonksiyonunu çağırmaktır. Zaten akış main fonksiyonunu bitirdiğinde yeniden start-up modüldeki koda döner orada exit işlemi yapılmıştır. Start-up modülün kodlarını şöyle düşünebilirsiniz: ... ... ... call main call exit O halde link aşamasına bu start-up modül katıldığı için aslında main isimli bir fonksiyon aranmaktadır. Yani start-up modül main fonksiyonunu çağırmasaydı linker onu aramayacaktı. Biz aslında hiçbir kütüphaneyi link aşamasına dahil etmeden programın entry-point'ini kendimiz belirleyerek akışı istediğimiz fonksiyondan başlatabiliriz. Tabii bu durumda sistem fonksiyonlarını bile sembolik makine dilinde ya da gcc'nin inline sembolik makine dilinde kendimizin yazması gerekecektir. Aşağıda böyle bir örnek verilmiştir. Buradaki programın isminin "x.c" olduğunu varsayalım. Bu programı aşağıdaki gibi derleyip link edebilirsiniz: $ gcc -c x.c $ ld -o x x.o --entry=foo ---------------------------------------------------------------------------------------------------------------------------*/ /* x.c */ #include #include #include ssize_t my_write(int fd, const void *buf, size_t size) { register int64_t rax __asm__ ("rax") = 1; register int rdi __asm__ ("rdi") = fd; register const void *rsi __asm__ ("rsi") = buf; register size_t rdx __asm__ ("rdx") = size; __asm__ __volatile__ ( "syscall" : "+r" (rax) : "r" (rdi), "r" (rsi), "r" (rdx) : "rcx", "r11", "memory" ); return rax; } void my_exit(int status) { __asm__ __volatile__ ( "movl $60, %%eax\n\t" "movl %0, %%edi\n\t" "syscall" : : "g" (status) : "eax", "edi", "cc" ); } void foo() { my_write(1, "this is a test\n", 15); my_exit(0); } /*-------------------------------------------------------------------------------------------------------------------------- O anda makinemizdeki işletim sistemi hakındaki bilgi "uname" komutuyla elde edilebilir. Bu komut -r ile kullanılırsa o makinede yüklü olan kernel versiyonu elde edilmektedir. Örneğin: $ uname -r 4.15.0-20-generic ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kernel'ın bir parçası gibi işlev gören, herhangi bir koruma engeline takılmayan, kernel mode'da çalışan özel olarak hazırlanmış modüllere (yani kod parçalarına) Linux dünyasında "kernel modülleri (kernel modules)" denilmektedir. Kernel modülleri eğer kesme gibi bazı mekanizmaları kullanıyorsa ve bir donanım aygıtını yönetme iddiasındaysa bunlara özel olarak "aygıt sürücüleri (device drivers)" da denilmektedir. Nasıl bir masaüstü bilgisayara kart taktığımızda artık o kart donanımın bir parçası haline geliyorsa kernel modülleri ve aygıt sürücüleri de install edildiklerinde adeta kernel'ın bir parçası haline gelmektedir. Her aygıt sürücü bir kernel modülüdür ancak her kernel modülü bir aygıt sürücü değildir. Bu nedenle biz yalnızca "kernel modülü" dediğimizde genel olarak aygıt sürücüleri de dahil etmiş olacağız. Biz bu bölümde Linux sistemleri için kernel modüllerinin ve aygıt sürücülerinin nasıl yazılacağını ele alacağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kernel modülleri ve aygıt sürücüler genel bir konu değildir. Her işletim sisteminin o sisteme özgü bir aygıt sürücü mimarisi vardır. Hatta bu mimari işletim sisteminin versiyonundan versiyonuna da değişebilmektedir. Bu nedenle aygıt sürücü yazmak genel bir konu değil, o işletim sistemine hatta işletim sisteminin belirli versiyonlarına özgü bir konudur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kernel modüllerinde ve aygıt sürücülerde her türlü fonksiyon kullanılamaz. Bunları yazabilmek için özel başlık dosyalarına ve kütüphanelere gereksinim duyulmaktadır. Bu nedenle ilk yapılacak şey bu başlık dosyalarının ve kütüphanelerin ilgili sisteme yüklenmesidir. Genellikle bir Linux sistemini yüklediğimizde zaten kernel modüllerini ve aygıt sürücüleri oluşturabilmek için gereken kütüphaneler ve başlık dosyaları zaten yüklü biçimde bulunmaktadır. Tabii programcı kernel kodlarını da kendi makinesine indirmek isteyebilir. Bunun için aşağıdaki komut kullanılabilir: $ sudo apt-get install linux-source Eğer sisteminizde Linux'un kaynak kodları yüklü ise bu kaynak kodlar "/usr/src" dizininde bulunmaktadır. Bu dizindeki "linux-headers-$(uname -r)" dizini kaynak kodlar yüklü olmasa bile bulunan bir dizindir ve bu dizin çekirdek modülleri ve aygıt sürücülerin "build edilmeleri" için gereken başlık dosyalarını barındırmaktadır. Benzer biçimde "/lib/modules" isimli dizinde $(uname -r) isimli bir dizin vardır. Bu dizin kernel modüllerinin build edilmesi için gereken bazı kodları ve kütüphaneleri bulundurmaktadır. Özetle kernel modülleri ve aygıt sürücüleri yazmak için gerekli olan dizinler şunlardır: 1) linux-headers-$(uname -r) dizini. Bu dizinde başlık dosyaları bulunmaktadır. 2) /lib/modules/$(uname -r) dizini. Burada da kernel modülün build edilmesi için gerekli olan dosyalar ve kütüphaneler bulunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir kernel modülünde biz user mod için yazılmış kodları kullanamayız. Çünkü orası ayrı bir dünyadır. Ayrıca biz kernel modüllerinde kernel içerisindeki her fonksiyonu kullanamayız. Yalnızca bazı fonksiyonları kullanabiliriz. Bunlara "kernel tarafından export edilmiş fonksiyonlar" denilmektedir. "Kernel tarafından export edilmiş fonksiyon" kavramıyla "sistem fonksiyonu" kavramının bir ilgisi yoktur. Sistem fonksiyonları user mode'dan çağrılmak üzere tasarlanmış ayrı bir grup fonksiyondur. Oysa kernel tarafından export edilmiş fonksiyonlar user mode'dan çağrılamazlar. Yalnızca kernel modüllerinden çağrılabilirler. Buradan çıkan sonuç şudur: Bir kernel modül yazılırken ancak kernel'ın export ettiği fonksiyon ve datalar kullanılabilmektedir. Tabii kernel'ın kaynak kodları çok büyüktür ancak buradaki kısıtlı sayıda fonksiyon export edilmiştir. Benzer biçimde programcının oluşturduğu bir kernel modül içerisindeki belli fonksiyonları da programcı export edebilir. Bu durumda bu fonksiyonlar da başka kernel modüllerinden kullanılabilirler. O halde özetle: 1) Kernel modülleri yalnızca kernel içerisindeki export edilmiş fonksiyonları kullanabilirler. 2) Kendi kernel modülümüzde biz de istediğimiz fonksiyonu export edebiliriz. Bu durumda bizim kernel modülümüz kernel'ın bir parçası haline geldiğine göre başka kernel modüller de bizim export ettiğimiz fonksiyonları kullanabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Mademki kernel modüller işletim sisteminin kernel kodlarındaki fonksiyon ve dataları kullanabiliyorlar o zaman kernel modüller o anda çalışılan kernel'ın yapısına da bağlı durumdadırlar. Yukarıda da ifade ettiğimiz gibi işletim sistemlerinde "kernel modül yazmak" ya da "aygıt sürücü yazmak" biçiminde genel bir konu yoktur. Her işletim sisteminin kernel modül ve aygıt sürücü mimarisi diğerlerinden farklıdır. Dolayısıyla bu konu spesifik bir işletim sistemi için geçerli olabilecek oldukça platform bağımlı bir konudur. Hatta işletim sistemlerinde bazı versiyonlarda genel aygıt sürücü mimarisi bile değiştirilebilmektedir. Dolayısıyla eski aygıt sürücüler yeni versiyonlarda, yenileri de eski versiyonlarda çalışamamaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kernel modüllerinin ve aygıt sürücülerin yazımı için programcının genel olarak kernel yapısını bilmesi gerekmektedir. Çünkü bunları yazarken kernel'ın içerisindeki export edilmiş fonksiyonlar kullanılmaktadır. Linux kernel modüller ve aygıt sürücüler hakkında yazılmış birkaç kitap vardır. Bunların en klasik olanı "Linux Device Drivers (3. Edition)" kitabıdır. Bu konudaki resmi dokümanlar kernel.org sitesindeki "documentation" kısmında bulunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir kernel modülünü derlemek ve link etmek maalesef sanıldığından daha zordur. Her ne kadar kernel modüller ELF object dosyaları biçimindeyse de bunlarda özel bazı "bölümler (sections)" bulunmaktadır. Dolayısıyla bu modüllerin derlenmesinde özel gcc seçenekleri devreye sokulur. Kernel modüllerin link edilmeleri de bazı kütüphane dosyalarının devreye sokulmasıyla yapılmaktadır. Dolayısıyla bir kernel modülün manuel biçimde "build edilmesi" için bazı ayrıntılı bilgilere gereksinim duyulmaktadır. İşte kernel tasarımcıları bu sıkıcı işlemleri kolaylaştırmak için özel "make dosyaları" düzenlemişlerdir. Programcı bu make dosyalarından faydalanarak build işlemini çok daha kolay yapabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kernel modüller için build işlemini yapan örnek bir Makefile aşağıdaki gibi olabilir: obj-m += generic.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Burada önce /lib/modules/$(uname -r)/build dizinindeki "Makefile" çalıştırılmış ondan sonra çalışma bu yazdığımız make dosyasından devam ettirilmiştir. Özetle bu make dosyası "generic.c" isimli dosyanın derlenerek kernel modül biçiminde link edilmesini sağlamaktadır. Kernel modül birden fazla kaynak dosyadan oluşturulabilir. Bu durumda ilk satır şöyle oluşturulabilir: obj-m += a.o b.o c.o... Ya da bu belirtme işlemi ayrı satır halinde de yapılabilir: obj-m += a.o obj-m += b.o obj-m += c.o ... Bizim oluşturduğumuz Makefile dosyasındaki "all" hedefine dikkat ediniz: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules make programının -C seçeneği Makefile dosyasını aramadan önce bu seçeneğin argümanında belirtilen dizine geçiş yapmaktadır. Dolayısıyla aslında yukarıdaki satırla /lib/modules/$(shell uname -r)/build dizinindeki Makefile dosyası çalıştırılacaktır. Make dosyasının başındaki kısım aslında standart bir make yönergesi değildir. Ana make dosyası bu dosyayı ele almaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ # 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 /*-------------------------------------------------------------------------------------------------------------------------- Tabii aslında make dosyası parametrik biçimde de oluşturabilmektedir. Bu durumda make programı çalıştırılırken bu parametrenin değeri de belirtilmelidir. Örneğin: make file=hellomodule ---------------------------------------------------------------------------------------------------------------------------*/ # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /*-------------------------------------------------------------------------------------------------------------------------- Şimdi en basit bir kernel modülü oluşturup bunu bir başlangıç noktası olarak kullanalım. Bu modülümüze "helloworld" ismini verelim: #include #include MODULE_LICENSE("GPL"); int init_module(void) { printk(KERN_INFO "Hello World...\n"); return 0; } void cleanup_module(void) { printk(KERN_INFO "Goodbye World...\n"); } Bu kernel modül aşağıdaki gibi build edilebilir: $ make file=helloworld Build işlemi bittiğinde kernel modül "helloworld.ko" dosyası biçiminde oluşturulacaktır. Burada "ko" uzantısı "kernel object" sözcüklerinden kısaltılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir kernel modül, kernel'ın içerisine "insmod" isimli programla yerleştirilmektedir. Tabii bu programın sudo ile "root" önceliğinde çalıştırılması gerekmektedir. Örneğin: $ sudo insmod helloworld.ko Artık kernel modülümüz kernel'ın içerisine yerleştirilmiştir. Yani modülümüz adeta kernel'ın bir parçası gibi işlev görecektir. Kernel modüller istenildiği zaman "rmmod" isimli programla kernel'dan çıkartılabilirler. Bu programın da yine sudo ile "root" önceliğinde çalıştırılması gerekir. Örneğin: $ sudo rmmod helloworld.ko Aşağıda örnek için gerekli olan dosyalar verilmiştir. make işlemi şöyle yapılabilir: $ make file=helloworld ---------------------------------------------------------------------------------------------------------------------------*/ /* helloworld.c */ #include #include MODULE_LICENSE("GPL"); int init_module(void) { printk(KERN_INFO "Hello World...\n"); return 0; } void cleanup_module(void) { printk(KERN_INFO "Goodbye World...\n"); } # 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 /*-------------------------------------------------------------------------------------------------------------------------- 128. Ders 17/03/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- En basit bir kernel modülde aşağıdaki iki temel dosya include edilmelidir: #include #include Bu iki dosya "/lib/modules/$(uname -r)/build/include" dizini içerisindedir. (Yani libc ve POSIX kütüphanelerinin başlık dosyalarının bulunduğu "/usr/include" içerisinde değildir.) Yukarıda kullandığımız make dosyası include dosyalarının bu dizinde aranmasını sağlamaktadır. Eskiden kernel modüllerine modül lisansının eklenmesi zorunlu değildi. Ancak belli bir süreden sonra bu zorunlu hale getirilmiştir. Modül lisansı MODULE_LICENSE isimli makro ile belirtilmektedir. Bu makro dosyası içerisinde bildirilmiştir. Tipik modül lisansı aşağıdaki gibi "GPL" biçiminde oluşturulabilir: MODULE_LICENSE("GPL"); Bir kernel modül yüklendiğinde kernel modül içerisinde belirlenmiş olan bir fonksiyon çağrılır (bu fonksiyon C++'taki "constructor" gibi düşünülebilir.) Default çağrılacak fonksiyonun ismi init_module biçimindedir. Bu fonksiyonun geri dönüş değeri int türdendir ve parametresi yoktur. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda negatif hata koduna geri dönmelidir. Bu fonksiyon başarısızlıkla geri dönerse modülün yüklenmesinden vazgeçilmektedir. Benzer biçimde bir modül kernel alanından boşaltılırken de yine bir fonksiyon çağrılmaktadır. (Bu fonksiyon da C++'taki "destructor" gibi düşünülebilir.) Default çağrılacak fonksiyonun ismi cleanup_module biçimindedir. Bu fonksiyonun geri dönüş değeri ve parametresi void biçimdedir. Kernel modüller tıpkı daha önce görmüş olduğumuz daemon'lar gibi ekrana değil log dosyalarına yazarlar. Bunun için kernel içindeki printk isimli fonksiyon kullanılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 129. Ders 22/03/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- helloworld modülünde kullanmış olduğumuz printk fonksiyonu "kernel'ın printf fonksiyonu" gibi düşünülebilir. printk fonksiyonunun genel kullanımı printf fonksiyonu gibidir. Default durumda bu fonksiyon mesajların "/var/log/syslog" dosyasına yazdırılması sağlamaktadır. printk fonksiyonunun prototipi dosyası içerisindedir. printk fonksiyonunun örnek kullanımı şöyledir: printk(KERN_INFO "This is test\n"); Mesajın solundaki KERN_XXX biçimindeki makrolar aslında bir string açımı yapmaktadır. Dolayısıyla yan yana iki string birleştirildiği için mesaj yazısının başında küçük bir önek bulunur. Bu önek (yani bu makro) mesajın türünü ve aciliyetini belirtmektedir. Tipik KERN_XXX makroları şunlardır: KERN_EMERG KERN ALERT KERN_CRIT KERN_ERR KERN_WARN KERN_NOTICE KERN_INFO KERN_DEBUG Bu makroların tipik yazım biçimi şöyledir: #define KERN_SOH "\001" /* ASCII Start Of Header */ #define KERN_SOH_ASCII '\001' #define KERN_EMERG KERN_SOH "0" /* system is unusable */ #define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */ #define KERN_CRIT KERN_SOH "2" /* critical conditions */ #define KERN_ERR KERN_SOH "3" /* error conditions */ #define KERN_WARNING KERN_SOH "4" /* warning conditions */ #define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */ #define KERN_INFO KERN_SOH "6" /* informational */ #define KERN_DEBUG KERN_SOH "7" /* debug-level messages */ Ancak bu makrolarda çeşitli kernel versiyonlarında değişiklikler yapılabilmektedir. C'de aralarında hiçbir operatör bulunmayan iki string'in derleyici tarafından birleştirildiğini anımsayınız.Bu durumda aslında örneğin: printk(KERN_INFO "Hello World...\n"); ile aşağıdaki çağrı eşdeğerdir: printk("\0017Hello World...\n"); Ancak yukarıda da belirttiğimiz gibi bu makrolar üzerinde değişiklikler yapılabilmektedir. Dolayısıyla makroların kendisinin kullanılması gerekir. Aslında KERN_XXX makroları ile printk fonksiyonunu kullanmak yerine pr_xxx makroları da kullanılabilir. Şöyle ki: printk(KERN_INFO "Hello World...\n"); ile pr_info("Hello World...\n"); tamamen eşdeğerdir. Diğer pr_xxx makroları şunlardır: pr_emerg pr_alert pr_crit pr_err pr_warning pr_notice pr_info pr_debug printk fonksiyonunun yazdıklarını "/var/log/syslog" dosyasına bakarak görebiliriz. Örneğin: tail /var/log/syslog Ya da "dmesg" programı ile de aynı bilgi elde edilebilir. Kernel modüller kernel'ın içerisine yerleştirildiği için kernel modüllerde biz user mode'daki kütüphaneleri kullanamayız. Örneğin kernel mode içerisinde standart C fonksiyonlarını ve POSIX fonksiyonlarını kullanamayız. Çünkü standart C fonksiyonları ve POSIX fonksiyonları "user mode" programlar için oluşturulmuş kütüphanelerin içerisindedir. Biz kernel modüllerin içerisinde yalnızca "export edilmiş kernel fonksiyonlarını" kullanabiliriz. Kernel modüller içerisinde kullanılabilecek export edilmiş kernel fonksiyonları "Linux Kernel API" ismi altında "kernel.org" tarafından dokümante edilmiştir. Örneğin bu fonksiyonların dokümantasyonuna aşağıdaki bağlantıdan erişebilirsiniz: https://docs.kernel.org/core-api/kernel-api.html ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Belli bir anda yüklenmiş olan modüller "/proc/modules" dosyasından elde edilebilir. Bu dosya bir text dosyadır. Dosyanın her satırında bir kernel modülün bilgisi vardır. Örneğin: $ cat /proc/modules helloworld 16384 0 - Live 0x0000000000000000 (OE) vmw_vsock_vmci_transport 32768 2 - Live 0x0000000000000000 vsock 40960 3 vmw_vsock_vmci_transport, Live 0x0000000000000000 snd_ens1371 28672 2 - Live 0x0000000000000000 snd_ac97_codec 131072 1 snd_ens1371, Live 0x0000000000000000 gameport 20480 1 snd_ens1371, Live 0x0000000000000000 ac97_bus 16384 1 snd_ac97_codec, Live 0x0000000000000000 binfmt_misc 24576 1 - Live 0x0000000000000000 intel_rapl_msr 20480 0 - Live 0x0000000000000000 ... Aslında yüklü modüllerin bilgileri "lsmod" isimli bir yardımcı programla da görüntülenebilmektedir. Tabii "lsmod" aslında "/proc/modules" dosyasını okuyup onu daha anlaşılır biçimde görüntülemektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında init_module ve cleanup_module fonksiyonlarının ismi değiştirilebilir. Fakat bunun için bildirimde bulunmak gerekir. Bildirimde bulunmak için ise module_init(...) ve module_exit(...) makroları kullanılmaktadır. Bu makrolar kaynak kodun herhangi bir yerinde bulundurulabilir. Ancak makro içerisinde belirtilen fonksiyonların daha yukarıda bildirilmiş olması gerekmektedir. Bu makrolar tipik olarak kaynak kodun sonuna yerleştirilmektedir. Örneğin: #include #include int helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } void helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); Aşağıda örnek bütünsel olarak verilmiştir. make işlemi şöyle yapılabilir: $ make file=helloworld ---------------------------------------------------------------------------------------------------------------------------*/ /* helloworld.c */ #include #include int helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } void helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); # 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 /*-------------------------------------------------------------------------------------------------------------------------- Genellikle kernel modül içerisindeki global değişkenlerin ve fonksiyonların "internal linkage" yapılması tercih edilmektedir. Bu durum birtakım isim çakışmalarını da engelleyecektir. ---------------------------------------------------------------------------------------------------------------------------*/ /* helloworld.c */ #include #include static int helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } static void helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); # 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 /*-------------------------------------------------------------------------------------------------------------------------- Kernel modüllerde init ve cleanup fonksiyonlarında fonksiyon isimlerinin soluna __init ve __exit makroları getirilebilmektedir. Bu makrolar dosyası içerisindedir. Bu dosya da dosyası içerisinde include edilmiştir. __init makrosu ilgili fonksiyonu ELF dosyasının özel bir bölümüne (section) yerleştirir. Modül yüklendikten sonra bu bölüm kernel alanından atılmaktadır. __exit makrosu ise kernel'ın içine gömülmüş modüllerde fonksiyonun dikkate alınmayacağını (dolayısıyla hiç yüklenmeyeceğini) belirtir. Ancak sonradan yüklemelerde bu makronun bir etkisi yoktur. ---------------------------------------------------------------------------------------------------------------------------*/ /* helloworld.c */ #include #include static int __init helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } static void __exit helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); # 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 /*-------------------------------------------------------------------------------------------------------------------------- insmod ile yüklediğimiz her modül için "/sys/module" dizinin içerisinde ismi modül ismiyle aynı olan bir dizin yaratılmaktadır. "/proc/modules" dosyası ile bu dizini karıştırmayınız. "/proc/modules" dosyasının satırları yüklü olan modüllerin isimlerini ve bazı temel bilgilerini tutmaktadır. Modüllere ilişkin asıl önemli bilgiler kernel tarafından "/sys/module" dizininde tutulmaktadır. sys dosya sistemi de proc dosya sistemi gibi kernel tarafından bellek üzerinde oluşturulan ve içeriği kernel tarafından güncellenen bir dosya sistemidir. Örneğin "helloworld.ko" modülünü yükledikten sonra bu dizinin içeriği şöyle görüntülenmektedir: $ ls /sys/module/helloworld -l toplam 0 -r--r--r-- 1 root root 4096 Mar 22 21:25 coresize drwxr-xr-x 2 root root 0 Mar 22 21:25 holders -r--r--r-- 1 root root 4096 Mar 22 21:25 initsize -r--r--r-- 1 root root 4096 Mar 22 21:25 initstate drwxr-xr-x 2 root root 0 Mar 22 21:25 notes -r--r--r-- 1 root root 4096 Mar 22 21:25 refcnt drwxr-xr-x 2 root root 0 Mar 22 21:25 sections -r--r--r-- 1 root root 4096 Mar 22 21:25 srcversion -r--r--r-- 1 root root 4096 Mar 22 21:25 taint --w------- 1 root root 4096 Mar 22 21:22 uevent ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Nasıl user mode programlarda main fonksiyonuna komut satırı argümanları geçirilebiliyorsa benzer biçimde kernel modüllere de argüman (ya da parametre diyebiliriz) geçirilebilmektedir. Bu konuya genel olarak "kernel modül parametreleri" denilmektedir. Kernel modüllere parametre geçirme işlemi insmod ile modül yüklenirken komut satırında modül isminden sonra "değişken=değer" çiftleriyle yapılmaktadır. Örneğin: $ sudo insmod helloworld.ko number=10 msg="\"This is a test\"" values=10,20,30,40,50 Bu örnekte number parametresi int bir değerden, msg parametresi ise bir yazıdan oluşmaktadır. values parametresi birden fazla int değerden oluşmaktadır. Bu tür parametrelere modülün dizi parametreleri denilmektedir. Kernel modüllere geçirilen parametreleri modül içerisinde almak için module_param ve module_param_array isimli makrolar kullanılır. module_param makrosunun üç parametresi vardır: module_param(name, type, perm); name parametresi ilgili değişkenin ismini belirtmektedir. Biz makroyu çağırmadan önce bu isimde bir global değişkeni tanımlamalıyız. Ancak buradaki değişken isminin komut satırında verilen parametre (argüman da diyebiliriz) ismi ile aynı olması gerekmektedir. type ilgili parametrenin türünü belirtir. Bu tür şunlardan biri olabilir: int long short uint ulong ushort charp bool invbool Buradaki charp char türden adresi, invbool ise geçirilen argümanın bool bakımdan tersini temsil etmektedir. module_param makrosunun perm parametresi "/sys/modules/" dizininde yaratılacak olan parameters dizininin erişim haklarını belirtir. Bu makrolar global alanda herhangi bir yere yerleştirilebilir. Örneğin kernel modülümüzde count ve msg isimli iki parametre olsun. Bunlara ilişkin module_param makroları şöyle oluşturulmalıdır: int count = 0; char *msg = "Ok"; module_param(count, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param(msg, charp, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); char * türünden modül parametresi için makrodaki türün "charp" biçiminde olduğuna dikkat ediniz. Buradaki gösterici const olamamaktadır. Bizim bir parametre için module_param makrosunu kullanmış olmamız modül yüklenirken bu parametrenin belirtilmesini zorunlu hale getirmemektedir. Bu durumda bu parametreler default değerlerde kalacaktır. Yukarıdaki parametreleri helloworld modülüne aşağıdaki gibi geçirebiliriz: $ sudo insmod helloworld.ko count=100 msg="\"this is a test\"" Burada neden iç içe tırnakların kullanıldığını merak edebilirsiniz. Kabuk üzerinde tırnaklar "boşluklarla ayrılmış olan yazıların tek bir komut satırı argümanı olarak ele alınacağını belirtmektedir. Ancak bizim ayrıca yazısal argümanları modüllere parametre yoluyla aktarırken onları tırnaklamamız gerekir. Bu nedenle iç içe iki tırnak kullanılmıştır. Modül parametreleri kernel tarafından "/sys/module" içerisindeki modül ismine ilişkin dizinin altındaki parameters dizininde dosyalar biçiminde dış dünyaya sunulmaktadır. İşte makrodaki erişim hakları buradaki parametre dosyalarının erişim haklarını belirtmektedir. Kernel modül root kullanıcısı tarafından yüklendiğine göre bu dosyaların da kullanıcı ve grup id'leri root olacaktır. Örneğin helloworld modülü için bu dosyalar "/sys/module/helloworld/parameters" dizini içerisindedir: $ ls -l /sys/module/helloworld/parameters toplam 0 -rw-r--r-- 1 root root 4096 Mar 22 22:24 count -rw-r--r-- 1 root root 4096 Mar 22 22:24 msg Bu dosyalar doğrudan kernel modüldeki parametre değişkenlerini temsil etmektedir. Yani örneğin biz buradaki count dosyasına başka bir değer yazdığımızda kernel modülümüzdeki count değeri de değişmiş olacaktır. Tabii yukarıdaki erişim haklarıyla biz dosyaya yazma yapamayız. Bu erişim haklarıyla yazma yapabilmemiz için yazmayı yapan programın root olması gerekir. Terminalden bu işlem aşağıdaki gibi yapılabilir: $ sudo bash -c "echo 200 > /sys/module/helloworld/parameters/count" yada $ echo 200 | sudo tee /sys/module/helloworld/parameters/count Burada işlemi aşağıdaki gibi yapamayacağımıza dikkat ediniz: $ sudo echo 200 > /sys/module/helloworld/parameters/count Çünkü burada her ne kadar echo programı root önceliğinde çalıştırılıyorsa da dosyayı açan kullanıcı root değildir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 130. Ders 24/03/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kernel modüle birden fazla değer de bir dizi gibi aktarılabilir. Bunun için module_param_array makrosu kullanılmaktadır. module_param_array makrosu da şöyledir: module_param_array(name, type, nump, perm) Makronun birinci ve ikinci parametreleri yine değişken ismi ve türünü belirtir. Tabii buradaki değişken isminin bir dizi ismi olarak girilmesi gerekmektedir. Üçüncü parametre toplam kaç değerin modüle dizi biçiminde aktarıldığını belirten int bir nesnenin adresini (ismini değil) alır. Son parametre yine oluşturulacak dosyanın erişim haklarını belirtmektedir. Örneğin: static int values[5]; static int size; module_param_array(values, int, &size, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param_array makrosuyla bir diziye değer aktarırken değerlerin virgüllerle ayrılmış bir biçimde girilmesi gerekmektedir. Örneğin: $ sudo insmod helloworld.ko values=1,2,3,4,5 Burada eğer verilen değerler dizinin uzunluğundan fazla olursa zaten modül yüklenmemektedir. Bu örnekte biz girilen değerlerin sayısını "size" nesnesinden alabiliriz. Aşağıdaki örnekte üç parametre komut satırından kernel modüle geçirilmiştir. Komut satırındaki isimlerle programın içerisindeki değişken isimlerinin aynı olması gerektiğine dikkat ediniz. Yazıların geçirilmesinde iki tırnaklar kullanılır. Dizi geçirirken yanlışlıkla virgüllerin arasına boşluk karakterleri yerleştirmeyiniz. Programı şöyle make yapabilirsiniz: $ make file=helloworld Yüklemeyi şöyle yapabilirsiniz: $ sudo insmod helloworld.ko count=100 msg="\"this is a test\"" values=1,2,3,4,5 ---------------------------------------------------------------------------------------------------------------------------*/ /* helloworld.c */ #include #include MODULE_LICENSE("GPL"); static int count = 0; static char *msg = "Ok"; static int values[5]; static int size; module_param(count, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param(msg, charp, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param_array(values, int, &size, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init helloworld_init(void) { int i; printk(KERN_INFO "Hello World...\n"); printk(KERN_INFO "count = %d\n", count); printk(KERN_INFO "msg = %s\n", msg); for (i = 0; i < size; ++i) { printk(KERN_INFO "%d\n", values[i]); } return 0; } static void __exit helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); # 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 /*-------------------------------------------------------------------------------------------------------------------------- errno değişkeni aslında libc kütüphanesinin (standart C ve POSIX kütüphanesi) içerisinde tanımlanmış bir değişkendir. Kernel mode'da yani kernel'ın içerisinde errno isimli bir değişken yoktur. Bu nedenle kernel'daki fonksiyonlar POSIX fonksiyonları gibi başarısızlık durumunda -1 ile geri dönüp errno değişkenini set etmezler. Kernel içerisindeki fonksiyonlar başarısızlık durumunda negatif errno değeri ile geri dönerler. Örneğin "open" POSIX fonksiyonu "sys_open" isimli kernel içerisinde bulunan sistem fonksiyonunu çağırdığında onun negatif bir değerle geri dönüp dönmediğine bakar. Eğer "sys_open" fonksiyonu negatif değerle geri dönerse bu durumda bu değerin pozitiflisini errno değişkenine yerleştirip -1 ile geri dönmektedir. Başka bir deyişle aslında bizim çağırdığımız int geri dönüş değerine sahip POSIX fonksiyonları sistem fonksiyonlarını çağırıp o fonksiyonlar negatif bir değerle geri dönmüş ise bir hata oluştuğunu düşünerek o negatif değerin pozitiflisini errno değişkenine yerleştirip -1 ile geri dönmektedir. Kernel modül yazan programcıların da bu geleneğe uyması iyi bir tekniktir. Örneğin: if (some_control_failed) // burada kontrol yapılıyor return -EXXX; // fonksiyon başarısız ise negatif errno değeriyle geri döndürülüyor Özetle biz kernel içerisindeki geri dönüş değeri int olan bir fonksiyonu çağırdığımızda onun başarılı olup olmadığını geri dönüş değerinin negatif olup olmadığı ile kontrol ederiz. Eğer çağırdığımız fonksiyonun geri dönüş değeri negatif ise onun pozitif hali başarısızlığa ilişkin errno numarasını vermektedir. POSIX arayüzünde adrese geri dönen fonksiyonlar genel olarak başarısızlık durumunda NULL adrese geri dönmektedir. Oysa kernel kodlarında adrese geri dönen fonksiyonlar başarısız olduklarında yine sanki bir adresmiş gibi negatif errno değerine geri dönerler. Örneğin şöyle bir kernel fonksiyonu olsun: void *foo(void); Biz bu fonksiyonu kernel modülümüz içerisinde çağırdığımızda eğer fonksiyon başarısızsa negatif errno değerini bir adres gibi geri döndürmektedir. Negatif küçük değerlerin 2'ye tümleyen aritmetiğinde başı 1'lerle dolu olan bir sayı olacağına dikkat ediniz. Örneğin bu foo fonksiyonu EPERM değeri ile geri dönüyor olsun. EPERM değeri 1'dir. 64 bit sistemdeki -1 değeri ise şöyledir: FF FF FF FF FF FF FF FF Bu değer ise çok yüksek bir adres gibidir. O zaman eğer fonksiyon çok yüksek bir adres geri döndürdüyse başarısız olduğu sonucunu çıkartabiliriz. Tabi bu işlemler için makrolar bulundurulmuştur. Kernel kodlarındaki ERR_PTR isimli makro ya da inline fonksiyon bir tamsayı değeri alıp onu adres türüne dönüştürmektedir. Bu nedenle adrese geri dönen fonksiyonlarda aşağıdaki gibi kodlar görebilirsiniz: void *foo(void) { ... if (expression) return ERR_PTR(-EXXX); ... } ERR_PTR aşağıdaki gibi tanımlanmıştır: static inline void *ERR_PTR(long error) { return (void *) error; } Bu işlemin tersi de PTR_ERR makrosu ya da inline fonksiyonu ile yapılmaktadır. Yani PTR_ERR bir adresi alıp onu tamsayıya dönüştürmektedir. Bu fonksiyon da şöyle tanımlanmıştır: static inline long PTR_ERR(const void *ptr) { return (long) ptr; } Yani PTR_ERR makrosu bize aslında adres olarak kodlanmış olan negatif errno değerini geri döndürmektedir. Pekiyi bir adres değerinin içerisinde errno hata kodunun olduğunu nasıl anlarız? İşte negatif errno değerleri bir adres gibi ele alındığında adeta adres alanının sonundaki adresler gibi bir görünümde olacaktır. errno değerleri için toplamda ayrılan sayılar da sınırlı olduğu için kontrol kolaylıkla yapılabilir. Ancak bu kontrol için IS_ERR isimli bir makro ya da inline fonksiyon da bulundurulmuştur: static inline long IS_ERR(const void *ptr) { return (unsigned long)ptr > (unsigned long)-4095; } Burada fonksiyon, adresin adres alanının son 4095 adresinden biri içerisinde mi kontrolünü yapmaktadır. Negatif errno değerlerinin hepsi bu aralıktadır. Tabii 4095 errno değeri yoktur. Burada geleceğe uyumu korumak için 4095'lik bir alan ayrılmıştır. Bu durumda kernel kodlarında adrese geri dönen fonksiyonların başarısızlığı aşağıdaki gibi kontrol edilebilmektedir. void *ptr; ptr = foo(); if (IS_ERR(ptr)) return PTR_ERR(ptr) Kernel modül programcılarının da buradaki konvansiyona uygun kod yazması iyi bir tekniktir. Linux çekirdeğindeki EXXX sembolik sabitleri POSIX arayüzündeki EXXX sabitleriyle aynı değerdedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux'ta bir kernel modül artık user mode'dan kullanılabilir hale getirildiyse buna "aygıt sürücü (device driver)" denilmektedir. Aygıt sürücüler open fonksiyonuyla bir dosya gibi açılırlar. Bu açma işleminden bir dosya betimleyicisi elde edilir. Bu dosya betimleyicisi read, write, lseek, close gibi fonksiyonlarda kullanılabilir. Aygıt sürücülere ilişkin dosya betimleyicileri bu fonksiyonlarla kullanıldığında aygıt sürücü içerisindeki belirlenen bazı fonksiyonlar çağrılmaktadır. Yani tersten gidersek biz örneğin aygıt sürücümüze ilişkin dosya betimleyicisi ile read ve write fonksiyonlarını çağırdığımızda aslında aygıt sürücümüzdeki belli fonksiyonlar çalıştırılmaktadır. Böylece aygıt sürücü ile user mode arasında veri transferleri yine dosyalarda olduğu gibi dosya fonksiyonlarıyla yapılabilmektedir. Ayrıca user mode'dan aygıt sürücümüzdeki herhangi bir fonksiyonu da çağırabiliriz. Bunun için ioctl isimli bir POSIX fonksiyonu (tabii bu POSIX fonksiyonu sys_ioctl isimli sistem fonksiyonunu çağırmaktadır) kullanılmaktadır. Aygıt sürücü içerisinde fonksiyonlara birer kod numarası atanır. Sonra ioctl fonksiyonunda bu kod numarası belirtilir. Böylece akış user mode'dan kernel mode'a geçerek belirlenen fonksiyonu kernel mode'da çalıştıracaktır. Özetle bir aygıt sürücüsü kernel mode'da çalışmaktadır. Biz aygıt sürücüsünü open fonksiyonu ile açıp elde ettiğimiz betimleyici ile read, write, lseek ve close fonksiyonlarını çağırdığımızda aygıt sürücü içerisindeki ilgili fonksiyon çalıştırılmaktadır. Aygıt sürücüsü içerisindeki herhangi bir fonksiyon ise user mode'dan ioctl isimli fonksiyonla çalıştırılmaktadır. Tabii user mode programlar aygıt sürücü içerisindeki kodları read, write, lseek, close, ioctl gibi fonksiyonlar yoluyla çalıştırdıklarında proses user mode'dan geçici süre kernel mode'a geçer, ilgili kodlar kernel mode'da koruma engeline takılmadan çalıştırılır. Fonksiyonların çalışması bittiğinde proses yine user mode'da döner. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir kernel modülü yazarken o modül ile ilgili önemli bazı belirlemeler "modül makroları" denilen MODULE_XXX biçimindeki makrolarla yapılmaktadır. Her ne kadar bu modül makrolarının bulundurulması zorunlu değilse de şiddetle tavsiye edilmektedir. En önemli üç makronun tipik kullanımı şöyledir: MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); Modül lisansı herhangi bir open source lisans olabilir. Tipik olarak "GPL" tercih edilmektedir. MODULE_AUTHOR makrosu ile modülün yazarı belirtilir. MODULE_DESCRPTION modülün ne iş yapacağına yönelik kısa bir başlık yazısı içermektedir. Bu makrolar global alanda herhangi bir yere yerleştirilebilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 131. Ders 29/03/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi aygıt sürücüleri açmak için open fonksiyonunda yol ifadesi olarak (yani dosya ismi olarak) ne verilecektir? İşte aygıt sürücüler dosya sisteminde bir dizin girişiyle temsil edilmektedir. O dizin girişi open ile açıldığında aslında o dizin girişinin temsil ettiği aygıt sürücü açılmış olur. Bu biçimdeki aygıt sürücüleri temsil eden dizin girişlerine "aygıt dosyaları (device files)" denilmektedir. Aygıt dosyaları diskte bir dosya belirtmemektedir. Kernel içerisindeki aygıt sürücüyü temsil eden bir dizin girişi belirtmektedir. Aygıt dosyalarının i-node tablosunda bir i-node elemanı vardır ancak bu i-node elemanı diskte bir yer değil kernel'da bir aygıt sürücü belirtmektedir. Pekiyi bir aygıt dosyası nasıl yaratılmaktadır ve nasıl bir aygıt sürücüyü temsil eder hale getirilmektedir? İşte her aygıt sürücünün majör ve minör numaraları vardır. Aynı zamanda aygıt dosyalarının da majör ve minör numaraları vardır. Bir aygıt sürücünün majör ve minör numarası bir aygıt dosyasının majör ve minör numarasıyla aynıysa bu durumda o aygıt dosyası o aygıt sürücüyü temsil eder. Aygıt dosyaları özel dosyalardır. Bir dosyanın aygıt dosyası olup olmadığı "ls -l" komutunda dosya türü olarak 'c' (karakter aygıt sürücüsü) ya da 'b' (blok aygıt sürücüsü) ile temsil edilmektedir. Anımsanacağı gibi dosya bilgileri stat, fstat, lstat fonksiyonlarıyla elde ediliyordu. İşte struct stat yapısının dev_t türünden st_rdev elemanı eğer dosya bir aygıt dosyasıysa dosyanın majör ve minör numaralarını belirtir. Biz de dosyasındaki S_ISCHR ve S_ISBLK makrolarıyla ilgili dosyanın bir aygıt dosyası olup olmadığını öğrenebiliriz. Yukarıda da belirttiğimiz gibi aygıt sürücüler "karakter aygıt sürücüleri (character device driver)" ve "blok aygıt sürücüleri (block device driver)" olmak üzere ikiye ayrılmaktadır. Karakter aygıt sürücüleri daha yaygın kullanılmaktadır. Biz kursumuzda önce karakter aygıt sürücülerini sonra blok aygıt sürücülerini ele alacağız. O halde şimdi bizim bir aygıt dosyasını nasıl oluşturacağımızı ve aygıt sürücüye nasıl majör ve minör numara atayacağımızı bilmemiz gerekir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aygıt dosyaları mknod isimli POSIX fonksiyonuyla (bu fonksiyon Linux'ta doğrudan sys_node isimli sistem fonksiyonunu çağırmaktadır) ya da komut satırından mknod komutuyla (bu komut da mknod fonksiyonu ile işlemini yapmaktadır) yaratılabilir. mknod fonksiyonunun prototipi şöyledir: #include int mknod(const char *pathname, mode_t mode, dev_t dev); Fonksiyonun birinci parametresi yaratılacak aygıt dosyasının yol ifadesini, ikinci parametresi erişim haklarını ve üçüncü parametresi de aygıt dosyasının majör ve minör numaralarını belirtmektedir. Aygıt dosyasının majör ve minör numaraları dev_t türünden tek bir değer ile belirtilmektedir. dev_t türü POSIX standartlarına göre herhangi bir tamsayı türü olabilmektedir. Biz majör ve minör numaraları user mod programlarda makedev isimli makroyla oluştururuz. Bir dev_t türünden değerin içerisinden major numarayı almak için major makrosu, minor numarayı almak için ise minor makrosu bulunmaktadır: #include dev_t makedev(unsigned int maj, unsigned int min); unsigned int major(dev_t dev); unsigned int minor(dev_t dev); Yani aslında majör ve minör numaralar dev_t türünden bir değerin belli bitlerinde bulunmaktadır. Ancak bu numaraların dev_t türünden değerin hangi bitlerinde bulunduğu sistemden sisteme değişebileceği için bu makrolar kullanılmaktadır. Ancak kernel mode'da bu makrolar yerine aşağıdakiler kullanılmaktadır: #include MKDEV(major, minor) MAJOR(dev) MINOR(dev) Linux'ta son versiyonlar da dikkate alındığında dev_t 32 bitlik işaretsiz bir tamsayı türündendir. Bu 32 bitin yüksek anlamlı 12 biti majör numarayı, düşük anlamlı 20 biti ise minör numarayı temsil etmektedir. Ancak programcı bu varsayımlarla kodunu düzenlememeli yukarıda belirtilen makroları kullanmalıdır. mknod fonksiyonunun ikinci parametresindeki erişim haklarına aygıt dosyasının türünü belirten aşağıdaki sembolik sabitlerden biri de bit OR operatörü ile eklenmelidir: S_IFCHR (Karakter aygıt sürücüsü) S_IFBLK (Blok aygıt sürücüsü) Aslında mknod fonksiyonu ile Linux sistemlerinde isimli boru dosyaları, UNIX domain soket dosyaları ve hatta normal dosyalar da yaratılabilmektedir. Bu durumda fonksiyonun aygıt numarasını belirten üçüncü parametresi fonksiyon tarafından dikkate alınmamaktadır. Bu özel dosyalar için erişim haklarına eklenecek makrolar da şunlardır: S_IFREG (Disk dosyası yaratmak için) S_IFIFO (İsimli boru dosyası yaratmak için) S_IFSOCK (UNIX domain soket dosyası yaratmak için) Aslında mknod fonksiyonu aygıt dosyaları yaratmak için kullanılıyor olsa da yukarıda belirttiğimiz özel dosyaları da yaratabilmektedir. Tabii zaten isimli boru dosyasını yaratmak için mkfifo fonksiyonu, normal dosyaları yaratmak için open fonksiyonu kullanılabilmektedir. mknod fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Ayrıca mknod POSIX fonksiyonunun mknodat isimli at'li bir versiyonu da bulunmaktadır: #include int mknodat(int fd, const char *path, mode_t mode, dev_t dev); Bu at'li versiyon daha önce görmüş olduğumuz at'li fonksiyonlar gibi çalışmaktadır. Yani fonksiyon ilgili dizine ilişkin dosya betimleyicisini ve göreli yol ifadesini parametre olarak alır. O dizinden göreli biçimde yol ifadesini oluşturur. Yine fonksiyonun birinci parametresine AT_FDCWD özel değeri geçilebilir. Bu durumda fonksiyon at'siz versiyondaki gibi çalışır. Diğer at'li fonksiyonlarda olduğu gibi bu fonksiyonun da ikinci parametresindeki yol ifadesi mutlak ise birinci parametresindeki dizin hiç kullanılmamaktadır. mknod ve mknodat fonksiyonları prosesin umask değerini dikkate almaktadır. Bu fonksiyonlarla aygıt dosyası yaratabilmek için (diğer özel dosyalar için gerekmemektedir) prosesin uygun önceliğe sahip olması gerekmektedir. Aşağıdaki aygıt dosyası yaratan mymknode isimli bir fonksiyon yazılmıştır. Fonksiyonun genel kullanımı şöyledir: ./mymknod [-m ya da --mode ] Örnek bir çalıştırma şöyle olabilir: $ sudo ./mymknode -m 666 mydriver c 25 0 Programı sudo ile çalıştırmayı unutmayınız. ---------------------------------------------------------------------------------------------------------------------------*/ /* mymknod.c */ #include #include #include #include #include #include #include bool ismode_correct(const char *mode); void exit_sys(const char *msg); int main(int argc, char *argv[]) /* ./mymknod [-m ] */ { int m_flag; int err_flag; char *m_arg; int result; int mode; dev_t dev; struct option options[] = { {"mode", required_argument, NULL, 'm'}, {0, 0, 0, 0} }; m_flag = err_flag = 0; opterr = 0; while ((result = getopt_long(argc, argv, "m:", options, NULL)) != -1) { switch (result) { case 'm': m_flag = 1; m_arg = optarg; break; case '?': if (optopt == 'm') fprintf(stderr, "option -m or --mode without argument!...\n"); else if (optopt != 0) fprintf(stderr, "invalid option: -%c\n", optopt); else fprintf(stderr, "invalid long option!...\n"); err_flag = 1; break; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind != 4) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (m_flag) { if (!ismode_correct(m_arg)) { fprintf(stderr, "incorrect mode argument!...\n"); exit(EXIT_FAILURE); } sscanf(m_arg, "%o", &mode); } else mode = 0644; if (argv[optind + 1][1] != '\0') { fprintf(stderr, "invalid type argument: %s\n", argv[optind + 1]); exit(EXIT_FAILURE); } if (argv[optind + 1][0] == 'c') mode |= S_IFCHR; else if (argv[optind + 1][0] == 'b') mode |= S_IFBLK; else { fprintf(stderr, "invalid type argument: %s\n", argv[optind + 1]); exit(EXIT_FAILURE); } dev = makedev(atoi(argv[optind + 2]), atoi(argv[optind + 3])); umask(0); if (mknod(argv[optind + 0], mode, dev) == -1) exit_sys("mknod"); return 0; } bool ismode_correct(const char *mode) { if (strlen(mode) > 3) return false; while (*mode != '\0') { if (*mode < '0' || *mode > '7') return false; ++mode; } return true; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aslında yukarıda yazdığımız mymknod programının aynısı zaten mknod isimli kabuk komutu biçiminde bulunmaktadır. Bu komutun genel biçimi şöyledir: sudo mknod [-m ya da --mode ] Örneğin: $ sudo mknod devfile c 25 0 mknod komutunu sudo ile çalıştırmayı unutmayınız. Yukarıdaki komut uygulandığında oluşturulan dosya şöyle olacaktır: crw-rw-rw- 1 root root 25, 0 Mar 29 22:05 mydriver ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir kernel modülün karakter aygıt sürücüsü haline getirilebilmesi için öncelikle bir aygıt numarasıyla (majör ve minör numara ile) temsil edilip çekirdeğe kaydettirilmesi (register ettirilmesi) gerekmektedir. Bu işlem tipik olarak register_chrdev_region isimli fonksiyonla yapılır. Fonksiyonun prototipi şöyledir: #include int register_chrdev_region(dev_t from, unsigned count, const char *name); Fonksiyonun birinci parametresi aygıt sürücünün majör ve minör numaralarına ilişkin dev_t türünden değeri almaktadır. Bu parametre için argüman genellikle MKDEV makrosuyla oluşturulmaktadır. MKDEV makrosu majör ve minör numarayı argüman olarak alıp bundan dev_t türünden aygıt numarası oluşturmaktadır. Fonksiyonun ikinci parametresi ilk parametrede belirtilen minör numaradan itibaren kaç minör numaranın kaydettirileceğini belirtmektedir. Örneğin biz majör=20, minör=0'dan itibaren 5 minör numarayı kaydettirebiliriz. Fonksiyonun son parametresi proc ve sys dosya sistemlerindeki görüntülenecek olan aygıt sürücünün ismini belirtmektedir. Kernel modüllerin isimleri kernel modül dosyasından gelmektedir. Ancak karakter aygıt sürücülerinin isimlerini biz istediğimiz gibi veririz. Tabii her aygıt sürücü bir kernel modül biçiminde yazılmak zorundadır. register_chrdev_region fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda negatif errno değerine geri döner. register_chrdev_region fonksiyonu ile register ettirilen majör ve minör numaralar unregister_chrdev_region fonksiyonuyla geri bırakılmalıdır. Aksi halde modül kernel alanından rmmod komutuyla atılsa bile bu aygıt numaraları tahsis edilmiş bir biçimde kalmaya devam etmektedir. unregister_chrdev_region fonksiyonunun prototipi şöyledir: #include void unregister_chrdev_region (dev_t from, unsigned count); Fonksiyonun birinci parametresi aygıt sürücünün register ettirilmiş olan majör ve minör numarasını, ikinci parametresi ise yine o noktadan başlayan kaç minör numaranın unregister ettirileceğidir. Bir aygıt sürücü register_chrdev_region fonksiyonuyla majör ve minör numarayı register ettirdiğinde artık "/proc/devices" dosyasında bu aygıt sürücü için bir satır yaratılmaktadır. Aygıt sürücü unregister_chrdev_region fonksiyonuyla yok edildiğinde "/proc/devices" dosyasındaki satır silinmektedir. Aşağıdaki örnekte kernel modülün init fonksiyonunda register_chrdev_region fonksiyonu ile Majör: 25, Minor:1 olacak biçimde bir aygıt numarası kernel'a kaydettirilmiştir. Bu kayıt modülün exit fonksiyonunda unregister_chrdev_region fonksiyonu ile silinmiştir. Kernel modülü aşağıdaki gibi derleyebilirsiniz: $ make file=generic-char-driver Modülü install ettikten sonra "/proc/modules" ve "/proc/devices" dosyalarına bakınız. "proc/devices" dosyasında aygıt sürücünün belirlediğimiz isimle kaydettirildiğini göreceksiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* generic-char-driver.c */ #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } module_init(generic_init); module_exit(generic_exit); # 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 /*-------------------------------------------------------------------------------------------------------------------------- 132. Ders 05/04/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir çekirdek modülü bir aygıt numarasıyla ilişkilendirdikten sonra artık ona gerçek anlamda bir karakter aygıt sürücü kimliği kazandırmak gerekmektedir. Bu işlem struct cdev isimli bir yapının için doldurularak sisteme eklenmesi (yerleştirilmesi) ile yapılır. Linux çekirdeği tüm çekirdek modülleri ve aygıt sürücüleri çeşitli veri yapılarıyla tutmaktadır. Aygıt sürücü yazan programcılar çekirdeğin bu organizasyonunu bilmek zorunda değillerdir. Ancak bazı işlemleri tam gerektiği gibi yapmak zorundadırlar. (Linux çekirdeğinin aygıt sürücü mimarisi oldukça karmaşıktır. Bu konu "Linux Kernel" kursunda ele alınmaktadır.) cdev yapısı aşağıdaki gibi bir yapıdır: #include struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; }; Bu türden bir yapı nesnesi programcı tarafından global olarak (statik ömürlü olarak) tanımlanabilir ya da alloc_cdev isimli çekirdek fonksiyonuyla çekirdeğin heap sistemi (slab allocator) kullanılarak dinamik bir biçimde tahsis edilebilir. (İşletim sistemlerinin çekirdeğinin ayrı bir heap sistemi vardır. Linux çekirdeğinde spesifik türden nesnelerin hızlı tahsis edilmesi için "slab allocator" denilen bir heap sistemi kullanılmaktadır.) Eğer bu yapı nesnesi programcı tarafından global bir biçimde tanımlanacaksa yapının elemanlarına ilk değer vermek için cdev_init fonksiyonu çağrılmalıdır. Eğer cdev yapısı cdev_alloc fonksiyonuyla dinamik bir biçimde tahsis edilecekse bu işlem cdev_init ile yapılmaz, çünkü zaten cdev_alloc bu işlemi de yapmaktadır. Fakat yine de programcının bu kez manuel olarak bu yapının bazı elemanlarına değer ataması gerekir. Bu iki yoldan biriyle oluşturulmuş olan cdev yapısının en sonunda cdev_add isimli fonksiyonla çekirdek veri yapılarına yerleştirilmeleri gerekir. Tabii aygıt sürücü boşaltılırken bu yerleştirme işlemi cdev_del fonksiyonuyla geri alınmalıdır. cdev_del fonksiyonu, struct cdev yapısı cdev_alloc ile tahsis edilmişse aynı zamanda onu free hale de getirmektedir. Özetle çekirdek modülümüzün tam bir karakter aygıt sürücüsü haline getirilmesi için şunlar yapılmalıdır: 1) struct cdev isimli bir yapı türünden nesne global olarak (statik ömürlü olarak) tanımlanmalı ya da cdev_alloc fonksiyonu ile çekirdeğin heap sistemi içerisinde tahsis edilmelidir. Eğer bu nesne global olarak tanımlanacaksa nesneye cdev_init fonksiyonu ile ilk değerleri verilmelidir. Eğer nesne cdev_alloc fonksiyonu ile çekirdeğin heap alanında tahsis edilecekse bu durumda ilk değer verme işlemi bu fonksiyon tarafından yapılmaktadır. Ancak programcının yine yapının bazı elemanlarını manuel olarak doldurması gerekmektedir. 2) Oluşturulan bu struct cdev nesnesi cdev_add çekirdek fonksiyonu ile çekirdeğe eklenmelidir. 3) Çekirdek modülü çekirdek alanından atılırken modülün exit fonksiyonunda cdev_add işleminin geri alınması için cdev_del fonksiyonunun çağrılması gerekmektedir. cdev_init fonksiyonunun parametrik yapısı şöyledir: #include void cdev_init(struct cdev *cdev, const struct file_operations *fops); Fonksiyonun birinci parametresi ilk değer verilecek global cdev nesnesinin adresini alır. İkinci parametre ise file_operations türünden bir yapı nesnesinin adresi almaktadır. file_operations isimli yapı birtakım fonksiyon adreslerinden oluşmaktadır. Yani yapının tüm elemanları birer fonksiyon göstericisidir. Bu yapı user mode'daki program tarafından ilgili aygıt dosyası açılıp çeşitli işlemler yapıldığında çağrılacak fonksiyonların adreslerini tutmaktadır. Örneğin user mode'daki program open, close, read, write yaptığında çağrılacak fonksiyonlarımızı burada belirtiriz. file_operations yapısı büyük bir yapıdır: struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, bool spin); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); unsigned long mmap_supported_flags; int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); }; Bu yapının bazı elemanlarına atama yapabiliriz. Bunun için gcc eklentileri kullanılabilir. (Bu eklentiler C99 ile birlikte C'ye eklenmiştir.) Örneğin: static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); struct file_operations g_file_ops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; Yapının owner elemanına THIS_MODULE makrosunun atanması iyi bir tekniktir. Biz burada "aygıt sürücümüz open fonksiyonuyla açıldığında generic_open isimli fonksiyon çağrılsın", aygıt sürücümüz close fonksiyonu ile kapatıldığında "generic_release isimli fonksiyonumuz çağrılsın" demiş olmaktayız. Yukarıda da belirttiğimiz gibi cdev yapısı cdev_alloc fonksiyonuyla dinamik bir biçimde de tahsis edilebilir: #include struct cdev *cdev_alloc(void); Fonksiyon başarı durumunda cdev yapısının adresine, başarısızlık durumunda NULL adrese geri dönmektedir. Yukarıda da belirttiğimiz gibi cdev yapısı cdev_alloc ile tahsis edilmişse cdev_init yapılmasına gerek yoktur. Ancak bu durumda programcının manuel olarak yapının owner ve ops elemanlarına değer ataması gerekir. Örneğin: struct cdev *g_cdev; ... if ((gcdev = cdev_alloc()) == NULL) { printk(KERN_ERROR "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; cdev yapı nesnesi başarılı bir biçimde oluşturulduktan sonra artık bu yapının çekirdek modülü içerisine yerleştirilmesi gerekir. Bu da cdev_add fonksiyonuyla yapılmaktadır: #include int cdev_add(struct cdev *devp, dev_t dev, unsigned count); Fonksiyonun birinci parametresi cdev türünden yapı nesnesinin adresini almaktadır. İkinci parametre aygıt sürücünün majör ve minör numarasını belirtmektedir. Üçüncü parametresi ise ilgili minör numaradan itibaren kaç minör numaranın kullanılacağı belirtir. Fonksiyon başarı durumunda sıfır değerine, başarısızlık durumunda negatif errno değerine geri döner. Örneğin: if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { ... return result; } Aygıt sürücü boşaltılırken cdev_add ile yapılan işlemin geri alınması gerekir. Bu da cdev_del fonksiyonuyla yapılmaktadır. (cdev_alloc işlemi için bunu free hale getiren ayrı bir fonksiyon yoktur. cdev_alloc ile tahsis edilen alan çekirdek tarafından otomatik olarak free hale getirilmektedir.) #include void cdev_del(struct cdev *devp); Fonksiyon parametre olarak cdev yapısının adresini almaktadır. Buradaki önemli bir nokta şudur: cdev_add fonksiyonu cdev nesnesinin içini çekirdekteki uygun veri yapısına kopyalamamaktadır. Bizzat bu nesnenin adresini kullanmaktadır. Yani çekirdek modülü var olduğu sürece bu cdev nesnesinin de yaşıyor olması gerekmektedir. Bu da cdev nesnesinin ve file_operations nesnesinin global biçimde (ya da statik ömürlü biçimde) tanımlanmasını gerektirmektedir. Aşağıda bu işlemlerin yapıldığı örnek bir karakter aygıt sürücüsü verilmiştir. Bu aygıt sürücü majör=25, minör=0 aygıtını kullanmaktadır. Dolayısıyla aşağıdaki programın testi için şöyle bir aygıt dosyasının yaratılmış olması gerekir. Yaratımı aşağıdaki gibi yapabilirsiniz: $ sudo mknod mydriver -m 666 c 25 0 Bu aygıt sürücü insmod ile yüklendiğinde artık biz user mode'da "mydriver" dosyasını açıp kapattığımızda file_operations yapısına yerleştirdiğimiz generic_open ve generic_release fonksiyonları çağrılacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver closed...\n"); return 0; } module_init(generic_init); module_exit(generic_exit); # 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 /* app.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Yukarıdaki programda biz cdev nesnesini global olarak tanımladık. Aşağıda nesnenin cdev_alloc fonksiyonu ile dinamik biçimde tahsis edilmesine bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot alloc cdev!...\n"); return result; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver closed...\n"); return 0; } module_init(generic_init); module_exit(generic_exit); # 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 /* app.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 133. Ders 07/04/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Çekirdek kodları ya da aygıt sürücü kodları çoğu zaman çekirdek alanı ile user alanı arasında veri transfer yapmak isterler. Örneğin sys_read sistem fonksiyonu çekirdek alanında elde ettiği bilgileri user alanındaki programcının verdiği adrese kopyalar. Benzer biçimde sys_write fonksiyonu da bunun tersini yapmaktadır. Çekirdek alanı ile user alanı arasında memcpy fonksiyonu ile transfer yapmaya çalışmak uygun değildir. Bunun birkaç nedeni vardır. Bu tür transferlerde kernel mod programcılarının user alanındaki adresin geçerliliğini kontrol etmesi gerekir. Aksi takdirde kernel mode'da geçersiz bir alana kopyalama yapmak sistemin çökmesine yol açabilmektedir. Ayrıca user alanına ilişkin prosesin sayfa tablosunun bazı bölümleri o anda bellekte olmayabilir (yani swap out yapılmış olabilir). Böyle bir durumda işleme devam etmek çekirdek tasarımı açısından sorun olmaktadır. Eğer böyle bir durum varsa çekirdek kodlarının önce sayfa tablosunu RAM'e geri yükleyip işlemine devam etmesi gerekmektedir. İşte yukarıda açıklanan bazı nedenlerden dolayı çekirdek alanı ile user alanı arasında kopyalama işlemi için özel çekirdek fonksiyonları kullanılmaktadır. Yani biz user mod programları ile kernel modülümüz arasında transferleri özel bazı kernel fonksiyonlarıyla yapmalıyız. Bu amaçla kullanılan çeşitli kernel fonksiyonları ve makroları bulunmaktadır. En temel iki fonksiyon copy_to_user ve copy_from_user fonksiyonlarıdır. Bu fonksiyonların prototipleri şöyledir: #include unsigned long copy_to_user(void *to, const void *from, unsigned len); unsigned long copy_from_user(void *to, const void *from, unsigned len); Fonksiyonların birinci parametreleri kopyalamanın yapılacağı hedef adresi belirtmektedir. Yani copy_to_user için birinci parametre user alanındaki adres, copy_from_user için birinci parametre kernel alanındaki adrestir. İkinci parametre kaynak adresi belirtmektedir. Bu kaynak adres copy_to_user için kernel alanındaki adres, copy_from_user için user alanındaki adrestir. Son parametre transfer edilecek byte sayısını belirtmektedir. Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda transfer edilemeyen byte sayısına geri dönerler. Kernel mod programcılarının bu fonksiyonlar başarısızken bunu çağıran fonksiyonlarını -EFAULT (Bad address) ile geri döndürmesi uygun olur. (Örneğin sys_read ve sys_write fonksiyonlarına biz geçersiz bir user mode adresi verirsek bu sistem fonksiyonları da -EFAULT değeri ile geri dönmektedir. Bu hata kodunun yazısal karşılığı "Bad address" biçimindedir.) Örneğin: if (copy_to_user(...) != 0) return -EFAULT; Bazen user alanındaki adresin zaten geçerliliği sınanmıştır. Bu durumda yeniden geçerlilik sınaması yapmadan yukarıdaki işlemleri yapan __copy_to_user ve __copy_from_user fonksiyonları kullanılabilir. Bu fonksiyonların parametrik yapıları aynıdır. Bu fonksiyonların yukarıdakilerden tek farkı adres geçerliliğine ilişkin sınama yapmamalarıdır: #include unsigned long __copy_to_user(void *to, const void *from, unsigned len); unsigned long __copy_from_user(void *to, const void *from, unsigned len); Bazı durumlarda programcı 1 byte, 2 byte, 4 byte, 8 byte'lık verileri transfer etmek isteyebilir. Bu küçük miktardaki verilerin transfer edilmesi için daha hızlı çalışan özel iki makro vardır: put_user ve get_user. Bu makroların parametrik yapısı şöyledir: #include put_user(x, ptr); get_user(x, ptr); Burada x aktarılacak nesneyi belirtir. (Bu nesnenin adresini programcı almaz, makro içinde bu işlem yapılmaktadır.) ptr ise transfer adresini belirtmektedir. Aktarım ikinci parametrede belirtilen adresin türünün uzunluğu kadar yapılmaktadır. Başka bir deyişle biz makroya hangi türden nesne verirsek zaten makro o uzunlukta tranfer yapmaktadır. Makrolar başarı durumunda 0, başarısızlık durumunda negatif hata koduna geri dönmektedir. Kullanım şöyle olabilir: if (put_user(...) != 0) return -EFAULT; Bu makroların da geçerlilik kontrolü yapmayan __put_user ve __get_user isimli versiyonları vardır: #include __put_user(x, ptr); __get_user(x, ptr); Örneğin biz çekirdek modülümüzdeki 4 byte'lık int bir x nesnesinin içerisindeki bilgiyi puser ile temsil edilen user adresine kopyalamak isteyelim. Bu işlemi şöyle yaparız: int x; int *puser; ... put_user(x, puser); Nihayet user alanındaki adresin geçerliliği de access_ok isimli makroyla sorgulanabilmektedir. Makro şöyledir: #include access_ok(type, addr, size); Buradaki type sınanacak geçerliliğin türünü anlatmaktadır. Okuma geçerliliği için bu parametre VERIFY_READ, yazma geçerliliği için VERIFY_WRITE ve hem okuma hem de yazma geçerliliği için VERIFY_READ|VERIFY_WRITE biçiminde girilmelidir. İkinci parametre geçerliliği sınanacak adresi ve üçüncü parametre de o adresten başlayan alanın uzunluğunu belirtmektedir. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir. Örneğin biz user alanında puser adresiyle başlayan 100 byte'lık alanın yazma bakımından geçerli bir alan olup olmadığını sınamak isteyelim. Bu sınamayı çekirdek modülümüzde şöyle yapabiliriz: if (access_ok(VERIFY_WRITE, puser, 100)) { // adres geçerli ... } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar aygıt dosyası open ile açıldığında ve close ile kapatıldığında aygıt sürücümüz içerisindeki fonksiyonlarımızın çağrılmasını sağladık. Şimdi de aygıt dosyası üzerinde read ve write fonksiyonları uygulandığında aygıt sürücümüzdeki ilgili fonksiyonların çağrılması üzerinde duracağız. Aygıt sürücümüz için read ve write fonksiyonları aşağıdaki parametrik yapıya uygun olacak biçiminde file_operations yapısına yerleştirilmelidir: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct file_operations g_file_ops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release, .read = generic_read, .write = generic_write, }; Artık aygıt dosyası üzerinde read POSIX fonksiyonu çağrıldığında aygıt sürücümüzdeki generic_read fonksiyonu write POSIX fonksiyonu çağrıldığında aygıt sürücümüzdeki generic_write POSIX fonksiyonu çağrılacaktır. read ve write fonksiyonlarının birinci parametresi açılmış dosyaya ilişkin struct file nesnesinin adresini belirtir. Anımsanacağı gibi bir dosya açıldığında kernel sys_open fonksiyonunda bir dosya nesnesi (struct file) tahsis edip bu dosya nesnesinin adresini dosya betimleyici tablosunda bir slota yerleştirip onun indeksini dosya betimleyicisi olarak geri döndürüyordu. İşte bu read ve write fonksiyonlarının birinci parametreleri bu dosya nesnesinin adresini belirtmektedir. Daha önceden de belirttiğimiz gibi file yapısı içerisinde dosya göstericisinin konumu, dosyanın erişim hakları, referans sayacının değeri, dosyanın açış modu ve açış bayrakları ve başka birtakım bilgiler bulunmaktadır. Linux kernel 2.4.30'daki file yapısı şöyledir: struct file { struct list_head f_list; struct dentry *f_dentry; struct vfsmount *f_vfsmnt; struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; struct fown_struct f_owner; unsigned int f_uid, f_gid; int f_error; size_t f_maxcount; unsigned long f_version; // needed for tty driver, and maybe others void *private_data; // preallocated helper kiobuf to speedup O_DIRECT struct kiobuf *f_iobuf; long f_iobuf_lock; }; Biz burada bilerek sadelik yüzünden eski bir çekirdeğin file yapısını verdik. Yeni çekirdeklerde buna birkaç eleman daha eklenmiştir. Ancak temel elemanlar yine aynıdır. read ve write fonksiyonlarının ikinci parametresi user alanındaki transfer adresini belirtir. Üçüncü parametreler okunacak ya da yazılacak byte miktarını belirtmektedir. Son parametre dosya göstericisinin konumunu belirtir. Ancak bu parametre file yapısı içerisindeki f_pos elemanının adresi değildir. Çekirdek tarafından read ve write fonksiyonları çağrılmadan önce file yapısı içerisindeki f_pos elemanının değeri başka bir nesneye atanıp o nesnenin adresi read ve write fonksiyonlarına geçirilmektedir. read ve write fonksiyonları sonlandığında çekirdek adresini geçirdiği nesnenin değerini file yapısının f_pos elemanına kendisi yerleştirmektedir. Fonksiyon başarı durumunda transfer edilen byte sayısına, başarısızlık durumunda negatif errno değerine geri dönmelidir. Biz aygıt sürücümüz için read ve write fonksiyonlarını yazarken transfer edilen byte miktarı kadar dosya göstericisini kendimizin ilerletmesi gerekir. Bu işlem fonksiyonların son parametresi olan off göstericisinin gösterdiği yerin güncellenmesi ile yapılır. Örneğin n byte transfer edilmiş olsun. Bu durumda dosya göstericisinin konumu aşağıdaki gibi güncellenebilir: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { ... *off += n; return n; } Aygıt sürücüsünüzün read ve write fonksiyonlarında dosya göstericisini konumlandırmak için file yapısının f_pos elemanını güncellemeyiniz. Dosya göstericisinin konumlandırılması her zaman read ve write fonksiyonlarının son parametresi yoluyla yapılmaktadır. Çekirdeğin dosya göstericisini nasıl güncellediğine ilişkin aşağıdaki gibi bir temsili kod örneği verebiliriz: loff_t off; ... off = filp->f_pos; read(filp, buf, size, &off); filp_f_pos = off; Aşağıdaki örnekte aygıt sürücü için read fonksiyonu yazılmıştır. Bu fonksiyon aslında g_buf isimli dizinin içini dosya gibi vermektedir. Aşağıda aygıt sürücüye read ve write fonksiyonları içi boş bir biçimde yerleştirilmiştir. User mode'dan read ve write yapıldığında aygıt sürücümüzün içerisindeki bu fonksiyonların çalıştığını gözlemleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "generic_read called...\n"); return size; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "generic_write called...\n"); return size; } module_init(generic_init); module_exit(generic_exit); # 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 /* app.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[100]; if ((fd = open("mydriver", O_RDWR)) == -1) exit_sys("open"); read(fd, buf, 100); write(fd, buf, 100); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 134. Ders 19/04/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de aygıt sürücümüzün read fonksiyonunun gerçekten bir dosyadan okuma yapıyormuş gibi davranmasını sağlayalım. Bunun için dosyamızı temsil eden aşağıdaki gibi global bir dizi kullanacağız: static char g_buf[] = "01234567890ABCDEFGH"; Buradaki diziyi sanki bir dosya gibi ele alacağız. Aygıt sürücümüzün read fonksiyonu aşağıdaki gibi olacaktır: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t slen; slen = strlen(g_buf); esize = *off + size > slen ? slen - *off : size; if (copy_to_user(buf, g_buf + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } Burada önce dosya göstericisinin gösterdiği yerden itibaren "size" kadar byte'ın gerçekten dizi içerisinde olup olmadığına bakılmıştır. Eğer "*off + size" değeri bu dizinin uzunluğundan fazlaysa "size" kadar değer değil, "slen - *off" kadar değer okunmuştur. Aygıt sürücülerin read ve write fonksiyonlarında dosya göstericisinin ilerletilmesi programcının sorumluluğundadır. Bu nedenle okuma işlemi yapıldığında dosya göstericisinin konumu aşağıdaki gibi artırılmıştır: *off += size; read fonksiyonunun okunabilen byte sayısına geri döndürüldüğüne dikkat ediniz. copy_to_user fonksiyonu ile tüm byte'lar user alanına kopyalanamamışsa fonksiyon -EFAULT değeri ile geri döndürülmüştür. ---------------------------------------------------------------------------------------------------------------------------*/ /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static char g_buf[] = "01234567890ABCDEFGH"; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t slen; slen = strlen(g_buf); esize = *off + size > slen ? slen - *off : size; if (copy_to_user(buf, g_buf + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "generic_write called...\n"); return size; } module_init(generic_init); module_exit(generic_exit); # 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 /* app.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[1024]; ssize_t result; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); if ((result = read(fd, buf, 3)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); if ((result = read(fd, buf, 5)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); if ((result = read(fd, buf, 30)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); if ((result = read(fd, buf, 30)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aygıt sürücü için write fonksiyonu da tamamen read fonksiyonuna benzer biçimde yazılmaktadır. write fonksiyonu içerisinde biz user mode'daki bilgiyi copy_from_user ya da get_user fonksiyonlarıyla alırız. Yine write fonksiyonu da bir sorun çıktığında -EFAULT değeri ile, başarılı sonlanmada ise yazılan (kernel alanına yazılan) byte miktarı ile geri dönmelidir. Aşağıdaki örnekte aygıt sürücü bellekte oluşturulmuş bir dosya gibi davranmaktadır. Aygıt sürücünün taklit ettiği dosya en fazla 4096 byte olabilmektedir: #define FILE_MEMORY_MAX_SIZE 4096 ... static char g_fmem[FILE_MEMORY_MAX_SIZE]; Ancak buradaki FILE_MEMORY_MAX_SIZE bellek dosyasının maksimum uzunluğunu belirtmektedir. Bellek dosyasının gerçek uzunluğu g_fmem_size nesnesinde tutulmaktadır. Aygıt sürücünün write fonksiyonu aşağıdaki gibi yazılmıştır: static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } Burada yine dosya göstericisinin gösterdiği yerden itibaren yazılmak istenen byte sayısı FILE_MEMORY_MAX_SIZE değerini aşıyorsa geri kalan miktar kadar yazma yapılmıştır. Burada yine dosya göstericisinin ilerletildiğine dikkat ediniz. Dosya göstericisinin ilerletilmesi her zaman programcının sorumluluğundadır. Aygıt sürücümüzün read fonksiyonu da şöyledir: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } Burada da dosya göstericisinin gösterdiği yerden itibaren okunmak istenen byte sayısının g_fmem_size değerinden büyük olup olmadığına bakılmıştır. Yine göstericisi fonksiyon tarafından güncellenmiştir. Buradaki aygıt sürücüyü test etmek için "app-write.c" ve "app-read.c" isimli iki ayrı programdan faydalanılmıştır. "app-write.c" bellek dosyasına yazma yapmakta, "app-read.c" ise bellek dosyasından okuma yapmaktadır. Bu örnekte bellek dosyasına yazılanların aygıt sürücü çekirdekte bulunduğu sürece kalıcı olduğuna dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 #define FILE_MEMORY_MAX_SIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static char g_fmem[FILE_MEMORY_MAX_SIZE]; static size_t g_fmem_size; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } module_init(generic_init); module_exit(generic_exit); # 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 /* app-write.c */ #include #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[5000]; if ((fd = open("mydriver", O_WRONLY)) == -1) exit_sys("open"); if ((result = write(fd, "ankara", 6)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if ((result = write(fd, "izmir", 5)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); memset(buf, 'x', 5000); if ((result = write(fd, buf, 5000)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* app-read.c */ #include #include #include #include #include #define BUFFER_SIZE 5 void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[BUFFER_SIZE + 1]; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); while ((result = read(fd, buf, BUFFER_SIZE)) > 0) { buf[result] = '\0'; printf("%s", buf); fflush(stdout); } putchar('\n'); if (result == -1) exit_sys("read"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- User mode'dan aygıt dosyası betimleyicisi ile lseek işlemi yapıldığında aygıt sürücünün file_operations yapısı içerisine yerleştirilen llseek fonksiyonu çağrılmaktadır. Fonksiyonun parametrik yapısı şöyledir: static loff_t generic_llseek(struct file *filp, loff_t off, int whence); Fonksiyonun birinci parametresi dosya nesnesini, ikinci parametresi konumlandırılmak istenen offset'i, üçüncü parametresi ise konumlandırmanın nereye göre yapılacağını belirtmektedir. Bu fonksiyonu gerçekleştirirken programcı file yapısı içerisindeki f_pos elemanını güncellemelidir. Tipik olarak programcı whence parametresini switch içerisine alır. Hedeflenen offset'i hesaplar ve en sonunda file yapısının f_pos elemanına bu hedeflenen offset'i yerleştirir. Hedeflenen offset uygun değilse fonksiyon tipik olarak -EINVAL değeriyle geri döndürülür. Eğer konumlandırma offset'i başarılı ise fonksiyon dosya göstericisinin yeni değerine geri dönmelidir. Aşağıda daha önce yapmış olduğumuz bellek dosyası örneğine llseek fonksiyonu da eklenmiştir. Fonksiyon aşağıdaki gibi yazılmıştır: static loff_t generic_llseek(struct file *filp, loff_t off, int whence) { loff_t newpos; switch (whence) { case 0: newpos = off; break; case 1: newpos = filp->f_pos + off; break; case 2: newpos = g_fmem_size + off; break; default: return -EINVAL; } if (newpos < 0 || newpos > g_fmem_size) return -EINVAL; filp->f_pos = newpos; return newpos; } Burada önce whence parametresine bakılarak dosya göstericisinin konumlandırılacağı offset belirlenmiştir. Sonra dosya nesnesinin f_pos elemanı güncellenmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 #define FILE_MEMORY_MAX_SIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static loff_t generic_llseek(struct file *filp, loff_t off, int whence); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .llseek = generic_llseek, .release = generic_release }; static char g_fmem[FILE_MEMORY_MAX_SIZE]; static size_t g_fmem_size; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } static loff_t generic_llseek(struct file *filp, loff_t off, int whence) { loff_t newpos; switch (whence) { case 0: newpos = off; break; case 1: newpos = filp->f_pos + off; break; case 2: newpos = g_fmem_size + off; break; default: return -EINVAL; } if (newpos < 0 || newpos > g_fmem_size) return -EINVAL; filp->f_pos = newpos; return newpos; } module_init(generic_init); module_exit(generic_exit); # 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 /* app.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[4096]; if ((fd = open("mydriver", O_RDWR)) == -1) exit_sys("open"); if ((result = write(fd, "ankara", 6)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if ((result = write(fd, "izmir", 5)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if (lseek(fd, 0, 0) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); if (lseek(fd, -2, 1) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; if (lseek(fd, -2, 2) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 135. Ders 21/04/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadarki örneklerimizde aygıt sürücümüzün majör ve minör numarasını baştan belirledik. Bunun en önemli sakıncası zaten o numaralı bir aygıt sürücünün yüklü olarak bulunuyor olmasıdır. Bu durumda aygıt sürücümüz yüklenemeyecektir. Aslında daha doğru bir strateji tersten gitmektir. Yani önce aygıt sürücümüz içerisinde biz boş bir aygıt numarasını bulup onu kullanabiliriz. Tabii sonra user mode'dan bu aygıt numarasına ilişkin bir aygıt dosyasını da yaratmamız gerekir. Boş bir aygıt numarasını bize veren alloc_chrdev_region isimli bir kernel fonksiyonu vardır. Fonksiyonun parametrik yapısı şöyledir: int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); Fonksiyonun birinci parametresi boş aygıt numarasının yerleştirileceği dev_t nesnesinin adresini alır. İkinci ve üçüncü parametreler başlangıç minör numarası ve onun sayısını belirtir. Son parametre ise aygıt sürücüsünün "/proc/devices" dosyasında ve "/sys/dev" dizininde görüntülenecek olan ismini belirtmektedir. alloc_chrdev_region fonksiyonu zaten register_chrdev_region fonksiyonunun yaptığını da yapmaktadır. Dolayısıyla bu iki fonksiyondan yalnızca biri kullanılmalıdır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda negatif errno değerine geri döner. Örneğin: dev_t g_dev; ... if ((result = alloc_chrdev_region(&g_dev, 0, 1, "generic-char-driver")) != 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } Aygıt sürücümüzde alloc_chrdev_region fonksiyonu ile boş bir majör numara numaranın bulunup aygıt sürücümüzün register ettirildiğini düşünelim. Pekiyi biz bu numarayı nasıl bilip bu numaraya uygun aygıt dosyası yaratacağız? İşte bunun için genellikle izlenen yöntem "/proc/devices" dosyasına bakıp oradan majör numarayı alıp aygıt dosyasını yaratmaktır. Tabii bu manuel olarak yapılabilir ancak bir shell script ile otomatize de edilebilir. Aşağıdaki "load" isimli script bu işlemi yapmaktadır: #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 Artık biz bu "load" script'i ile aygıt sürücümüzü yükleyip aygıt dosyamızı yaratacağız. Bu script'i "load" ismiyle yazıp aşağıdaki gibi x hakkı vermelisiniz: $ chmod +x load Çalıştırmayı komut satırı argümanı vererek aşağıdaki gibi yapmalısınız: $ sudo ./load generic-char-driver Burada "load" script'i çalıştırıldığında hem aygıt sürücü çekirdek alanına yüklenmekte hem de yüklenen aygıt sürücünün majör numarasıyla minör numarası 1 olacak biçimde "generic-char-driver" isimli aygıt dosyası yaratılmaktadır. Aygıt sürücünün çekirdek alanından atılması manuel bir biçimde "rmmod" komutuyla yapılabilir. Tabii aynı zamanda bu aygıt sürücü için yaratılan aygıt dosyasının da silinmesi gerekir. Yukarıdaki script'te aygıt dosyası zaten varsa aynı zamanda o silinmektedir. Tabii aygıt dosyasını çekirdek alanından atarak silen ayrı bir "unload" isimli script'i de aşağıdaki gibi yazabiliriz: #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module Tabii yine bu script dosyasının da "x" hakkına sahip olması gerekmektedir: $ chmod +x unload "unload" script'ini aşağıdaki gibi çalıştırabilirsiniz: $ sudo ./unload generic-char-driver Aşağıdaki örnekte alloc_chrdev_region fonksiyonuyla hem boş aygıt numarası elde edilip hem de bu aygıt numarası register ettirilmiştir. Yükleme işlemi yukarıdaki "load" script'i ile yapılmalıdır. Kernel modülün boşaltılması işlemi manuel olarak ya da "unload" script'i ile yapılabilir. Örneğin: $ sudo ./load generic-char-driver ... $ sudo ./unload generic-char-driver ---------------------------------------------------------------------------------------------------------------------------*/ /* generic-char-driver.c */ #include #include #include #include #include #define FILE_MEMORY_MAX_SIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static loff_t generic_llseek(struct file *filp, loff_t off, int whence); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .llseek = generic_llseek, .release = generic_release }; static char g_fmem[FILE_MEMORY_MAX_SIZE]; static size_t g_fmem_size; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "generic-char-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } static loff_t generic_llseek(struct file *filp, loff_t off, int whence) { loff_t newpos; switch (whence) { case 0: newpos = off; break; case 1: newpos = filp->f_pos + off; break; case 2: newpos = g_fmem_size + off; break; default: return -EINVAL; } if (newpos < 0 || newpos > g_fmem_size) return -EINVAL; filp->f_pos = newpos; return newpos; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız ) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* app.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[4096]; if ((fd = open("generic-char-driver", O_RDWR)) == -1) exit_sys("open"); if ((result = write(fd, "ankara", 6)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if ((result = write(fd, "izmir", 5)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if (lseek(fd, 0, 0) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); if (lseek(fd, -2, 1) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; if (lseek(fd, -2, 2) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 136. Ders 26/04/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıda gelinen noktaya kadar görülmüş olan konular kullanılarak yazılmış basit bir boru örneği verilmiştir. Bu boru örneğinde bir proses boruyu yazma modunda açar ve prosesin write fonksiyonuyla yazdıkları aygıt sürücü içerisindeki bir FIFO kuyruk sistemine yazılır. Diğer proses de read fonksiyonuyla bu FIFO kuyruk sisteminden okuma yapar. Bu gerçekleştirim orijinal "isimli boru (named pipe)" gerçekleştirimine benziyorsa da ondan farklıdır. Burada yapılan gerçekleştirimin önemli noktaları şunlardır: - write fonksiyonu borudaki boş alan miktarından daha fazla bilgi yazılmaya çalışılırsa blokeye yol açmaz, yazabildiği kadar byte'ı yazar ve boruya yazabildiği byte sayısına geri döner. Halbuki anımsanacağı gibi orijinal borularda talep edilen miktarın tamamı yazılana kadar write bloke oluşturmaktadır. - read fonksiyonu boruda hiçbir bilgi yoksa blokeye yol açmaz 0 ile geri döner. Ancak read eğer boruda en az bir byte varsa okuyabildiği kadar byte'ı okuyup, okuyabildiği byte sayısına geri döner. - Aygıt sürücünün read/write fonksiyonlarında hiçbir senkronizasyon uygulanmamıştır. Dolayısıyla eşzamanlı işlemlerde tanımsız davranışlar ortaya çıkabilir. Örneğin iki farklı proses bu boruya yazma yaparsa senkronizasyondan kaynaklanan sorunlar oluşabilir. - İki proses de boruyu kapatsa bile boru silinmemektedir. Halbuki orijinal borularda prosesler isimli boruyu kapatınca boru içerisindeki tüm bilgiler silinmektedir. - Bu uygulamada sistem genelinde tek bir boru yaratılmaktadır. Yani bizim boru aygıt sürücümüz tek bir boru üzerinde işlemler yapmaktadır. Halbuki orijinal isimli borularda programcılar birbirinden bağımsız istedikleri kadar çok isimli boru yaratabilmektedir. Aygıt sürücünüzü önce build edip sonra aşağıdaki gibi yüklemelisiniz: $ make file=pipe-driver $ sudo ./load pipe-driver Buradaki boru aygıt sürücüsünü test etmek için "prog1" ve "prog2" isimli iki program yazılmıştır. "prog1" klavyeden alınan yazıları boruya yazmakta, "prog2" ise klavyeden alınan uzunlukta byte'ı borudan okumaktadır. Boruyu test etmek için boru uzunluğunu azaltabilirsiniz. Biz örneğimizde boru uzunluğunu 4096 aldık. ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.c */ #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız ) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 137. Ders 28/04/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aygıt sürücülerdeki kodlar user mode'dan farklı prosesler tarafından kullanılıyor olabilir. Ayrıca ileride göreceğimiz gibi aygıt sürücüler donanım kesmelerini de kullanıyor olabilir. Dolayısıyla aygıt sürücü kodları eşzamanlı (concurrent) erişime uygun biçimde yazılmalıdır. User mode'daki bir proses aygıt sürücü içerisindeki bir kaynağı kullanıyorken user mode'daki diğer prosesin o kaynağın bozulmaması için diğerini beklemesi gerekebilmektedir. Kernel mode'da aygıt sürücü kodları daha önce user mode'da gördüğümüz senkronizasyon nesnelerini kullanamaz. Çünkü daha önce gördüğümüz senkronizasyon nesneleri user mode'dan kullanılsın diye oluşturulmuştur. Çekirdeğin içerisinde kernel mode'dan kullanılabilecek ayrı senkronizasyon nesneleri bulunmaktadır. Bu bölümde aygıt sürücülerin kernel mode'da kullanabileceği senkronizasyon nesnelerini göreceğiz. Kernel mode için user mode'dakine benzer senkronizasyon nesneleri kullanılmaktadır. Bunların genel çalışma biçimi user mode'dakilere benzemektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kernel mutex mekanizması 2.6 çekirdeğinde çekirdeğe eklenmiştir. Bundan önce mutex işlemleri binary semaphore'larla yapılıyordu. Bu mutex mekanizması user mode'daki mutex mekanizmasına çok benzemektedir. Yine kernel mode'daki mutex'in thread temelinde sahipliği vardır. Bu mutex mekanizması yine thread'i bloke edip sleep kuyruklarında bekletebilmektedir. Kernel mode mutex mekanizmasının tipik gerçekleştirimi şöyledir: 1) lock işlemi sırasında işlemcinin maliyetsiz compare/set (compare/exchange) komutlarıyla mutex'in kilitli olup olmadığına bakılır. 2) diğer bir işlemcideki proses mutex'i kilitlemişse boşuna bloke olmamak için yine compare/set komutlarıyla biraz spin işlemi yapılır. 3) spin işleminden sonuç elde edilemezse bloke oluşturulur. Kernel mode'daki mutex'ler tipik olarak şöyle kullanılmaktadır: 1) Mutex nesnesi "mutex" isimli bir yapıyla temsil edilmektedir. Sistem programcısı bu yapı türünden global bir nesne yaratır ve ona ilk değerini verir. DEFINE_MUTEX(name) makrosu hem mutex türünden nesneyi tanımlamakta hem de ona ilk değerini vermektedir. Örneğin: #include static DEFINE_MUTEX(g_mutex); Burada biz hem g_mutex isminde bir global nesne tanımlamış olduk hem de ona ilk değer vermiş olduk. Aynı işlem önce nesneyi tanımlayıp sonra mutex_init fonksiyonuyla da yapılabilir. Örneğin: static struct mutex g_mutex; ... mutex_init(&g_mutex); DEFINE_MUTEX makrosuna nesnenin adresinin verilmediğine dikkat ediniz. Bu makro ve mutex_init fonksiyonunun prototipleri başlık dosyasında bulunmaktadır. Her ne kadar mutex_init bir fonksiyon görünümündeyse de aslında çekirdek kodlarında hem bir makro olarak hem de bir fonksiyon olarak bulunmaktadır. Mevcut Linux çekirdeklerinde fonksiyonların makro gerçekleştirimleri aşağıdaki gibidir: #define DEFINE_MUTEX(mutexname) \ struct mutex mutexname = __MUTEX_INITIALIZER(mutexname) #define mutex_init(mutex) \ do { \ static struct lock_class_key __key; \ \ __mutex_init((mutex), #mutex, &__key); \ } while (0) 2) Mutex'i kilitlemek için mutex_lock fonksiyonu kullanılır: #include void mutex_lock(struct mutex *lock); Mutex'in kilitli olup olmadığı ise mutex_trylock fonksiyonuyla kontrol edilebilir: #include int mutex_trylock(struct mutex *lock); Eğer mutex kilitliyse fonksiyon bloke olmadan 0 değeriyle geri döner. Eğer mutex kilitli değilse mutex kilitlenir ve fonksiyon 1 değeri ile geri döner. Mutex nesnesi mutex_lock ile kilitlenmek istendiğinde bloke oluşursa bu blokeden sinyal yoluyla çıkılamamaktadır. Örneğin mutex_lock ile kernel mode'da biz mutex kilidini alamadığımızdan dolayı bloke oluştuğunu düşünelim. Bu durumda ilgili prosese bir sinyal gelirse ve eğer o sinyal için sinyal fonksiyonu set edilmişse thread uyandırılıp sinyal fonksiyonu çalıştırılmamaktadır. İşte eğer mutex'in kilitli olması nedeniyle bloke oluştuğunda sinyal yoluyla thread'in uyandırılıp sinyal fonksiyonunun çalıştırması isteniyorsa mutex nesnesi mutex_lock ile değil, mutex_lock_interrupible fonksiyonu ile kilitlenmeye çalışılmalıdır. mutex_lock_interruptible fonksiyonunun prototipi şöyledir: #include int mutex_lock_interruptible(struct mutex *lock); Fonksiyon eğer mutex kilidini alarak sonlanırsa 0 değerine, bloke olup sinyal dolayısıyla sonlanırsa -EINTR değerine geri dönmektedir. Programcı bu fonksiyonun -EINTR ile sonlandığını tespit ettiğinde ilgili sistem fonksiyonunun yeniden çalıştırılabilirliğini sağlamak için -ERESTARTSYS ile geri dönebilir. Örneğin: if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; Tabii buradaki kontrolü != 0 biçiminde de yapabilirdik. Değişen bir şey olmazdı: if (mutex_lock_interruptible(&g_mutex) != 0) return -ERESTARTSYS; 3) Mutex nesnesinin kilidini bırakmak için (unlock etmek için) mutex_unlock fonksiyonu kullanılmaktadır: void mutex_unlock(struct mutex *lock); Bu durumda örneğin tipik olarak aygıt sürücü içerisinde belli bir bölgeyi mutex yoluyla koruma şöyle yapılmaktadır: DEFINE_MUTEX(g_mutex); ... if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; ... ... ... mutex_unlock(&g_mutex); Mutex nesnesini kilitledikten sonra fonksiyonlarınızı geri döndürürken kilidi açnayı unutmayınız. Aşağıdaki örnekte yukarıdaki boru sürücüsü daha güvenli olacak biçimde mutex nesneleriyle senkronize edilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.c */ #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static DEFINE_MUTEX(g_mutex); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) { mutex_unlock(&g_mutex); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) { mutex_unlock(&g_mutex); return -EFAULT; } g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; mutex_unlock(&g_mutex); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) { mutex_unlock(&g_mutex); return -EFAULT; } if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) { mutex_unlock(&g_mutex); return -EFAULT; } g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; mutex_unlock(&g_mutex); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 138. Ders 03/05/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kernel'da da user moddakine benzer semaphore nesneleri vardır. Kernel semaphore nesneleri de sayaçlıdır. Yine bunların sayaçları 0'dan büyükse semaphore açık durumdadır, sayaçlar 0 değerinde ise semaphore kapalı durumdadır. Kritik koda girildiğinde yine sayaç 1 eksiltilir. Sayaç 0 olduğunda thread bloke edilir. Yine bloke işleminde biraz spin işlemi yapılıp sonra bloke uygulanmaktadır. Kernel semaphore nesneleri şöyle kullanılmaktadır: 1) Semaphore nesnesi struct semaphore isimli bir yapıyla temsil edilmiştir. Bir semaphore nesnesi DEFINE_SEMAPHORE(name) makrosuyla aşağıdaki gibi oluşturulabilir. #define static DEFINE_SEMAPHORE(g_sem); Bu biçimde yaratılan semaphore nesnesinin başlangıçta sayaç değeri 1'dir. Yeni çekirdeklerde (v6.4-rc1 ve sonrası) bu makro iki parametreli olarak da kullanılabilmektedir: static DEFINE_SEMAPHORE(g_sem, n); Buradaki ikinci parametre semaphore sayacının başlangıçtaki değerini belirtmektedir. Semaphore nesneleri sema_init fonksiyonuyla da yaratılabilmektedir: static struct semaphore g_sem; ... sema_init(&g_sem, 1); Fonksiyonun ikinci parametresi başlangıç sayaç numarasıdır. 2) Kritik kod "down" ve "up" fonksiyonları arasına alınır. "down" fonksiyonları sayacı bir eksilterek kritik koda giriş yapar. "up" fonksiyonu ise sayacı bir artırmaktadır. Fonksiyonların prototipleri şöyledir: #define void down(struct semaphore *sem); int down_interruptible(struct semaphore *sem); int down_killable(struct semaphore *sem); int down_trylock(struct semaphore *sem); int down_timeout(struct semaphore *sem, long jiffies); void up(struct semaphore *sem); Kritik kod "down" fonksiyonu ile oluşturulduğunda thread bloke olursa sinyal yoluyla uyandırılamamaktadır. Ancak kritik kod "down_interruptible" fonksiyonu ile oluşturulduğunda thread bloke olursa sinyal yoluyla uyandırılabilmektedir. "down_killable" bloke olmuş thread'i yalnızca SIGKILL sinyali geldiğinde blokeden kurtarıp sonlandırabilmektedir. "down_killable" fonksiyonunda eğer thread bloke olursa diğer sinyaller yine blokeyi sonlandıramamaktadır. "down_trylock" yine nesnenin açık olup olmadığına bakmak için kullanılır. Eğer nesne açıksa yine sayaç 1 eksiltilir ve kritik koda girilir. Bu durumda fonksiyon 0 dışı bir değerle geri döner. Nesne kapalıysa fonksiyon bloke olmadan 0 değerine geri döner. "down_timeout" ise en kötü olasılıkla belli miktar "jiffy" zamanı kadar blokeye yol açmaktadır. ("jiffy" kavramı ileride ele alınacaktır.) Fonksiyon zaman aşımı dolduğundan dolayı sonlanmışsa negatif hata koduna, normal bir biçimde sonlanmışsa 0 değerine geri dönmektedir. "down_interruptible" fonksiyonu normal sonlanmada 0 değerine, sinyal yoluyla sonlanmada -ERESTARTSYS değeri ile geri döner. Normal uygulama eğer bu fonksiyonlar -ERESTARTSYS ile geri dönerse aygıt sürücüdeki fonksiyonun da aynı değerle geri döndürülmesidir. Zaten çekirdek bu -ERESTARTSYS geri dönüş değerini aldığında asıl sistem fonksiyonunu eğer sinyal için otomatik restart mekanizması aktif değilse -EINTR değeri ile geri döndürmektedir. Bu da tabii POSIX fonksiyonlarının başarısız olup errno değerini EINTR biçiminde set edilmesine yol açmaktadır. "up" fonksiyonu yukarıda da belirttiğimiz gibi semaphore sayacını 1 artırmaktadır. Kernel semaphore nesneleriyle kritik kod aşağıdaki gibi oluşturulmaktadır: DEFINE_SEMAPHORE(g_sem); ... down_interruptible(&g_sem); ... ... ... up(&g_sem); Yukarıdaki boru örneğinde biz mutex nesnesi yerine binary semaphore nesnesi de kullanabilirdik. Aşağıda aynı örneğin binary semaphore ile gerçekleştirimi görülmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.c */ #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem) < 0) return -ERESTARTSYS; esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) { up(&g_sem); return -EFAULT; } g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem) < 0) return -ERESTARTSYS; esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) { up(&g_sem); return -EFAULT; } g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- User mode'da gördüğümüz diğer senkronizasyon nesnelerinin benzerleri de çekirdek içerisinde bulunmaktadır. Örneğin spinlock kullanımına çekirdek kodlarında ve aygıt sürücülerde sıkça rastlanmaktadır. Anımsanacağı gibi spinlock uykuya dalarak değil, (yani bloke olarak değil) meşgul bir döngü içerisinde kilidin açılmasını bekleyen senkronizasyon nesnelerini belirtiyordu. Spinlock nesnelerinin çok işlemcili ya da çekirdekli sistemlerde kullanılabileceğini belirtmiştik. Tek işlemcili ya da çekirdekli sistemlerde spinlock ile kritik kod oluşturmak kötü bir tekniktir. Çünkü kilidi başka bir thread alırsa diğer thread CPU'yu meşgul bir döngüde bekleyecektir. Spinlock nesneleri küçük kod blokları için ve özellikle çok işlemcili ya da çok çekirdekli sistemlerde kullanılması gereken senkronizasyon nesneleridir. Spinlock nesnesinin kilidini alan thread'in bloke olmaması gerekir. Aksi takdirde istenmeyen sonuçlar oluşabilir. Özetle spinlock nesnesinin kilidini alan thread şu durumlara dikkat etmelidir: - Thread, kilidi uzun süre kapalı tutmamalıdır. - Thread, kilidi aldıktan sonra bloke olmamalıdır. - Thread, kilidi aldıktan sonra IRQ (donanım kesmeleri) dolayısıyla kontrolü bırakma konusunda dikkatli olmalıdır. Linux'ta bir thread spinlock kilidini almışsa artık quanta süresi dolsa bile thread'ler arası geçiş kapatılmaktadır. Kernel spinlock nesneleri tipik olarak şöyle kullanılmaktadır: 1) Spinlock nesnesi spinlock_t türü ile temsil edilmektedir. Spinlock nesnesini aşağıdaki gibi tanımlayabilirsiniz: static spinlock_t g_spinlock; Yeni çekirdeklerde DEFINE_SPINLOCK makrosu spinlock nesnesini kapalı olarak oluşturmakta kullanılabilmektedir. Örneğin: #include static DEFINE_SPINLOCK(g_spinlock); spinlock_t nesnesine ilkdeğer verme işlemi spin_lock_init fonksiyonuyla da yapılabilmektedir. spin_lock_init fonksiyonu spinlock_t nesnesine açık olacak biçimde (unlocked) ilkdeğerlerini vermektedir: #include void spin_lock_init(spinlock_t *lock); 2) Kritik koda giriş için aşağıdaki fonksiyonlar kullanılmaktadır: #include void spin_lock(spinlock_t *lock); void spin_lock_irq(spinlock_t *lock); void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); void spin_lock_bh(spinlock_t *lock); "spin_lock" fonksiyonu klasik spin yapan fonksiyondur. "spin_lock_irq" fonksiyonu o anda çalışılan işlemci ya da çekirdekteki IRQ'ları (yani donanım kesmelerini) kapatarak kilidi almaktadır. Yani biz bu fonksiyonla kilidi almışsak kilidi bırakana kadar donanım kesmeleri oluşmayacaktır. "spin_lock_irqsave" fonksiyonu kritik koda girerken donanım kesmelerini kapatmakla birlikte önceki bir durumu geri yükleme yeteneğine sahiptir. Aslında bu fonksiyonların bazıları makro olarak yazılmıştır. Örneğin "spin_lock_irqsave" aslında bir makrodur. Biz bu fonksiyonun ikinci parametresine nesne adresini geçmemiş olsak da bu bir makro olduğu için aslında ikinci parametrede verdiğimiz nesnenin içerisine IRQ durumlarını yazmaktadır. "spin_lock_bh" fonksiyonu yalnızca yazılım kesmelerini kapatmaktadır. 3) Kilidin geri bırakılması için ise aşağıdaki fonksiyonlar kullanılmaktadır: #include void spin_unlock(spinlock_t *lock); void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags); void spin_unlock_irq(spinlock_t *lock); void spin_unlock_bh(spinlock_t *lock); Yukarıdaki lock fonksiyonlarının hepsinin bir unlock karşılığının olduğunu görüyorsunuz. Biz kilidi hangi lock fonksiyonu ile almışsa o unlock fonksiyonu ile bırakmalıyız. Örneğin: spin_lock(&g_spinlock); ... ... ... spin_unlock(&g_spinlock); Ya da örneğin: ... unsigned long irqstate; ... spin_lock_irqsave(&g_spinlock, irqstate); ... ... ... spin_unlock_irqrestore(&g_spinlock, irqstate); Yine kernel spinlock nesnelerinde de try'lı lock fonksiyonları bulunmaktadır: #include int spin_trylock(spinlock_t *lock); int spin_trylock_bh(spinlock_t *lock); Bu fonksiyonlar eğer spinlock kilitliyse spin yapmazlar ve 0 ile geri dönerler. Eğer kilidi alırlarsa sıfır dışı bir değerle geri dönerler. Her ne kadar yukarıdaki boru sürücüsündeki read ve write fonksiyonlarında kuyruğu korumak için spinlock kullanımı uygun değilse de biz yine kullanım biçimini göstermek için aşağıdaki örneği veriyoruz. ----------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.c */ #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; spinlock_t g_spinlock; static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } spin_lock_init(&g_spinlock); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; spin_lock(&g_spinlock); esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) { spin_unlock(&g_spinlock); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) { spin_unlock(&g_spinlock); return -EFAULT; } g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; spin_unlock(&g_spinlock); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; spin_lock(&g_spinlock); esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) { spin_unlock(&g_spinlock); return -EFAULT; } if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) { spin_unlock(&g_spinlock); return -EFAULT; } g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; spin_unlock(&g_spinlock); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Biz daha önce user mode'da reader/writer lock nesnelerini görmüştük. Bu nesneler birden fazla thread'in kritik koda okuma amaçlı girmesine izin veriyordu. Ancak bir thread kritik koda yazma amaçlı girmişse diğer bir thread'in okuma ya da yazma amaçlı kritik koda girmesine izin vermiyordu. İşte user mode'daki reader/write lock nesnelerinin bir benzeri kernel mode'da reader/writer spinlock nesneleri biçiminde bulunmaktadır. Yine kernel mode'da da kritik koda okuma amaçlı ya da yazma amaçlı giren fonksiyonlar vardır. reader/writer spinlock nesneleri rwlock_t türüyle temsil edilmektedir. Bunların yaratılması rwlock_init fonksiyonuyla yapılmaktadır: #include void rwlock_init(rwlock_t *lock); reader/writer spinlock nesneleri ile ilgili diğer çekirdek fonksiyonları şunlardır: #include void read_lock(rwlock_t *lock); void read_lock_irqsave(rwlock_t *lock, unsigned long flags); void read_lock_irq(rwlock_t *lock); void read_lock_bh(rwlock_t *lock); void read_unlock(rwlock_t *lock); void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void read_unlock_irq(rwlock_t *lock); void read_unlock_bh(rwlock_t *lock); void write_lock(rwlock_t *lock); void write_lock_irqsave(rwlock_t *lock, unsigned long flags); void write_lock_irq(rwlock_t *lock); void write_lock_bh(rwlock_t *lock); int write_trylock(rwlock_t *lock); void write_unlock(rwlock_t *lock); void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void write_unlock_irq(rwlock_t *lock); void write_unlock_bh(rwlock_t *lock); Nesne read amaçlı lock edilmişse read amaçlı unlock işlemi, write amaçlı lock edilmişse write amaçlı unlock işlemi uygulanmalıdır. Fonksiyonların diğer işlevleri normal spinlock nesnelerinde olduğu gibidir. ----------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- User mode'daki senkronizasyon nesnelerinin benzerlerinin çekirdek içerisinde de bulunduğunu görüyorsunuz. Ancak user mode'daki her senkronizasyon nesnesinin bir kernel mod karşılığı yoktur. Örneğin user mode'daki "koşul değişkenlerinin (condition variables)" bir kernel mod karşılığı bulunmamaktadır. Ayrıca burada ele almadığımız (belki ileride ele alacağımız) yalnızca çekirdek içerisinde kullanılan birkaç senkronizasyon nesnesi daha bulunmaktadır. ----------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Tıpkı user mode'da olduğu gibi aygıt sürücülerde de basit atama, artırma, eksiltme gibi işlemlerin atomic yapılmasını sağlayan özel fonksiyonlar vardır. Aslında bu işlemler thread'ler konusunda görmüş olduğumuz gcc'nin built-in atomic fonksiyonlarıyla yapılabilir. Ancak çekirdek içerisindeki fonksiyonların kullanılması uyum bakımından daha uygundur. Bu fonksiyonların hepsi nesneyi atomic_t türü biçiminde istemektedir. Bu aslında içerisinde yalnızca int bir nesne olan bir yapı türüdür. Bu yapı nesnesinin içerisindeki değeri alan atomic_read isimli bir fonksiyon da vardır. Atomic fonksiyonların bazıları şunlardır: #include int atomic_read(const atomic_t *v); void atomic_set(atomic_t *v, int i); void atomic_add(int i, atomic_t *v); void atomic_sub(int i, atomic_t *v); void atomic_inc(atomic_t *v); void atomic_dec(atomic_t *v) ... Bu fonksiyonların hepsinin atomic_t türünden nesnenin adresini alan bir parametresi vardır. atomic_set fonksiyonunun ikinci parametresi set edilecek değeri almaktadır. Yukarıda da belirttiğimiz gibi atomic_t türü aslında int bir elemana sahip bir yapı biçimindedir. atomic_t türünden bir değişkene ilkdeğer vermek için ATOMIC_INIT makrosu da kullanılabilir. Örneğin: atomic_t g_count = ATOMIC_INIT(0); Yukarıda da belirttiğimiz gibi atomic_t nesnesi içerisindeki değeri atomic_read makrosuyla elde edebiliriz. Örneğin: val = atomic_read(&g_count); Bit işlemlerine yönelik atomik işlemler de yapılabilmektedir: void set_bit(nr, void *addr); void clear_bit(nr, void *addr); void change_bit(nr, void *addr); test_bit(nr, void *addr); int test_and_set_bit(nr, void *addr); int test_and_clear_bit(nr, void *addr); int test_and_change_bit(nr, void *addr); ----------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 139. Ders 05/05/2024 - Pazar ----------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz user mode'da çeşitli fonksiyonların çeşitli koşullar altında blokeye yol açtığını belirtmiştik. Bir thread bloke olduğunda thread geçici süre (belli bir koşul sağlanana kadar) ilgili CPU'nun "çalışma kuyruğundan (run queue)" çıkartılır, ismine "bekleme kuyruğu (wait queue)" denilen bir kuyruğa yerleştirilir. Blokeye yol açan koşul ortadan kalktığında ise thread yeniden bekleme kuyruğundan alınarak CPU'nun çalışma kuyruğuna yerleştirilir. Biz şimdiye kadar user mode'da hep sistem fonksiyonları yoluyla blokelerin oluştuğunu gördük. Ancak kernel mode'daki aygıt sürücülerde blokeyi aygıt sürücünün kendisi oluşturmaktadır. Örneğin biz boru aygıt sürücümüzde read işlemi yapıldığında eğer boruda okunacak hiç bilgi yoksa read işlemini yapan user mode'daki thread'i bloke edebiliriz. Boruya bilgi geldiğinde de thread'i yeniden çalışma kuyruğuna yerleştirip blokeyi çözebiliriz. İşte bu bölümde aygıt sürücüde thread'lerin nasıl bloke edileceği ve blokenin nasıl çözüleceği üzerinde duracağız. Mevcut Linux sistemlerinde her CPU ya da çekirdeğin ayrı bir çalışma kuyruğu (run queue) bulunmaktadır. Ancak bir ara O(1) çizelgelemesi ismiyle Linux'ta bu konuda bir değişikliğe gidilmişti. O(1) çizelgelemesi tekniğinde toplam tek bir çalışma kuyruğu bulunuyordu. Hangi CPU ya da çekirdeğe atama yapılacaksa bu tek olan çalışma kuyruğundan thread alınıyordu. O(1) çizelgelemesi Linux'ta kısa bir süre kullanılmıştır. Bunun yerine "CFS (Completely Fair Scheduling)" çizelgeleme sistemine geçilmiştir. Çalışmakta olan bir thread'in bloke olması sırasında thread'in yerleştirileceği tek bir "bekleme kuyruğu (wait queue)" yoktur. Her CPU ya da çekirdek için de ayrı bir bekleme kuyruğu bulundurulmamaktadır. Bekleme kuyrukları ilgili olay temelinde oluşturulmaktadır. Örneğin sleep fonksiyonu dolayısıyla bloke olan thread'ler ayrı bir bekleme kuyruğuna, boru dolayısıyla bloke olan thread'ler ayrı bir bekleme kuyruğuna yerleştirilmektedir. Aygıt sürücüleri yazanlar da kendi olayları için kendi bekleme kuyruklarını yaratmaktadır. Tabii kernel'daki mutex ve semaphore fonksiyonları da aslında kendi içerisinde bir bekleme kuyruğu kullanmaktadır. Çünkü bu fonksiyonlar da blokeye yol açmaktadır. ----------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 140. Ders 10/05/2024 - Cuma ----------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi her aygıt sürücü kendi bloke olayları için kendinin kullanacağı bekleme kuyrukları yaratabilmektedir. Çekirdek içerisinde bekleme kuyruklarını yaratan ve yok eden çekirdek fonksiyonları bulunmaktadır. Yine çekirdek içerisinde bir thread'i çalışma kuyruğundan çıkartıp bekleme kuyruğuna yerleştiren, bekleme kuyruğundan çıkartıp çalışma kuyruğuna yerleştiren fonksiyonlar bulunmaktadır. Linux'ta bekleme kuyrukları wait_queue_head_t isimli bir yapıyla temsil edilmektedir. Bir bekleme kuyruğu DECLARE_WAIT_QUEUE_HEAD(name) makrosuyla oluşturulabilir. Örneğin: #include static DECLARE_WAIT_QUEUE_HEAD(g_wq); Ya da nesne tanımlanıp init_waitqueue_head fonksiyonuyla da ilk değerlenebilir: #include static wait_queue_head_t g_wq; ... init_waitqueue_head(&g_wq); Bir thread'i (yani task_struct nesnesini) çalışma kuyruğundan çıkartıp istenilen bekleme kuyruğuna yerleştirme işlemi wait_event makrolarıyla gerçekleştirilmektedir. Temel wait_event makroları şunlardır: wait_event(wq_head, condition); wait_event_interruptible(wq_head, condition); wait_event_killable(wq_head, condition); wait_event_timeout(wq_head, condition, timeout); wait_event_interruptible_timeout(wq_head, condition, timeout); wait_event_interruptible_exclusive(wq_head, condition); wait_event makrosu thread'i "uninterruptible" biçimde bekleme kuyruğuna yerleştirir. Bu biçimde bloke olmuş thread'lerin blokeleri sinyal dolayısıyla çözülememektedir. wait_event_interruptible makrosu ise aynı işlemi "interruptible" olarak yapmaktadır. Yani sinyal geldiğinde thread bekleme kuyruğundan uyandırılır. wait_event_killable makrosu yalnızca SIGKILL sinyali için thread'i uyandırmaktadır. Yani bu biçimde bekleme kuyruğuna yerleştirilmiş bir thread'in blokesi sinyal geldiğinde çözülmez, ancak SIGKILL sinyali ile thread yok edilebilir. wait_event_timeout ve wait_event_interruptible_timeout makrolarının wait_event makrolarından farkı thread'i en kötü olasılıkla belli bir jiffy zaman aşımı ile uyandırabilmesidir. Jiffy kavramı izleyen bölümlerde ele alınacaktır. Makrolardaki "condition (koşul)" parametresi bool bir ifade biçiminde oluşturulmalıdır. Bu ifade ya sıfır olur ya da sıfır dışı bir değer olur. Bu koşul ifadesi "uyanık kalmak için bir koşul" belirtmektedir. Yani bu koşul uyandırma koşulu değildir, uyanık kalma koşuludur. Çünkü bu makrolarda koşula bakılması uyumadan önce ve uyandırılma işleminden sonra yapılmaktadır. Yani önce koşula bakılır. Koşul sağlanmıyorsa thread uyutulur. Thread uyandırıldığında yeniden koşula bakılır. Koşul sağlanmıyorsa yeniden uyutulur. Dolayısıyla uyanma işlemi çekirdek kodlarında tıpkı koşul değişkenlerinde (condition variable) olduğu gibi döngü içerisinde yapılmaktadır. Örneğin: DECLARE_WAIT_QUEUE_HEAD(g_wq); int g_flag = 0; ... wait_event(g_wq, g_flag != 0); Burada koşul g_flag != 0 biçimindedir. wait_event makroları fonksiyon değil makro biçiminde yazıldığı için bu koşul bu haliyle makronun içinde kullanılmaktadır. (Yani ifadenin sonucu değil, kendisi makroda kullanılmaktadır.) Makronun içerisinde önce koşula bakılmakta, bu koşul sağlanıyorsa thread zaten uyutulmamaktadır. Eğer koşul sağlanmıyorsa thread uyutulmaktadır. Thread uykudan uyandırıldığında tıpkı koşul değişkenlerinde olduğu gibi yeniden koşula bakılmakta eğer koşul sağlanmıyorsa thread yeniden uyutulmaktadır. wait_event_interruptible makrosunun wait_event makrosundan farkı eğer thread uyutulmuşsa uykudan bir sinyalle uyandırılabilmesidir. Halbuki wait_event ile uykuya dalmış olan thread sinyal oluşsa bile uykudan uyandırılmamaktadır. wait_event_killable ile thread uykuya dalındığında ise yalnızca SIGKILL sinyali ile thread uykudan uyandırılabilmektedir. Tabii programcı wait_event_interruptible makrosunun geri dönüş değerine bakmalı, eğer thread sinyal dolayısıyla uykudan uyandırılmışsa -ERESTARTSYS değeriyle kendi fonksiyonundan geri dönmelidir. wait_event_interruptible makrosu eğer sinyal dolayısıyla uyanmışsa -ERESTARTSYS değeri ile, koşul sağlandığından dolayı uyanmışsa 0 değeri ile geri dönmektedir. Örneğin: DECLARE_WAIT_QUEUE_HEAD(g_wq); int g_flag = 0; ... if (wait_event_interruptible(g_wq, g_flag != 0) != 0) return -ERESTARTSYS; Bu tür durumlarda böylesi flag değişkenlerini atomic almak iyi bir tekniktir. Örneğin: DECLARE_WAIT_QUEUE_HEAD(g_wq); atomic_t g_flag = ATOMIC_INIT(0); ... if (wait_event_interruptible(g_wq, atomic_read(&g_flag) != 0) != 0) return -ERESTARTSYS; wait_event_interruptible_exclusive (bunun interrutible olmayan biçimi yoktur) makrosu Linux çekirdeklerine 2.6'ının belli sürümünden sonra sokulmuştur. Yine bu makroyla birlikte aşağıda ele alınan wake_up_xxx_nr makroları da eklenmiştir. Bir prosesin exclusive olarak wait kuyruğuna yerleştirilmesi onlardan belli sayıda olanların uyandırılabilmesini sağlamaktadır. Tabii wait_event makroları o andaki thread'i çizelgeden (yani run kuyruğundan) çıkartıp wait kuyruğuna yerleştirdikten sonra "context switch" işlemini de yapmaktadır. Context switch işlemi sonrasında artık run kuyruğundaki yeni bir thread çalışır. wait_event makrolarının temsili kodunu şöyle düşünebilirsiniz: while (koşul_sağlanmadığı_sürece) { } Bekleme kuyruğunda blokede bekletilen thread wake_up makrolarıyla uyandırılmaktadır. Uyandırılmaktan kastedilen şey thread'in bekleme kuyruğundan çıkartılıp yeniden çalışma kuyruğuna (run queue) yerleştirilmesidir. wait_event makrolarındaki koşula wake_up bakmamaktadır. wake_up makroları yalnızca thread'i bekleme kuyruklarından çalışma kuyruğuna taşımaktadır. Koşula uyandırılmış thread'in kendisi bakmaktadır. Eğer koşul sağlanmıyorsa thread yeniden uyutulmaktadır. Yani biz koşulu sağlanır duruma getirmeden wake_up işlemi yaparsak thread yeniden uykuya dalacaktır. (Zaten "koşulu sağlayan thread'i uyandırma" işlemi mümkün değildir.) En çok kullanılan wake_up makroları şunlardır: wake_up(wq_head); wake_up_nr(wq_head, nr); wake_up_all(wq_head); wake_up_interruptible(wq_head); wake_up_interruptible_nr(wq_head, nr); wake_up_interruptible_all(wq_head); Bu makroların çalışmasının anlaşılması için bekleme kuyrukları hakkında biraz ayrıntıya girmek gerekir. Bekleme kuyruğunu temsil eden wait_queue_head_t yapısı şöyle bildirilmiştir: struct wait_queue_head { spinlock_t lock; struct list_head head; }; typedef struct wait_queue_head wait_queue_head_t; Görüldüğü gibi bu bir bağlı listedir. Bağlı liste spinlock ile korunmaktadır. Bu bağlı listenin düğümleri wait_queue_entry yapılarından oluşmaktadır. struct wait_queue_entry { unsigned int flags; void *private; wait_queue_func_t func; struct list_head entry; }; Bu yapının ayrıntısına girmeyeceğiz. Ancak yapıdaki flags elemanına dikkat ediniz. Bekleme kuyruğuna yerleştirilen bir thread'in exclusive bekleme yapıp yapmadığı (yani wait_event_intrerruptible_exclusive ile bekleme yapıp yapmadığı) bu flags elemanında saklanmaktadır. Bu wait kuyruğunun bekleyen thread'leri (onların task_struct adreslerini) tutan bir bağlı liste olduğunu varsayabilirsiniz. (Yapının private elemanı thread'leri temsil eden task_struct yapı nesnelerinin adreslerini tutmaktadır.) Yani bekleme kuyrukları aşağıdaki gibi düşünülebilir: T1 ---> T2 ---> T3 ---> T4 ---> T5 ---> T6 ---> T7 ---> T8 ---> NULL Bu thread'lerden bazıları exclusive bekleme yapmış olabilir. Bunları (E) ile belirtelim: T1 ---> T2 ---> T3 ---> T4(E) ---> T5 ---> T6(E) ---> T7 ---> T8(E) ---> NULL wake_up makrosu kuyruğun başından itibaren ilk exclusive bekleme yapan thread'e kadar bu thread de dahil olmak üzere tüm thread'leri uyandırmaktadır. Tabii bu thread'lerin hepsi uyandırıldıktan sonra ayrıca koşula da bakmaktadır. Örneğimizde wake_up makrosu çağrıldığında T1, T2, T3 ve T4 thread'leri uyandırılacaktır. Görüldüğü gibi wake_up makrosu aslında 1 tane exclusive thread uyandırmaya çalışmaktadır. Ancak onu uyandırırken kuyruğun önündeki exclusive olmayanları da uyandırmaktadır. Tabii bu anlatımdan anlaşılacağı gibi wake_up makrosu eğer kuyrukta hiç exclusive bekleme yapan therad yoksa thread'lerin hepsini uyandırmaktadır. wake_up_nr makrosu, wake_up makrosu gibi davranır ancak 1 tane değil en fazla nr parametresiyle belirtilen sayıda exclusive thread'i uyandırmaya çalışır. Başka bir deyişle wake_up(g_wq) çağrısı ile wake_up_nr(g_qw, 1) çağrısı aynı anlamdadır. Eğer yukarıdaki örnekte wake_up_nr(g_wq, 2) çağrısını yapmış olsaydık T1, T2, T2, T4, T5, T6 thread'leri uyandırılırdı. Tabii bu thread'lerin uyandırılmış olması wait_event makrolarından çıkılacağı anlamına gelmemektedir. Uyandırma işleminden sonra koşula yeniden bakılmaktadır. wake_up_all makrosu bekleme kuyruğundaki tüm exclusive thread'leri ve exclusive olmayan thread'leri yani kısaca tüm thread'leri uyandırmaktadır. Tabii yine uyanan thread'ler koşula bakmaktadır. wake_up_interruptible, wake_up_interruptible_nr ve wake_up_interruptible_all makroları interruptible olmayan makrolar gibi çalışmaktadır. Ancak bu makrolar bekleme kuyruğunda yalnızca "interruptible" wait_event fonksiyonlarıyla bekletilmiş thread'lerle ilgilenmektedir. Diğer thread'ler kuyrukta yokmuş gibi davranmaktadır. wake_up makroları birden fazla thread'i uyandırabildiğine göre uyanan thread'lerin yeniden uykuya dalması gerekebilir. Bu durumda tıpkı user moddaki koşul değişkenlerinde yaptığımız gibi bir kalıbı kullanabilirsiniz: if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; while (koşul_sağlanmadığı_sürece) { mutex_unlock(&g_mutex); if (wait_event_interruptible(g_wq, uyanık_kalma_koşulu) != 0) { mutex_unlock(&g_mutex); return -ERESTARTSYS; } mutex_lock(&g_mutex); } /* Kritik kod */ mutex_unlock(&g_mutex); Burada birden fazla thread uyandırıldığında bunlardan biri mutex kilidini alarak ve kritik koda girmektedir. Eğer kritik kod içerisinde koşul sağlanmaz hale getirilirse bu durumda diğer thread'ler while döngüsü nedeniyle yeniden uyguya dalacaktır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aygıt sürücümüzün read ve write fonksiyonları aşağıdaki gibi olsun: wait_queue_head_t g_wq; atomic_t g_flag; ... static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver read...\n"); atomic_set(&g_flag, 0); if (wait_event_interruptible(g_wq, atomic_read(&g_flag) != 0) != 0) { printk(KERN_INFO "Signal occured..."); return -ERESTARTSYS; } return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver write...\n"); atomic_set(&g_flag, 1); wake_up_interruptible(&g_wq); return 0; } Burada eğer birden fazla thread read yaparsa exclusive olmayan bir biçimde bekleme kuyruğunda bekleyecektir. write işleminde wake_up_interruptible makrosu ile uyandırma yapıldığına dikkat ediniz. Bekleme kuyruğunda exclusive bekleyen thread olmadığına göre burada tüm read yapan thread'ler uyandırılacaktır. Onların koşulları sağlandığı için hepsi read fonksiyonundan çıkacaktır. Şimdi bu read fonksiyonunda exclusive bekleme yapmış olalım: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver read...\n"); atomic_set(&g_flag, 0); if (wait_event_interruptible_exclusive(g_wq, atomic_read(&g_flag) != 0) != 0) { printk(KERN_INFO "Signal occured..."); return -ERESTARTSYS; } return 0; } Artık write fonksiyonunda wake_up makrosu çağrıldığında yalnızca bir tane exclusive bekleme yapan thread uyandırılacağı için read fonksiyonundan yalnızca bir thread çıkacaktır. Test için aşağıdaki kodları kullanabilirsiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* wait-driver.c */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Wait-Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wq; static atomic_t g_flag; static int __init generic_init(void) { int result; printk(KERN_INFO "wait-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "wait-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } init_waitqueue_head(&g_wq); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "wait-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "wait-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "wait-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver read...\n"); atomic_set(&g_flag, 0); if (wait_event_interruptible_exclusive(g_wq, atomic_read(&g_flag) != 0) != 0) { printk(KERN_INFO "Signal occured..."); return -ERESTARTSYS; } return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver write...\n"); atomic_set(&g_flag, 1); wake_up_interruptible(&g_wq); return 0; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* wait-test-read.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[32]; ssize_t result; if ((fd = open("wait-driver", O_RDONLY)) == -1) exit_sys("open"); printf("reading begins...\n"); if ((result = read(fd, buf, 32)) == -1) exit_sys("result"); printf("Ok\n"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* wait-test-write.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[32] = {0}; if ((fd = open("wait-driver", O_WRONLY)) == -1) exit_sys("open"); if (write(fd, buf, 32) == -1) exit_sys("write"); printf("Ok\n"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 141. Ders 12/05/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Burada bir noktaya dikkatinizi çekmek istiyoruz. Daha önce görmüş olduğumuz mutex, semaphore, read/write kilitleri gibi senkronizasyon nesnelerinin kendilerinin oluşturduğu bekleme kuyrukları vardır. Bu senkronizasyon nesneleri bloke oluşturmak için kendi bekleme kuyruklarını kullanmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de daha önce yapmış olduğumuz boru örneğimizi gerçek bir boru haline getirelim. Yani eğer boruda en az 1 byte boş alan kalmadıysa read fonksiyonu blokede en az 1 byte okuyana kadar beklesin. Eğer boruda tüm bilgileri yazacak kadar boş yer kalmadıysa bu kez de yazan taraf blokede beklesin. Burada izlenecek temel yöntem aslında kursumuzda "koşul değişkenleri (condition variable)" denilen senkronizasyon nesnelerindeki yöntemin aynısı olmalıdır. Okuyan thread kuyruktaki byte sayısını belirten g_count == 0 olduğu sürece bekleme kuyruğunda beklemelidir. Tabii bizim kuyruk üzerinde işlem yaptığımız kısımları senkronize etmemiz gerekir. Bunu da bir binary semaphore nesnesi ya da mutex nesnesi yapabiliriz. Semaphore nesnesini ve bekleme kuyruğunu aşağıdaki gibi yaratabiliriz: static wait_queue_head_t g_wq; DEFINE_SEMAPHORE(g_sem); Okuyan taraf önce semaphore kilidini eline almalı ancak eğer uykuya dalacaksa onu serbest bırakıp uykuya dalmalıdır. Kuyruk üzerinde aynı anda işlemler yapılabileceği için tüm işlemlerin kritik kod içerisinde yapılması uygun olur. O halde read işleminin tipik çatısı şöyle olmalıdır: ... if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } // kuyruktan okuma işlemleri up(&g_sem); Burada önce down_interruptible fonksiyonu ile semaphore kilitlenmeye çalışılmıştır. Eğer semaphore zaten kilitliyse semaphore'un kendi bekleme kuyruğunda thread uykuya dalacaktır. Daha sonra g_count değerine bakılmıştır. Eğer g_count değeri 0 ise önce semaphore serbest bırakılıp sonra thread bekleme kuyruğunda uyutulmuştur. Thread bekleme kuyruğundan uyandırıldığında yeniden semaphore kontrolünü ele almaktadır. Tabii eğer birden fazla thread bekleme kuyruğundan uyandırılırsa yalnızca bunlardan biri semaphore kontrolünü ele alacaktır. Tabii bundan sonra kuyruktan bilgiler okunacak ve semaphore kilidi serbest bırakılacaktır. Eğer birden fazla thread bekleme kuyruğundan uyanmışsa bu kez diğer bir thread semaphore kontrolünü ele alacak ve g_count değerine bakacaktır. Yukarıda da belirttiğimiz gibi aslında bu bir "koşul değişkeni" kodu gibidir. Çekirdek içerisinde böyle bir nesne olmadığı için manuel uygulanmıştır. Benzer biçimde write işleminin de çatısı aşağıdaki gibidir: if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } // kuyruğa yazma işlemleri up(&g_sem); Burada benzer işlemler uygulanmıştır. Eğer kuyrukta yazma yapılmak istenen kadar boş alan varsa akış while döngüsünün içerisine girmeyecektir. (Buradaki while koşulunun "PIPE_BUFSIZE - g_count < size" biçiminde olduğuna dikkat ediniz.) Dolayısıyla yazma işlemi kritik kod içerisinde yapılabilecektir. Ancak kuyrukta yeteri kadar yer yoksa semaphore kilidi serbest bırakılıp thread bekleme kuyruğunda bekletilecektir. Çıkışta benzer işlemler yapılmaktadır. Aslında burada spinlock nesneleri de kullanılabilir. Ancak zaten mutex, semaphore ve read/write lock nesneleri kendi içerisinde bir miktar spin yapmaktadır. Bu örnekte semaphore yerine spinlock kullanabilir miyiz? Spinlock için şu durumları gözden geçirmelisiniz: - Spinlock nesnesinde bekleme CPU zamanı harcanarak meşgul bir döngü içerisinde yapılmaktadır. Dolayısıyla spinlock nesneleri kilidin kısa süreli bırakılacağından emin olunabiliyorsa kullanılmalıdır. - Spinlock içerisinde sinyal işlemleri bekletilmektedir. Yani spinlock beklemelerinin "interruptible" bir biçimi yoktur. Aslında bu uygulamada spinlock nesneleri de kullanılabilir. Ancak yine de kilitli kalınan kod miktarı dikkate alındığında semaphore nesnesi daha uygun bir seçenektir. Burada yazma işlemleri için "yazma bekleme kuyruğu" ve okuma işlemleri için "okuma bekleme kuyruğu" biçiminde iki bekleme kuyruğu olduğuna dikkat ediniz. Çünkü yazan taraf okuma bekleme kuyruğundaki thread'leri okuyan taraf ise yazma bekleme kuyruğundaki thread'leri uyandırmak isteyecektir. ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.c */ #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) { up(&g_sem); return -EFAULT; } g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); /* eski g_count değeri sıfır ise biçiminde bir koşul altında da yapılabilir */ return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size > PIPE_BUFSIZE) size = PIPE_BUFSIZE; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) { up(&g_sem); return -EFAULT; } g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len; if ((fd = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 142. Ders 24/05/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında bekleme kuyrukları wait_queue_entry isimli yapı nesnelerinden oluşan bir çift bağlı listedir. wait_queue_head_t yapısı da bağlı listenin ilk ve son elemanlarının adresini tutmaktadır: wait_queue_head <-----> wait_queue_entry <-----> wait_queue_entry <-----> wait_queue_entry <-----> wait_queue_entry ... Çekirdek kodlarında bu yapılar "include/linux/wait.h" dosyası içerisinde aşağıdaki gibi bildirilmiştir: struct wait_queue_head { spinlock_t lock; struct list_head head; }; struct wait_queue_entry { unsigned int flags; void *private; wait_queue_func_t func; struct list_head entry; }; ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz aygıt sürücü kodumuzda o anda quanta süresini bırakıp çizelgeleyicinin kendi algortimasına göre sıradaki thread'i çizelgelemesini sağlayabiliriz. Bunun için schedule isimli fonksiyon kullanılmaktadır. Bu fonksiyon bloke oluşturmamaktadır. Yalnızca thread'ler arası geçiş (context switch) oluşturmaktadır. Fonksiyon herhangi bir parametre almamaktadır: #include void schedule(void); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında wait_event fonksiyonları export edilmiş birkaç fonksiyon çağrılarak yazılmıştır. Dolayısıyla wait_event fonksiyonlarını çağırmak yerine programcı daha aşağı seviyeli (zaten wait_event fonksiyonlarının çağırmış olduğu) fonksiyonları çağırabilir. Yani bu işlemi daha aşağı seviyede manuel de yapabilir. Prosesin manuel olarak wait kuyruğuna alınması prepare_to_wait ve prepare_to_wait_exclusive isimli fonksiyonlar tarafından yapılmaktadır: #include void prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state); void prepare_to_wait_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state); Bu fonksiyonların birinci parametreleri bekleme kuyruğu nesnesinin adresini almaktadır. İkinci parametreleri bu kuyruğa yerleştirilecek wait_queue_entry nesnesinin adresini almaktadır. Fonksiyonların üçüncü parametreleri TASK_UNINTERRUPTIBLE ya da TASK_INTERRUPTIBLE biçiminde geçilebilir. Bir wait_queue_entry nesnesi şöyle oluşturulabilir: DEFINE_WAIT(wqentry); Ya da açıkça tanımlanıp init_wait makrosuyla ilk değerlenebilir. Örneğin: struct wait_queue_entry wqentry; ... init_wait(&wqentry); DEFINE_WAIT makrosu global tanımlamalarda kullanılamamaktadır. Çünkü bu makro küme parantezleri içerisinde sabit ifadesi olmayan ifadeler barındırmaktadır. Ancak makro yerel tanımlamalarda kullanılabilir. Dolayısıyla prepare_to_wait ve prepare_to_wait_exclusive fonksiyonları da aslında bekleme kuyruğuna bir wait_queue_entry nesnesi eklemektedir. Yani programcının bunun için yeni bir wait_queue_entry nesnesi oluşturması gerekmektedir. prepare_to_wait_exclusive exclusive uyuma için kullanılmaktadır. prepare_to_wait ve prepare_to_wait_exclusive fonksiyonları şunları yapmaktadır: 1) Thread'i çalışma kuyruğundan çıkartıp bekleme kuyruğuna yerleştirir. (Çalışma kuyruğunun organizasyonu ve bu işlemin gerçek ayrıntıları biraz karmaşıktır.) 2) Thread'in durum bilgisini (task state) state parametresiyle belirtilen duruma çeker. 3) prepare_to_wait fonksiyonu kuyruk elemanını exclusive olmaktan çıkartırken, prepare_to_wait_exclusive onu exclusive yapar. Thread'in çalışma kuyruğundan bekleme kuyruğuna aktarılması onun uykuya dalması anlamına gelmemektedir. Programcı artık thread çalışma kuyruğunda olmadığına göre schedule fonksiyonu ile thread'ler arası geçiş (context switch) uygulamalı ve akış kontrolünü başka bir thread'e bırakmalıdır. Zaten thread'in çalışma kuyruğundan çıkartılması artık yeniden çalışma kuyruğuna alınmadıktan sonra uykuda bekletilmesi anlamına gelmektedir. Tabii biz prepare_to_wait ya da prepare_to_wait_exclusive fonksiyonlarını çağırdıktan sonra bir biçimde koşul durumuna bakmalıyız. Eğer koşul sağlanmışsa hiç prosesi uykuya daldırmadan hemen bekleme kuyruğundan çıkarmalıyız. Eğer koşul sağlanmamışsa gerçekten artık schedule fonksiyonuyla "thread'ler arası geçiş" yapmalıyız. Thread'imiz schedule fonksiyonunu çağırdıktan sonra artık uyandırılana kadar bir daha çizelgelenmeyecektir. Bu da bizim uykuya dalmamız anlamına gelmektedir. Pekiyi thread'imiz uyandırıldığında nereden çalışmaya devam edecektir? İşte schedule fonksiyonu thread'ler arası geçiş yaparken kalınan yeri thread'e ilişkin task_struct yapısının içerisine kaydetmektedir. Kalınan yer schedule fonksiyonunun içerisinde bir yerdir. O halde thread'imiz uyandırıldığında schedule fonksiyonunun içerisinden çalışma devam eder. schedule fonksiyonu geri dönecek ve thread akışı devam edecektir. wake_up fonksiyonları thread'i bekleme kuyruklarından çıkartıp çalışma kuyruğuna eklemektedir. Ancak prepare_to_wait ve prepare_to_wait_exclusive fonksiyonları çağrıldıktan sonra eğer koşulun zaten sağlandığı görülürse bu durumda uyandırma wake_up fonksiyonlarıyla yapılmadığı için bekleme kuyruğundan thread'in geri çıkartılması da programcının sorumluluğundadır. Bu işlem finish_wait fonksiyonu ile yapılmaktadır. #include void finish_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry); Bu fonksiyon zaten thread wake_up fonksiyonları tarafından bekleme kuyruğundan çıkartılmışsa herhangi bir işlem yapmamaktadır. Bu durumda manuel uyuma şöyle yapılabilir. DEFINE_WAIT(wqentry); prepare_to_wait(&g_wq, &wqentry, TASK_UNINTERRUPTIBLE); if (!condition) schedule(); finish_wait(&wqentry); Tabii eğer thread INTERRUPTIBLE olarak uyuyorsa schedule fonksiyonundan çıkıldığında sinyal dolayısıyla da çıkılmış olabilir. Bunu anlamak için signal_pending isimli fonksiyon çağrılır. Bu fonksiyon sıfır dışı bir değerle geri dönmüşse uyandırma işleminin sinyal yoluyla yapıldığı anlaşılır. Bu durumda tabii aygıt sürücüdeki fonksiyon -ERESTARTSYS ile geri döndürülmelidir. signal_pending fonksiyonunun prototipi şöyledir: #include int signal_pending(struct task_struct *p); Fonksiyon parametre olarak thread'e ilişkin task_struct yapısının adresini parametre olarak almaktadır. Bu durumda INTERRUPTIBLE uyuma aşağıdaki gibi yapılabilir: DEFINE_WAIT(wqentry); prepare_to_wait(&g_wq, &wqentry, TASK_INTERRUPTIBLE); if (!condition) schedule(); if (signal_pending(current)) return -ERESTARTSYS; finish_wait(&wqentry); wake_up makrolarının şunları yaptığını anımsayınız: 1) Wait kuyruğundaki prosesleri çıkartarak run kuyruğuna yerleştirir. 2) Prosesin durumunu TASK_RUNNING haline getirir. Aşağıdaki boru örneğinde manuel uykuya dalma işlemi uygulanmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver-manual-wait.c */ #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver-manual-wait module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver-manual-wait")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver-manual-wait module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-manual-wait opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-manual-wait closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; DEFINE_WAIT(wqentry); if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); prepare_to_wait(&g_wqread, &wqentry, TASK_INTERRUPTIBLE); schedule(); if (signal_pending(current)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) { up(&g_sem); return -EFAULT; } g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; DEFINE_WAIT(wqentry); if (size > PIPE_BUFSIZE) size = PIPE_BUFSIZE; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); prepare_to_wait(&g_wqwrite, &wqentry, TASK_INTERRUPTIBLE); schedule(); if (signal_pending(current)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) { up(&g_sem); return -EFAULT; } g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver-manual-wait", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver-manual-wait", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 143. Ders 26/05/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aslında wait_event fonksiyonları yukarıda açıkladığımız daha aşağı seviyeli fonksiyonlar kullanılarak gerçekleştirilmiştir. Mevcut son Linux çekirdeğinde wait_event_interruptible fonksiyonu şöyle yazılmıştır: #define wait_event_interruptible(wq_head, condition) \ ({ \ int __ret = 0; \ might_sleep(); \ if (!(condition)) \ __ret = __wait_event_interruptible(wq_head, condition); \ __ret; \ }) Burada gcc'nin bileşik ifade de denilen bir eklentisi (extension) kullanılmıştır. Bu makro ayrıntılar göz ardı edilirse __wait_event_interruptible makrosunu çağırmaktadır. Bu makro şöyle tanımlanmıştır: #define __wait_event_interruptible(wq_head, condition) \ ___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, \ schedule()) Burada ___wait_event makrosunun interruptible olan ve olmayan kodların ortak makrosu olduğu görülmektedir. Bu makro da şöyle tanımlanmıştır: #define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \ ({ \ __label__ __out; \ struct wait_queue_entry __wq_entry; \ long __ret = ret; /* explicit shadow */ \ \ init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \ for (;;) { \ long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state); \ \ if (condition) \ break; \ \ if (___wait_is_interruptible(state) && __int) { \ __ret = __int; \ goto __out; \ } \ \ cmd; \ } \ finish_wait(&wq_head, &__wq_entry); \ __out: __ret; \ }) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aygıt sürücülerimize arzu edersek "blokesiz (nonbloking)" okuma yazma desteği verebiliriz. Tabii bu detseğin verilebilmesi için aygıt sürücünün okuma yazma sırasında bloke oluşturması gerekmektedir. Anımsanacağı gibi blokesiz işlem yapabilmek için open POSIX fonksiyonunda fonksiyonun ikinci parametresine O_NONBLOCK bayrağının eklenmelidir. Normal disk dosyalarında O_NONBLOCK bayrağının bir anlamı yoktur. Ancak boru gibi özel dosyalarda ve aygıt sürücülerde bu bayrak şu anlama gelmektedir: 1) Okuma sırasında eğer okunacak bir bilgi yoksa read fonksiyonu bloke oluşturmaz, başarısızlıkla geri döner ve errno değeri EAGAIN olarak set edilir. 2) Yazma sırasında yazma eylemi meşguliyet yüzünden yapılamıyorsa write fonksiyonu bloke oluşturmaz, başarısızlıkla geri döner ve errno değeri yine EAGAIN olarak set edilir. Aygıt sürücü açıldığında open fonksiyonunun ikinci parametresi file yapısının (dosya nesnesinin) f_flags elemanına yerleştirilmektedir. Dosya nesnesinin adresinin aygıt sürücüdeki fonksiyonlara filp parametresiyle aktarıldığını anımsayınız. Bu durumda biz aygıt dosyasının blokesiz modda açılıp açılmadığını şöyle test edebiliriz: if (filp->f_flags & O_NONBLOCK) { /* blokesiz modda mı açılmış */ /* open fonksiyonunda aygıt O_NONBLOCK bayrağı ile açılmış */ } Aygıt sürücümüz blokesiz modda işlemlere izin vermiyorsa biz bu durumu kontrol etmeyebiliriz. Yani böyle bir aygıt sürücüde programcı aygıt sürücüyü O_NONBLOCK bayrağını kullanarak açmışsa bu durumu hiç dikkate almayabiliriz. (Örneğin disk dosyalarında blokesiz işlemlerin bir anlamı olmadığı halde Linux çekirdeği disk dosyaları O_NONBLOCK bayrağıyla açıldığında hata ile geri dönmeden bayrağı dikkate almamaktadır.) Eğer bu kontrol yapılmak isteniyorsa aygıt sürücünün açılması sırasında kontrol aygıt sürücünün open fonksiyonunda yapılabilir. Bu durumda open fonksiyonunu -EINVAL değeriyle geri döndürebilirsiniz. Örneğin: static int generic_open(struct inode *inodep, struct file *filp) { if (filp->f_flags & O_NONBLOCK) return -EINVAL; return 0; } Pekiyi boru aygıt sürücümüze nasıl blokesiz mod desteği verebiliriz? Aslında bunun için iki şeyi yapmamız gerekir: 1) Yazma yapıldığı zaman boruda yazılanları alacak kadar yer yoksa aygıt sürücümüzün write fonksiyonunu -EAGAIN değeriyle geri döndürmeliyiz. Örneğin: ... if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } ... 2) Okuma yapıldığı zaman eğer boruda hiç bilgi yoksa aygıt sürücümüzün read fonksiyonunu -EAGAIN değeriyle geri döndürmeliyiz. Örneğin: ... if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } ... read ve write fonksiyonlarının -EAGAIN değeriyle geri döndürülmeden önce aygıt dosyasının blokesiz modda açılıp açılmadığının kontrol edilmesi gerektiğine dikkat ediniz. Aşağıdaki örnekte boru aygıt sürücüsüne blokesiz okuma ve yazma desteği verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.c */ #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) { up(&g_sem); return -EFAULT; } g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size > PIPE_BUFSIZE) size = PIPE_BUFSIZE; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) { up(&g_sem); return -EFAULT; } g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY|O_NONBLOCK)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) if (errno == EAGAIN) { printf("write returns -1 with errno = EAGAIN...\n"); continue; } printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY|O_NONBLOCK)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) if (errno == EAGAIN) { printf("read returns -1 with errno = EAGAIN...\n"); continue; } buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 144. Ders 31/05/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Çekirdek modülleri ve aygıt sürücüler dinamik bellek tahsis etmeye gereksinim duyabilirler. Ancak kernel mod programlar dinamik tahsisatları malloc, calloc ve realloc gibi fonksiyonlarla yapamazlar. Çünkü bu fonksiyonlar user mod programlar tarafından kullanılacak biçimde prosesin bellek alanında tahsisat yapmak için tasarlanmışlardır. Oysa çekirdeğin ayrı bir heap sistemi vardır. Bu nedenle çekirdek modülleri ve aygıt sürücüler çekirdeğin sunduğu fonksiyonlarla çekirdeğin heap alanında tahsisat yapabilirler. Biz de bu bölümde bu fonksiyonlar üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi Linux sistemlerinde proseslerin bellek alanları sayfa tabloları yoluyla izole edilmişti. Ancak çekirdek tüm proseslerin sayfa tablosunda aynı yerde bulunmaktadır. Başka bir deyişle her prosesin sayfa tablosunda çekirdek hep aynı sanal adreslerde bulunmaktadır. Örneğin sys_open sistem fonksiyonuna girildiğinde bu fonksiyonun sanal adresi her proseste aynıdır. 32 bit linux sistemlerinde proseslerin sanal bellek alanları 3 GB user, 1 GB kernel olmak üzere 2 bölüme ayrılmıştır. 64 bit Linux sistemlerinde ise yalnızca sanal bellek alanının 256 TB'si kullanılmaktadır. Bu sistemlerde user alanı için 128 TB, kernel alanı için de 128 TB yer ayrılmıştır. 32 bit Linux sistemlerindeki prosesin sanal bellek alanı şöyle gösterilebilir: 00000000 USER ALANI (3 GB) C0000000 KERNEL ALANI (1 GB) 64 bit Linux sistemlerindeki sanal bellek alanı ise kabaca şöyledir: 0000000000000000 USER ALANI (128 TB) 0000800000000000 BOŞ BÖLGE (yaklaşık 16M TB) FFFF800000000000 KERNEL ALANI (128 TB) FFFFFFFFFFFFFFFF ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir sistem fonksiyonunun çağrıldığını düşünelim. İşlemci kernel mode'a otomatik olarak geçirilecektir. Bu durumda sayfa tablosu değişmeyecektir. Pekiyi kernel nasıl tüm fiziksel belleğe erişebilmektedir? İşte 32 bitlik sistemlerde proseslerin sayfa tablolarının son 1 GB'yi sayfalandırdığı girişleri tamamen fiziksel belleği eşlemektedir. Başka bir deyişle bu sistemlerde çekirdek alanının başlangıcı olan C0000000 adresi aslında sayfa tablosunda 00000000 fiziksel adresini belirtmektedir. Böylece kernel'ın herhangi bir fiziksel adrese erişmek için yapacağı tek şey bu adrese C00000000 değerini toplamaktır. Bu sistemlerde C0000000 adresinden itibaren proseslerin sayfa tabloları zaten fiziksel belleği 0'dan itibaren haritalandırmaktadır. Ancak 32 bit sistemlerde şöyle bir sorun vardır: Sayfa tablosunda C0000000'dan itibaren sayfalar fiziksel belleği haritalandırdığına göre 32 bit sistemlerin maksimum sahip olacağı 4 GB fiziksel RAM'in hepsi haritalandırılamamaktadır. İşte Linux tasarımcıları sayfa tablolarında C0000000'dan itibaren fiziksel RAM'in 1 GB'sini değil 896 MB'sini haritalandırmıştır. Geri kalan 128 MB'lik sayfa tablosu alanı fiziksel RAM'de 896MB'nin ötesine erişmek için değiştirilerek kullanılmaktadır. Yani 32 bit sistemlerde kernel fiziksel RAM'in ilk 896 MB'sine doğrudan ancak bunun ötesine sayfa tablosunun son 128 MB'lik bölgesini değiştirerek erişmektedir. 32 bit sistemlerde 896 MB'nin ötesine dolaylı biçimde erişildiği için bu bölgeye "high memory zone" denilmektedir. Tabii 64 bit sistemlerde böyle bir problem yoktur. Çünkü bu sistemlerde yine sayfa tablolarının kernel alanı fiziksel RAM'i başından itibaren haritalandırmaktadır. Ancak 128 TB'lik alan zaten şimdiki bilgisayarlara takılabilecek fiziksel RAM'in çok ötesindedir. Bu nedenle 64 bit sistemlerde "high memory zone" kavramı yoktur. Çekirdek kodların kernel alanın başlangıcı PAGE_OFFSET makrosuyla belirlenmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz kernel mode'da kod yazarken belli bir fiziksel adrese erişmek istersek onun sanal adresini bulmamız gerekir. Bu işin manuel yapılması yerine bunun için __va isimli makro kullanılmaktadır. Biz bu makroya bir fiziksel adres veririz o da bize o fiziksel adrese erişmek için gereken sanal adresi verir. Benzer biçimde bir sanal adresin fiziksel RAM karşılığını bulmak için de __pa makrosu kullanılmaktadır. Biz bu makroya sanal adresi veririz o da bize o sanal adresin aslında RAM'deki hangi fiziksel adres olduğunu verir. __va makrosu parametre olarak unsigned long biçiminde fiziksel adresi alır, o fiziksel adrese erişmek için gerekli olan sanal adresi void * türünden bize verir. __pa makrosu bunun tam tersini yapmaktadır. Bu makro bizden unsigned long biçiminde sanal adresi alır. O sanal adrese sayfa tablosunda karşı gelen fiziksel adresi bize verir. Kernel mode'da RAM'in her yerine erişebildiğimize ve bu konuda bizi engelleyen hiçbir mekanizmanın olmadığına dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux çekirdeği için fiziksel RAM temel olarak 3 bölgeye (zone) ayrılmıştır: ZONE_DMA ZONE_NORMAL ZONE_HIGHMEM ZONE_DMA ilgili sistemde disk ile RAM arasında transfer yapan DMA'nın erişebildiği RAM alanıdır. Bazı sistemlerde DMA tüm fiziksel RAM'in her yerine transfer yapamamaktadır. ZONE_NORMAL doğrudan çekirdeğin sayfa tablosu yoluyla haritalandırdığı fiziksel bellek bölgesidir. Intel 32 bit Linux sistemlerinde bu bölge ilk 896 MB'dir. Ancak 64 bit Linux sistemlerinde bu bölge tüm fiziksel RAM'i içermektedir. ZONE_HIGHMEM ise 32 bit sistemlerde çekirdeğin doğrudan haritalandıramadığı sayfa tablosunda değişiklik yapılarak erişilebilen fiziksel RAM alanıdır. 32 bit Linux sistemlerinde 896 MB'nin yukarısındaki fiziksel RAM ZONE_HIGHMEM alanıdır. Yukarıda da belirttiğimiz gibi 64 bit Intel işlemcilerinde ZONE_HIGHMEM biçiminde bir alan yoktur. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- User mode programlarda kullandığımız malloc fonksiyonunun uyguladığı klasik tahsisat yöntemi "boş alan bağlı liste" denilen yöntemdir. Bu yöntemde yalnızca boş alanlar bir bağlı listede tutulmaktadır. Dolayısıyla malloc gibi bir fonksiyon bu bağlı listede uygun bir elemanı bağlı listeyi dolaşarak bulmaktadır. free fonksiyonu da tahsis edilmiş olan alanı bu boş bağlı listeye eklemektedir. Tabii free fonksiyonu aynı zamanda bağlı listedeki komşu alanları da daha büyük bir boş alan oluşturacak biçimde birleştirmektedir. Ancak bu klasik yöntem çekirdek heap sistemi için çok yavaş kalmaktadır. Bu nedenle çekirdeğin heap sistemi için hızlı çalışan tahsisat algoritmaları kullanılmaktadır. Eğer tahsis edilecek bloklar eşit uzunlukta olursa bu durumda tahsisat işlemi ve geri bırakmak işlemi O(1) karmaşıklıkta yapılabilir. Örneğin heap içerisindeki tüm blokların 16 byte uzunlukta olduğunu düşünelim. Bu durumda 16 byte'lık tahsisat sırasında uygun bir boş alan aramaya gerek kalmaz. Bir dizisi içerisinde boş alanlar tutulabilir. Bu boş alanlardan herhangi biri verilebilir. Tabii uygulamalarda tahsis edilecek alanların büyükleri farklı olmaktadır. İşte BSD ve Linux sistemlerinde kullanılan "dilimli tahsisat sistemi (slab allocator)" denilen tahsisat sisteminin anahtar noktası eşit uzunlukta ismine "dilim (slab)" denilen blokların tahsis edilmesidir. Kernel içerisinde çeşitli nesneler için o nesnelerin uzunluğuna ilişkin farklı dilimli tahsisat sistemleri oluşturulmuştur. Örneğin bir proses yaratıldığında task_struct yapısı çekirdeğin heap alanında tahsis edilmektedir. İşte dilimli tahsisat sistemlerinden biri sizeof(struct task_struct) kadar dilimlerden oluşan sistemdir. Böylece pek çok kernel nesnesi için ayrı dilimli tahsisat sistemleri oluşturulmuştur. Bunların yanı sıra ayrıca bir de genel kullanım için blok uzunlukları 32, 64, 96, 128, 192, 256 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, ... biçiminde olan farklı dilimli tahsisat sistemleri de bulundurulmuştur. Böylece kernel mod programcısı belli uzunlukta bir alan tahsis etmek istediğinde bu uzunluğa en yakın bu uzunluktan büyük bir dilimli tahsisat sistemini kullanır. Tabii kernel mode programcılar isterse kendi nesneleri için o nesnelerin uzunluğu kadar yeni dilimli tahsisat sistemleri de oluşturabilmektedir. Aslında dilimli tahsisat sisteminin hazırda bulundurduğu dilimler işletim sisteminin sayfa tahsisatı yapan başka bir tahsisat algoritmasından elde edilmektedir. Linux sistemlerinde sayfa temelinde tahsisat yapmak için kullanılan tahsisat sistemine "buddy allocator" denilmektedir. (CSD işletim sisteminde buna "ikiz blok sistemi" denilmektedir.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Çekirdek kodlamasında çekirdek alanında dinamik tahsisat yapmak için kullanılan en genel fonksiyon kmalloc isimli fonksiyondur. Bu fonksiyon aslında parametresiyle belirtilen uzunluğa en yakon önceden yaratılmış olan dilimli tahsisat sisteminden dilim vermektedir (yani blok tahsis etmektedir). Örneğin biz kmalloc fonksiyonu ile 100 byte tahsis etmek istesek 100 byte'lık blokların bulunduğu önceden yaratılmış bir dilimli tahsisat sistemi olmadığı için kmalloc 128 byte'lık bloklara sahip dilimli tahsisat sisteminden bir dilim tahsis ederek bize vermektedir. Tabii bu örnekte 28 byte boşuna tahsis edilmiş olacaktır. Ancak çekirdek tahsisat sisteminin amacı en uygun miktarda belleği tahsis etmek değil, talep edilen miktarda belleği hızlı tahsis etmektir. kmalloc fonksiyonu ile tahsis edilen dilimler kfree fonksiyonu ile serbest bırakılmaktadır. Fonksiyonların prototipleri şöyledir: #include void *kmalloc (size_t size, int flags); void kfree (const void *objp); kmalloc fonksiyonunun birinci parametresi tahsis edilecek byte sayısını belirtir. İkincisi parametresi ise tahsis edilecek alan ve biçim hakkında çeşitli bayrakları içermektedir. Bu ikinci parametre çeşitli sembolik sabitlerden oluşturulmaktadır. Burada önemli birkaç bayrak şunlardır: GFP_KERNEL: Kernel alanı içerisinde normal tahsisat yapmak için kullanılır. Bu bayrak en sık bu kullanılan bayraktır. Burada eğer RAM doluysa işletim sistemi prosesi bloke ederek swap işlemi ile yer açabilmektedir. Bu işlem sırasında akış kernel mode'da bekleme kuyruklarında bekletilebilir. Tahsisat işlemi ZONE_NORMAL alanından yapılmaktadır. GFP_NOWAIT: GFP_KERNEL gibidir. Ancak hazırda bellek yoksa proses uykuya dalmaz. Fonksiyon başarısız olur. GFP_HIGHUSER: 32 bit sistemlerde ZONE_HIGHMEM alanından tahsisat yapar. GFP_DMA: İlgili sistemde DMA'nın erişebildiği fiziksel RAM alanından tahsisat yapar. kmalloc fonksiyonu başarı durumunda tahsis edilen alanın sanal bellek adresiyle, başarısızlık durumunda NULL adresle geri dönmektedir. Çekirdek modülleri ve aygıt sürücüler dinamik tahsisat başarısız olursa tipik olarak -ENOMEM değerine geri dönmelidir. kfree fonksiyonu ise daha önce kmalloc ile tahsis edilmiş olan alanın başlangıç adresini parametre olarak almaktadır. Aşağıda daha önce yapmış olduğumuz boru aygıt sürücüsündeki kuyruk sistemini kmalloc fonksiyonu ile tahsis edilip kfree fonksiyonu ile serbest bırakılmasına örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.c */ #include #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); struct QUEUE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; }; static struct QUEUE *g_queue; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } if ((g_queue = (struct QUEUE *)kmalloc(sizeof(struct QUEUE), GFP_KERNEL)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { kfree(g_queue); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_queue->count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_queue->count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_queue->count, size); if (g_queue->tail <= g_queue->head) size1 = MIN(PIPE_BUFSIZE - g_queue->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_queue->pipebuf + g_queue->head, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, g_queue->pipebuf, size2) != 0) { up(&g_sem); return -EFAULT; } g_queue->head = (g_queue->head + esize) % PIPE_BUFSIZE; g_queue->count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size > PIPE_BUFSIZE) size = PIPE_BUFSIZE; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_queue->count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_queue->count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_queue->count, size); if (g_queue->tail >= g_queue->head) size1 = MIN(PIPE_BUFSIZE - g_queue->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_queue->pipebuf + g_queue->tail, buf, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(g_queue->pipebuf, buf + size1, size2) != 0) { up(&g_sem); return -EFAULT; } g_queue->tail = (g_queue->tail + esize) % PIPE_BUFSIZE; g_queue->count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len; if ((fd = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 145. Ders 02/06/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi istersek genel amaçlı kmalloc fonksiyonunu kullanmak yerine kendimiz tam istediğimiz büyüklükte dilimlere sahip olan yeni bir dilimli tahsisat sistemi yaratıp onu kullanabiliriz. Yeni bir dilimli tahsisat sisteminin yaratılması kmem_cache_create fonksiyonu ile yapılmaktadır. Fonksiyonun prototipi şöyledir: #include struct kmem_cache *kmem_cache_create(const char *name, unsigned int size, unsigned int align, slab_flags_t flags, void (*ctor)(void *)); Fonksiyonun birinci parametresi yeni yaratılacak dilim sisteminin ismini belirtmektedir. Herhangi bir isim verilebilir. İkinci parametre dilimlerin büyüklüğünü belirtmektedir. Üçüncü parametre hizalama değerini belirtir. Bu parametreye 0 geçilirse default hizalama kullanılır. Fonksiyonun dördüncü parametresi yaratılacak dilim sistemine ilişkin bazı özelliklerin belirlenmesi için kullanılmaktadır. Buradaki bayrakların önemli birkaç tanesi şöyledir: SLAB_NO_REAP: Fiziksel RAM'in dolması nedeniyle kullanılmayan dilimlerin otomatik olarak sisteme iade edileceği anlamına gelir. Uç durumlarda bu bayrak kullanılabilir. SLAB_HWCACHE_ALIGN: Bu bayrak özellikle SMP sistemlerinde işlemci ya da çekirdeklerin cache alanları için hizalama yapılmasının sağlamaktadır. Yaratım sırasında bu parametreyi kullanabilirsiniz. SLAB_CACHE_DMA: Bu parametre DMA alanında (DMA zone) tahsisat için kullanılmaktadır. Fonksiyonun son parametresi dilim sistemi yaratıldığında çağrılacak callback fonksiyonu belirtmektedir. Bu parametre NULL geçilebilir. Fonksiyon başarı durumunda kmem_cache_create fonksiyonu kmem_cache türünden bir yapı nesnesinin adresiyle, başarısızlık durumunda NULL adrese geri dönmektedir. Başarısızlık durumunda aygıt sürücü fonksiyonunun -ENOMEM değeri ile geri döndürülmesi uygundur. Örneğin: struct kmem_cache *g_pipe_cachep; if ((g_pipe_cachep = kmem_cache_create("pipe-driver-cachep", sizeof(struct PIPE_INFO), 0, SLAB_HWCACHE_ALIGN, NULL)) == NULL) { ... return -ENOMEM; } Yaratılmış olan bir dilim sisteminden tahsisatlar kmem_cache_alloc fonksiyonu ile yapılmaktadır. Fonksiyonun parametresi şöyledir: #include void *kmem_cache_alloc(kmem_cache_t *cache, int flags); Fonksiyonun birinci parametresi yaratılmış olan dilim sisteminin handle değerini, ikinci parametresi yaratım bayraklarını almaktadır. Bu bayraklar kmalloc fonksiyonundaki bayraklarla aynıdır. Yani örneğin bu parametreye GFP_KERNEL geçilebilir. Fonksiyon başarı durumunda tahsis edilen sanal adrese, başarısızlık durumunda NULL adrese geri dönmektedir. Bu durumda aygıt sürücüdeki fonksiyonun -ENOMEM değeri ile geri döndürülmesi uygundur. Örneğin: if ((g_pinfo = (struct QUEUE *)kmem_cache_alloc(g_pipe_cachep, GFP_KERNEL)) == NULL) { ... return -ENOMEM; } kmem_cache_alloc fonksiyonu ile tahsis edilen dinamik alan kmem_cache_free fonksiyonu ile serbest bırakılabilir. Fonksiyonun prototipi şöyledir: #include void kmem_cache_free(kmem_cache_t *cache, const void *obj); Fonksiyonun birinci parametresi dilim sisteminin handle değerini, ikincisi parametresi ise serbest bırakılacak dilimin adresini belirtmektedir. Örneğin: kmem_cache_free(g_pipe_cachep, g_queue); kmem_cache_create fonksiyonu ile yaratılmış olan dilim sistemi kmem_cache_destroy fonksiyonu ile serbest bırakılabilir. Fonksiyonun prototipi şöyledir. #include int kmem_cache_destroy(kmem_cache_t *cache); Fonksiyon dilim sisteminin handle değerini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda negatif errno değerine geri döner. Örneğin: kmem_cache_destroy(g_pipe_cachep); Pekiyi kmalloc yerine yeni bir dilimli tahsisat sisteminin yaratılması tercih edilmeli midir? Yukarıda da belirttiğimiz gibi kmalloc fonksiyonu da aslında önceden yaratılmış belli uzunluktaki dilim sistemlerinden tahsisat yapmaktadır. Ancak "çok sayıda aynı büyüklükte alanların" tahsis edildiği durumlarda programcının talep ettiği uzunlukta kendi dilim sistemini yaratması tavsiye edilebilir. Bunun dışında genel amaçlı kmalloc fonksiyonu tercih edilebilir. Örneğin boru aygıt sürücümüzde yeni bir dilim sisteminin yaratılmasına hiç gerek yoktur. Ancak bir aşağıda örnek vermek amacıyla boru aygıt sürücüsünde yeni bir dilim sistemi yarattık. Örneği inceleyiniz. ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.c */ #include #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); struct PIPE_INFO { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; }; static struct PIPE_INFO *g_pinfo; struct kmem_cache *g_pipe_cachep; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } if ((g_pipe_cachep = kmem_cache_create("pipe-driver-cachep", sizeof(struct PIPE_INFO), 0, SLAB_HWCACHE_ALIGN, NULL)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } if ((g_pinfo = (struct PIPE_INFO *)kmem_cache_alloc(g_pipe_cachep, GFP_KERNEL)) == NULL) { kmem_cache_destroy(g_pipe_cachep); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { kmem_cache_free(g_pipe_cachep, g_pinfo); kmem_cache_destroy(g_pipe_cachep); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_pinfo->count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_pinfo->count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_pinfo->count, size); if (g_pinfo->tail <= g_pinfo->head) size1 = MIN(PIPE_BUFSIZE - g_pinfo->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pinfo->pipebuf + g_pinfo->head, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, g_pinfo->pipebuf, size2) != 0) { up(&g_sem); return -EFAULT; } g_pinfo->head = (g_pinfo->head + esize) % PIPE_BUFSIZE; g_pinfo->count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size > PIPE_BUFSIZE) size = PIPE_BUFSIZE; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_pinfo->count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_pinfo->count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_pinfo->count, size); if (g_pinfo->tail >= g_pinfo->head) size1 = MIN(PIPE_BUFSIZE - g_pinfo->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pinfo->pipebuf + g_pinfo->tail, buf, size1) != 0) { up(&g_sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(g_pinfo->pipebuf, buf + size1, size2) != 0) { up(&g_sem); return -EFAULT; } g_pinfo->tail = (g_pinfo->tail + esize) % PIPE_BUFSIZE; g_pinfo->count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len; if ((fd = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Linux'un dosya sistemi için önemli üç yapı vardır. Bunlar file, inode ve dentry yapılarıdır. file isimli yapıya biz "dosya nesnesi" demiştik. Anımsanacağı gibi ne zaman bir dosya açılsa dosya betimleyici tablosunda dosya betimleyicisi denilen bir indeks bu dosya nesnesini göstermektedir. Biz user mode'daki dosya işlemlerinde bu konuyu zaten açıklamıştık. Ancak kısaca bir anımsatma yapalım: Dosya Betimleyici Tablosu -------------------------- 0 ----> dosya nesnesi (struct file) 1 ----> dosya nesnesi (struct file) 2 ----> dosya nesnesi (struct file) 3 ----> dosya nesnesi (struct file) ... Dosya nesnesi "açık dosyaların bilgilerini" tutmaktadır. Aşağıda file yapısının mevcut çekirdeklerdeki içeriğini görüyorsunuz: struct file { union { /* fput() uses task work when closing and freeing file (default). */ struct callback_head f_task_work; /* fput() must use workqueue (most kernel threads). */ struct llist_node f_llist; unsigned int f_iocb_flags; }; /* * Protects f_ep, f_flags. * Must not be taken from IRQ context. */ spinlock_t f_lock; fmode_t f_mode; atomic_long_t f_count; struct mutex f_pos_lock; loff_t f_pos; unsigned int f_flags; struct fown_struct f_owner; const struct cred *f_cred; struct file_ra_state f_ra; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; u64 f_version; #ifdef CONFIG_SECURITY void *f_security; #endif /* needed for tty driver, and maybe others */ void *private_data; #ifdef CONFIG_EPOLL /* Used by fs/eventpoll.c to link all the hooks to this file */ struct hlist_head *f_ep; #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; errseq_t f_wb_err; errseq_t f_sb_err; /* for syncfs */ } __randomize_layout __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */ inode yapısı dosyanın diskteki bilgilerini tutmaktadır. Yani aynı dosya üç kez açılsa çekirdek üç farklı file nesnesi oluşturmaktadır. Ancak bu dosya diskte bir tane olduğuna göre çekirdek bunun için toplamda bir tane inode yapısı oluşturacaktır. file yapısının içerisinde dosyanın diskteki bilgilerine ilişkin bu inode yapısına f_inode elemanı yoluyla erişilebilmektedir. Daha önceden de gördüğümüz gibi dosya isimleri diskte dizin girişlerinde tutulmaktadır. Örneğin "/home/kaan/Study/test.c" isimli dosyanın i-node elemanına erişmek için işletim sistemi sırasıyla "/home", "/home/kaan", "/home/kaan/Study" ve "/home/kaan/Study/test.txt" dizin girişlerini taramak zorundadır. Bu işleme işletim sistemlerinde "yol ifadelerinin çözümlenmesi (pathname resolution)" denilmektedir. Aynı dosyaların tekrar tekrar açılması durumunda bu işlemlerin yeniden yapılması oldukça zahmetlidir. Dolayısıyla bulunan dizin girişlerinin bir yapı ile temsil edilerek bir cache sisteminde saklanması uygundur. İşte Linux çekirdeğinde dizin girişleri "dentry" isimli bir yapıyla temsil edilmektedir. Linux sistemleri yukarıda açıkladığımız "inode" ve "dentry" nesnelerini bir cache sisteminde tutmaktadır. Böylece bir dosya yeniden açıldığında onun bilgilerine diske hiç başvurmadan hızlı bir biçimde erişilmektedir. Linux dünyasında bu cache sistemlerine "inode cache" ve "dentry cache" denilmektedir. file, inode ve denrty nesneleri için bu yapıların büyüklüğünde ayrı dilimli tahsisat sistemleri oluşturulmuştur. Pekiyi yukarıdaki nesneler arasındaki ilişki nasıldır? Dosya sistemine dosya betimleyicisi yoluyla erişildiğini anımsayınız. Dosya betimleyicisinden dosya nesnesi (file nesnesi) elde edilmektedir. Dosya nesnesinin içerisinde o dosyanın dizin girişi bilgilerini tutan dentry nesnesinin adresi tutulur. Bu nesnenin içerisinde de inode nesnesinin adresi tutulmaktadır: file ---> dentry ---> inode Ancak 2.6 çekirdekleriyle birlikte file yapısından inode bilgilerine kolay erişebilmek için ayrıca file yapısı içerisinde doğrudan inode nesnesinin adresi de tutulmaya başlanmıştır. Bir aygıt sürücü üzerinde dosya işlemi yapıldığında çekirdek aygıt sürücü fonksiyonlarına dosya nesnesinin adresini (filp göstericisi) geçirmektedir. Yalnızca aygıt sürücü open fonksiyonuyla açılırken ve close fonksiyonu ile kapatılırken inode nesnesinin adresi de bu fonksiyonlara geçirilmektedir. Aygıt sürücünün fonksiyonlarının parametrik yapılarını aşağıda yeniden veriyoruz: int open(struct inode *inodep, struct file *filp); int release(struct inode *inodep, struct file *filp); ssize_t read(struct file *filp, char *buf, size_t size, loff_t *off); ssize_t write(struct file *filp, const char *buf, size_t size, loff_t *off); loff_t llseek(struct file *filp, loff_t off, int whence); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aygıt sürücünün majör ve minör numaraları ne anlam ifade etmektedir? Majör numara aygıt sürücünün türünü belirtir. Minör numara ise aynı türden aygıt sürücülerin farklı örneklerini (instance'larını) belirtmektedir. Örneğin biz yukarıdaki "pipe-driver" aygıt sürücümüzün tek bir boruyu değil on farklı boruyu idare etmesini isteyebiliriz. Bu durumda aygıt sürücümüzün bir tane majör numarası ancak 10 tane minör numarası olacaktır. Aygıt sürücülerin majör numaraları aynı ise bunların kodları da aynıdır. O aynı kod birden fazla aygıt için işlev görmektedir. Örneğin seri portu kontrol eden bir aygıt sürücü söz konusu olsun. Ancak bilgisayarımızda dört seri port olsun. İşte bu durumda bu seri porta ilişkin aygıt dosyalarının hepsinin majör numaraları aynıdır. Ancak minör numaraları farklıdır. Ya da örneğin terminal aygıt sürücüsü bir tanedir. Ancak bu aygıt sürücü birden fazla terminali yönetebilmektedir. O halde her terminale ilişkin aygıt dosyasının majör numaraları aynı minör numaraları farklı olacaktır. Örneğin: /dev$ ls -l tty1 tty2 tty3 tty4 tty5 crw--w---- 1 root tty 4, 1 Haz 2 15:05 tty1 crw--w---- 1 root tty 4, 2 Haz 2 15:05 tty2 crw--w---- 1 root tty 4, 3 Haz 2 15:05 tty3 crw--w---- 1 root tty 4, 4 Haz 2 15:05 tty4 crw--w---- 1 root tty 4, 5 Haz 2 15:05 tty5 ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 146. Ders 07/06/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi birden fazla aygıtı yönetecek (yani birden fazla minör numaraya sahip olan) bir aygıt sürücü nasıl yazılabilir? Her şeyden önce birden fazla minör numara kullanan aygıt sürücüleri yazarken dikkatli olmak gerekir. Çünkü tek bir kod birden fazla aynı türden bağımsız aygıtı idare edecektir. Dolayısıyla bu tür durumlarda bazı nesnelerin senkronize edilmesi gerekebilir. Birden fazla minör numara üzerinde çalışacak aygıt sürücüleri tipik olarak şöyle yazılmaktadır. 1) Minör numara sayısının aşağıdaki gibi ndevices isimli parametre yoluyla komut satırından aşağıdaki gibi aygıt sürücüye aktarıldığını varsayacağız: #define NDEVICES 10 ... static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); Programcının majör ve minör numaraları tahsis etmesi gerekir. Yukarıda da yaptığımız gibi majör numara alloc_chrdev_region fonksiyonuyla dinamik olarak belirlenebilmektedir. Bu fonksiyon aynı zamanda belli bir minör numaradan başlayarak n tane minör numarayı da tahsis edebilmektedir. Örneğin: if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } Burada 0'ıncı minör numaradan ndevices tane minör numara için aygıt tahsisatı yapılmıştır. 2) Her aygıt bir yapıyla temsil edilmelidir. Bunun için N elemanlı bir yapı dizisi yaratabilirsiniz. Bu dizi global düzeyde tanımlanabileceği gibi kmalloc fonksiyonuyla dinamik biçimde de tahsis edilebilir. Oluşturulan bu yapının içerisine struct cdev nesnesi de eklenmelidir. Örneğin: struct PIPE_DEVICE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; static struct PIPE_DEVICE *g_pdevices; ... if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } Burada görüldüğü gibi her farklı borunun farklı bekleme kuyrukları ve semaphore nesnesi vardır. cdev yapı nesnesinin yapının içerisine yerleştirilmesinin amacı ileride görüleceği gibi bu adresten hareketle yapı nesnesinin adresinin elde edilmesini sağlamaktır. Bunun nasıl yapıldığı izleyen paragraflarda görülecektir. 3) N tane minör numaralı aygıt için cdev_add fonksiyonuyla aygıtlar çekirdeğe eklenmelidir. Örneğin: for (i = 0; i < ndevices; ++i) { g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); if ((result = cdev_add(&g_pdevices[i].cdev, dev, 1)) < 0) { for (k = 0; k < i; ++k) cdev_del(&g_pdevices[k].cdev); kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "cannot add device!...\n"); return result; } } Burada yapı dizisinin her elemanındaki elemanlara ilkdeğerleri verilmiştir. Sonra her boru için ayrı bir cdev nesnesi cdev_add fonksiyonu ile eklenmiştir. Eklemelerden biri başarısız olursa daha önce eklenenlerin de cdev_del fonksiyonu ile silindiğine dikkat ediniz. 4) Bizim read, write gibi fonksiyonlarında file yapısı türünden adres belirten filp parametre değişkeni yoluyla PIPE_DEVICE yapısına erişmemiz gerekir. Bu işlem dolaylı bir biçimde şöyle yapılmaktadır: - Önce aygıt sürücünün open fonksiyonunda programcı inode yapısının i_cdev elemanından hareketle cdev nesnesinin içinde bulunduğu yapı nesnesinin başlangıç adresini container_of makrosuyla elde eder. Çünkü inode yapısının i_cdev elemanı cdev_add fonksiyonuyla eklenen cdev yapı nesnesinin adresini tutmaktadır. - Programcı device nesnesinin adresini elde ettikten sonra onu file yapısının private_data elemanına yerleştirir. file yapısının private_data elemanı programcının kendisinin isteğe bağlı olarak yerleştirebileceği bilgiler için bulundurulmuştur. Bu işlemler aşağıdaki gibi yapılabilir: static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } 5) Aygıt sürücünün read ve write fonksiyonları yazılır. 6) release (close) işleminde yapılacak birtakım son işlemler varsa yapılır. 7) Aygıt sürücünün exit fonksiyonunda yine tüm minör numaralar için cdev_del fonksiyonu çağrılır ve unregister_chrdev_region işlemi yapılır. 8) Birden fazla minör numara için çalışacak aygıt sürücülerin birden fazla aygıt dosyası yaratması gerekir. Yani aygıt sürücüsü kaç minör numarayı destekliyorsa o sayıda aygıt dosyalarının yaratılması gerekmektedir. Bu da onları yüklemek için kullandığımız load scriptinde değişiklik yapmayı gerektirmektedir. N tane minör numaraya ilişkin aygıt dosyası yaratacak biçimde yeni bir "loadmulti" isimli script aşağıdaki gibi yazılabilir: #!/bin/bash module=$2 mode=666 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod -m $mode ${module}$i c $major $i done Buradaki "loadmulti" script'i iki komut satırı argümanıyla aşağıdaki örnekteki gibi çalıştırılmalıdır: $ sudo ./loadmulti 10 pipe-driver ndevices=10 Burada "loadmulti" script'i hem aygıt sürücüyü yükleyecek hem de pipe-driver0, pipe-driver1, ..., pipedriver9 biçiminde aygıt dosyalarını yaratacaktır. Aşağıda yaratılmış olan örnek aygıt dosyalarına dikkat ediniz: crw-rw-rw- 1 root root 236, 0 Haz 7 22:09 pipe-driver0 crw-rw-rw- 1 root root 236, 1 Haz 7 22:09 pipe-driver1 crw-rw-rw- 1 root root 236, 2 Haz 7 22:09 pipe-driver2 crw-rw-rw- 1 root root 236, 3 Haz 7 22:09 pipe-driver3 crw-rw-rw- 1 root root 236, 4 Haz 7 22:09 pipe-driver4 crw-rw-rw- 1 root root 236, 5 Haz 7 22:09 pipe-driver5 crw-rw-rw- 1 root root 236, 6 Haz 7 22:09 pipe-driver6 crw-rw-rw- 1 root root 236, 7 Haz 7 22:09 pipe-driver7 crw-rw-rw- 1 root root 236, 8 Haz 7 22:09 pipe-driver8 crw-rw-rw- 1 root root 236, 9 Haz 7 22:09 pipe-driver9 Aygıt dosyalarının majör numaralarının hepsi aynıdır ancak minör numaraları farklıdır. Burada adeta birbirinden bağımsız 10 ayrı boru aygıtı var gibidir. Ancak aslında tek bir aygıt sürücü kodu bulunmaktadır. Tabii bizim benzer biçimde "unload" script'ini de tüm aygıt dosyalarını silecek biçimde düzeltmemiz gerekir. Bunun için "unloadmulti" script'ini aşağıdaki gibi yazabiliriz: #!/bin/bash module=$2 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done Bu script'te biz modülü önce çekirdekten sonra da "loadmulti" ile yarattığımız aygıt dosyalarını dosya sisteminden sildik. Script aşağıdaki örnekteki gibi kullanılmalıdır: $ sudo ./unloadmulti 10 pipe-driver Daha önce yapmış olduğumuz boru aygıt sürücüsünün 10 farklı minör numarayı destekleyen biçimini aşağıda veriyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.c */ #include #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define NDEVICES 10 #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; struct PIPE_DEVICE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; static dev_t g_dev; static struct PIPE_DEVICE *g_pdevices; static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init generic_init(void) { int result; dev_t dev; int i, k; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } for (i = 0; i < ndevices; ++i) { g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); if ((result = cdev_add(&g_pdevices[i].cdev, dev, 1)) < 0) { for (k = 0; k < i; ++k) cdev_del(&g_pdevices[k].cdev); kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "cannot add device!...\n"); return result; } } return 0; } static void __exit generic_exit(void) { int i; for (i = 0; i < ndevices; ++i) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); unregister_chrdev_region(g_dev, ndevices); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (pdevice->count == 0) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqread, pdevice->count > 0)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->count, size); if (pdevice->tail <= pdevice->head) size1 = MIN(PIPE_BUFSIZE - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, pdevice->pipebuf + pdevice->head, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, pdevice->pipebuf, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->head = (pdevice->head + esize) % PIPE_BUFSIZE; pdevice->count -= esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; if (size > PIPE_BUFSIZE) size = PIPE_BUFSIZE; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - pdevice->count < size) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqwrite, PIPE_BUFSIZE - pdevice->count >= size)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - pdevice->count, size); if (pdevice->tail >= pdevice->head) size1 = MIN(PIPE_BUFSIZE - pdevice->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(pdevice->pipebuf + pdevice->tail, buf, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(pdevice->pipebuf, buf + size1, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->tail = (pdevice->tail + esize) % PIPE_BUFSIZE; pdevice->count += esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 mode=666 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod -m $mode ${module}$i c $major $i done /* unloadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done /* prog1.c */ #include #include #include #include #include #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len; if ((fd = open("pipe-driver5", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver5", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 147. Ders 09/06/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 148. Ders 16/06/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aygıt sürücüden bilgi okumak için read fonksiyonun, aygıt sürücüye bilgi göndermek için ise write fonksiyonun kullanıldığını gördük. Ancak bazen aygıt sürücüye write fonksiyonunu kullanmadan bazı bilgilerin gönderilmesi, aygıt sürücüden read fonksiyonunu kullanmadan bazı bilgilerin alınması gerekebilmektedir. Bazen hiç bilgi okumadan ve bilgi göndermeden aygıt sürüceden bazı şeyleri yapmasını da isteyebiliriz. Bu tür bazı işlemlerin read ve write fonksiyonlarıyla yaptırılması mümkün olsa bile kullanışsızdır. Örneğin yukarıdaki boru aygıt sürücümüzde (pipe-driver) biz aygıt sürücüden kullandığı FIFO alanının uzunluğunu isteyebiliriz. Ya da bu alanın boyutunu değiştirmek isteyebiliriz. Bu işlemleri read ve write fonksiyonlarıyla yapmaya çalışsak aygıt sürücümüz sanki boruyu temsil eden kuyruktan okuma yazma yapmak istediğimizi sanacaktır. Tabii yukarıda da belirttiğimiz gibi zorlanırsa bu tür işlemler read ve write fonksiyonlarıyla yine de yapılabilir. Ancak böyle bir kullanımın mümkün hale getirilmesi ve user mode'dan kullanılması oldukça zor olacaktır. İşte aygıt sürücüye komut gönderip ondan bilgi almak için genel amaçlı ioctl isminde özel bir POSIX fonksiyonu bulundurulmuştur. Linux sistemlerinde ioctl fonksiyonu sys_ioctl isimli sistem fonksiyonunu çağırmaktadır. ioctl fonksiyonunun parametrik yapısı şöyledir: #include int ioctl(int fd, unsigned long request, ...); Fonksiyonun birinci parametresi aygıt sürücüye ilişkin dosya betimleyicisini belirtir. İkinci parametre ileride açıklanacak olan komut kodudur. Programcı aygıt sürücüsünde farklı komutlar için farklı komut kodları (yani numaralar) oluşturur. Sonra bu komut kodlarını switch içerisine sokarak hangi numaralı istekte bulunulmuşsa ona yönelik işlemleri yapar. ioctl fonksiyonu iki parametreyle ya da üç parametreyle kullanılmaktadır. Yani fonksiyonun üçüncü parametresi isteğe bağlıdır. Eğer bir veri transferi söz konusu değilse ioctl genellikle iki argümanla çağrılır. Ancak bir veri transferi söz konusu ise ioctl üç argümanla çağrılmalıdır. Bu durumda üçüncü argüman user mode'daki transfer adresini belirtir. Tabii aslında bu üçüncü parametrenin veri transferi ile ilgili olması dolayısıyla da bir adres belirtmesi zorunlu değildir. ioctl fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. errno uygun biçimde set edilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- User mode'dan bir program aygıt sürücü için ioctl fonksiyonunu çağırdığında akış user mode'dan kernel mode'a geçer ve aygıt sürücüdeki file_operations yapısının unlocked_ioctl elemanında belirtilen fonksiyon çağrılır. Bu fonksiyonun parametrik yapısı şöyle olmalıdır: long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); Fonksiyonun birinci parametresi yine dosya nesnesinin adresini, ikinci parametresi ioctl fonksiyonunda kullanılan komut kodunu (yani ioctl fonksiyonuna geçirilen ikinci argümanı) ve üçüncü parametresi de ek argümanı (yani ioctl fonksiyonuna geçirilen üçüncü argümanı) belirtmektedir. Tabii programcının eğer ioctl fonksiyonu iki argümanlı çağrılmışsa bu üçüncü parametreye erişmemesi gerekir. Bu fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda negatif hata koduna geri dönmelidir. Fakat bazen programcı doğrudan iletilecek değeri geri dönüş değeri biçiminde oluşturabilir. Bu durumda geri dönüş değeri pozitif değer olabilir. Örneğin: static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); ... static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release, .unlocked_ioctl = generic_ioctl }; ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- ioctl işleminde ioctl fonksiyonunun ikinci parametresi olan kontrol kodu dört parçanın bit düzeyinde birleştirilmesiyle oluşturulmaktadır. Bu parçaların belli bir uzunlukları vardır. Ancak bu parçalara ilişkin bitlerin 32 bit içerisinde belli pozisyonlara yerleştirilmesini kolaylaştırmak için _IOC isimli bir makro bulundurulmuştur. Bu makronun parametreleri şöyledir: _IOC(dir, type, nr, size) Bu makro buradaki parçaları bit düzeyinde birleştirerek bir 4 byte'lık bir tamsayı biçiminde vermektedir. Makronun parametrelerini oluşturan dört parçanın anlamları ve bit uzunlukları şöyledir: dir (direction): Bu 2 bitlik bir alandır ([30, 31] bitler). Burada kullanılacak sembolik sabitler _IOC_NONE, _IOC_READ, _IOC_WRITE ve _IOC_READ|_IOC_WRITE biçimindedir. Buradaki _IOC_READ aygıt sürücüden bilgi alınacağını _IOC_WRITE ise aygıt sürücüye bilgi gönderileceğini belirtmektedir. Buradaki yön ioctl sistem fonksiyonu tarafından dosyanın açış moduyla kontrol edilmemektedir. Örneğin biz buradaki yönü _IOC_READ|_IOC_WRITE biçiminde vermiş olsak bile dosyası O_RDONLY modunda açıp bu ioctl işlemini yapabiliriz. Eğer programcı böyle bir kontrol yapmak istiyorsa aygıt sürücünün ioctl fonksiyonu içerisinde bu kontrolü yapabilir. type: Bu 8 bitlik bir alandır ([8, 15] bitleri). Bu alana aygıt sürücüyü yazan istediği herhangi bir byte'ı verebilir. Genellikle bu byte bir karakter sabiti olarak verilmektedir. Buna "magic number" da denilmektedir. nr: Bu 8 bitlik bir alandır ([0, 7] bitleri). Programcı tarafından kontrol koduna verilen sıra numarasını temsil etmektedir. Genellikle aygıt sürücü programcıları 0'dan başlayarak her koda bir numara vermektedir. size: Bu 14 bitlik bir alandır ([16, 29] bitleri). Bu alan kaç byte'lık bir transferin yapılacağını belirtmektedir. Buradaki size değeri aslında çekirdek tarafından kullanılmamaktadır. Dolayısıyla biz 14 bitten daha büyük transferleri de yapabiliriz. Kullanım kolaylığı sağlamak için genellikle _IOC makrosu bir sembolik sabit biçiminde define edilir. Örneğin: #define PIPE_MAGIC 'x' #define IOC_PIPE_GETBUFSIZE _IOC(_IOC_READ, PIPE_MAGIC, 0, 4) Aslında _IOC makrosundan daha kolay kullanılabilen aşağıdaki makrolar da oluşturulmuştur: #ifndef __KERNEL__ #define _IOC_TYPECHECK(t) (sizeof(t)) #endif #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) Bu makrolarda _IOC makrosunun birinci parametresinin artık belirtilmediğine dikkat ediniz. Çünkü makrolar zaten isimlerine göre _IOC makrosunun birinci parametresini kendisi oluşturmaktadır. Ayrıca artık uzunluk (size parametresi) byte olarak değil tür olarak belirtilmelidir. Makrolar bu türleri sizeof operatörüne kendisi sokmaktadır. Görüldüğü gibi _IO makrosu veri transferinin söz konusu olmadığı durumda kullanılır. _IOR aygıt sürücüden okuma yapıldığı durumda, _IOW aygıt sürücüye yazma yapıldığı durumda, _IOWR ise aygıt sürücüden hem okuma hem de yazma yapıldığı durumlarda kullanılmaktadır. Örneğin: #define PIPE_MAGIC 'x' #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_MAGIC, 0, int) ioctl için kontrol kodları hem aygıt sürücünün içerisinden hem de user mode'dan kullanılacağına göre ortak bir başlık dosyasının oluşturulması uygun olabilir. Burada ioctl kontrol kodları bulundurulabilir. Örneğin boru aygıt sürücümüz için "pipe-driver.h" dosyası aşağıdaki gibi düzenlenebilir: // pipe-driver.h #ifndef PIPEDRIVER_H_ #define PIPEDRIVER_H_ #include #include #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #endif Aygıt sürücüdeki ioctl fonksiyonunu yazarken iki noktaya dikkat etmek gerekir: 1) ioctl fonksiyonunun üçüncü parametresi unsigned long türden olmasına karşın aslında genellikle user mod programcısı buraya bir nesnesin adresini geçirmektedir. Dolayısıyla bu transfer adresine aktarım gerekmektedir. Bunun için copy_to_user, copy_from_use, put_user, get_user gibi "adresin geçerliliğini sorguladıktan sonra transfer yapan fonksiyonlar" kullanılabilir. 2) User mod programcısının olmayan bir komut kodu girmesi durumunda ioctl fonksiyonu -ENOTTY değeri ile geri döndürülmelidir. Bu tuhaf hata kodu (TTY tele type terminal sözcüklerinden kısaltmadır) tarihsel bir durumdan kaynaklanmaktadır. Bu hata kodu için user mode'da "Inappropriate ioctl for device" biçiminde bir hata yazısı elde edilmektedir. User mode'daki ioctl fonksiyonu başarı durumunda 0 değerine geri döndüğü için aygıt sürücüsündeki ioctl fonksiyonu da genel olarak başarı durumunda 0 ile geri döndürülmelidir. Yukarıda da belirttiğimiz gibi olmayan bir ioctl kodu için aygıt sürücüdeki fonksiyonun -ENOTTY ile geri döndürülmesi uygundur. Bazı aygıt sürücülerinde başarı durumunda aygıt sürücüden bilgi ioctl fonksiyonunun üçüncü parametresi yoluyla değil, geri dönüş değeri yoluyla elde edilmektedir. Bu durumda aygıt sürücüdeki ioctl fonksiyonu pozitif değerle de geri döndürülebilir. Ancak bu durum seyrektir. Biz transferin ioctl fonksiyonunun üçüncü parametresi yoluyla yapılmasını tavsiye ediyoruz. Aygıt sürücüdeki ioctl fonksiyonu tipik olarak bir switch deyimi ile gerçekleştirilmektedir. Örneğin: static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case IOC_PIPE_GETBUFSIZE: // ... break; default: return -ENOTTY; } return 0; } Burada switch deyiminin default bölümünde fonksiyonun -NOTTY değeri ile geri döndürüldüğüne dikkat ediniz. Tabii fonksiyon üçüncü parametresi ile belirtilen transfer adresi geçersiz bir adresse yine -EFAULT değeri ile döndürülmelidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 149. Ders 23/06/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte boru aygıt sürücüsünün kullandığı boru uzunluğu IOC_PIPE_GETBUFSIZE ioctl koduyla elde edilip IOC_PIPE_SETBUFSIZE fonksiyonuyla değiştirilebilmektedir. Buradaki IOCTL kodları şöyle oluşturulmuştur: #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETCOUNT _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 1, size_t) #define IOC_PIPE_SETBUFSIZE _IOW(PIPE_DRIVER_MAGIC, 2, size_t) #define IOC_PIPE_PEEK _IOWR(PIPE_DRIVER_MAGIC, 3, struct PIPE_PEEK) Ancak bu örnek için yukarıda vermiş olduğumuz boru aygıt sürücüsünde bazı değişiklikler yaptık. Bu değişiklikler şunlardır: - Artık borunun uzunluğu PIPE_DEVICE yapısının içerisinde tutulmaya başlanmıştır: struct PIPE_DEVICE { unsigned char *pipebuf; size_t head; size_t tail; size_t count; size_t bufsize; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; Buradaki bufsize elemanı ilgili borunun uzunluğunu belirtmektedir. Default uzunluk test için kolaylık sağlamak amacıyla yine 10 olarak tutulmuştur. Bu değişiklik sayesinde artık her minör numaraya ilişkin boru uzunluğu farklılaşabilecektir. - Aygıt sürücümüz içerisindeki ioctl fonksiyonu aşağıdaki gibi yazılmıştır: static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct PIPE_DEVICE *pdevice; printk(KERN_INFO "ioctl"); pdevice = (struct PIPE_DEVICE *)filp->private_data; switch (cmd) { case IOC_PIPE_GETCOUNT: return put_user(pdevice->count, (size_t *)arg); case IOC_PIPE_GETBUFSIZE: return put_user(pdevice->bufsize, (size_t *)arg); case IOC_PIPE_SETBUFSIZE: return set_bufsize(pdevice, arg); case IOC_PIPE_PEEK: return read_peek(pdevice, arg); default: return -ENOTTY; } return 0; } Burada gördüğünüz gibi ilgili minör numaradaki borudaki byte sayısı IOC_PIPE_GETCOUNT, uzunluğu ise IOC_PIPE_GETBUFSIZE ioctl kodu ile alınmakta ve bu boru uzunluğu IOC_PIPE_SETBUFSIZE ioctl kodu ile değiştirilebilmektedir. Boru için kullanılan tampon değiştirilirken eşzamanlı erişimlere dikkat edilmelidir. Çünkü daha önceden de belirttiğimiz gibi aygıt sürücünün içerisindeki fonksiyonlar farklı prosesler tarafından aynı anda çağrılabilmektedir. Bu tür durumlarda daha önce görmüş olduğumuz çekirdek senkronizasyon nesneleri ile işlemlerin senkronize edilmesi gerekmektedir. Örneğimizdeki senkronizasyon PIPE_DEVICE yapısının içerisindeki semaphore nesnesi yoluyla yapılmıştır. IOC_PIPE_PEEK ioctl kodu borudan atmadan okuma yapmakta kullanılmaktadır. Normal olarak borudan read fonksiyonu ile okuma yapıldığında okunanlar borudan atılmaktadır. Ancak bu ioctl kodu ile borudan okuma yapıldığında okunanlar borudan atılmamaktadır. Bu ioctl kodu için aşağıdaki gibi bir yapı oluşturulmuştur: struct PIPE_PEEK { size_t size; void *buf; }; Yapının size elemanı kaç byte peek işleminin yapılacağını, buf elemanı ise peek edilen byte'ların yerleştirileceği adresi belirtmektedir. Tabii boruda mevcut olan byte sayısından daha fazla byte peek edilmek istenirse boruda olan kadar byte peek edilmektedir. Peek edilen byte sayısı aygıt sürücü tarafından yapının size elemanına aktarılmaktadır. Aygıt sürücümüzü yine "loadmulti" script'i ile aşağıdaki gibi yükleyebilirsiniz: $ sudo ./loadmulti 10 pipe-driver ndevices=10 Aygıt sürücünün çekirdekten atılması da yine "unloadmulti" script'i ile yapılabilir: $ sudo ./unloadmulti 10 pipe-driver Aşağıda örneğin tüm kodlarını veriyoruz. ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.h */ #ifndef PIPEDRIVER_H_ #define PIPEDRIVER_H_ #include #include struct PIPE_PEEK { size_t size; void *buf; }; #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETCOUNT _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 1, size_t) #define IOC_PIPE_SETBUFSIZE _IOW(PIPE_DRIVER_MAGIC, 2, size_t) #define IOC_PIPE_PEEK _IOWR(PIPE_DRIVER_MAGIC, 3, struct PIPE_PEEK) #endif /* pipe-driver.c */ #include #include #include #include #include #include #include #include #include "pipe-driver.h" #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define NDEVICES 10 #define DEF_PIPE_BUFSIZE 10 #define MAX_PIPE_BUFSIZE 131072 /* 128K */ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release, .unlocked_ioctl = generic_ioctl }; struct PIPE_DEVICE { unsigned char *pipebuf; size_t head; size_t tail; size_t count; size_t bufsize; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; static int set_bufsize(struct PIPE_DEVICE *pdevice, unsigned long arg); static int read_peek(struct PIPE_DEVICE *pdevice, unsigned long arg); static dev_t g_dev; static struct PIPE_DEVICE *g_pdevices; static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init generic_init(void) { int result; dev_t dev; int i, k; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } for (i = 0; i < ndevices; ++i) { g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; g_pdevices[i].bufsize = DEF_PIPE_BUFSIZE; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); g_pdevices[i].pipebuf = (char *)kmalloc(DEF_PIPE_BUFSIZE, GFP_KERNEL); result = cdev_add(&g_pdevices[i].cdev, dev, 1); if (g_pdevices[i].pipebuf == NULL || result < 0) { if (g_pdevices[i].pipebuf != NULL) kfree(g_pdevices[i].pipebuf); for (k = 0; k < i; ++k) { cdev_del(&g_pdevices[k].cdev); kfree(g_pdevices[k].pipebuf); } kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "cannot add device!...\n"); return result; } } return 0; } static void __exit generic_exit(void) { int i; for (i = 0; i < ndevices; ++i) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); unregister_chrdev_region(g_dev, ndevices); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (pdevice->count == 0) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqread, pdevice->count > 0)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->count, size); if (pdevice->tail <= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, pdevice->pipebuf + pdevice->head, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, pdevice->pipebuf, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->head = (pdevice->head + esize) % pdevice->bufsize; pdevice->count -= esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; if (size > pdevice->bufsize) size = pdevice->bufsize; while (pdevice->bufsize - pdevice->count < size) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqwrite, pdevice->bufsize - pdevice->count >= size)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->bufsize - pdevice->count, size); if (pdevice->tail >= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(pdevice->pipebuf + pdevice->tail, buf, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(pdevice->pipebuf, buf + size1, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->tail = (pdevice->tail + esize) % pdevice->bufsize; pdevice->count += esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqread); return esize; } static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct PIPE_DEVICE *pdevice; printk(KERN_INFO "ioctl"); pdevice = (struct PIPE_DEVICE *)filp->private_data; switch (cmd) { case IOC_PIPE_GETCOUNT: return put_user(pdevice->count, (size_t *)arg); case IOC_PIPE_GETBUFSIZE: return put_user(pdevice->bufsize, (size_t *)arg); case IOC_PIPE_SETBUFSIZE: return set_bufsize(pdevice, arg); case IOC_PIPE_PEEK: return read_peek(pdevice, arg); default: return -ENOTTY; } return 0; } static int set_bufsize(struct PIPE_DEVICE *pdevice, unsigned long arg) { char *new_pipebuf; size_t size; if (arg > MAX_PIPE_BUFSIZE) return -EINVAL; if (arg <= pdevice->count) return -EINVAL; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; if ((new_pipebuf = (char *)kmalloc(arg, GFP_KERNEL)) == NULL) { up(&pdevice->sem); return -ENOMEM; } if (pdevice->count != 0) { if (pdevice->tail <= pdevice->head) { size = pdevice->bufsize - pdevice->head; memcpy(new_pipebuf, pdevice->pipebuf + pdevice->head, size); memcpy(new_pipebuf + size, pdevice->pipebuf, pdevice->count - size); } else memcpy(new_pipebuf, pdevice->pipebuf + pdevice->head, pdevice->count); } pdevice->head = 0; pdevice->tail = pdevice->count; kfree(pdevice->pipebuf); pdevice->pipebuf = new_pipebuf; pdevice->bufsize = arg; up(&pdevice->sem); return 0; } static int read_peek(struct PIPE_DEVICE *pdevice, unsigned long arg) { size_t esize, size1, size2; struct PIPE_PEEK *userpp = (struct PIPE_PEEK *)arg; struct PIPE_PEEK pp; int status = 0; if (copy_from_user(&pp, userpp, sizeof(struct PIPE_PEEK)) != 0) return -EFAULT; if (pp.size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; esize = MIN(pdevice->count, pp.size); if (pdevice->tail <= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(pp.buf, pdevice->pipebuf + pdevice->head, size1) != 0) { status = -EFAULT; goto EXIT; } if (size2 != 0) if (copy_to_user(pp.buf + size1, pdevice->pipebuf, size2) != 0) { status = -EFAULT; goto EXIT; } if (put_user(esize, &userpp->size) != 0) status = -EFAULT; EXIT: up(&pdevice->sem); return status; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 mode=666 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod -m $mode ${module}$i c $major $i done /* unloadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done /* prog1.c */ #include #include #include #include #include #include #include "pipe-driver.h" #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len, bufsize, new_bufsize; if ((fd = open("pipe-driver5", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if (buf[0] == '!') { new_bufsize = atoi(&buf[1]); printf("%zd\n", new_bufsize); if (ioctl(fd, IOC_PIPE_SETBUFSIZE, new_bufsize) == -1) exit_sys("ioctl"); if (ioctl(fd, IOC_PIPE_GETBUFSIZE, &bufsize) == -1) exit_sys("ioctl"); printf("new pipe buffer size is %zu\n", bufsize); } else { len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #include #include "pipe-driver.h" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int count, size; ssize_t result; struct PIPE_PEEK pp; char *peekbuf; if ((pdriver = open("pipe-driver5", O_RDONLY)) == -1) exit_sys("open"); for (;;) { if (ioctl(pdriver, IOC_PIPE_GETCOUNT, &count) == -1) exit_sys("ioctl"); printf("There are (is) %d byte(s) in the pipe\n", count); printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if (size < 0) { pp.size = -size; if ((pp.buf = malloc(-size)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if (ioctl(pdriver, IOC_PIPE_PEEK, &pp) == -1) exit_sys("ioctl"); peekbuf = (char *)pp.buf; for (size_t i = 0; i < pp.size; ++i) putchar(peekbuf[i]); putchar('\n'); free(pp.buf); } else { if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 150. Ders 28/06/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi proc dosya sistemi disk tabanlı bir dosya sistemi değildir. Çekirdek çalışması sırasında dış dünyaya bilgi vermek için bazen de davranışını dış dünyadan gelen verilerle değiştirebilmek için proc dosya sistemini kullanmaktadır. Daha sonra proc gibi sys isimli bir dosya sistemi de Linux'a eklenmiştir. proc dosya sistemi aslında yalnızca çekirdek tarafından değil aygıt sürücüler tarafından da kullanılabilmektedir. Ancak bu dosya sisteminin içerisinde user mode'dan dosyalar ya da dizinler yaratılamamaktadır. proc dosya sistemindeki tüm girişlerin dosya uzunlukları 0 biçiminde rapor edilmektedir. proc dosya sisteminin kullanımına yönelik çekirdek fonksiyonları çekirdeğin versiyonları ile zamanla birkaç kez değiştirilmiştir. Dolayısıyla eski çekirdeklerde çalışan kodlar yeni çekirdeklerde derlenmeyecektir. Biz burada en yeni fonksiyonları ele alacağız. User mode'dan prog dosya sistemindeki bir dosya üzerinde open, read, write, lseek, close işlemler yapıldığında aslında aygıt sürücülerin belirlediği fonksiyonlar çağrılmaktadır. Yani örneğin biz user mode'dan proc dosya sistemi içerisindeki bir dosyadan okuma yapmak istediğimizde aslında onu oluşturan aygıt sürücünün içerisindeki bir fonksiyon çalıştırılır. Bu fonksiyon bize okuma sonucunda elde edilecek bilgileri verir. Benzer biçimde proc dosya sistemindeki bir dosyaya user mode'dan yazma yapılmak istendiğinde aslında o dosyaya ilişkin aygıt sürücünün bir fonksiyonu çağrılmaktadır. Yani proc dosya sistemi aslında aygıt sürücüden fonksiyon çağıran bir mekanizmaya sahiptir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- proc dosya sisteminde bir dosya yaratabilmek için proc_create isimli fonksiyon kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct proc_ops *proc_ops); Fonksiyonun birinci parametresi yaratılacak dosyanın ismini belirtir. İkinci parametresi erişim haklarını belirtmektedir. Bu parametre 0 geçilirse default erişim hakları kullanılır. Üçüncü parametre dosyanın hangi dizinde yaratılacağını belirtmektedir. Bu parametre NULL geçilirse dosya ana "/proc" dizini içerisinde yaratılır. proc dosya sistemi içerisinde dizinlerin nasıl yaratıldığını izleyen paragraflarda açıklayacağız. Son parametre proc dosya sistemindeki ilgi dosyaya yazma ve okuma yapıldığında çalıştırılacak fonksiyonları belirtir. Aslında birkaç sene önceki çekirdeklerde (3.10 çekirdeklerine kadarki çekirdeklerde) bu fonksiyonun son parametresi proc_ops yapısını değil, file_operations yapısını kullanıyordu. Dolayısıyla çekirdeğinizdeki fonksiyonun son parametresinin ne olduğuna dikkat ediniz. Örneğin önceki kursun yapıldığı makinede bu son parametre file_operations yapısına ilişkinken bu kursun yapıldığı makinede proc_ops yapısına ilişkindir. proc_ops yapısı şöyle bildirilmiştir: #include struct proc_ops { unsigned int proc_flags; int (*proc_open)(struct inode *, struct file *); ssize_t (*proc_read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*proc_read_iter)(struct kiocb *, struct iov_iter *); ssize_t (*proc_write)(struct file *, const char __user *, size_t, loff_t *); /* mandatory unless nonseekable_open() or equivalent is used */ loff_t (*proc_lseek)(struct file *, loff_t, int); int (*proc_release)(struct inode *, struct file *); __poll_t (*proc_poll)(struct file *, struct poll_table_struct *); long (*proc_ioctl)(struct file *, unsigned int, unsigned long); #ifdef CONFIG_COMPAT long (*proc_compat_ioctl)(struct file *, unsigned int, unsigned long); #endif int (*proc_mmap)(struct file *, struct vm_area_struct *); unsigned long (*proc_get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); }; proc_ops yapısının elemanlarına ilişkin fonksiyon göstericilerinin türlerinin file_operations yapısındaki elemanlara ilişkin fonksiyon göstericilerinin türleri ile aynı olduğuna dikkat ediniz. Bu fonksiyonların kullanımı tamamen aygıt sürücü için oluşturduğumuz file_operations yapısı ile aynı biçimdedir. proc dosya sistemi genel olarak text tabanlı bir dosya sistemi biçiminde düşünülmüştür. Yani buradaki dosyalar genel olarak text içeriğe sahiptir. Siz de aygıt sürücünüz için proc dosya sisteminde dosya oluşturacaksanız onların içeriğini text olarak oluşturmalısınız. Fonksiyon başarı durumunda yaratılan dosyanın bilgilerini içeren proc_dir_entry türünden bir yapı nesnesinin adresiyle, başarısızlık durumunda NULL adresle geri dönmektedir. Bu durumda çağıran fonksiyonun -ENOMEM gibi bir hata değeriyle geri döndürülmesi yaygındır. proc dosya sisteminde yaratılan dosya remove_proc_entry fonksiyonuyla silinebilmektedir. #include void remove_proc_entry(const char *name, struct proc_dir_entry *parent); Fonksiyonun birinci parametresi silinecek dosyanın ismini, ikinci parametresi dosyanın içinde bulunduğu dizine ilişkin proc_dir_entry nesnesinin adresini almaktadır. Yine bu parametre NULL adres girilirse dosyanın ana "/proc" dizininde olduğu kabul edilmektedir. Aşağıdaki örnekte proc sisteminde dosya yaratan iskelet bir aygıt sürücü programı verilmiştir. Bu aygıt sürücüde "/proc" dizininde "procfs-driver" isminde bir dosya yaratılmaktadır. Aygıt sürücüyü install ettikten sonra "/proc" dizininde bu dosyanın yaratılıp yaratılmadığını kontrol ediniz. Bu dosyayı "cat" ile komut satırından okumak istediğinizde "cat" programı bu dosyayı açıp, read işlemi uygulayıp kapatacaktır. "cat" işleminden sonra "dmesg" komutu ile aygıt sürücümüzde belirlediğimiz fonksiyonların çağrıldığını doğrulayınız. ---------------------------------------------------------------------------------------------------------------------------*/ /* procfs-driver.c */ #include #include #include #include #include #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static int proc_open(struct inode *inodep, struct file *filp); static int proc_release(struct inode *inodep, struct file *filp); static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static struct proc_ops g_proc_ops = { .proc_open = proc_open, .proc_release = proc_release, .proc_read = proc_read, .proc_write = proc_write }; static int __init generic_init(void) { int result; struct proc_dir_entry *pde; printk(KERN_INFO "procfs-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "procfs-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } if ((pde = proc_create("procfs-driver", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, NULL, &g_proc_ops)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("procfs-driver", NULL); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "procfs driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver read\n"); return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver write...\n"); return 0; } static int proc_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file opened...\n"); return 0; } static int proc_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file closed...\n"); return 0; } static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver proc file read...\n"); return 0; } static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver proc file write...\n"); return 0; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte aygıt sürücüsü içerisindeki count isimli global değişken proc dosya sistemindeki "procfs-driver" isimli bir dosya ile temsil edilmiştir. Bu dosyadan okuma yapıldığında bu count değişkeninin değeri elde edilmektedir. Dosyaya yazma yapıldığında bu count değişkeninin değeri güncellenmektedir. Yazma işleminde dosya göstericisi dikkate alınmamış ve yazma işlemi her zaman sanki ilgili dosyanın başından itibaren yapılıyormuş gibi bir etki oluşturulmuştur. ---------------------------------------------------------------------------------------------------------------------------*/ /* procfs-driver.c */ #include #include #include #include #include #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static int proc_open(struct inode *inodep, struct file *filp); static int proc_release(struct inode *inodep, struct file *filp); static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static struct proc_ops g_proc_ops = { .proc_open = proc_open, .proc_release = proc_release, .proc_read = proc_read, .proc_write = proc_write }; static int g_count = 123; static char g_count_str[32]; static int __init generic_init(void) { int result; struct proc_dir_entry *pde; printk(KERN_INFO "procfs-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "procfs-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } if ((pde = proc_create("procfs-driver", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, NULL, &g_proc_ops)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("procfs-driver", NULL); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "procfs-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver read\n"); return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver write...\n"); return 0; } static int proc_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file opened...\n"); return 0; } static int proc_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file closed...\n"); return 0; } static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_count_str, "%d\n", g_count); left = strlen(g_count_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_count_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "procfs-driver proc file read...\n"); return esize; } static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; char count_str[31]; int count; esize = size > 31 ? 31 : size; if (esize != 0) { if (copy_from_user(count_str, buf, esize) != 0) return -EFAULT; } count_str[esize] = '\0'; if (kstrtoint(count_str, 10, &count) != 0) return -EINVAL; if (count < 0 || count > 1000) return -EINVAL; g_count = count; strcpy(g_count_str, count_str); printk(KERN_INFO "procfs-driver proc file write...\n"); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /*-------------------------------------------------------------------------------------------------------------------------- 151. Ders 30/06/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz yukarıdaki örneklerde dosyayı proc dosya sisteminin kök dizininde yarattık. İstersek proc dizininde bir dizin yaratıp dosyalarımızı o dizinin içerisinde de oluşturabilirdik. proc dosya sisteminde bir dizin yaratmak için proc_mkdir fonksiyonu kullanılmaktadır: #include struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent); Fonksiyonun birinci parametresi yaratılacak dizin'in ismini, ikinci parametresi dizinin hangi dizin içerisinde yaratılacağını belirtir. Bu parametre NULL geçilirse dizin proc dosya sisteminin kök dizininde yaratılır. Buradan aldığımız geri dönüş değerini proc_create fonksiyonunun parent parametresinde kullanırsak ilgili dosyamızı bu dizinde yaratmış oluruz. Tabii benzer biçimde dizin içerisinde dizin de yaratabiliriz. proc_mkdir fonksiyonu başarısızlık durumunda NULL adrese geri dönmektedir. Çağıran fonksiyonun yine -ENOMEM değeriyle geri döndürülmesi uygundur. Örneğin: struct proc_dir_entry *pdir; pdir = proc_mkdir("procfs-driver", NULL); proc_create("info", 0, pdir, &g_proc_ops); Dizinlerin silinmesi yine remove_proc_entry fonksiyonuyla yapılmaktadır. Tabii dizin içerisindeki dosyaları silerken remove_proc_entry fonksiyonunda dosyanın hangi dizin içerisinde olduğu belirtilmelidir. Bu fonksiyon ile dizin silinirken dizinin içi boş değilse bile o dizin ve onun içindeki girişlerin hepsi silinmektedir. Ayrıca kök dizindeki girişleri silmek için proc_remove fonksiyonu da bulundurulmuştur. Fonksiyonun prototipi şöyledir: #include void proc_remove(struct proc_dir_entry *de); Bu fonksiyon parametre olarak proc_create ya da proc_mkdir fonksiyonunun verdiği geri dönüş değerini alır. proc dosya sisteminin kök dizininde silme yapılmak isteniyorsa aşağıdaki her iki çağrım eşdeğerdir: remove_proc_entry("file_name", NULL); proc_remove("file_name"); Aşağıdaki örnekte proc dosya sisteminin kök dizininde "procfs-driver" isimli bir dizin yaratılmış, onun içerisinde de "count" bir dosya yaratılmıştır. Bu örneğin yukarıdaki örnekten tek farkı dosyanın proc dosya sisteminin kökünde değil bir dizinin içerisinde yaratılmış olmasıdır. ---------------------------------------------------------------------------------------------------------------------------*/ /* procfs-driver.c */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static int proc_open(struct inode *inodep, struct file *filp); static int proc_release(struct inode *inodep, struct file *filp); static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static struct proc_ops g_proc_ops = { .proc_open = proc_open, .proc_release = proc_release, .proc_read = proc_read, .proc_write = proc_write }; static int g_count = 123; static char g_count_str[32]; static int __init generic_init(void) { int result; struct proc_dir_entry *pde_dir; struct proc_dir_entry *pde; printk(KERN_INFO "procfs-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "procfs-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } if ((pde_dir = proc_mkdir("procfs-driver", NULL)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } if ((pde = proc_create("count", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_proc_ops)) == NULL) { remove_proc_entry("procfs-driver", NULL); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("procfs-driver", NULL); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "procfs driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver read\n"); return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver write...\n"); return 0; } static int proc_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file opened...\n"); return 0; } static int proc_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file closed...\n"); return 0; } static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_count_str, "%d\n", g_count); left = strlen(g_count_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_count_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "procfs-driver proc file read...\n"); return esize; } static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; char count_str[31]; int count; esize = size > 31 ? 31 : size; if (esize != 0) { if (copy_from_user(count_str, buf, esize) != 0) return -EFAULT; } count_str[esize] = '\0'; if (kstrtoint(count_str, 10, &count) != 0) return -EINVAL; if (count < 0 || count > 1000) return -EINVAL; g_count = count; strcpy(g_count_str, count_str); printk(KERN_INFO "procfs-driver proc file write...\n"); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /*-------------------------------------------------------------------------------------------------------------------------- 152. Ders 06/07/2024 - Cumartesi ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 153. Ders 07/07/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de boru aygıt sürücüsüne proc dosya sistemi desteği verelim. Aygıt sürücümüz proc kök dizininde minör numara kadar ayrı dizin oluşturmaktadır. Sonra da bu dizinlerin içerisinde ilgili aygıtların bufsize ve count değerlerini iki dosya ile dış dünyaya vermektedir. Örneğimizde dikkat edilmesi gereken birkaç nokta üzerinde durmak istiyoruz: - proc dosya sistemi içerisindeki bir dosya user mode'dan açıldığında aygıt sürücümüz hangi dosyanın açıldığını nereden bilecektir? İşte dosya nesnesi görevinde olan file yapısının f_path elemanı path isimli bir yapı türündendir. Bu yapı şöyle bildirilmiştir: struct path { struct vfsmount *mnt; struct dentry *dentry; } __randomize_layout; Yapının dentry elemanı dosya hakkında bilgiler içermektedir: struct dentry { /* RCU lookup touched fields */ unsigned int d_flags; /* protected by d_lock */ seqcount_spinlock_t d_seq; /* per dentry seqlock */ struct hlist_bl_node d_hash; /* lookup hash list */ struct dentry *d_parent; /* parent directory */ struct qstr d_name; struct inode *d_inode; /* Where the name belongs to - NULL is negative */ unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */ /* Ref lookup also touches following */ struct lockref d_lockref; /* per-dentry lock and refcount */ const struct dentry_operations *d_op; struct super_block *d_sb; /* The root of the dentry tree */ unsigned long d_time; /* used by d_revalidate */ void *d_fsdata; /* fs-specific data */ union { struct list_head d_lru; /* LRU list */ wait_queue_head_t *d_wait; /* in-lookup ones only */ }; struct hlist_node d_sib; /* child of parent list */ struct hlist_head d_children; /* our children */ /* * d_alias and d_rcu can share memory */ union { struct hlist_node d_alias; /* inode alias list */ struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */ struct rcu_head d_rcu; } d_u; }; Burada yapının d_name elemanı qstr isimli bir yapı türündedir: struct qstr { union { struct { HASH_LEN_DECLARE; }; u64 hash_len; }; const unsigned char *name; }; İşte buradaki name elemanı ilgili dosyanın ismini belirtmektedir. Özetle biz file yapısından hareketle dosyanın ismini elde edebilmekteyiz. Böylece biz proc dosya sistemi içerisinde bir dosya açıldığında o dosyanın isminden hareketle hangi dosyanın açılmış olduğunu anlayabiliriz. Ayrıca dentry yapısının d_parent elemanı dosya ya da dizinin içinde bulunduğu dizine ilişkin dentry nesnesini vermektedir. Yani biz istersek dosyanın içinde bulunduğu dizinin ismini de alabiliriz. Aşağıda örneği bir bütün olarak veriyoruz. Aygıt sürücüyü yine aşağıdaki gibi derleyebilirsiniz: $ make file=pipe-driver Yüklemeyi aşağıdaki gibi yapabilirsiniz: $ sudo ./loadmulti 5 pipe-driver ndevices=5 Aygıt sürücüyü yükledikten sonra artık proc dosya sisteminde "pipe-driver" isimli bir dizin oluşturulacak ve bu dizin içerisinde de aşağıdaki gibi 5 dizin yaratılmış olacaktır: $ ls /proc/pipe-driver pipe0 pipe1 pipe2 pipe3 pipe4 Bu dosyaların her birinin içerisinde de "bufsize" ve "count" isimli iki dosya bulunacaktır. Burada testi "prog1.c" ve "prog2.c" programları yardımıyla yapabilirsiniz. Örneğin "prog1" programı ile "pipe-driver3" borusunu açıp içerisine bir şeyler yazarsanız "/proc/pipe-driver/pipe3/count" dosyasının içerisinde yazılan byte sayısını görebilirsiniz. Aygıt sürücümüzü yine "unloadmulti" script'i ile boşaltabilirsiniz. $ sudo ./unloadmulti 5 pipe-drive ---------------------------------------------------------------------------------------------------------------------------*/ /* pipe-driver.h */ #ifndef PIPEDRIVER_H_ #define PIPEDRIVER_H_ #include #include struct PIPE_PEEK { size_t size; void *buf; }; #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETCOUNT _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 1, size_t) #define IOC_PIPE_SETBUFSIZE _IOW(PIPE_DRIVER_MAGIC, 2, size_t) #define IOC_PIPE_PEEK _IOWR(PIPE_DRIVER_MAGIC, 3, struct PIPE_PEEK) #endif /* pipe-driver.c */ #include #include #include #include #include #include #include #include #include #include "pipe-driver.h" #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define NDEVICES 10 #define DEF_PIPE_BUFSIZE 10 #define MAX_PIPE_BUFSIZE 131072 /* 128K */ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static int proc_open(struct inode *inodep, struct file *filp); static int proc_release(struct inode *inodep, struct file *filp); static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off); static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release, .unlocked_ioctl = generic_ioctl }; static struct proc_ops g_proc_ops = { .proc_open = proc_open, .proc_release = proc_release, .proc_read = proc_read, }; /* static struct file_operations g_proc_ops = { .open = proc_open, .release = proc_release, .read = proc_read, }; */ struct PIPE_DEVICE { unsigned char *pipebuf; size_t head; size_t tail; size_t count; size_t bufsize; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; struct PROC_INFO { struct PIPE_DEVICE *pdevice; int filetype; /* 0 = count, 1 = bufsize */ char strbuf[32]; }; static int set_bufsize(struct PIPE_DEVICE *pdevice, unsigned long arg); static int read_peek(struct PIPE_DEVICE *pdevice, unsigned long arg); static dev_t g_dev; static struct PIPE_DEVICE *g_pdevices; static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init generic_init(void) { int result; dev_t dev; int i, k; struct proc_dir_entry *pde_root, *pde_pipe; char namebuf[16]; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } if ((pde_root = proc_mkdir("pipe-driver", NULL)) == NULL) { kfree(g_pdevices); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } for (i = 0; i < ndevices; ++i) { sprintf(namebuf, "pipe%d", i); if ((pde_pipe = proc_mkdir(namebuf, pde_root)) == NULL) { kfree(g_pdevices); unregister_chrdev_region(g_dev, 1); remove_proc_entry("pipe-driver", NULL); return -ENOMEM; } g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; g_pdevices[i].bufsize = DEF_PIPE_BUFSIZE; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); g_pdevices[i].pipebuf = (char *)kmalloc(DEF_PIPE_BUFSIZE, GFP_KERNEL); result = cdev_add(&g_pdevices[i].cdev, dev, 1); if (g_pdevices[i].pipebuf == NULL || result < 0) { if (g_pdevices[i].pipebuf != NULL) kfree(g_pdevices[i].pipebuf); for (k = 0; k < i; ++k) { cdev_del(&g_pdevices[k].cdev); kfree(g_pdevices[k].pipebuf); } kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "cannot add device!...\n"); return result; } if ((proc_create("count", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_pipe, &g_proc_ops)) == NULL || proc_create("bufsize", S_IRUSR|S_IRGRP|S_IROTH, pde_pipe, &g_proc_ops) == NULL) { remove_proc_entry("pipe-driver", NULL); for (k = 0; k < i; ++k) { cdev_del(&g_pdevices[k].cdev); kfree(g_pdevices[k].pipebuf); } unregister_chrdev_region(g_dev, 1); return -ENOMEM; } } return 0; } static void __exit generic_exit(void) { int i; for (i = 0; i < ndevices; ++i) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); remove_proc_entry("pipe-driver", NULL); unregister_chrdev_region(g_dev, ndevices); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (pdevice->count == 0) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqread, pdevice->count > 0)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->count, size); if (pdevice->tail <= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, pdevice->pipebuf + pdevice->head, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, pdevice->pipebuf, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->head = (pdevice->head + esize) % pdevice->bufsize; pdevice->count -= esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; if (size > pdevice->bufsize) size = pdevice->bufsize; while (pdevice->bufsize - pdevice->count < size) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqwrite, pdevice->bufsize - pdevice->count >= size)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->bufsize - pdevice->count, size); if (pdevice->tail >= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(pdevice->pipebuf + pdevice->tail, buf, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(pdevice->pipebuf, buf + size1, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->tail = (pdevice->tail + esize) % pdevice->bufsize; pdevice->count += esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqread); return esize; } static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct PIPE_DEVICE *pdevice; printk(KERN_INFO "ioctl"); pdevice = (struct PIPE_DEVICE *)filp->private_data; switch (cmd) { case IOC_PIPE_GETCOUNT: return put_user(pdevice->count, (size_t *)arg); case IOC_PIPE_GETBUFSIZE: return put_user(pdevice->bufsize, (size_t *)arg); case IOC_PIPE_SETBUFSIZE: return set_bufsize(pdevice, arg); case IOC_PIPE_PEEK: return read_peek(pdevice, arg); default: return -ENOTTY; } return 0; } static int set_bufsize(struct PIPE_DEVICE *pdevice, unsigned long arg) { char *new_pipebuf; size_t size; if (arg > MAX_PIPE_BUFSIZE) return -EINVAL; if (arg <= pdevice->count) return -EINVAL; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; if ((new_pipebuf = (char *)kmalloc(arg, GFP_KERNEL)) == NULL) { up(&pdevice->sem); return -ENOMEM; } if (pdevice->count != 0) { if (pdevice->tail <= pdevice->head) { size = pdevice->bufsize - pdevice->head; memcpy(new_pipebuf, pdevice->pipebuf + pdevice->head, size); memcpy(new_pipebuf + size, pdevice->pipebuf, pdevice->count - size); } else memcpy(new_pipebuf, pdevice->pipebuf + pdevice->head, pdevice->count); } pdevice->head = 0; pdevice->tail = pdevice->count; kfree(pdevice->pipebuf); pdevice->pipebuf = new_pipebuf; pdevice->bufsize = arg; up(&pdevice->sem); return 0; } static int read_peek(struct PIPE_DEVICE *pdevice, unsigned long arg) { size_t esize, size1, size2; struct PIPE_PEEK *userpp = (struct PIPE_PEEK *)arg; struct PIPE_PEEK pp; int status = 0; if (copy_from_user(&pp, userpp, sizeof(struct PIPE_PEEK)) != 0) return -EFAULT; if (pp.size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; esize = MIN(pdevice->count, pp.size); if (pdevice->tail <= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(pp.buf, pdevice->pipebuf + pdevice->head, size1) != 0) { status = -EFAULT; goto EXIT; } if (size2 != 0) if (copy_to_user(pp.buf + size1, pdevice->pipebuf, size2) != 0) { status = -EFAULT; goto EXIT; } if (put_user(esize, &userpp->size) != 0) status = -EFAULT; EXIT: up(&pdevice->sem); return status; } static int proc_open(struct inode *inodep, struct file *filp) { const char *file_name = filp->f_path.dentry->d_name.name; const char *parent_file_name = filp->f_path.dentry->d_parent->d_name.name; struct PROC_INFO *pi; printk(KERN_INFO "pipe-driver proc file opened...\n"); if ((pi = (struct PROC_INFO *)kmalloc(sizeof(struct PROC_INFO), GFP_KERNEL)) == NULL) return -ENOMEM; pi->pdevice = &g_pdevices[parent_file_name[4] - '0']; if (!strcmp(file_name, "bufsize")) pi->filetype = 1; else if (!strcmp(file_name, "count")) pi->filetype = 0; filp->private_data = pi; return 0; } static int proc_release(struct inode *inodep, struct file *filp) { struct PROC_INFO *pi; pi = (struct PROC_INFO *)filp->private_data; kfree(pi); return 0; } static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off) { struct PROC_INFO *pi; size_t esize, left; pi = (struct PROC_INFO *)filp->private_data; switch (pi->filetype) { case 0: sprintf(pi->strbuf, "%lu\n", (unsigned long)pi->pdevice->count); left = strlen(pi->strbuf) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, pi->strbuf + *off, esize) != 0) return -EFAULT; *off += esize; } break; case 1: sprintf(pi->strbuf, "%lu\n", (unsigned long)pi->pdevice->bufsize); left = strlen(pi->strbuf) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, pi->strbuf + *off, esize) != 0) return -EFAULT; *off += esize; } break; } return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod -m $mode ${module}$i c $major $i done /* unloadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done /* prog1.c */ #include #include #include #include #include #include #include "pipe-driver.h" #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len, bufsize, new_bufsize; if ((fd = open("pipe-driver3", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if (buf[0] == '!') { new_bufsize = atoi(&buf[1]); printf("%zd\n", new_bufsize); if (ioctl(fd, IOC_PIPE_SETBUFSIZE, new_bufsize) == -1) exit_sys("ioctl"); if (ioctl(fd, IOC_PIPE_GETBUFSIZE, &bufsize) == -1) exit_sys("ioctl"); printf("new pipe buffer size is %zu\n", bufsize); } else { len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #include #include "pipe-driver.h" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int count, size; ssize_t result; struct PIPE_PEEK pp; char *peekbuf; if ((pdriver = open("pipe-driver3", O_RDONLY)) == -1) exit_sys("open"); for (;;) { if (ioctl(pdriver, IOC_PIPE_GETCOUNT, &count) == -1) exit_sys("ioctl"); printf("There are (is) %d byte(s) in the pipe\n", count); printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if (size < 0) { pp.size = -size; if ((pp.buf = malloc(-size)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if (ioctl(pdriver, IOC_PIPE_PEEK, &pp) == -1) exit_sys("ioctl"); peekbuf = (char *)pp.buf; for (size_t i = 0; i < pp.size; ++i) putchar(peekbuf[i]); putchar('\n'); free(pp.buf); } else { if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de aygıt sürücülerde zamanlama işlemlerinin nasıl yapılacağı üzerinde duracağız. Çekirdeğin zamanlama mekanizması periyodik oluşturulan donanım kesmeleriyle sağlanmaktadır. Bu kesmelere genel olarak "timer kesmeleri" ya da Linux terminolojisinde "jiffy" denilmektedir. Eskiden tek CPU'lu makineler kullanıyordu ve eski işlemcilerde işlemcinin içerisinde periyodik kesme oluşturacak bir mekanizma yoktu. Ancak daha sonraları işlemcilere kendi içerisinde periyodik kesme oluşturabilecek timer devreleri eklendi. Bugün ağırlıklı olarak birden fazla çekirdeğe sahip işlemcileri kullanıyoruz. Bu işlemcilerin içerisindeki çekirdeklerin her birinde o çekirdekte periyodik kesme oluşturan (local interrupts) timer devreleri bulunmaktadır. Böylece her çekirdek kendi timer devresiyle "context switch" yapmakta ve proses istatistiklerini güncellemektedir. Bugün PC mimarisinde yerel çekirdeklerin kesme mekanizmaları dışında ayrıca bir de eski sistemlerde zaten var olan IRQ0 hattına bağlı global bir timer devresi de bulunmaktadır. Bu global timer devresi "context switch" yapmak için değil sistem zamanının ilerletilmesi amacıyla kullanılmaktadır. Bugünkü Linux sistemlerinde söz konusu olan bu timer devrelerinin hepsi 1 milisaniye, 4 milisaniye ya da 10 milisaniyeye kurulmaktadır. Eskiden ilk Linux çekirdeklerinde 10 milisaniyelik periyotlar kullanılıyordu. Sonra bilgisayarlar hızlanınca 1 milisaniye periyot yaygın olarak kullanılmaya başlandı. Ancak bugünlerde 4 milisaniye periyotları kullanan çekirdekler de yaygın biçimde bulunmaktadır. Aslında timer frekansı çekirdek konfigüre edilirken kullanıcılar tarafından da değiştirilebilmektedir. (Çekirdek derlenmeden önce çekirdeğin davranışları üzerinde etkili olan parametrelerin belirlenmesi sürecine "çekirdeğin konfigüre" edilmesi denilmektedir.) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 154. Ders 12/07/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Global timer kesmelerine (PC mimarisinde IRQ0) ilişkin kesme kodları çekirdek içerisindeki jiffies isimli bir global değişkeni artırmaktadır. Böylece eğer timer kesme periyodu biliniyorsa iki jiffies değeri arasındaki farka bakılarak bir zaman ölçümü mümkün olabilmektedir. Timer frekansı Linux kernel içerisindeki HZ isimli sembolik sabitle belirtilmiştir. Timer periyodu çekirdek konfigüre edilirken değiştirilebilmektedir. Genellikle bu süre 1 ms, 4 ms ya da 10 ms olmaktadır. (Ancak değişik mimarilerde farklı değerlerde olabilir.) Örneğin kursun yapıldığı sanal makinede timer periyodu 4 milisaniye'dir. Bu da saniyede 250 kez timer kesmesinin oluşacağı anlamına gelmektedir. Başka bir deyişle bu makinede HZ sembolik sabiti 250 olarak define edilmiştir. İşte timer kesmesi her oluştuğunda işletim sisteminin kesme kodu (interrupt handler) devreye girip "jiffies" isimli global değişkeni 1 artırmaktadır. Bu jiffies değişkeni unsigned long türdendir. Bilindiği gibi unsigned long türü 32 bit Linux sistemlerinde 32 bit, 64 bit Linux sistemlerinde 64 bittir. 32 bit Linux sistemlerinde ayrıca jiffies_64 isimli bir değişken daha vardır. Bu değişken hem 32 bit sistemde hem de 64 bit sistemde 64 bitliktir. 32 bit sistemde jiffies değişkeni 32 bit olduğu için bilgisayar uzun süre açık kalırsa taşma (overflow) oluşabilmektedir. Ancak 64 bit sistemlerde taşma mümkün değildir. 32 bit sistemlerde jiffies_64 değeri çekirdek tarafından iki ayrı makine komutuyla güncellenmektedir. Çünkü 32 bit sistemlerde 64 bit değeri belleğe tek hamlede yazmak mümkün değildir. Bu nedenle jiffies_64 değerinin taşma durumunda yanlış okunabilme olasılığı vardır. Hem 32 bit hem 64 bit sistemlerde 64 bitlik jiffies değerini düzgün bir biçimde okuyabilmek için get_jiffies_64 isimli fonksiyon bulundurulmuştur: #include u64 get_jiffies_64(void); Biz 32 bit sistemde de olsak bu fonksiyonla 64 bitlik jiffies değerini düzgün bir biçimde okuyabiliriz. Aşağıdaki örnekte çekirdek modülü içerisinde proc dosya sisteminde "jiffy-module" isimli bir dizin, dizinin içerisinde de "jiffy" ve "hertz" isimli iki dosya yaratılmıştır. "jiffy" dosyası okunduğunda o anki jiffies değeri elde edilmektedir. "hertz" dosyası okunduğunda ise timer frekansı elde edilmektedir. Aygıt sürücüyü aşağıdaki gibi derleyip yükleyebilirsiniz: $ make file=jiffy-module $ sudo insmod jiffy-module.ko Boşaltımı da şöyle yapabilirsiniz: $ sudo rmmod jiffy-module.ko ---------------------------------------------------------------------------------------------------------------------------*/ /* jiffy-module.c */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("jiffy module"); static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off); static struct proc_ops g_procops_jiffy = { .proc_read = proc_read_jiffy, }; static struct proc_ops g_procops_hertz = { .proc_read = proc_read_hertz, }; static char g_jiffies_str[32]; static char g_hertz_str[32]; static int __init generic_init(void) { struct proc_dir_entry *pde_dir; if ((pde_dir = proc_mkdir("jiffy-module", NULL)) == NULL) return -ENOMEM; if (proc_create("jiffy", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_jiffy) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("hertz", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_hertz) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("jiffy-module", NULL); printk(KERN_INFO "jiffy-module module exit...\n"); } static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_jiffies_str, "%lu\n", jiffies); left = strlen(g_jiffies_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_jiffies_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "jiffy file read...\n"); return esize; } static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_hertz_str, "%d\n", HZ); left = strlen(g_hertz_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_hertz_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "hertz file read...\n"); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /*-------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi eğer 64 bit sistemde çalışılıyorsa jiffies değerinin taşması (overflow olması) mümkün değildir. Ancak 32 bit sistemlerde timer frekansı 1000 ise 49 günde taşma meydana gelebilmektedir. Aygıt sürücü programcısı bazen geçen zamanı hesaplamak için iki noktada jiffies değerini alıp aradaki farka bakmak isteyebilmektedir. Ancak bu durumda 32 bit sistemlerde "overflow" olasılığının ele alınması gerekir. İşaretli sayıların ikili sistemdeki temsiline dayanarak iki jiffies arasındaki fark aşağıdaki gibi tek bir ifadeyle de hesaplanabilmektedir: unsigned long int prev_jiffies, next_jiffies; ... net_jiffies = (long) next_jiffies - (long) prev_jiffies; Çekirdek içerisinde iki jiffy değerini alarak bunları öncelik sonralık ilişkisi altında karşılaştıran aşağıdaki makrolar bulunmaktadır: #include time_after(jiffy1, jiffy2) time_before(jiffy1, jiffy2) time_after_eq(jiffy1, jiffy2) time_before_eq(jiffy1, jiffy2) Bu fonksiyonların hepsi bool bir değere geri dönmektedir. Bu fonksiyonlar 32 bit sistemlerde taşma durumunu da dikkate almaktadır. time_after fonksiyonu birinci parametresiyle belirtilen jiffy değerinin ikinci parametresiyle belirtilen jiffy değerinden sonraki bir jiffy değeri olup olmadığını belirlemekte kullanılmaktadır. Diğer fonksiyonlar da bu biçimde birinci parametredeki jiffy değeri ile ikinci parametredeki jiffy değerini karşılaştırmaktadır. Çekirdek içerisinde jiffies değerini çeşitli biçimlere dönüştüren aşağıdaki fonksiyonlar da bulunmaktadır: #include unsigned long msecs_to_jiffies(const unsigned int m); unsigned long usecs_to_jiffies(const unsigned int m); unsigned long usecs_to_jiffies(const unsigned int m); Bu işlemin tersini yapan da üç fonksiyon vardır: unsigned int jiffies_to_msecs(const unsigned long j); unsigned int jiffies_to_usecs(const unsigned long j); unsigned int jiffies_to_nsecs(const unsigned long j); Bu fonksiyonlar o andaki aktif HZ değerini dikkate almaktadır. Ayrıca jiffies değerini saniye ve nanosaniye biçiminde ayırıp bize struct timespec64 biçiminde bir yapı nesnesi olarak veren jiffies_to_timespec64 isimli bir fonksiyon da vardır. Bunun tersi timespec64_to_jiffies fonksiyonuyla yapılmaktadır. timespec64 yapısı da şöyledir: struct timespec64 { time64_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; Eski çekirdeklerde bu fonksiyonların yerine aşağıdaki fonksiyonlar bulunuyordu: #include unsigned long timespec_to_jiffies(struct timespec *value); void jiffies_to_timespec(unsigned long jiffies, struct timespec *value); unsigned long timeval_to_jiffies(struct timeval *value); void jiffies_to_timeval(unsigned long jiffies, struct timeval *value); Aşağıdaki örnekte proc dosya sisteminde "jiffy-module" dizini içerisinde ayrıca "difference" isimli bir dosya da yaratılmıştır. Bu dosya her okunduğunda önceki okumayla aradaki jiffy farkı yazdırılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* jiffy-module.c */ #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("jiffy module"); static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off); static struct proc_ops g_procops_jiffy = { .proc_read = proc_read_jiffy, }; static struct proc_ops g_procops_hertz = { .proc_read = proc_read_hertz, }; static struct proc_ops g_procops_difference = { .proc_read = proc_read_difference, }; static char g_jiffies_str[32]; static char g_hertz_str[32]; static char g_difference_str[512]; static int __init generic_init(void) { struct proc_dir_entry *pde_dir; if ((pde_dir = proc_mkdir("jiffy-module", NULL)) == NULL) return -ENOMEM; if (proc_create("jiffy", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_jiffy) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("hertz", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_hertz) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("difference", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_difference) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("jiffy-module", NULL); printk(KERN_INFO "jiffy-module module exit...\n"); } static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_jiffies_str, "%lu\n", jiffies); left = strlen(g_jiffies_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_jiffies_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "jiffy file read...\n"); return esize; } static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_hertz_str, "%d\n", HZ); left = strlen(g_hertz_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_hertz_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "hertz file read...\n"); return esize; } static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off) { static unsigned long prev_jiffies; loff_t left, esize; long int net_jiffies; struct timespec64 ts; net_jiffies = (long)jiffies - (long)prev_jiffies; jiffies_to_timespec64(net_jiffies, &ts); sprintf(g_difference_str, "Jiffy difference: %10ld (%ld seconds + %ld nanoseconds)\n", net_jiffies, (long)ts.tv_sec, (long)ts.tv_nsec); left = (loff_t)strlen(g_difference_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_difference_str + *off, esize) != 0) return -EFAULT; *off += esize; } prev_jiffies = jiffies; return (ssize_t)esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /*-------------------------------------------------------------------------------------------------------------------------- Aygıt sürücü içerisinde bazen belli bir süre bekleme yapmak gerekebilmektedir. Biz kursumuzda daha önce user modda bekleme yapan fonksiyonları görmüştük. Ancak o fonksiyonlar kernel modda kullanılamamaktadır. Kernel modda çekirdek içerisindeki olanaklarla bekleme yapılabilmektedir. Eğer bekleme süresi kısa ise bekleme işlemi meşgul bir döngü ile yapılabilir. Örneğin: while (time_before(jiffies, jiffies_target)) schedule(); Burada o anki jiffies değeri hedef jiffies değerinden küçükse schedule fonksiyonu çağrılmıştır. schedule fonksiyonu thread'i uykuya yatırmamaktadır. Yalnızca thread'ler arası geçiş oluşmasına yol açmaktadır. Yani bu fonksiyon uykuya dalmadan CPU'yu bırakmak için kullanılmaktadır. schedule fonksiyonunu çağıran thread çalışma kuyruğunda (run queue) kalmaya devam eder. Yine çalışma sırası ona geldiğinde kaldığı yerden çalışmaya devam eder. Ancak meşgul bir döngü içerisinde schedule işlemi yine önemli bir CPU zamanın harcanmasına yol açmaktadır. Bu nedenle uzun beklemelerin yukarıdaki gibi yapılması tavsiye edilmemektedir. Uzun beklemelerin uykuya dalarak yapılması gerekir. Uzun beklemeler için bir wait kuyruğu oluşturulup wait_event_timeout ya da wait_event_interruptible_timeout fonksiyonlarıyla koşul 0 yapılarak gerçekleştirilebilir. Ancak bunun için bir wait kuyruğunun oluşturulması gerekir. Bu işlemi zaten kendi içerisinde yapan özel fonksiyonlar vardır. schedule_timeout fonksiyonu belli bir jiffy zamanı geçene kadar thread'i çekirdek tarafından bu amaçla oluşturulmuş olan bir wait kuyruğunda bekletir. #include signed long schedule_timeout(signed long timeout); Fonksiyon parametre olarak beklenecek jiffy değerini alır. Eğer sinyal dolayısıyla fonksiyon sonlanırsa kalan jiffy sayısına, eğer zaman aşımının dolması nedeniyle fonksiyon sonlanırsa 0 değerine geri döner. Fonksiyon başarısız olmamaktadır. Fonksiyonu kullanmadan önce prosesin durum bilgisini set_current_state isimli fonksiyonla değiştirmek gerekir. Değiştirilecek durum TASK_UNINTERRUPTIBLE ya da TASK_INTERRUPTIBLE olabilir. Bu işlem yapılmazsa bekleme gerçekleşmemektedir. Örneğin: set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(jiffies + 5 * HZ); Uzun beklemeyi kendi içerisinde schedule_timeout kullanarak yapan üç yardımcı fonksiyon da vardır: #include void msleep(unsigned int msecs); unsigned long msleep_interruptible(unsigned int msecs); void ssleep(unsigned int secs); Aşağıdaki örnekte "jiffy-module" dizinindeki "sleep" dosyasından okuma yapıldığında (denemeyi cat komutuyla yapabilirsiniz) 10 saniye bekleme oluşacaktır. $ cat /proc/jiffy-module/sleep ---------------------------------------------------------------------------------------------------------------------------*/ /* jiffy-module.c */ #include #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("jiffy module"); static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_sleep(struct file *filp, char *buf, size_t size, loff_t *off); static struct proc_ops g_procops_jiffy = { .proc_read = proc_read_jiffy, }; static struct proc_ops g_procops_hertz = { .proc_read = proc_read_hertz, }; static struct proc_ops g_procops_difference = { .proc_read = proc_read_difference, }; static struct proc_ops g_procops_sleep = { .proc_read = proc_read_sleep, }; static char g_jiffies_str[32]; static char g_hertz_str[32]; static char g_difference_str[512]; static int __init generic_init(void) { struct proc_dir_entry *pde_dir; if ((pde_dir = proc_mkdir("jiffy-module", NULL)) == NULL) return -ENOMEM; if (proc_create("jiffy", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_jiffy) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("hertz", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_hertz) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("difference", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_difference) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("sleep", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_sleep) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("jiffy-module", NULL); printk(KERN_INFO "jiffy-module module exit...\n"); } static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_jiffies_str, "%lu\n", jiffies); left = strlen(g_jiffies_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_jiffies_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "jiffy file read...\n"); return esize; } static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_hertz_str, "%d\n", HZ); left = strlen(g_hertz_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_hertz_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "hertz file read...\n"); return esize; } static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off) { static unsigned long prev_jiffies; loff_t left, esize; long int net_jiffies; struct timespec64 ts; net_jiffies = (long)jiffies - (long)prev_jiffies; jiffies_to_timespec64(net_jiffies, &ts); sprintf(g_difference_str, "Jiffy difference: %10ld (%ld seconds + %ld nanoseconds)\n", net_jiffies, (long)ts.tv_sec, (long)ts.tv_nsec); left = (loff_t)strlen(g_difference_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_difference_str + *off, esize) != 0) return -EFAULT; *off += esize; } prev_jiffies = jiffies; return (ssize_t)esize; } static ssize_t proc_read_sleep(struct file *filp, char *buf, size_t size, loff_t *off) { /* set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(HZ * 10); */ ssleep(10); return 0; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /*-------------------------------------------------------------------------------------------------------------------------- 155. Ders 14/07/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Aygıt sürücü içerisinde kısa beklemeler gerebilmektedir. Çünkü bazı donanım aygıtlarının programlanabilmesi için bazı beklemelere gereksinim duyulabilmektedir. Kısa beklemeler meşgul döngü yoluyla yani hiç sleep yapılmadan sağlanmaktadır. Ayrıca kısa bekleme yapan fonksiyonlar atomiktir. Atomiklikten kastedilen şey threadler arası geçiş işleminin kapatılmasıdır. Yani kısa bekleme yapan fonksiyonlar threadler arası geçiş işlemini o işlemci için kapatırlar. Bu sırada thread'ler arası geçiş söz konusu olmamaktadır. Ancak donanım kesmeleri bu süre içerisinde oluşabilmektedir. Kısa süreli döngü içerisinde bekleme yapan fonksiyonlar şunlardır: void ndelay(unsigned int nsecs); void udelay(unsigned int usecs); void mdelay(unsigned int msecs); Burada delay nano saniye cinsinden bekleme yapmak için, udelay mikrosaniye cinsinden bekleme yapmak için, mdelay ise milisaniye cinsinden bekleme yapmak için kullanılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux çekirdeklerine belli versiyondan sonra bir timer mekanizması da eklenmiştir. Bu sayede aygıt sürücü programcısı belli bir zaman sonra belirlediği bir fonksiyonun çağrılmasını saplayabilmektedir. Bu mekanizmaya "kernel timer" mekanizması denilmektedir. Maalesef kernel timer mekanizması da birkaç kere arayüz olarak değiştirilmiştir. Bu mekanizma kullanılırken dikkat edilmesi gereken bir nokta callback fonksiyonun bir proses bağlamında çağrılmadığıdır. Yani callback fonksiyon çağrıldığında biz current makrosu ile o andaki prosese erişemeyiz. O anda çalışan prosesin user alanına kopyalamalar yapamayız. Çünkü callback fonksiyon timer kesmeleri tarafından çağrılmaktadır. Dolayısıyla callback fonksiyon çağrıldığında o anda hangi prosesin çalışmakta olduğu belli değildir. Son Linux çekirdeklerindeki kernel timer kullanımı şöyledir: 1) struct timer_list türünden bir yapı nesnesi statik düzeyde tanımlanır ve bu yapı nesnesine ilk değeri verilir. DEFINE_TIMER makrosu ile hem tanımlama hem de ilkdeğer verme işlemi birlikte yapılabilir. Makro şöyledir: #include #define DEFINE_TIMER(_name, _function) Örneğin: DEFINE_TIMER(g_mytimer, timer_proc); Ya da alternatif olarak struct timer_list nesnesi yaratılıp timer_setup makrosuyla da ilkdeğer verilebilir. Makronun parametrik yapısı şöyledir: #include #define timer_setup(timer, callback, flags) Makronun birinci parametresi timer nesnesinin adresini almaktadır. İkinci parametresi çağrılacak fonksiyonu belirtir. flags parametresi 0 geçilebilir. Örneğin: static struct timer_list g_mytimer; timer_setup(&g_mytimer, timer_proc, 0); Buradaki timer fonksiyonunun parametrik yapısı şöyle olmalıdır: void timer_proc(struct timer_list *tlisr); 2) Tanımlanan struct timer_list nesnesi add_timer fonksiyonu ile bir bağlı listeye yerleştirilir. Bu bağlı liste çekirdeğin içerisinde çekirdek tarafından oluşturulmuş bir listedir. add_timer fonksiyonunun prototipi şöyledir: #include void add_timer(struct timer_list *timer); 3) Daha sonra ne zaman fonksiyonun çağrılacağını anlatmak için mod_timer fonksiyonu kullanılır. #include int mod_timer(struct timer_list *timer, unsigned long expires); Buradaki expires parametresi jiffy türündendir. Bu parametre hedef jiffy değerini içermelidir. (Yani jiffies + gecikme jiffy değeri) 4) Timer nesnesinin silinmesi için del_timer ya da del_timer_sync fonksiyonu kullanılmaktadır: #include int del_timer(struct timer_list * timer); int del_timer_sync(struct timer_list * timer); del_timer fonksiyonu eğer timer fonksiyonu o anda başka bir işlemcide çalışıyorsa asenkron biçimde silme yapar. Yani fonksiyon sonlandığında henüz silme gerçekleşmemiş göreli bir süre sonra gerçekleşecek olabilir. Halbuki del_timer_sync fonksiyonu geri dönünce timer silinmesi gerçekleşmiş olur. Eğer timer silinmezse modül çekirdekten atıldığında tüm sistem çökebilir. Normal olarak belirlenen fonksiyon yalnızca 1 kez çağrılmaktadır. Ancak bu fonksiyonun içerisinde yeniden mod_timer çağrılarak çağırma periyodik hale getirilebilir. Aşağıda kernel timer kullanımına basit bir örnek verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* timer-module.c */ #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Timer Module"); static void timer_proc(struct timer_list *tlist); DEFINE_TIMER(g_mytimer, timer_proc); static int __init generic_init(void) { add_timer(&g_mytimer); mod_timer(&g_mytimer, jiffies + msecs_to_jiffies(5000)); printk(KERN_INFO "timer-module module init...\n"); return 0; } static void timer_proc(struct timer_list *tlist) { static int count = 0; if (count == 5) { del_timer(&g_mytimer); count = 0; return; } ++count; printk(KERN_INFO "timer callback (%d)\n", count); mod_timer(&g_mytimer, jiffies + msecs_to_jiffies(5000)); } static void __exit generic_exit(void) { printk(KERN_INFO "timer-module module exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /*-------------------------------------------------------------------------------------------------------------------------- 156. Ders 19/07/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Önceki konularda da UNIX/Linux sistemlerinde kernel mode'da çalışan işletim sistemine ait thread'ler olduğundan bahsetmiştik. Bu thread'ler çalışma kuyruğunda (run queue) bulunan ve uykuya dalabilen işletim sisteminin bir parçası durumundaki thread'lerdir. İşletim sistemine ait bu thread'ler çeşitli işlemlerden sorumludurlar. Linux işletim sisteminde kernel thread'ler genellikle "user mode daemon"lar gibi sonu 'd' ile bitecek biçimde isimlendirilmiştir. Ancak çekirdeğe ait olan bu thread'lerin ismi 'k' (kernel'dan geliyor) ile başlatılmıştır. Örneğin "kupdated", "kswapd", "keventd" gibi. İşte aygıt sürücüler de isterlerse arka planda kernel mode'da bir proses gibi çalışan thread'ler yaratabilirler. Ancak bu thread'ler bir proses ile ilişkisiz biçimde çalıştırılmaktadır. Bu nedenle bunlar içerisinde current makrosu, ve copy_to_user ya da copy_from_user gibi fonksiyonlar kullanılamaz. Aygıt sürücü kodlarımız genellikle bir olay olduğunda (örneğin kesme gibi) ya da user mode'dan çağrıldığında (read, write, ioctl gibi) çalıştırılmaktadır. Ancak kernel thread'ler aygıt sürücüye sanki bir programmış gibi kernel mode'da sürekli çalışma imkanı vermektedir. Kernel thread'ler sırasıyla şu adımlarlardan geçilerek kullanılmaktadır: 1) Önce kernel thread aygıt sürücü içerisinde yaratılır. Yaratılma modülün init fonksiyonunda yapılabileceği gibi aygıt sürücü ilk kez açıldığında open fonksiyonunda ya da belli bir süre sonra belli bir fonksiyonda da yapılabilmektedir. Kernel thread'ler kthread_create fonksiyonuyla yaratılmaktadır: #include struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char *namefmt); Fonksiyonun birinci parametresi thread akışının başlatılacağı fonksiyonun adresini almaktadır. Bu fonksiyon void * türünden parametreye ve int geri dönüş değerine sahip olmak zorundadır. Fonksiyonun ikinci parametresi thread fonksiyonuna geçirilecek parametreyi belirtmektedir. Eğer bir kernel thread'e bir parametre geçirilmek istenmiyorsa bu parametre için NULL adres girilebilir. Fonksiyonun üçüncü parametresi proc dosya sisteminde (dolayısıyla "ps" komutunda) görüntülenecek ismi belirtir. Fonksiyon başarı durumunda yaratılan thread'in task_struct adresine, başarısızlık durumunda negatif errno değerine geri dönmektedir. Adrese geri dönen diğer kernel fonksiyonlarında olduğu gibi fonksiyonun başarı durumu IS_ERR makrosuyla test edilmelidir. Eğer fonksiyon başarısız olmuşsa negatif errno değeri PTR_ERR makrosuyle elde edilebilir. Örneğin: struct task_struct *ts; ts = kthread_create(...); if (IS_ERR(ts)) { printk(KERN_ERROR "cannot create kernel thread!..") return PTR_ERR(ts); } Anımsanacağı gibi Linux sistemlerinde prosesler ve thread'ler task_struct yapısıyla temsil edilmektedir. İşte bu fonksiyon da başarı durumunda çekirdek tarafından yaratılan task_struct nesnesinin adresini bize vermektedir. Kernel thread bu fonksiyonla yaratıldıktan sonra hemen çalışmaz. Onu çalıştırmak için wake_up_process fonksiyonun çağrılması gerekir: #include int wake_up_process(struct task_struct *tsk); Fonksiyon ilgili kernel thread'in task_struct adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda negatif errno değerine geri döner. Aslında yukarıdaki işlemi tek hamlede yapan kthread_run isimli bir fonksiyon da vardır: #include struct task_struct *kthread_run(int (*threadfn)(void *data), void *data, const char *namefmt); 2) Kernel thread kthread_stop fonksiyonuyla herhangi bir zaman ya da aygıt sürücü bellekten atılırken yok edilebilir: #include int kthread_stop(struct task_struct *ts); Fonksiyon thread sonlanana kadar blokeye yol açar. Fonksiyon thread fonksiyonunun exit koduyla (yani thread fonksiyonunun geri dönüş değeri ile) geri dönmektedir. Genellikle programcılar thread fonksiyonlarını başarı durumunda sıfır, başarısızlık durumunda sıfır dışı bir değerle geri döndürmektedir. Burada önemli nokta kthread_stop fonksiyonunun kernel thread'i zorla sonlandırılmadığıdır. Kernel thread'in sonlanması zorla yapılmaz. kthread_stop fonksiyonu bir bayrağı set eder. Kernel thread de tipik olarak bir döngü içerisinde "bu bayrak set edilmiş mi" diye bakar. Eğer bayrak set edilmişse kendini sonlandırır. Kernel thread'in bu bayrağa bakması kthread_should_stop fonksiyonuyla yapılmaktadır. #include bool kthread_should_stop(void); Fonksiyon eğer bu flag set edilmişse sıfır dışı bir değere, set edilmediyse 0 değerine geri dönmektedir. Tipik olarak kernel thread fonksiyonu aşağıdaki gibi bir döngüde yaşamını geçirir: while (!kthread_should_stop()) { ... } Tabii aslında biz kthread_create fonksiyonu ile bir kernel thread yaratmak istediğimizde asıl thread'in başlatıldığı fonksiyon çekirdek içerisindeki bir fonksiyondur. Bizim kthread_create fonksiyonuna verdiğimiz fonksiyon bu fonksiyon tarafından çağrılmaktadır. Dolayısıyla bizim fonksiyonumuz bittiğinde akış yine çekirdek içerisindeki asıl fonksiyona döner. O fonksiyonda da yaratılmış thread kaynakları otomatik boşaltılır. Yani biz bir thread yarattığımız zaman onun yok edilmesi thread fonksiyonu bittiğinde otomatik yapılmaktadır. Kernel thread'in kendisini sonlandırması do_exit fonksiyonuyla sağlanabilmektedir. Aslında do_exit fonksiyonu prosesleri sonlandıran sys_exit fonksiyonunun doğrudan çağırdığı taban fonksiyondur. #include void do_exit(long code); Fonksiyon thread'in exit kodunu parametre olarak almaktadır. Kernel thread normal biçimde ya da kthread_stop fonksiyonuyla sonlanmışsa artık thread'in tüm kaynakları (task_struct yapısı da) serbest bırakılmaktadır. Dolayısıyla artık ona kthread_stop uygulamamak gerekir. Aşağıdaki örnekte modül initialize edilirken kernel thread yaratılmış, modül yok edilirken kthread_stop ile kernel-thread'in sonlanması beklenmiştir. kernel-thread içerisinde msleep fonksiyonu ile 1 saniyelik beklemeler yapılmıştır. ---------------------------------------------------------------------------------------------------------------------------*/ /* kernel-thread-module.c */ #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Kernel Thread Module"); static int kernel_thread_proc(void *param); struct task_struct *g_ts; static int __init generic_init(void) { printk(KERN_INFO "kernel-thread-module init...\n"); g_ts = kthread_run(kernel_thread_proc, NULL, "kmythreadd"); if (IS_ERR(g_ts)) { printk(KERN_ERR "cannot create kernel thread!...\n"); return PTR_ERR(g_ts); } return 0; } static int kernel_thread_proc(void *param) { static int count; printk(KERN_INFO "kernel-thread starts...\n"); while (!kthread_should_stop()) { printk(KERN_INFO "kernel-thread is running: %d\n", count); msleep(1000); ++count; } return 0; } static void __exit generic_exit(void) { int ecode; ecode = kthread_stop(g_ts); printk(KERN_INFO "kernel thread exits with code \"%d\"\n", ecode); printk(KERN_INFO "kernel-thread-module exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /*-------------------------------------------------------------------------------------------------------------------------- 157. Ders 21/07/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İşlemcinin çalıştırmakta olduğu koda ara vererek başka bir kodu çalıştırması ve çalıştırma bittikten sonra kaldığı yerden devam etmesi sürecine "kesme (interrupt)" denilmektedir. Kesmeler oluşma biçiminde göre üçe ayrılmaktadır: 1) Donanım Kesmeleri (Hardware Interrupts) 2) İçsel Kesmeler (Internal Interrupts) 3) Yazılım Kesmeleri (Software Interrupts) Kesme denildiğinde akla default olarak donanım kesmeleri gelmektedir. Donanım kesmeleri CPU'nın bir ucunun (genellikle bu uca INT ucu denilmektedir) elektriksel olarak dışsal bir birim tarafından uyarılmasıyla oluşmaktadır. Yani donanım kesmeleri o anda çalışmakta olan koddan bağımsız bir biçimde dış dünyadaki birimler tarafından oluşturulmaktadır. PC terminolojisinde donanım kesmesi oluşturan kaynaklara IRQ da denilmektedir. İçsel kesmeler CPU'nun kendi çalışması sırasında kendisinin oluşturduğu kesmelerdir. Intel bu tür kesmelerin önemli bir bölümünü "fault" olarak isimlendirmektedir. Örneğin fiziksel RAM'de olmayan bir sayfaya erişildiğinde CPU "page fault" denilen içsel kesme oluşturmaktadır. Yazılım kesmeleri ise programcının program koduyla oluşturduğu kesmelerdir. Her türlü CPU'da yazılım kesmesi oluşturulamamaktadır. Bir kesme oluştuğunda çalıştırılan koda "kesme kodu (interrupt handler)" denilmektedir. Donanım kesmesi oluşturan elektronik birimlerin hepsi doğrudan CPU'nın INT ucuna bağlanmamaktadır. Çünkü bunun pek çok sakıncası vardır. Genellikle bu amaçla bu işe aracılık eden daha akıllı işlemciler kullanılmaktadır. Bu işlemcilere genel olarak "kesme denetleyicileri (interrupt controllers)" denilmektedir. Bazı mimarilerde kesme denetleyicisi işlemcinin içerisinde bulunmaktadır. Bazı mimarilerde ise dışarıda ayrı bir entegre devre olarak bulunmaktadır. Tabii artık pek çok entegre devre SoC (System on Chip) adı altında tek bir entegre devrenin içerisine yerleşirilmiş durumdadır. Kesme denetleyicilerinin temel işlevi şöyledir: 1) Birden fazla donanım biriminin aynı anda kesme oluşturması durumunda kesme denetleyicisi bunları sıraya dizebilmektedir. 2) Birden fazla donanım biriminin aynı anda kesme oluşturması durumunda kesme denetleyicisi bunlara öncelik verebilmektedir. 3) Belli birimlerden gelen kesme isteklerini kesme denetleyicisi görmezden gelebilmektedir. Buna ilgili IRQ'nun disable edilmesi denilmektedir. 4) Kesme denetleyicileri çok çekirdekli donanımlarda kesmenin belli bir çekirdekte çalıştırılabilmesini sağlayabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bugün kullandığımız PC'lerde (laptop ve notebook'lar da dahil olmak üzere) eskiden kesme denetleyicisi olarak bir tane Intel'in 8259 (PIC) denilen entegre devresi kullanılıyordu. Bunun 8 girişi bulunuyordu. Yani bu kesme denetleyicisinin uçları 8 ayrı donanım birimine bağlanabiliyordu. | (INT ucu CPU'ya bağlanır) <8259 (PIC)> | | | | | | | | 0 1 2 3 4 5 6 7 Bu uçlara IRQ uçları deniliyordu ve bu uçlar değişik donanım birimlerine bağlıydı. Böylece bir donanım birimi kesme oluşturmak isterse kesme denetleyicisinin igili ucunu uyarıyordu. Kesme denetleyicisi de CPU'nun INT ucunu uyarıyordu. İlk PC'lerde toplam 8 IRQ vardı. Ancak 80'li yılların ortalarında PC mimarisinde değişikler yapılarak kesme denetleyicisinin sayısı ikiye yükseltildi. Böylece IRQ uçlarının sayısı da 15'e yükseltilmiş oldu. Intel'in iki 8259 işlemcisini katkat bağlayabilmek için birinci kesme denetleyicisinin (Master PIC) bir ucunun ikinci kesme denetleyicisinin INT ucuna bağlanması gerekmektedir. İşte PC mimarisinde birinci kesme denetleyicisinin 2 numaralı ucu ikinci kesme denetleyicisine bağlanmıştır. Böylece toplam IRQ'ların sayısı 16 değil, 15 olmaktadır. | (INT ucu CPU'ya bağlanır) | <8259 (PIC)> <8259 (PIC)> | | X | | | | | | | | | | | | | 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Ancak zamanla 15 IRQ ucu da yetersiz kalmaya başlamıştır. Çok çekirdekli sistemlerde her çekirdeğin (yani CPU'nun) ayrı bir INT ucu vardır. Yani bu çekirdekler diğerlerinden bağımsız kesme alabilmektedir. İşte zamanla Intel'in klasik 8259 kesme denetleyicisi daha gelişmiş olan ve ismine IOAPIC denilen kesme denetleyicisi ile değiştirilmiştir. Bugün kullandığımız Intel tabanlı bilgisayar mimarisinde artık IOAPIC kesme denetleyicileri bulunmaktadır. Bu yeni kesme denetleyicisinin 24 IRQ ucu vardır. IOAPIC birden fazla çekirdeğin bulunduğu durumda tek bir çekirdeğe değil, tüm çekirdeklere bağlanmaktadır. Dolayısıyla istenilen bir çekirdekte kesme oluşturabilmektedir. IOAPIC devresinin bazı uçları bazı donanım birimlerine bağlı biçimdedir. Ancak bazı uçları boştadır. Bugün kullanılan ve ismine PCI ya da PCI-X denilen genişleme yuvalarının bazı uçları bu IOAPIC ile bağlantılıdır. Dolayısıyla genişleme yuvalarına takılan kartlar da IRQ oluşturabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bugün Pentium ve eşdeğer AMD işlemcilerinin içerisinde (her çekirdeğin içerisinde) aynı zamanda ismine "Local APIC" denilen bir kesme denetleyicisi de vardır. Bu local APIC iki uca sahiptir. Local APIC içerisinde aynı zamanda bir timer devresi de bulunmaktadır. Bu timer devresi periyodik donanım kesmesi oluşturmak için kullanılmaktadır. Intel ve AMD çekirdeklerinin içerisinde bulunan APIC devresinin en önemli özelliği kesmeleri artık uçlarla değil, veri yoluyla (data bus) oluşturabilmesidir. Bu özellik sayesinde hiç işlemcinin INT uyarılmadan çok fazla sayıda kesme sanki belleğe bir değer yazıyormuş gibi oluşturulabilmektedir. Bu tekniğe "Message Signaled Interrupt (MSI)" denilmektedir. Gerçekten de bugün PCI slotlara takılan bazı kartlar kesmeleri doğrudan belli bir çekirdekte MSI kullanarak oluşturmaktadır. O halde kullanığımız Intel tabanlı PC mimarisindeki bugünkü durum şöyledir: - Bazı donanım birimleri built-in biçimde IOAPIC'in uçlarına bağlı durumdadır. Bu uçlar eskiye uyumu korumak için 8259'un uçlarıyla aynı biçimde bağlıymış gibi IRQ oluşturmaktadır. - Bazı PCI kartlar slot üzerindeki 4 IRQ hattından (INTA, INTB, INTC, INTD) birini kullanarak kesme oluşturmaktadır. Bu hatlar IOAPIC'in bazı uçlarına bağlıdır. - Bazı PCI kartlar ise doğrudan modern MSI sistemini kullanarak IOAPIC'i pass geçerek bellek işlemleriyle doğrudan ilgili çekirdekte kesme oluşturabilmektedir. Bir aygıt sürücü programcısı mademki birtakım kartlar için onu işler hale getiren temel yazılımları da yazma iddiasındadır. O halde o kartın kullanacağı kesme için kesme kodlarını (interrupt handlers) yazabilmelidir. Tabii işletim sisteminin aygıt sürücü mimarisinde bu işlemler de özel kernel fonksiyonlarıyla yapılır. Yani kesme kodu yazmanın belli bir kuralı vardır. Pekiyi çok çekirdekli bilgisayar sistemlerinde oluşan bir kesme Intel tabanlı PC mimarisinde hangi çekirdek tarafından işlenmektedir? İşte bugün kullanılan IOAPIC devreleri bu bakımdan şu özelliklere sahiptir: 1) Kesme IOAPIC tarafından donanım biriminin istediği bir çekirdekte oluşturulabilir. 2) Kesme IOAPIC tarafından en az yüklü çekirdeğe karar verilerek orada oluşturulabilmektedir. 3) Kesme IOAPIC tarafından döngüsel bir biçimde (yani sırasıyla her bir çekirdekte) oluşturulabilmektedir. IOAPIC'in en az yüklü işlemciyi bilmesi mümkün değildir. Onu ancak işletim sistemi bilebilir. İşte işlemcilerin Local APIC'leri içerisinde özel bazı yazmaçlar vardır. Aslında IOAPIC bu yazmaçtaki değerlere bakıp en düşüğünü seçmektedir. Bu değerleri de işletim sistemi set eder. İşletim sisteminin yaptığı bu faaliyete "kesme dengeleme (IRQ balancing)" denilmektedir. Linux sistemlerinde bir süredir kesme dengelemesi işletim sisteminin kernel thread'i (irqbalance) tarafından yapılmaktadır. Böylece Linux sistemlerinde aslında donanım kesmeleri her defasında farklı çekirdeklerde çalıştırılıyor olabilir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pek çok CPU ailesinde donanım kesmelerinin teorik maksimum bir limiti vardır. Örneğin Intel mimarisinde toplam kesme sayısı 256'yı geçememektedir. Yani bu mimaride en fazla 256 farklı kesme oluşturulabilmektedir. Bu mimaride her kesmenin bir numarası vardır. IRQ numarası ile kesme numarasının bir ilgisi yoktur. Biz örneğin PIC ya da IOAPIC'i programlayarak belli bir kesmenin belli bir IRQ için belli numaralı bir kesmenin oluşmasını sağlayabiliriz. Örneğin timer (IRQ-0) için 8 numaralı kesmenin çalışmasını sağlayabiliriz. Pekiyi bir IRQ oluşturulduğunda çekirdek kaç numaralı kesme kodunun çalıştırılacağını nereden anlamaktadır? İşte PIC ya da IOAPIC CPU'nun INT ucunu uyararak kesme oluştururken veri yolunun ilk 8 ucundan kesme numarasını da CPU'ya bildirmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 158. Ders 28/07/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Şimdi de aygıt sürücüler içerisinde kesmelerin nasıl ele alınacağı üzerinde duralım. Bir donanım kesmesi oluştuğunda aslında işletim sisteminin kesme kodu (interrupt handler) devreye girmektedir. Ancak işletim sisteminin kesme kodu istek doğrultusunda aygıt sürücülerin içerisindeki fonksiyonları çağırabilmektedir. Farklı aygıt sürücüleri aynı IRQ için istekte bulunabilir. Bu durumda işletim sistemi IRQ oluştuğunda farklı aygıt sürücülerdeki fonksiyonları belli bir düzen içerisinde çağırmaktadır. Aygıt sürücü programcısı bir kesme oluştuğunda aygıt sürücüsünün içerisindeki bir fonksiyonunun çağrılmasını istiyorsa önce onu request_irq isimli kernel fonksiyonuyla register ettirmelidir. #include int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id); Fonksiyonun birinci parametresi IRQ numarasını, ikinci parametresi IRQ oluştuğunda çağrılacak fonksiyonu belirtmektedir. Bu fonksiyonun geri dönüş değeri irqreturn_t türünden parametreleri de sırasıyla int ve void * türündendir. Örneğin: irqreturn_t my_irq_handler(int irq, void *dev_id) { ... } Buradaki irqreturn_t türü bir enum türü olarak typedef edilmiştir. Bu enum türünün elemanları şunlardır: enum irqreturn { IRQ_NONE = (0 << 0), IRQ_HANDLED = (1 << 0), IRQ_WAKE_THREAD = (1 << 1), }; typedef enum irqreturn irqreturn_t; request_irt fonksiyonunun üçüncü parametresi bazı bayraklardan oluşur. Bu bayrak 0 geçilebilir ya da örneğin IRQF_SHARED geçilebilir. Diğer seçenekler için dokümanlara başvurabilirsiniz. IRQF_SHARED aynı kesmenin birden fazla aygıt sürücü tarafından kullanılabileceği anlamına gelmektedir. (Tabii biz ilk register ettiren değilsek daha önce register ettirenlerin bu bayrağı kullanmış olması gerekir. Aksi halde biz de bu bayrağı kullanamayız.) Fonksiyonun dördüncü parametresi "/proc/interrupts" dosyasında görüntülenecek ismi belirtir. Son parametre ise sistem genelinde tek olan bir nesnenin adresi olarak girilmelidir. Aygıt sürücü programcıları bu parametreye tipik olarak aygıt yapısını ya da çağrılacak fonksiyonu girerler. Bu parametre IRQ handler fonksiyonuna ikinci parametre olarak geçilmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda negatif hata değerine geri dönmelidir. Örneğin: if ((result = request_irq(1, my_irq_handler, IRQF_SHARED, "my_irq1", NULL)) != 0) { ... return result; } Bir kesme kodu request_irq fonksiyonuyla register ettirilmişse bunun geri alınması free_irq fonksiyonuyla yapılmaktadır: #include const void *free_irq(unsigned int irq, void *dev_id); Fonksiyonun birinci parametresi silinecek irq numarasını, ikinci parametresi irq_reuest fonksiyonuna girilen son parametreyi belirtir. Fonksiyon başarı durumunda aygıt irq_request fonksiyonunda verilen isme, başarısızlık durumunda NULL adrese geri dönmektedir. Geri dönüş değeri bir hata kodu içermemektedir. Normal olarak fonksiyonun ikinci parametresine request_irq fonksiyonunun son parametresiyle aynı değer geçilir. Bu parametrenin neden bu fonksiyona geçirildiğinin bazı ayrıntıları vardır. Örneğin: if (free_irq(1, NULL) == NULL) printk(KERN_INFO "cannot free IRQ\n"); Pekiyi IRQ fonksiyonundan (IRQ handler) hangi değerle geri dönülmelidir. Aslında programcı bu fonksiyondan ya IRQ_NONE değeri ile ya da IRQ_HANDLED değeri ile geri döner. Eğer programcı kesme kodu içerisinde yapmak istediği şeyi yapmışsa fonksiyondan IRQ_HANDLED, yapamamışsa ya da yapmak istememişse fonksiyondan IRQ_NONE değeri ile geri döner. Örneğin: static irqreturn_t my_irq_handler(int irq, void *dev_id) { ... return IRQ_HANDLED; } ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bazen sistem programcısı belli bir IRQ'yu belli süre için disable etmek isteyebilir. Bunun için disable_irq ve enable_irq isimli iki kernel fonksiyonu kullanılmaktadır. Bu fonksiyonlar belli numaralı bir IRQ'yu disable ve enable etmektedir. Ancak bu fonksiyonlar bu işlemi doğrudan kesme denetleyicisini (PIC ya da IOAPIC) programlayarak yapmamaktadır. #include void disable_irq(unsigned int irq); void enable_irq(unsigned int irq); Fonksiyonlar IRQ numarasını parametre olarak alır. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Donanımsal kesme mekanizmasının tipik örneklerinden biri klavye kullanımıdır. PC klavyesinde bir tuşa basıldığında klavye içerisindeki işlemci (keyboard encoder - Eskiden Intel 8048 ya da Holtek HT82K629B) basılan ya da çekilen tuşun klavyedeki sıra numarasını (buna "scan code" denilmektedir) dış dünyaya seri bir biçimde kodlamaktadır. Bu bilgi bilgisayardaki klavye denetleyicisine (Eskiden Intel 8042) gelir. Klavye denetleyicisi (keyboard controller) bu scan kodu kendi içerisinde bir yazmaçta saklar. PIC ya da IOAPIC'in 1 numaralı ucu klavye denetleyicisine bağlıdır ve bu uçtan IRQ1 kesmesini oluşturulmaktadır. Dolayısıyla biz bir tuşa bastığımızda otomatik olarak basılan tuşa ilişkin klavye scan kodu bilgisayar tarafına iletilir ve IRQ1 kesmesi oluşturulur. IRQ1 kesme kodu birincil olarak işletim sistemi tarafından ele alınmaktadır. İşletim sistemi de bu IRQ oluştuğunda aygıt sürücülerin belirlediği fonksiyonları çağırmaktadır. Klavyede yalnızca bir tuşa basılınca değil, parmak tuştan çekildiğinde de yine klavye işlemcisi çekilen tuşun scan kodunu klavye denetleyicisine gönderip IRQ1 kesmesinin oluşmasına yol açmaktadır. Yani hem tuşa basınca hem de parmak tuştan çekildiğinde IRQ1 oluşmaktadır. Klavye terminolojisinde parmağın tuşa basılmasıyla gönderilen scan koda "make code", parmağın tuştan çekilmesiyle gönderilen koda ise "break code" denilmektedir. Bugün PC'lerde kullandığımız klavyelerde parmak tuştan çekildiğinde önce PC tarafında bir F0 byte'ı ve sonra da tuşun scan kodu gönderilmektedir. Örneğin parmağımızı "A" tuşuna basıp çekelim. Şu scan kodlar bilgisayar tarafına gönderilecektir: Ctrl, Shift, Alt, Caps-Lock gibi tuşların diğer tuşlardan bir farkı yoktur. Ctrl+C gibi bir tuşa basıldığı işletim sisteminin tuttuğu flag değişkenlerle tespit edilmektedir. Örneğin biz Ctrl+C tuşlarına basıp çekmiş olalım. Klavye işlemcisi bilgisayar tarafında şu kodları gönderecektir: İşte Ctrl tuşuna basıldığını fark eden işletim sistemi bir flag'i set eder, parmak bu tuştan bırakıldığında flag'i reset eder. Böylece diğer tuşlara basıldığında bu flag'e bakılarak Ctrl tuşu ile bu tuşa basılıp basılmadığı anlaşılmaktadır. Pekiyi biz Linux'ta stdin dosyasından (0 numaralı betimleyici) okuma yaptığımızda neler olmaktadır? İşte aslında işletim sistemi bir tuşa basıldığında basılan tuşları klavye denetleyicisinden alır ve onları bir kuyruk sisteminde saklar. Terminal aygıt sürücüsü de bu kuyruğa başvurur. Kuyrukta hiç tuş yoksa thread'i bu amaçla oluşturulmuş bir wait kuyruğunda bekletir. Klavyeden tuşa basılınca wait kuyruğunda bekleyen thread'leri uyandırır. Yani okuma yapıldığında o anda klavyeden okuma yapılmamaktadır. Kuyruklanmış tuşlar okunmaktadır. Klavyedeki tuşların üzerinde yazan harflerin hiçbir önemi yoktur. Yani İngilizce klavye ile Türkçe klavye aynı tuşlar için aynı scan kodu göndermektedir. Basılan tuşun hangi tuş olduğu aslında dil ayarlarına bakılarak işletim sistemi tarafından anlamlandırılmaktadır. Klavye ile bilgisayar arasındaki iletişim tek yönlü değil çift yönlüdür. Yani klavye denetleyicisi de (PC tarafındaki denetleyici) isterse klavye içerisindeki işlemciye komutlar gönderebilmektedir. Aslında klavye üzerindeki ışıkların yakılması da klavyenin içerisinde tuşa basılınca yapılmamaktadır. Işıklı tuşlara basıldığında gönderilen scan kod klavye denetleyicisi tarafından alınır, eğer bu tuş ışıklı tuşlardan biri ise klavye denetleyicisi klavye işlemcisine "falanca ışığı yak" komutunu göndermektedir. Özetle yeniden ifade edersek klavyedeki ışıklar klavye devresi tarafından ilgili tuşlara basılınca yakılmamaktadır. Karşı taraftan emir geldiğinde yakılmaktadır. Tasarımın bu biçimde yapılmış olması çok daha esnek bir kullanım oluşturmaktadır. Aşağıdaki örnekte klavyeden tuşa basıldığında ve çekildiğinde oluşan 1 numaralı IRQ ele alınıp işlenmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* irq-driver.c */ #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, }; static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static int __init generic_init(void) { int result; if ((result = alloc_chrdev_region(&g_dev, 0, 1, "irq-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; if ((result = cdev_add(g_cdev, g_dev, 1)) != 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "cannot add character device driver!...\n"); return result; } if ((result = request_irq(1, keyboard_irq_handler, IRQF_SHARED, "irq-driver", &g_cdev)) != 0) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "interrupt couldn't registered!...\n"); return result; } printk(KERN_INFO "irq-driver init...\n"); return 0; } static irqreturn_t keyboard_irq_handler(int irq, void *dev_id) { static int count = 0; ++count; if (count % 1000 == 0) printk(KERN_INFO "Keyboard IRQ occurred: %d\n", count); return IRQ_HANDLED; } static void __exit generic_exit(void) { free_irq(1, &g_cdev); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "irq-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); /*-------------------------------------------------------------------------------------------------------------------------- 159. Ders 02/08/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- CPU ile RAM arasında veri transferi aslında tamamen elektriksel düzeyde 1'lerle 0'larla gerçekleşmektedir. CPU'nun adres uçları (address bus) RAM'in adres uçlarına bağlanır. Bu adres uçları RAM'den transfer edilecek bilginin fiziksel adresini belirtmek için kullanılmaktadır. CPU'nun veri uçları (data bus) ise bilginin alınıp gönderilmesinde kullanılmaktadır. İşlemin okuma mı yazma mı olduğu genellikle R/W biçiminde isimlendirilen ayrı bir kontrol ucuyla yapılmaktadır. Örneğin 32 bit Intel işlemcilerinde MOV EAX, [XXXXXXXX] komutu RAM'deki XXXXXXXX adresinden başlayan 4 byte bilginin CPU içerisindeki EAX yazmacına çekileceği anlamına gelmektedir. Bu makine komutu işletilirken CPU önce erişilecek adres olan XXXXXXXX adresini adres uçlarına elektriksel işaret olarak kodlar. RAM bu adresi alır, bu adresten başlayan 4 byte'lık bilgiyi veri uçlarına elektriksel olarak kodlar. CPU'da bu uçlardan bilgiyi yine elektriksel olarak alır ve EAX yazmacına yerleştirir. CPU'nun adres uçları RAM'in adres uçlarına, CPU'nun veri uçları ise RAM'in veri uçlarına bağlıdır. Tranfer yönü R/W ucuyla belirlenmektedir. Tabii CPU'lar bugün DRAM belleklerden daha hızlıdır. Dolayısıyla CPU RAM'den yanıt gelene kadar beklemektedir (wait state). ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir bilgisayar sisteminde yalnızca Merkezi İşlemci (CPU) değil, aynı zamanda yerel birtakım olaylardan sorumlu yardımcı işlemciler de vardır. Bu yardımcı işlemcilere genellikle "controller (denetleyici)" denilmektedir. Örneğin klasik PC mimarisinde "Kesme Denetleyicisi (Intel 8250-PIC)", "Klavye Denetleyicisi (Intel 8042-KC)", "UART Denetleyicisi (Intel 8250/NS 16550-UART)" gibi pek çok işlemci vardır. Bu işlemcilere komutlar tıpkı CPU/RAM haberleşmesinde olduğu gibi elektriksel düzeyde CPU'nun adres ve veri uçları yoluyla gönderilmekte ve bu işlemcilerden bilgiler yine tıpkı RAM'de olduğu gibi adres ve veri uçları yoluyla alınmaktadır. Yani CPU'nun adres ve veri uçları yalnızca RAM'e değil, yardımcı işlemcilere de bağlıdır. Pekiyi bu durumda CPU, RAM'e erişirken aynı zamanda yardımcı işlemcilere de erişmez mi? İşte CPU'ların genellikle IO/Mem biçiminde isimlendirilen bir uçları daha vardır. Bu ucun 5V ya da 0V olması erişimin RAM'e mi yoksa yardımcı işlemciye mi yapılacağını belirtir. Yardımcı işlemcileri tasarlayanlar bu uca bakarak bilginin RAM'e değil, kendilerine geldiğini anlayabilirler. Normal RAM erişimlerine ilişkin MOV ya da LOAD/STORE makine komutlarında bu IO/Mem ucu "mem" biçiminde aktive edilir. Ancak bazı IN, OUT gibi komutlarda bu uç "IO" biçiminde aktive edilmektedir. Bu durumda yardımcı işlemcilere erişmek için MOV, LOAD/STORE komutları değil, genellikle IN, OUT biçiminde isimlendirilen komutları kullanılmaktadır. Ancak bazı yardımcı işlemciler bu "IO/Mem" ucu tam tersine "Mem" olarak aktive edildiğinde de işlevini yapacak biçimde konfigüre edilmiş olabilir. Bu durumda bu işlemcilere biz IN, OUT komutlarıyla değil, RAM'e erişiyormuş gibi MOV, LOAD/STORE komutlarıyla erişiriz. İşte bu tekniğe "Memory Mapped IO" denilmektedir. "Memory Mapped IO" yardımcı işlemcilere sanki RAM'miş gibi erişme anlamına gelir. Bunun da sistem programcısı için önemli avantajları vardır. Sistem programcısı bu sayede göstericileri kullanarak bu işlemcilere erişebilmektedir. Normal "IO/Mem" ucu "IO" biçiminde aktive edilerek yapılan erişimlere "Port Mapped IO" da denilmektedir. Bazı mimarilerde her iki teknik de yoğun kullanılmaktadır. Ancak bazı mimarilerde "Memory Mapped IO" tekniği daha yoğun kullanılabilmektedir. "Memory Mapped IO" tekniği kullanılırken artık RAM'in ilgili adresteki kısmına erişilemez ya da bu erişimin bir anlamı kalmaz. Yani adeta bu teknikte sanki RAM'in bir bölümü çıkartılmış onun yerine ilgili işlemci oraya takılmış gibi bir etki oluşmaktadır. Örneğin "Memory Mapped IO" bilgisayar sistemlerinde grafik kartları tarafından yoğun olarak kullanılmaktadır. Grafik kartlarını tasarlayanlar kartın üzerindeki RAM'in (bu ana RAM değil) içeriğini belli periyotlarla ekrana göndermektedir. Programcı da C'de göstericileri kullanarak belli adrese yazma yapma yoluyla ekrana belirli şeylerin çıkmasını sağlayabilmektedir. Pekiyi yardımcı işlemcileri birbirinden ayıran şey nedir? İşte CPU'nun adres uçları bu yardımcı işlemciler tarafından özel bazı değerlerde ise dikkate alınmaktadır. Yani nasıl RAM'deki byte'ların adresleri varsa yardımcı işlemcilerin de birer donanımsal adresleri vardır. Bu adreslere genellikle "port numaraları" da denilmektedir. Yardımcı işlemcilerin port numaraları donanım mimarisini tasarlayanlar tarafından donanımsal olarak önceden belirlenmiştir. Ancak modern sistemlerde programlama yoluyla değiştirilebilen port adresleri de söz konusu olmaktadır. PC mimarisinde programlanabilen port numarasına sahip olan işlemcilere "Plug and Play (PnP)" işlemciler de denilmektedir. O halde bizim bir yardımcı işlemciyi programlayabilmemiz için şu bilgileri edinmiş olmamız gerekmektedir: 1) Yardımcı işlemci "Port Mapped IO" mu yoksa "Memory Mapped IO" mu kullanmaktadır? 2) Yardımcı işlemcinin port numaraları (ya da "Memory Mapped IO" söz konusu ise bellek adresleri) nedir? 3) Bu yardımcı işlemcinin hangi portuna (ya da "Memory Mapped IO" söz konusu ise hangi adrese) hangi değerler gönderildiğinde bu işlemci ne yapacaktır? 4) İşlemci bize bilgi verecekse bunu hangi portu okuyarak (ya da "Memory Mapped IO" söz konusu ise hangi adresi okuyarak) vermektedir. Verilen bilginin biçimi nedir? Yardımcı işlemciler yalnızca bilgisayar donanımın içerisinde donanımsal olarak çivilenmiş bir biçimde bulunmayabilirler. Bazı bilgisayar sistemlerinde (örneğin masasüstü PC'lerde) genişleme yuvaları vardır. Bu genişleme yuvaları CPU'nun adres ve veri yoluna erişebilmektedir. Bu genişleme yuvaları için kart tasarlayan tasarımcılar kartlarının üzerinde yardımcı işlemcileri bulundurabilirler. Böylece ilgili kart takıldığında sanki sisteme yeni bir yardımcı işlemci takılmış gibi etki oluşmaktadır. CPU'ya neden "merkezi (central)" işlemci denildiğini artık anlayabilirsiniz. Bilgisayar sistemlerinde kendi yerel işlemlerinden sorumlu pek çok yardımcı işlemci olabilir. Ancak bunların hepsini elektriksel olarak CPU programlamaktadır. Bu nedenle CPU'ya merkezi işlemci denilmiştir. Tabii CPU aslında bizim yazdığımız programları çalıştırır. Yani yardımcı işlemcileri de sonuç olarak biz programlamış oluruz. Pekiyi yardımcı işlemcileri programlarken onlara tek hamlede kaç byte bilgi gönderip onlardan kaç byte bilgi okuyabiliriz? İşte bazı işlemciler (özellikle eskiden tasarlanmış olanlar) byte düzeyinde programlanmaktadır. Bazıları ise WORD düzeyinde bazıları ise DWORD düzeyinde programlanabilmektedir. O halde bizim bir haberleşme portuna 1 byte, 2 byte, 4 byte gönderip alabilmemiz gerekir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi bir yardımcı işlemci kernel modda programlanabiliyorsa user mode programlar bu yardımcı işlemciyi nasıl kullanmaktadır? İşte tipik olarak user mode programlar ioctl işlemleriyle aygıt sürücünün kodlarını çalıştırırlar. Aygıt sürücüler de bu kodlarda ilgili yardımcı işlemciye komutlar yollayabilir. Bazen read/write işlemleri de bu amaçla kullanılabilmektedir. Tabii karmaşık yardımcı işlemciler için aygıt sürücüleri yazanlar faydalı işlemlerin daha kolay yapılabilmesi için daha yüksek seviyeli fonksiyonları bir API kütüphanesi yoluyla sağlayabilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- İşlemcilerin IN, OUT gibi makine komutları "özel (privileged)" komutlardır. Bunlar user mode'dan kullanılırsa işlemci koruma mekanizması gereği bir içsel kesme oluşturur, işletim sistemi de bu kesme kodunda prosesi sonlandırır. Dolayısıyla bu komutları kullanarak donanım aygıtlarıyla konuşabilmek için kernel mod aygıt sürücü yazmak gerekir. Aygıtlara erişmekte kullanılan komutlar CPU mimarisine göre değişebildiğinden Linux çekirdeğinde bunlar için ortak arayüze sahip inline fonksiyonlar bulundurulmuştur. Bu fonksiyonlar şunlardır: #include unsigned char inb(int addr); unsigned short inw(int addr); unsigned int inl(int addr); void outb(unsigned char b, int addr); void outw(unsigned short b, int addr); void outl(unsigned int b, int addr); inb haberleşme portlarından 1 byte, inw 2 byte, inl 4 byte okumak için kullanılmaktadır. Benzer biçimde haberleşme portlarına outb 1 byte, outw 2 byte ve outl 4 byte göndermek için kullanılmaktadır. Bazı mimarilerde bir bellek adresinden başlayarak belli bir sayıda byte'ı belli bir porta gönderen ve belli bir porttan yapılan okumaları belli bir adresten itibaren belleğe yerleştiren özel makine komutları vardır. Bu komutlara string komutları denilmektedir. (Intel'de string komutları yalnızca IO işlemleri ile ilgili değildir.) İşte bu komutlara sahip mimarilerde bu string komutlarıyla IN, OUT yapan çekirdek fonksiyonları da bulundurulmuştur: #include void insb(unsigned long addr, void *buffer, unsigned int count); void insw(unsigned long addr, void *buffer, unsigned int count); void insl(unsigned long addr, void *buffer, unsigned int count); void outsb(unsigned long addr, const void *buffer, unsigned int count); void outsw(unsigned long addr, const void *buffer, unsigned int count); void outsl(unsigned long addr, const void *buffer, unsigned int count); insb, insw ve insl sırasıyla 1 byte 2 byte ve 4 byte'lık string fonksiyonlarıdır. Bu fonksiyonlar birinci parametresiyle belirtilen port numarasından 1, 2 ya da 4 byte'lık bilgileri ikinci parametresinde belirtilen adresten itibaren belleğe yerleştirirler. Bu işlemi de count kere tekrar ederler. Yani bu fonksiyonlar porttan count defa okuma yapıp okunanları buffer ile belirtilen adresten itibaren belleğe yerleştirmektedir. outsb, outsw ve outsl fonksiyonları ise bu işlemin tam tersini yapmaktadır. Yani bellekte bir adresten başlayarak count tane byte'ı birinci parametresiyle belirtilen port'a yerleştirmektedir. Bazı sistemlerde aygıtlar yavaş kalabilmektedir. Yani bus çok hızlı, aygıt yavaş ise o aygıt port'larına peşi sıra bilgiler gönderilip alınırken sorunlar oluşabilmektedir. Bunun için bilgiyi porta gönderdikten ya da bilgiyi port'tan aldıktan sonra kısa bir süre bekleme yapmak gerekebilir. İşte bu nedenle yukarıdaki fonksiyonların bekleme yapan p'li (pause) versiyonları da bulundurulmuştur. #include unsigned char inb_p(int addr); unsigned short inw_p(int addr); unsigned int inl_p(int addr); void outb_p(unsigned char b, int addr); void outw_p(unsigned short b, int addr); void outl_p(unsigned int b, int addr); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux'ta user mode'dan haberleşme portlarına IN ve OUT yapmak için basit bir aygıt sürücüsü de bulundurulmuştur. Bu aygıt sürücüsüne "/dev/port" aygıt dosyasıyla erişilebilir. Tabii user mode programların bu biçimde her defasında kernel mode'a geçerek IN/OUT yapması verimsiz bir yöntemdir. Ancak yine de basit uygulamalar için faydalı kullanımlar söz konusu olabilmektedir. Bu aygıt sürücü dosya gibi açıldıktan sonra sanki her dosya offset'i bir haberleşme portuymuş gibi işlem görmektedir. Yine okuma yazma sırasında dosya göstericisi ilerletilmekte dolayısıyla başka porta konumlandırılmaktadır. Aşağıdaki örnekte 0x60 numaralı porttan "/dev/port" aygıt sürücüsü yoluyla 1 byte okunmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* app.c*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; int result; unsigned char ch; if ((fd = open("/dev/port", O_RDWR)) == -1) exit_sys("open"); if (lseek(fd, 0x60, SEEK_SET) == -1) exit_sys("lseek"); if ((result = read(fd, &ch, 1)) == -1) exit_sys("read"); printf("%02X\n", ch); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- 160. Ders 04/08/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bir haberleşme portu ile çalışmadan önce o portun boşta olup olmadığını belirlemek gerekebilir. Çünkü başka aygıtların kullandığı port'lara erişmek sorunlara yol açabilmektedir. Tabii eğer biz ilgili port'un kullanılmasının bir soruna yol açmayacağından emin isek başkalarının kullandığı port'ları doğrudan kullanabiliriz. Çekirdek bu bakımdan bir kontrol yapmamaktadır. Kullanmadan önce bir portun başkaları tarafından kullanılıp kullanılmadığının sorgulanması için request_region isimli çekirdek fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include struct resource *request_region(unsigned long first, unsigned long n, const char *name); Fonksiyonun birinci parametresi kullanılmak istenen port numarasının başlangıç numarasını, ikinci parametresi ilgili port numarasından itibaren ardışıl kaç port numarasının kullanılacağını, üçüncü parametresi ise "/proc/ioports" dosyasında görüntülenecek ismi belirtmektedir. Fonksiyon başarı durumunda portları betimleyen resource isimli yapının başlangıç adresine, başarısızlık durumunda NULL adrese geri dönmektedir. request_region fonksiyonu ile tahsis edilen port numaraları release_region fonksiyonu ile serbest bırakılmalıdır: #include void release_region(unsigned long start, unsigned long n); Yukarıda da belirttiğimiz gibi portların kullanılması için bu biçimde tahsisat yapma zorunluluğu yoktur. Ancak programcı programlanabilir IO portları söz konusu olduğunda ilgili port numaralarını başkalarının kullanmadığından emin olmak için bu yöntemi izlemelidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- PC'lerdeki klavye denetleyicisinin (klavye içerisindeki değil, PC tarafındaki denetleyicinin (orijinali Intel 8042)) 60H ve 64H numaralı iki port'u vardır. 60H portu hem okunabilir hem de yazılabilir durumdadır. 60H portu 1 byte olarak okunduğunda son basılan ya da çekilen tuşun klavye scan kodu elde edilmektedir. Yukarıda da belirttiğimiz gibi klavye terminolojisinde tuşa basılırken oluşturulan scan koduna "make code", parmak tuştan çekildiğinde oluşturulan scan koduna ise "break code" denilmektedir. Klavye içerisindeki işlemcinin (keyboard encoder) break code olarak önce bir F0 byte sonra da make code byte'ını gönderdiğini belirtmiştik. İşte PC içerisindeki klavye denetleyicisi bu break kodu aldığında bunu iki byte olarak değil, yüksek anlamlı biti 1 olan byte olarak saklamaktadır. Böylece biz 60H port'unu okuduğumuzda onun yüksek anlamlı bitine bakarak okuduğumuz scan kodunun make code mu yoksa break code mu olduğunu anlayabiliriz. Klavye denetleyicisinin 60H portuna gönderilen 1 byte değere "keyboard encoder command" denilmektedir. Bu 1 byte'lık komut klavye denetleyicisi tarafından klavye içerisindeki işlemciye gönderilir. Ancak bu 1 byte'tan sonra bazı komutlar parametre almaktadır. Parametreler de komuttan sonra 1 byte olarak aynı port yoluyla iletilmektedir. Aşağıdaki örnekte klavyeden tuşlara basıldığında basılan ve çekilen tuşların make ve break code'ları yazdırılmaktadır. ---------------------------------------------------------------------------------------------------------------------------*/ /* irq-driver.c */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, }; static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static unsigned char g_keymap[128] = { [30] = 'A', [31] = 'S', [32] = 'D', [33] = 'F', }; static int __init generic_init(void) { int result; if ((result = alloc_chrdev_region(&g_dev, 0, 1, "irq-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; if ((result = cdev_add(g_cdev, g_dev, 1)) != 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "cannot add character device driver!...\n"); return result; } if ((result = request_irq(1, keyboard_irq_handler, IRQF_SHARED, "irq-driver", &g_cdev)) != 0) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "interrupt couldn't registered!...\n"); return result; } printk(KERN_INFO "irq-driver init...\n"); return 0; } static irqreturn_t keyboard_irq_handler(int irq, void *dev_id) { unsigned char code; char *code_type; code = inb(0x60); code_type = code & 0x80 ? "Break code: " : "Make code: "; if (g_keymap[code & 0x7F]) printk(KERN_INFO "%s %c (%02X)\n", code_type, g_keymap[code & 0x7F], code); else printk(KERN_INFO "%s %02X\n", code_type, code); return IRQ_HANDLED; } static void __exit generic_exit(void) { free_irq(1, &g_cdev); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "irq-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); /*-------------------------------------------------------------------------------------------------------------------------- 161. Ders 09/08/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Kesme kodları bazen bilgiyi bir kaynaktan alıp (örneğin network kartından, seri porttan, klavye denetleyicisinden) onu bir yere (genellikle bir kuyruk sistemi) yerleştirip, uyuyan thread'leri uyandırmaktır. Örneğin bir thread'in klavyeden bir tuşa basılana kadar bekleyeceğini düşünelim. Bu durumda thread işletim sistemi tarafından bir bekleme kuyruğuna alınır. Klavyeden bir tuşa basıldığında oluşan IRQ içerisinde bu bekleme kuyruğunda bekleyen thread'ler uyandırılır. Aşağıda örnekte aygıt sürücüye aygıt sürücünün bir IOCTL komutunda thread bloke edilmiştir. Sonra klavyeden bir tuşa basıldığında thread uykudan uyandırılıp basılmış olan tuşuna scan kodu IOCTL kodu tarafından thread'e verilmiştir. ---------------------------------------------------------------------------------------------------------------------------*/ /* irq-driver.h */ #ifndef IRQDRIVER_H_ #define IRQDRIVER_H_ #include #define KEYBOARD_MAGIC 'k' #define IOC_GETKEY _IOR(KEYBOARD_MAGIC, 0, int) #endif /* irq-driver.c */ #include #include #include #include #include #include #include #include "irq-driver.h" MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, .unlocked_ioctl = keyboard_ioctl, }; static DECLARE_WAIT_QUEUE_HEAD(g_wq); static int g_key; static int __init generic_init(void) { int result; if ((result = alloc_chrdev_region(&g_dev, 0, 1, "irq-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; if ((result = cdev_add(g_cdev, g_dev, 1)) != 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "cannot add character device driver!...\n"); return result; } if ((result = request_irq(1, keyboard_irq_handler, IRQF_SHARED, "irq-driver", &g_cdev)) != 0) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "interrupt couldn't registered!...\n"); return result; } printk(KERN_INFO "irq-driver init...\n"); return 0; } static irqreturn_t keyboard_irq_handler(int irq, void *dev_id) { int key; if (g_key != 0) return IRQ_NONE; key = inb(0x60); if (key & 0x80) return IRQ_NONE; g_key = key; wake_up_all(&g_wq); return IRQ_HANDLED; } static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case IOC_GETKEY: g_key = 0; if (wait_event_interruptible(g_wq, g_key != 0)) return -ERESTARTSYS; if (copy_to_user((void *)arg, &g_key, sizeof(int)) != 0) return -EFAULT; return g_key; default: return -ENOTTY; } } static void __exit generic_exit(void) { free_irq(1, &g_cdev); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "irq-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* app.c */ #include #include #include #include #include #include "irq-driver.h" void exit_sys(const char *msg); int main(void) { int fd; int key; if ((fd = open("irq-driver", O_RDONLY)) == -1) exit_sys("open"); if (ioctl(fd, IOC_GETKEY, &key) == -1) exit_sys("ioctl"); printf("Scan code: %d (%02x)\n", key, key); close(fd); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte IOC_SETLIGHTS ioctl komutu ile 8042 klavye denetleyicisine komut gönderme yoluyla klavye ışıkları yakılıp söndürülmektedir. Klavyede üç ışıklı tuş vardır: Caps-Lock, Num-Lock ve Scroll-Lock. Bu ışıkları yakıp söndürebilmek için önce 60H portuna 0xED komutu gönderilir. Sonra yine 60H portuna ışıkların durumunu belirten 1 byte gönderilir. Bu byte'ın düşük anlamlı 3 biti sırasıyla Scroll-Lock, Num-Lock ve Caps-Lock tuşlarının ışıklarını belirtmektedir: 7 6 5 4 3 CL NL SL x x x x x x x x 60H portuna komut göndermeden önce 64H portundan elde edilen değerin 2 numaralı bitinin 0 olması gerekmektedir. Ayrıntılı bilgi için http://www.brokenthorn.com/Resources/OSDev19.html sayfasını inceleyebilirsiniz. Aşağıdaki aygıt sürücüde IOC_SETLIGHTS IOCTK komutunda klavye ışıklarının yakılıp söndürülmesi sağlanmıştır. Burada "app.c" isimli user mode program bir komut satırı argümanı almış ve o komut satırı argümanınındaki sayıyı yukarıda anlattığımız gibi klavye denetleyicisine göndermiştir. Artık pek çok klavyede Scroll-Lock ve Num-Lock tuşlarının ışıkları bulunmamaktadır. Programın Caps-Lock ışığını yakmasını istiyorsanız 4 argümanıyla (CL bitinin 2 numaralı bit olduğuna dikkat ediniz) söndürmek istiyorsanız 0 argümanıyla çalıştırabilirsiniz. Örneğin: $ ./app 4 $ ./app 0 ---------------------------------------------------------------------------------------------------------------------------*/ /* irq-driver.h */ #ifndef IRQDRIVER_H_ #define IRQDRIVER_H_ #include #define KEYBOARD_MAGIC 'k' #define IOC_GETKEY _IOR(KEYBOARD_MAGIC, 0, int) #endif /* irq-driver.c */ #include #include #include #include #include #include #include #include "irq-driver.h" MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, .unlocked_ioctl = keyboard_ioctl, }; #ifndef IRQDRIVER_H_ #define IRQDRIVER_H_ #include #define KEYBOARD_MAGIC 'k' #define IOC_GETKEY _IOR(KEYBOARD_MAGIC, 0, int) #endif static DECLARE_WAIT_QUEUE_HEAD(g_wq); static int g_key; static int __init generic_init(void) { int result; if ((result = alloc_chrdev_region(&g_dev, 0, 1, "irq-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; if ((result = cdev_add(g_cdev, g_dev, 1)) != 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "cannot add character device driver!...\n"); return result; } if ((result = request_irq(1, keyboard_irq_handler, IRQF_SHARED, "irq-driver", &g_cdev)) != 0) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "interrupt couldn't registered!...\n"); return result; } printk(KERN_INFO "irq-driver init...\n"); return 0; } static irqreturn_t keyboard_irq_handler(int irq, void *dev_id) { int key; if (g_key != 0) return IRQ_NONE; key = inb(0x60); if (key & 0x80) return IRQ_NONE; g_key = key; wake_up_all(&g_wq); return IRQ_HANDLED; } static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case IOC_GETKEY: g_key = 0; if (wait_event_interruptible(g_wq, g_key != 0)) return -ERESTARTSYS; if (copy_to_user((void *)arg, &g_key, sizeof(int)) != 0) return -EFAULT; return g_key; case IOC_SETLIGHTS: while ((inb(0x64) & 2) != 0) ; outb(0xED, 0x60); while ((inb(0x64) & 2) != 0) ; outb(arg, 0x60); break; default: return -ENOTTY; } return 0; } static void __exit generic_exit(void) { free_irq(1, &g_cdev); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "irq-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* app.c */ #include #include #include #include #include #include "irq-driver.h" #define CAPS_LOCK 0x04 #define NUM_LOCK 0x02 #define SCROLL_LOCK 0x04 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; int key; int keycode; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } keycode = atoi(argv[1]); if ((fd = open("irq-driver", O_RDONLY)) == -1) exit_sys("open"); if (ioctl(fd, IOC_SETLIGHTS, keycode) == -1) exit_sys("ioctl"); close(fd); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*-------------------------------------------------------------------------------------------------------------------------- Derleyiciler ve işlemciler tarafından yapılan önemli bir optimizasyon temasına "komutların yer değiştirilmesi (instruction reordering)" denilmektedir. Bu optimizasyon derleyici tarafından da bizzat işlemcinin kendisi tarafından da yapılabilmektedir. Burada birbirlerini normal bir durumda etkilemeyecek iki ya da daha fazla ayrı makine komutunun yerleri daha hızlı çalışma sağlamak için değiştirilmektedir. Bu tür yer değiştirmeler normal user mode programlarda hiçbir davranış değişikliğine yol açmazlar. Ancak işletim sistemi ve aygıt sürücü kodlarında ve özellikle IO portlarına erişim söz konusu olduğunda bu optimizasyon olumsuz yan etkilere yol açabilmektedir. Örneğin birbirleriyle alakasız iki adrese yazma yapılması durumunda yazma komutlarının yer değiştirmesi işlemcinin bu işleri daha hızlı yapabilmesine yol açabilmektedir. Fakat IO portları ve Memory Mapped IO söz konusu olduğunda bu sıralama değişikliği istenmeyen olumsuz sonuçlar doğurabilmektedir. İşte bu yer değiştirmeyi ortadan kaldırmak için "bariyer (barrier)" koyma yöntemi uygulanmaktadır. Derleyici ve işlemci bariyerin yukarısıyla aşağısını yer değiştirmemektedir. Bariyer fonksiyonları şunlardır: #include void rmb(void); void wmb(void); void mb(oid); rmb fonksiyonunun aşağısındaki kodlar yukarısındaki okuma işlemleri yapıldıktan sonra yapılırlar. wmb fonksiyonunun ise yukarısındaki yazma işlemleri yapıldıktan sonra aşağıdaki işlemler yapılırlar. mb fonksiyonu ise hem okuma hem yazma için yukarıdaki ve aşağıdaki kodları birbirlerinden ayırmaktadır. Örneğin PORT1 portuna yazma yapıldıktan sonra PORT2 portuna yazma yapılacak olsun. Şöyle bir bariyer kullanmalıyız: outb(cmd1, port1); wmb(); outb(cmd2, port2); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Memory Mapped IO işlemi pek çok mimaride normal göstericilerle yapılabilmektedir. Yani aslında bu mimarilerde Memory Mapped IO için özel kernel fonksiyonlarının kullanılmasına gerek yoktur. Ancak bazı mimarilerde Memory Mapped IO işlemi için özel bazı işlemlerin de yapılması gerekebilmektedir. Bu nedenle bu işlemlerin taşınabilir yapılabilmesi için özel kernel fonksiyonlarının kullanılması tavsiye edilir. Tıpkı normal IO işlemlerinde olduğu gibi Memory Mapped IO için de iki farklı aygıt aynı adres bölgesini kullanmasın diye bir registration işlemi söz konusudur. Bu işlemler request_mem_region ve release_mem_region fonksiyonlarıyla yapılmaktadır: #include struct resource *request_mem_region(unsigned long start, unsigned long len, const char *name); Fonksiyonun birinci parametresi başlangıç bellek adresini, ikinci parametresi alanın uzunluğunu belirtmektedir. Üçüncü parametre ise "/proc/iomem" dosyasında görüntülenecek isimdir. Fonksiyon başarı durumunda resource isimli bir yapı nesnesinin adresine, başarısızlık durumunda NULL adrese geri dönmektedir. Daha önce register ettirilmiş olan bellek bölgesini serbest bırakmak için (yani register ettirilmemiş hale getirmek için) release_mem_region fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include void release_mem_region(unsigned long start, unsigned long len); Fonksiyonun birinci parametresi başlangıç bellek adresini, ikinci parametresi ise uzunluğu belirtmektedir. Aşağıdaki fonksiyonlar addr ile belirtilen bellek adresinden 1 byte, 2 byte ve 4 byte okurlar. #include unsigned int ioread8(void *addr); unsigned int ioread16(void *addr); unsigned int ioread32(void *addr); Aşağıdaki fonksiyonlar ise addr ile belirtilen bellek adresine 1 byte, 2 byte ve 4 byte bilgi yazmaktadır: #include void iowrite8(u8 value, void *addr); void iowrite16(u16 value, void *addr); void iowrite32(u32 value, void *addr); Yukarıdaki fonksiyonların rep'li (repetition/tekrar) versiyonları da vardır: #include void ioread8_rep(void *addr, void *buf, unsigned long count); void ioread16_rep(void *addr, void *buf, unsigned long count); void ioread32_rep(void *addr, void *buf, unsigned long count); void iowrite8_rep(void *addr, const void *buf, unsigned long count); void iowrite16_rep(void *addr, const void *buf, unsigned long count); void iowrite32_rep(void *addr, const void *buf, unsigned long count); Bu fonksiyonlar Memory Mapped IO adresinden belli bir adrese belli miktarda (count parametresi) byte, word ya da dword transfer etmektedir. Tıpkı memcpy fonksiyonunda olduğu gibi Memory Mapped IO adresi ile bellek arasında blok kopyalaması yapan iki fonksiyon bulunmaktadır: #include void memcpy_fromio(void *dest, const void *source, unsigned int count); void memcpy_toio(void *dest, const void *source, unsigned int count); Belli bir Memory Mapped IO adresine belli bir byte'ı n defa dolduran fonksiyon da şöyledir: #include void memset_io(void *dest, u8 value, unsigned int count); Bu fonksiyonu memset fonksiyonuna benzetebilirsiniz. Aygıtın kullandığı bellek adresi genellikle fiziksel adrestir. Örneğin aygıt 0xFFFF8000 gibi bir adresi kullanıyorsa bu genellikle fiziksel anlamına gelir. Halbuki aygıt sürücünün bu fiziksel adrese erişebilmesi için bu dönüşümü yapacak sayfa tablosu girişlerinin olması gerekir. İşte bu girişleri elde edebilmek için şu fonksiyonlar bulundurulmuştur: #include void *ioremap(unsigned long physical_address, unsigned long size); Bu fonksiyon fiziksel adrese erişebilmek için gereken sayfa tablosu adresini bize verir. Gerçi genellikle fiziksel RAM daha önceden de belirtildiği gibi Linux'ta sanal belleğin PAGE_OFFSET ile belirtilen kısmından başlanarak map edilmiştir. Bu işlemi geri almak için iounmap fonksiyonu kullanılmaktadır: #include void iounmap(void *addr); ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 162. Ders 11/08/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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'e 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 dosyaları 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 aygıt dosyası diske bir bütün olarak erişmek için kullanılırken 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ştirilerek disk erişimleri azaltılmaktadır. Yani bu tür transferlerde transfer talep edildiği anda değil, biraz bekletilerek (çok kısa bir zaman) gerçekleştirilebilmektedir. Bu tür durumlarda işletim sistemleri ilgili 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şir. İşte onların yeniden sıralanması gibi faaliyetler IO çizelgeleyicisinin ö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 fonksiyonuyla 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 block 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 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. Bu iki script'e de "executable" haklarının verilmesi gerektiğini anımsayınız. $ sudo chmod +x loadblk unloadblk /* 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 ---------------------------------------------------------------------------------------------------------------------------*/ /* 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 b $major 0 /* unloadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /*-------------------------------------------------------------------------------------------------------------------------- 163. Ders 16/08/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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 -ENOMEM; } 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. Örneğ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 kullanı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 çekirdeklerde 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 adresiyle, 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) { ... return -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 nesnesinin adresini almaktadır. Yeni kuyruk fonksiyonları izleyen paragraflarda ele alınacaktır. - gendisk 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. Fonksiyonun 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.) Fonksiyonun 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 nesnesinin 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 olduğ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ğerine, 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. 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 "cannot 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); } /*-------------------------------------------------------------------------------------------------------------------------- 164. Ders 18/08/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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ğırmaktadı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 bulunduğu bloğun (blok ardışıl n sektördür) transferini isteyecektir. User mode programa o bloğun içerisindeki 10 byte'ı vermek çekirdeğin görevidir. Blok aygıt sürücülerinden transferler byte düzeyinde değil, blok düzeyinde yapılmaktadır. Yani blok 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 sistemidir.) Her request nesnesi kendi içerisinde bio isimli yapı nesnelerinden oluşmaktadır. request nesnesinin içerisindeki bio nesneleri 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 transferleri" 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 nesnelerini 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 edilmiş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ğa 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çiminde 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 yazı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 türü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 fonksiyonunun 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: #include 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 yapıldığı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 request_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 belirlenebilmesi 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 queuedata 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 blok aygıt sürücüsünün tüm kodlarını görüyorsunuz. ---------------------------------------------------------------------------------------------------------------------------*/ /* 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 "cannot 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: 1) Programcı blk_mq_tag_set isimli yapı türünden bir yapı nesnesi oluşturur. Bu yapı nesnesi blok aygı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; 2) 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 eriş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. 3) İç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 isimli 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ı blok 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 bulundurulması 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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 165. Ders 20/09/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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ü ne olursa olsun bu disk sistemini yöneten ondan sorumlu bir denetleyici birim bulunmaktadır. Buna "disk denetleyicisi (disk controller)" denilmektedir. Kernel mode aygı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 | <---> | Disk Denetleyici | <---> | Aygıt Sürücü | <---> | User Mode | +-------------+ +------------------+ +--------------+ +-----------+ | | +------------+ | Disk Cache | +------------+ Örneğin biz user mode'da bir diskten bir sektör okumak istediğimizde bu işlemi yapan blok aygıt sürücüsünden istekte bulunuruz. Blok aygıt sürücüleri disk denetleyicilerini programlar, disk denetleyicileri disk birimine erişir ve transfer gerçekleşir. Disk transferleri CPU aracılığıyla değil, "DMA (Direct Memory Access)" denilen özel denetleyicilerle sağlanmaktadır. Yani aygıt sürücü hem disk denetleyicisini hem de DMA'yı programlar ve transfer yapılana kadar bekler. Bu sırada 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 transfer eder. Oradan talep eden thread'e verir. Bu amaçla kullanılan cache'lerde cache algoritması (cache replacement algorithm) genel olarak LRU(Least Recently Used) algoritmasıdır. Yani son zamanlarda erişilen yerler mümkün olduğunca cache'te tutulmaktadır. İşletim 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 çekilmektedir. 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 mode'dan ç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 çizelgeleyicisi (IO scheduler)" denilmektedir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bugün artık SSD (yani elektronik devre biçiminde) diskler yaygın olarak kullanılıyorsa da bir dönem öncesine kadar manyetik tabanlı "hard diskler" yoğun kullanılıyordu. Burada bu hard disklerin genel yapısı üzerinde bazı açıklamalar da yapmak istiyoruz. Hard disklerde bir eksene monte edilmiş birden fazla yüzey vardır. Bu yüzeylere "platter" denilmektedir. Bir platter'da iki yüz bulunur. Her yüz bir disk kafası ile okunmaktadır. Örneğin bir hard diskte 4 platter varsa toplam 8 tane yüz ve 8 tane kafa bulunur. Bilgiler tıpkı eski plaklarda olduğu gibi yuvarlak yollara yazılıp okunmaktadır. Bunlara "track" denilmektedir. Dolayısıyla bir track'e bilginin yazılıp okunması için öncelikle kafanın o track hizasına konumlandırılması gerekmektedir. Tabii okuma yazma işlemi disk dönerken yapılmaktadır. Bu durumda manyetik tabanlı bu hard disklerde bir sektörün transfer edilmesi için üç zaman unsuru vardır: 1) Disk kafasının ilgili track hizasına konumlandırılması için gereken zaman (seek time). 2) Diskin dönerek kafa hizasına gelmesi için gereken zaman (rotation delay). 3) Transfer zamanı (transfer time) Buradaki en önemli zaman kaybı disk kafasının konumlandırılması sırasındaki kayıptır. Ortalama bir disk 6000 RPM hızında dönmektedir. Yani bir dakikada 6000 tur atmaktadır. İkinci önemli zaman kaybı ilgi sektörün track kafa hizasına gelmesi için harcanan zamandır. Nihayet en hızlı gerçekleşen işlem transfer işlemidir. İşletim sistemlerinin eski hard disklerde uyguladığı en önemli optimizasyon işlemi kafa hareketinin azaltılması üzerinedir. Eğer dosyayı oluşturan sektörler birbirine yakınsa daha az kafa hareketi oluşur ve toplam transfer zamanı azaltılmış olur. Tabii dosya sistemlerinde dosyanın parçaları zamanla birbirinden uzaklaşabilmektedir. Bunları birbirine yaklaştıran genellikle "defrag" biçiminde isimlendirilen yardımcı programlar vardır. Tabii artık şimdilerde kullandığımız SSD disklerde hiçbir mekanik unsur yoktur. Bunlar tamamen flash EPROM teknolojisi ile yani yarı iletken teknolojisiyle entegre devre biçiminde üretilmektedir. Bunlar rastgele erişimlidir. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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. Transferler bu sektör numarasıyla 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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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" (Solid Disk a) ismi hard 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. 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 (disk 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 şöyledir: Disk Bölümleme Tablosu Birinci 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 dosyasını açarsak tüm diski tek parça olarak ele alırız. Eğer "/dev/sda1" 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/sda2" aygıt dosyasından okuyacağımız 0 numaralı sektör aslında İkinci Disk Bölümünün ilk sektörüdür. 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) yazdı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. ---------------------------------------------------------------------------------------------------------------------------*/ #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 betimleyici tablosu, dosya nesnesi vs.) ilgilidir. Disk tarafı ise diskteki organizasyonla ilgilidir. Biz kursumuzda bellek tarafındaki organizasyonun temellerini gördük. Şimdi 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 disk organizasyonları birbirine çok benzemektedir. Bunlar adeta bir aile oluşturmaktadır. Örneğin Microsoft'un FAT dosya sistemleri, Linux'un ext dosya sistemleri kendi aralarında birbirine oldukça benzemektedir. Microsoft'un dünyanın ilk 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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 166. Ders 22/09/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Bilindiği gibi UNIX/Linux sistemlerinde Windows sistemlerinde olduğu gibi "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 fonksiyonunun 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, ikinci 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. 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ı aşağıdakine benzer biçimde çalıştırabilirsiniz: $ sudo ./mymount /dev/sdc1 mydisk vfat ---------------------------------------------------------------------------------------------------------------------------*/ /* 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 512 * 8192 = 4194304 olduğuna dikkat ediniz. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 167. Ders 27/09/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Biz kursumuzda önce FAT dosya sisteminden bahsedeceğiz sonra UNIX/Linux sistemlerindeki i-node tabanlı EXT dosya sistemleri üzerinde duracağız. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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: 1) Boot Sektör 2) FAT Bölümü 3) Root Dir Bölümü 4) 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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 168. Ders 29/09/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 169. Ders 06/10/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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. 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 - 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şi sı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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 170. Ders 11/10/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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 ---------------------------------------------------------------------------------------------------------------------------*/ /* 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); } /*-------------------------------------------------------------------------------------------------------------------------- Biz yukarı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ı) ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 171. Ders 13/10/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Ş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. ---------------------------------------------------------------------------------------------------------------------------*/ /* 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. ---------------------------------------------------------------------------------------------------------------------------*/ /* 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. ---------------------------------------------------------------------------------------------------------------------------*/ /* 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. ---------------------------------------------------------------------------------------------------------------------------*/ /* 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 bir 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 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 doısya 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......| - 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: 1) 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. 2) İş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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 176. Ders 01/11/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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. ---------------------------------------------------------------------------------------------------------------------------*/ /* 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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 177. Ders 03/11/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 178. Ders 08/11/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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: 1) Ö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. 2) 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.) 3) İ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 ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 182. Ders 24/11/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 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. ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 183. Ders 01/12/2024 - Pazar ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Ş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 fonksiyonlarrıyla okuma yaapabiliyorduk.) Yani dizinler aslında birer dosya gibidir. Dizin dosyaları "dizin girişleri (directory entries)" denilen girişlerdne oluşmaktadır. .... Bir dizin girişin format şöyledir: Offset (bytes) Size (bytes) Açıklama 0 DWORD inode 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 dosyaanın i-node numarası belirtilmektedir. Dizinler değişken uzulukta olduğu için ilgili girişin toplam kaç byte uzunlukta olduğu sonraki WORD elemandaa 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ürülerini 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 ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- 184. Ders 06/12/2024 - Cuma ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Pekiyi ext2 dosya sisteminde işletim sistemi bit 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 naısl 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 öncek kök dizinde "a" girişini arar. Sonra "a" girişin 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 offsetteki i_blocks elemanı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 inode 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 karalter 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 uzunlupunun 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şimin 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 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. Doısya türü 0x02'dir. Yani bu giriş bir dizin belirtmektedir Dos ismi "a" biçimindedir. İşte işletim sistemi 0x61B2 = 25010'inci 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 offseti'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. Herr blok grubunda bir i-node bitmap tablosu bulunur. Bu tablo da bitlerden oluşmaktadır. Her bit ilgili i-node elemanın boş mu dolu mu olduğunu bvelirtir. I-node bitmap tablosunun yeri de yine blok grup betimleyicinde 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 talosunun 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 prgramlama yoluyla nasıl yapabiliriz? Yukarıda eçı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. Kütüphanenin kurulumunu şöyle yapabilrisiniz: $ sudo apt-get install libext2fs-dev Aslında bu kütüphane ve ve başlık dosyası yalnızca ext2 dosya sistemine ilişkin değil ext4 dosya sistemine ilişkin de yapıları ve fonksiyonları bulundurmaktadır. Ö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 indireceğiniz pdf dosyasında dokğmante edilmiştir: https://www.dubeyko.com/development/FileSystems/ext2fs/libext2fs.pdf ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------------------------------------------------- Linux kernel istenirse kaynak kodlarından yeniden derlenip (yani build edilip) elde edilen yeni kernel ile sistem başlatılabilir. Pekiyi bu işlem neden yapılmak istenebilir? Tipik nedenleri şunlar olabilir: - Kernel'ın yükseltilmesi istenebilir. Yani örneğin kernel'ın yeni bir versiyonu çıkmış olabilir. Biz de bu yeni versiyonun kaynak kodlarını indirip onu derleyerek kullanmak isteyebiliriz. - Kernel kodlarının çeşitli yerleri çeşitli amaçlarla (deneysel olabilir, başka amaçlarla olabilir) değiştirilmek istenebilir. Örneğin çizelgeleyici alt sistem değiştirilerek yeni sistem denenmek istenebilir. Dosya sisteminde değişiklikler yapılmak istenebilir. Ya da mevcut kernel'daki çeşitli bug'lar düzeltilmek istenebilir (patch işlemi). - Kernel'dan çeşitli öğeler de çıkartılmak istenebilir. - Kernel'a yeni kodlar ve dolayısıyla işlevsellikler eklenmek istenebilir. (Örneğin kernel'a yeni bir sistem fonksiyonu eklenmek istenebilir.) Ya da bir kernel modül veya aygıt sürücü kernel'ınbir parçası haline getirilmek istenebilir. Şüphesiz yukarıdaki amaçlar için kernel yeniden derlenecekse öncelikle onun kaynak kodlarının indirilmesi ve o kodlar üzerinde istenen değişikliklerin yapılması gerekir. Sonra da bu yeni kodlar derlenmelidir. Derlendikten sonra da boot işleminde bu yeni kernel'ın devreye girmesi sağlanmalıdır. Linux çekirdekleri resmi olarak kernel.org sitesinde tutulmaktadır. İndirme için ana dizin kernel.org/pub dizinidir. Bu dizinin içerisinde linux/kernel dizinleri bulunmaktadır. Kernel versiyonları bu dizinin altında dizinler biçiminde bulunur. Genellikle kaynak kodlar .gz ve .xz biçiminde iki alternatif sıkıştırma formatı biçiminde burada bulundurulmaktadır. İşte ilk aşamada bir kernel versiyonu yerel makineye indirilmelidir. İndirme işlemi doğrudan tarayıcı yoluyla yapılabileceği gibi komut satırından wget utility'si ile de yapılabilmektedir. Örneğin: $ wget https://www.kernel.org/pub/linux/kernel/v5.x/linux-5.0.10.tar.xz Kaynak kodlar indirildikten sonra bunlar açılır. Kaynak kodların bulundurulması için en normal yer /usr/src dizinidir. Bu dizine yazma hakkımız olmadığı için açım işleminin sudo ile yapılması gerekir. Örneğin: $ sudo tar -xvf linux-5.0.10.tar.gz -C /usr/src Bu komutla artık kaynak kodlar /usr/src dizinin altına açılmıştır. Artık /usr/src dizinin altında linux-5.0.10 biçiminde (tabii bunun ismi indirdiğimiz kernel versiyonuna göre değişecektir) bir dizin oluşturulmuştur. Bu dizine cd komutuyla geçmek gerekir. Bu ana dizindeki Makefile dosyası ana make dosyasıdır. Bu dosya yüzlerce dizindeki make dosyalarını çalıştırarak build işlemini yapmaktadır. Kernel'ın build edilmesi için gereken başka yardımcı araçlar da bulunmaktadır. Dolayısıyla bu araçlar eğer sistemimizde yoksa onları da indirip kurmalıyız. Genellikle olmayan araçlar şunlardır: $ sudo apt-get install libssl-dev $ sudo apt-get install libelf-dev Eğer build işleminde sorun çıkarsa sisteminizde başka gerekli araçların olmadığından şüphelenmelisiniz. Pek çok sistemde aşağıdaki araçlar default durumda bulunmaktadır. Ancak eğer build işlemi başarısız olursa aşağıdaki araçların olmadığından şüphelenmelisiniz: $ sudo apt-get install libncurses5-dev $ sudo apt-get install flex $ sudo apt-get install bison Debian türevi sistemlerde ayrıca paket yöneticisinin repository dosyasının sudo apt-get update komutu ile güncellenmesi gerekebilmektedir. Artık build işlemi için gerekli olan tüm araçlar sağlanmış durumdadır. Şimdi bir "kernel konfigürasyon dosyasının" oluşturulması gerekir. Bu konfigürasyon dosyası build işlemi sırasında okunup oluşturulacak olan kernel image dosyasının neleri içerip içermeyeceğini belirtmektedir. Normal olarak kernel konfigürasyon dosyası utility'ler tarafında oluşturulmaktadır. Bu utility'ler bize onlarca soru sormaktadır. Tipik utility'ler şunlardır: $ make config $ make menuconfig $ make xconfig Söz konusu konfigürasyon dosyasının ismi ".config" biçimindedir. Bu konf,igürasyon dosyasının oluşturulması biraz zahmetli olduğu için daha çok mevcut bir .config dosyasını kullanma yöntemine gidilmektedir. Aslında aşağıdaki komutla default bir .config dosyası oluşturulabilir: $ sudo make defconfig Ancak daha sağlam bir yöntem /boot dizini içerisinde bulunan mevcut sistemin konfigürasyon dosyasını yeni kaynak kod dizinine .config ismiyle kopyalamak olabilir. Örneğin: $ sudo cp /boot/config-4.15.0-20-generic .config Kernel konfigürasyon dosyası bu biçimlerle elde edilmişse oluşturacağımız yeni kernel'a uyumunu sağlamak için aşağıdaki komut uygulanmalıdır: $ sudo make oldconfig Bu komut referans aldığımız .config dosyasının içine bakar. Versiyon uyuşmazlığı ile ilgili satırları siler. Bu komut uygulandığında biz uyuşmazlıklarla ilgili birtakım sorular sorabilmektedir. Bu sorular default seçeneklerle geçilebilir. Tabii biz yine istersek bu .config dosyası üzerinde yukarıda belirttiğimiz make mconfig, make menuconfig, make xconfig gibi komutlarla onun üzerinde güncellemeler yapabiliriz. Artık tüm hazırlıklar yapılmıştır ve uzun bir zaman alabilen build işlemi yapılacaktır. Build işlemi aşağıdaki komut ile başlatılabilir: $ sudo make Ancak işlemi hızlandırmak içib birden fazla çekirdeğin kullanılmasını -j seçeneği ile sağlayabiliriz. Örneğin: $ sudo make -j 4 Burada 4 çekirdek birlikte build işlemine katılacaktır. Build işlemi bittikten sonra artık yeni sistem için son iki işlem yapılmalıdır. Bunlardan birincisi oluşturulan ama kernel image içerisinde olmayan bazı aygıt sürücülerin uygun dizinlere çekilmesidir. Bu işlem aşağıdaki komutla yapılır: $ sudo make modules_install Böylece biz aygıt sürücüleri de yenilemiş olmaktayız. Burada söz konusu modüller kernel image içerisinde olmayan fakat çeşitli aşamalarda yüklenen aygıt sürücü modülleridir. Nihayet ikinci olarak gerçek kernel image dosyasının ve yardımcı birkaç dosyann /boot dizinine çekilip boot-loader'ın güncellenmesidir. Bu da şu komutla yapılmaktadır: $ sudo make install Linux işletim sisteminin boot edilmesi sırasında yüklenecekgerçek kernel image dosyasının /boot dizininde bulunması gerekmektedir. Genellikle bu kernel image dosyası vmlinuz-xxxx biçiminde isimlendirilmiş durumadır. Ancak kernel'ın boot edilmesi için "kök dosya sistemini (root file system)" geçici bir biçimde ramdisk olarak oluşturan bir geçici dosya sisteminin de /boot dizininde bulunması gerekmektedir. Bu dosya da genel olarak initrd.img.xxxx ismiyle bulunur. Diğer bir dosya da genellikle System.map.xxx biçiminde isimlendirilen kernel sembol dosyasıdır. Bu dosyanın da boot işlemi sırasında /boot dizininde bulundurulması gerekmektedir. İşte make install komutu bu dosyaları /boot dizinine kopyalamaktadır. Ayrıca make install komutu bunun yanı sıra boot-loader ayarlarını da yapar. İşletim sistemini asıl yükleyen programa "boot-loader" denilmektedir. boot-loader sayesine biz bilgisayarımızı istediğimiz bir kernel ile açabilmekteyiz. Eskiden LILO isimli boot-loader programı yoğun kullanılıyordu. Sonra GRUB isimli boot-loader daha popüler oldu. Şimdiki Linux sistemleri genellikle GRUB isimli boot-loader'ı kullanmaktadır. İşte make install aynı zamanda GRUB boot-loader konfigürasyon dosyasını da güncelleyerek yeni sistemin de GRUB menüsünde gözükmesini sağlamaktadır. make install işlemi ile eski kernel'ın yok edilmediğini yeni bir kernel'ın da /boot izininde bulundurulduğuna dikkat ediniz. Tabii artık programcı isterse eski kernel dosyalarını bu dizinden silebilir. GRUB boot loader ayarlarını kendisi manuel yolla ya da yardımcı programlarla yapabilir. GRUB boot-loader konfigürasyon dosyası normal olarak /boot/grub dizini içerisindedir. Yani boot-loader buradaki grub.cfg dosyası temel alınarak ayarlanır. Ama bu dosyanın güncellenmesi zordur. Bunun için daha basit yöntem /etc/default/grub dosyası üzerinde basit değişiklikler yapmak ama bunu yaptıktan sonra aşağıdaki komutu uygulamaktır: $ sudo update-grub Bu komutla birkte aslında /boot/grub/grub.cfg dosyası da güncellenmektedir. GRUB güncellemesinin diğer bir yolu da bunun için yazılmış olan GUI programlarını kullanmaktır. Yaygın olanlardan biri grub-customizer isimli programdır. Bu program şöyle kurulabilir: $ sudo add-apt-repository ppa:danielrichter2007/grub-customizer $ sudo apt-get update $ sudo apt-get install grub-customizer Böylece artık grub-customizer yazıldığnda bir GUI program görüntülenecektir. Kernel kodlarındaki bir dosya üzerinde değişiklik yaptıktan sonra yukarıdaki adımlar izlenirse artık yeni kernel o değişiklikleri içerecektir. Ancak çoğu kez amaç kernel kaynak dosyalarına yeni bir dosya eklemektir. Bu durumda genel olarak şöyle bir yöntem izlenebilir: 1) Ekleyeceğiniz dosya için ana kaynak dizininde bir dizin yaratabilirsiniz. Bu dizin mycode olsun. (Başka yerde de yaratılabilir. Ancak en basiti budur.) 2) Ekleneceğiniz dosyaları bu dizine yerleştirebilirsiniz. Bu dosyaların x.c ve y.c olduğunu varsayalım. 3) Yarattınız dizinin içerisinde ana Makefile tarafından devreye sokulacak olan bir Makefile dosyası yaratmalısınız. Bu dosyanın içerisinde şu satırlar olmalıdır: obj-y := x.o obj-y := y.o 4) Yarattığınız dizinin ana Makefile dosyası tarafından tanınabilmesi için o dosyada aşağıdaki satır bulunup o dizin de bu satıra eklenmelidir: core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/ Bu satır şu hale getirilmelidir: core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/ mycode/ ---------------------------------------------------------------------------------------------------------------------------*/