diff --git a/UNIX-Linux/Unix-Linux-SysProg-OzetNotlar-Ornekler.txt b/UNIX-Linux/Unix-Linux-SysProg-OzetNotlar-Ornekler.txt new file mode 100644 index 0000000..cdc7e70 --- /dev/null +++ b/UNIX-Linux/Unix-Linux-SysProg-OzetNotlar-Ornekler.txt @@ -0,0 +1,72866 @@ +/*-------------------------------------------------------------------------------------------------------------------------- + + 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: 25/02/2025 - Salı + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 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üyorsunuz: + + ... + 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. + + $ cat /proc/sys/kernel/threads-max + 15071 + + $ cat /proc/sys/kernel/pid_max + 4194304 + + $ 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 + + Environment 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 programları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 yeteneğini 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 fonksiyonunun 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ç programı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 fonksiyonunun 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. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Çekirdek 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 ve amaç dosyalara 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 çekirdek modüllerini ve aygıt sürücüleri oluşturabilmek için gereken + başlık dosyaları ve diğer gerekli öğeler zaten "/usr/src" dizini içerisindeki "linux-headers-$(uname -r)" dizininde yüklü + biçimde bulunmaktadır. Ancak bunlar yüklü değilse Debian tabanlı sistemlerde bunları şöyle yükleyebilirsiniz: + + $ sudo apt install linux-headers-$(uname -r) + + Tabii programcı o anda çalışılan çekirdeğin kodlarının hepsini de kendi makinesine indirmek isteyebilir. Bunun için aşağıdaki + komut kullanılabilir: + + $ sudo apt-get install linux-source + + Bu indirmeler "/usr/src" dizinine yapılmaktadır. + + Ayrıca "/lib/modules/$(uname -r)" isimli dizindeki "build" isimli dizin de çekirdek kaynak kodlarının bulunduğu dizine ya da + aygıt sürücülerin derlenmesi için gereken öğelerin bulunduğu dizine (tipik olarak "linux-headers-$(uname -r)" dizinine) + sembolik link yapılmış durumdadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir çekirdek modülünde biz user mod için yazılmış kodları kullanamayız. Çünkü orası ayrı bir dünyadır. Ayrıca biz çekirdek + modüllerinde çekirdek içerisindeki her fonksiyonu da kullanamayız. Yalnızca bazı fonksiyonları kullanabiliriz. Bunlara + "çekirdek tarafından export edilmiş fonksiyonlar" denilmektedir. "Çekirdek tarafından export edilmiş fonksiyon" kavramıyla + "sistem fonksiyonu" kavramının bir ilgisi yoktur. Sistem fonksiyonları kullanıcı modundan (user mode) çağrılmak üzere tasarlanmış + ayrı bir grup fonksiyondur. Oysa çekirdek tarafından export edilmiş fonksiyonlar kullanıcı modundan çağrılamazlar. Yalnızca + çekirdek modüllerinden çağrılabilirler. Buradan çıkan sonuç şudur: Bir çekirdek modülü yazılırken ancak çekirdeğim export + ettiği fonksiyonlar ve nesneler kullanılabilmektedir. Tabii çekirdeğin 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 çekirdek modül içerisindeki belli fonksiyonları + da programcı export edebilir. Bu durumda bu fonksiyonlar da başka çekirdek modüllerinden kullanılabilirler. O halde özetle: + + 1) Çekirdek modülleri yalnızca çekirdek içerisindeki export edilmiş fonksiyonları kullanabilir. + 2) Kendi çekirdek modülümüzde biz de istediğimiz fonksiyonu export edebiliriz. Bu durumda bizim çekirdek modülümüz çekirdeğin + bir parçası haline geldiğine göre başka çekirdek modülleri de bizim export ettiğimiz bu fonksiyonları kullanabilir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Mademki çekirdek modülleri işletim sisteminin çekirdek kodlarındaki fonksiyonları ve nesneleri kullanabiliyor o zaman + çekirdek modülleri o anda çalışılan çekirdeğin yapısına da bağlı durumdadır. Bu nedenle işletim sistemlerinde "çekirdek + modülü yazmak" ya da "aygıt sürücü yazmak" biçiminde genel bir konu yoktur. Her işletim sisteminin çekirdek modül ve aygıt + sürücü mimarisi diğerlerinden farklıdır. Dolayısıyla çekirdek modüllerinin ve aygıt sürücülerinin yazılması spesifik bir + işletim sistemi için geçerli olabilecek platform oldukça bağımlı bir konudur. Hatta işletim sistemlerinde bazı versiyonlarda + genel aygıt sürücü mimarisi bile değiştirilebilmektedir. Dolayısıyla bu tür durumlarda eski aygıt sürücüler yeni versiyonlarda, + yenileri de eski versiyonlarda çalışamamaktadır. Örneğin Linux'ta çekirdek versiyonları arasında çekirdekteki export edilmiş + bazı fonksiyonlar isim ya da parametrik yapı olarak değiştirilmiş durumdadır. Bu nedenle Linux çekirdeğinin belli bir + versiyonu için yazılmış olan aygıt sürücüler başka bir versiyonunda geçersiz hale gelebilmektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Çekirdek modüllerinin ve aygıt sürücülerin yazımı için programcının çekirdek yapısını ana hatlarıyla bilmesi gerekmektedir. + Çünkü bunları yazarken çekirdeğin içerisindeki çeşitli veri yapıları ve export edilmiş fonksiyonlar kullanılmaktadır. + + Linux çekirdek modülleri ve aygıt sürücüleri hakkında yazılmış birkaç kitap vardır. Bunların en klasik olanı "Linux Device + Drivers (3. Edition)" kitabıdır. Ancak bu kitaptaki bazı içerikler güncel çekirdeklerle uyumsuz hale gelmiştir. Bu konudaki + resmi dokümanlar ise "kernel.org" sitesindeki "documentation" kısmında bulunmaktadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir çekirdek modülünü derlemek ve link etmek maalesef sanıldığından daha zordur. Her ne kadar çekirdek modülleri 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 sokulmaktadır. Çekirdek modüllerinin link edilmeleri de bazı kütüphane dosyalarının devreye + sokulmasıyla yapılmaktadır. Dolayısıyla bir çekirdek modülünün manuel biçimde "build edilmesi" için bazı ayrıntılı bilgilere + gereksinim duyulmaktadır. İşte çekirdek modüllerinin build edilmesinde çekirdeğin KBuild sistemi devreye sokulmaktadır. Bu + nedenle çekirdek modüllerinin build edilmesi için çekirdek kaynak kodlarındaki birtakım başlık dosyalarının ve Make dosyalarının + build işleminin yapılacağı makinede bulunması gerekir. Biz yukarıda bu dosyalara "/lib/modules/$(uname -r)/build" dizini + yoluyla erişilebileceğini belirtmiştik. Bu dizin aslında Linux kaynak kod ağacının bulunduğu dizini belirtmektedir. Ancak + yukarıda da belirttiğimiz gibi çekidek modüllerinin ve aygıt sürücülerin derlenmesi için Linux'ın tüm kaynak kodlarına + gerek yoktur. Yalnızca başlık dosyaları ve make dosyalarının bulunması yeterlidir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Çekirdek modülleri o anda çalışılan host sistem için derlenebileceği gibi gömülü bir sistem için de derlenebilir. Eğer + derleme gömülü sistem için yapılacaksa süphesiz çapraz derleyicilerin de ilgili sistemde kurulu olması gerekir. Yukarıda + da belirttiğimiz gibi çekirdek modüllerinin derlenmesi için ilgili çekirdeğe yönelik başlık dosyaları ve çeşitli make + dosyaları gibi bazı öğelerin de bulunuyor olması gerekir. Eğer derleme bir gömülü sistem için yapılacaksa o gömülü sistemdeki + çekirdeğe ilişkin bu dosyalar da host makinede bulunuyor olmalıdır. Örneğin biz masaüstü bilgisayardaki Mint dağıtımında + çalışıyor olalım. Bu sistemin kendisi için çekirdek modülü derleyeceksek zaten tüm gerkeli öğeler hazır durumdadır. + Ancak biz bu makinede BeagleBone Black için çekirdek modülü ve aygıt sürücü derlemesi yapacaksak çapraz derleyicimizin ve + BeagleBone Black'teki çekirdeğe yönelik temel başlık dosyalarının ve Make dosyalarının bulunuyor olamsı gerekecektir. Tabii + BeagleBone Black'teki çekirdek sürümüneilişkin çekirdek kaynak kodları bu makineye çekerek bu gereksinimi karşılayabiliriz. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Masaüstü bir sistem için çekirdek modül derlemeinde kullanılabilecek minimal bir Makefile dosyası 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" dosyası çalıştırılmış ondan sonra çalışma bu yazdığımız + make dosyasından devam ettirilmiştir. Özetle bu make dosyası aslında çekirdeğin build sistemini kullanarak "generic.c" isimli + dosyanın derlenmesini ve çekirdek modülü biçiminde link edilmesini sağlamaktadır. Bu make dosyasını şöyle de düzenleyebilirdik: + + obj-m += generic.o + + KDIR := /lib/modules/$(shell uname -r)/build + PWD := $(shell pwd) + + all: + make -C ${KDIR} M=${PWD} modules + clean: + make -C ${KDIR} M=${PWD} clean + + Çekirdek modül birden fazla kaynak dosyadan oluşturulabilir. Bu durumda ilk satır şöyle oluşturulabilir: + + obj-m += a.o b.o c.o... + + Eğer bu dosyaları birden fazla satırda ayrı ayrı belirtirsek bu durumda birden fazla modül dosyası oluşturulacaktır: + + obj-m += a.o b.o c.o... + + Eğer bu dosyaları birden fazla satırda ayrı ayrı belirtirsek bu durumda birden fazla modül dosyası oluşturulur: + + 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. + Buradaki M=${PWD} derlenecek kaynak dosyaların o anda +---------------------------------------------------------------------------------------------------------------------------*/ + +# 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 + +/*-------------------------------------------------------------------------------------------------------------------------- + Çekirdek modüllerini va aygıt sürücüleri derlerken iki noktaya dikkat etmelisiniz: + + 1) Kullandığınız çekirdek kodları hedef mekinenin çekirdek sürümüne uygun olmalıdır. Eğer bu koşul sağlanmazsa çekirdekler + arasında farklılıklar söz konusu olabileceği için derlenmiş olan çekirdek modül dosyası hedef sisteme başarılı bir biçimde + yüklenemeyebilir. Tabii Linux çekirdeğindeki değişiklikler daha önce yazılmış olan her çekirdek modülünü ve aygıt sürücüyü + geçersiz hale getirmemektedir. Örneğin minör numara değişikliklerinde genellikle bir sorun oluşmamaktadır. Ancak ne olursa + olsun derleme yapılırken hedef sistemdeki çekirdeğe uygun kaynak dosyaların kullanılması şiddetle tavsiye edilmektedir. + Örneğin biz 6.9.2 çekirdeğinde çalışan makine için aygıt sürücüsü yazacaksak derleme yaptığımız makinede kullanacağımız + çekirdek kaynak kodlarının da bu 6.9.2 çekirdeğine ilişkin olması gerekir. Biz eski bir çekirdeğin kaynak kodlarıyla yeni + bir çekirdek için aygıt süsürücü derlemeye çalışmamalıyız. Tabii eski bir veriyon kullanılarak derleme yapılırsa çoğu + durumda bir sorun ortaya çıkmayabilecektir. Ancak sorunun ortaya çıkma olsılığı da vardır. + + 2) Kullanılan araç zincirinin de (yani derleyici, linker gibi programların da) çekirdeğin derlenmiş olduğu sistemle uyumlu + olmasına dikkat ediniz. Eğer bu temel araçların versiyonlarında geçmişe doğru uyumu bpzabilecek değişiklikler söz konusuysa + yine derleme işlemi başarısız olabilir ya da çekirdek modülü yüklenirken sorun oluşabilir. Aslında çekirdeğin KBuild sistemi + çekirdek konfigürasyon dosyası yoluyla bu kontrolü yapabilmektedir. Ancak bu kontrol bypass da edilebilmektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Tabii aslında make dosyası parametrik biçimde de oluşturabilmektedir: + + 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 + + Bu durumda make programı çalıştırılırken bu parametrenin değeri de belirtilmelidir. Örneğin: + + $ make file=helloworld +---------------------------------------------------------------------------------------------------------------------------*/ + +# Makefile + +obj-m += $(file).o + +all: + make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules +clean: + make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean + +/*-------------------------------------------------------------------------------------------------------------------------- + Ş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: + + /* 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"); + } + + 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 çekirdek modülü, çekirdeğin 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 çekirdek modülümüz çekirdeğin içerisine yerleştirilmiştir. Yani modülümüz adeta çekirdeğinbir parçası gibi işlev + görecektir. + + Çekirdek modülleri istenildiği zaman "rmmod" isimli programla çekirdekten çıkartılabilirler. Bu programın da yine sudo + ile "root" önceliğinde çalıştırılması gerekir. Örneğin: + + $ sudo rmmod helloworld.ko + + rmmod komutu kullanılırken ".ko" dosya uzantısı da belirtilmeyebilir. Örneğin: + + $ sudo rmmod generic + + 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 + +/*-------------------------------------------------------------------------------------------------------------------------- + Modüller "modprobe" isimli programla da yüklenebilir. Ancak modprobe programı yüklenecek modülleri "/lib/modules/$(uname -r)" + dizininde aramaktadır. Dolayısıyla biz kendi derlediğimiz modülleri bu dizine yerleştirmemişsek yüklemeyi modprobe ile + yapamayız. Ancak çekirdek derlenirken oluşturulmuş olan modüller bu dizinde olduğu için bunları modprobe ile yükleyebiliriz. + modprobe programı yüklenecek aygıt sürücünün yalnızca ismini almaktadır. Çünkü zaten arama işlemini kendisi yapmaktadır. + Örneğin: + + $ modprobe g_ether + + insmod programının yüklenecek aygıt sürücü dosyasının tüm yol ifadesini aldığına dikkat ediniz. modprobe programında dosyanın + ".ko" uzantısı da belirtilmemektedir. Halbuki insmod programında bu uazantının da belirtilmesi gerekmektedir. modprobe aslında + "modules.dep" isimi bir dosyaya başvurmaktadır. Bu dosya çekirdek kaynak kodlarının kök dizininde çekirdek modülleri derlenirken + oluşturulmaktadır. Bu dosya içerisinde bağımlılık bilgileri vardır. Bir çekirdek modülü yazılırken başka bir çekirdek modülünün + içerisindeki fonksiyonlar kullanılmış olabilir. Bu durumda kullanan modülün yüklenmesi için önce onun kullandığı modülün + yüklenmesi gerekir. İşte bu biçimde durum karmaşık bir hal alabilmektedir. "modules.dep" dosyası içerisinde bir modülün + yüklenebilmesi için hangi modüllerin de yüklenmesi gerektiği bilgileri bulunmaktadır. Eğer biz kendi çekirdek modülümüzün de + modprobe ile yüklenmesini istiyorsak önce onu "/lib/modules/$(uname -r)/kernel" dizininin içerisindeki dizinlerden birine + yerleştirip sonra bu "modules.dep" dosyasının güncllenmesini sağlamamız gerekir. Bu işlem "depmod" programıyla "-a" seçeneği + kullanılarak yapılmaktadır: + + $ sudo depmod -a + + Kendi çekirdek modülünüzü ya da aygıt sürücünüzü örneğin "/lib/modules/$(uname -r)/kernel/drivers/misc" dizinine yerleştirebilirsiniz. + Tabii aygıt sürücü geliştirirken ikide bir modülü buraya yerleştirmenin bir anlamı yoktur. Bu nedenle geliştirme aşamasında + genellikle "insmod" programı kullanılmaktadır. + + modprobe ile yüklenen aygıt sürücü "modeprobe -r" ile boşaltılabilir. Örneğin: + + $ modprobe -r g_ether + + Tabii boşaltım sırasında yine eğer aygıt sürünün bağımlı olduğu çekirdek modülleri başka modüller tarafından kullanılmıyorsa + onlar da çekirdekten çıkartılacaktır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 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_irq 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" (available) ile belirtilen elemanlar zaten BPB içerisinde olan 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ıyı 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şikliklerin 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 oluşmaktadı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 ve 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. +---------------------------------------------------------------------------------------------------------------------------*/ + +/* 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 */ + +#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); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + 172. Ders 18/10/2024 - Cuma +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 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 boyutu 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 saklamaya çalışır. Ancak bu durum mümkün olmayabilir. Belli bir süre + sonra artık dosyaların parçaları 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'ler 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 iki elemanı da kullanılmamaktadı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 tutar. 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 yazdığı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 (000C), 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. open_fatsys 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 hem 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); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + 173. Ders 20/10/2024 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 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 byte'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); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + 174. Ders 25/10/2024 - Cuma +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + İşletim sisteminin dosya sistemi bize aslında cluster'larda olan dosya parçalarını "dosya" adı altında ardışıl byte + topluluğu gibi göstermektedir. Biz işletim sisteminin sistem fonksiyonu ile dosyayı açarız ve read fonksiyonu ile okumayı + yaparız. Bütün diğer işlemler işletim sisteminin çekirdek kodları tarafından yapılmaktadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Biz yukarıda 16 bit FAT için işlemler yaptık. Pekiyi 12 bit ve 32 bit FAT bölümü nasıldır? 32 bit FAT önemli bir farklılığa + sahip değildir. Her FAT elemanı 32 bit yani 4 byte uzunluktadır. Dolayısıyla daha büyük bir volüm için kullanılabilir. + 16 bit FAT'te toplam 65536 FAT elemanı elemanı olabilir (Bazılarının kullanılmadığını da anımsayınız.) Bir cluster en fazla + 64 sektör uzunluğunda olabilmektedir. Bu durumda FAT16 sistemlerinde 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 byte olduğuna dikkat ediniz. Buradaki 12 bit şöyle elde edilmektedir. 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ı çiftse 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 + F0H - 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; +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 175. Ders 27/10/2024 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + FAT32 sisteminde her FAT elemanı 32 bittir. Ancak bu sistemde boot sektördeki BPB alanında da farklılıklar vardır. Bu nedenle + 32 bit FAT sistemi FAT12 ve FAT16 ile tam uyumlu değildir. FAT32 için bazı fonksiyonların yeniden yazılması gerekir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Biz FAT dosya sisteminin boot sektörünü, FAT ve Data bölümlerini ele aldık. Ele almadığımız tek bölüm "Root Dir" bölümüdür. + Şimdi "Root Dir" bölümü ve dosya bilgilerinin nasıl saklandığı konusu üzerinde duracağız. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Microsoft'un FAT dosya sisteminde ve UNIX/Linux sistemlerinde kullanılan i-node tabanlı dosya sistemlerinde dizinler de + tamamen bir dosya gibi organize edilmektedir. Yani dizinler de aslında birer dosyadır. Bir dosyanın içerisinde o dosyanın + bilgileri bulunurken bir dizin dosyasının içerisinde o dizindeki dosyalara ilişkin bilgiler bulunmaktadır. Yani dizinler + aslında "o dizindeki dosyaların bilgilerini içeren dosyalar" gibidir. Bir dizin dosyası "dizin girişlerinden (directory + entry) oluşmaktadır. FAT12 ve FAT16 dosya sistemlerinde bir dizin dosyasındaki dizin girişleri 32 byte uzunluğundaydı. + Yani dizin dosyaları 32 byte'lık kayıtların peşi sıra gelmesiyle oluşuyordu. O zamanlarda 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 bunu yaparken de geçmişe doğru 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 byte'lık klasik dizin girişi formatı şöyledir: + + Offset (Hex) Uzunluk Anlamı + + 00 8 Byte Dosya İsmi (File Name) + 08 3 Byte Dosya Uzantısı (Extension) + 0B BYTE Dosya Özelliği (Attribute) + 0C 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" dosyasının ve "mydir" dizininin 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 karakterleriyle (0x20) + padding yapılmaktadır. + + - Dosya Özelliği: Bu alanda dosyanın özelliklerine ilişkin bir 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'ının 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ı ilişkin 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, 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 zaman 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 + + Buradaki 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 noktaya 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 bilgilerinin saniyelerini default + durumda zaten göstermemektedir. Ö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 dosyanı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 offsetinden 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 karakteri 0xE5 olarak değiştirir. Böylece + dizin girişlerini tararken 32'lik girişin ilk karakteri 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 tersine 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 isimleri birden fazla + 32'lik girişle temsil edilmektedir. Ancak Microsoft geçmişe doğru uyumu 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 alana sığmayan + dosya isimleri aşağıdaki formata göre dizin girişlerinde bulundurulmaktadır: + + <32'lik giriş> + <32'lik giriş> + ... + <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üksek anlamlı bit olan D biti 32'lik girişin silinip silinmediğ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. Geri 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 ismine 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; + 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); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + 177. Ders 03/11/2024 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + İş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 programcı aşağıdaki gibi bir yol ifadesi kullanmış olsun: + + "\a\b\c\d.dat" + + İşletim sistemi buradaki "a", "b", "c" ve "d.dat" dosyalarına ilişkin dizin giriş bilgilerini bir cache sisteminde saklamaktadır. + Böylece benzer yol ifadeleri için hiç disk okuması yapılmadan bu cache sisteminden bu bilgiler elde edilebilmektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi FAT dosya sistemi için yol ifadelerini çözen basit yalın bir kodu nasıl yazabiliriz? Bizim bir yol ifadesi verildiğinde + o yol ifadesini parse edip oradaki yol bileşenlerini elde edebilmemiz gerekir. Sonra dizinler bir dosya olduğuna göre dizinlere + ilişkin cluster zincirinde diğer bileşenin aranması gerekir. İşlemler böyle devam ettirilir. FAT dosya sistemi için yol + ifadesini çözümleyen bir fonksiyonun parametrik yapısı şöyle olabilir: + + int resolve_path(FATSYS *fatsys, const char *path, DIRECTORY_ENTRY *de); + + Biz mutlak yol ifadelerini çözümleyecek olalım. Her dizinin bir cluster zinciri vardır. Ancak FAT12 ve FAT16 sistemlerinde + kök dizinin bir cluster zinciri yoktur. Kök dizinin yeri ve uzunluğu baştan bellidir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 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 faydalanabilirsiniz. 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 blok (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 blokta 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| Açıklama | + | |Byte | | + +-----------------------+-----+-------------------------------------------------------------------------------------------+ + | 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 edilmiş) blokların sayısı bulunmaktadır. + + s_free_blocks_count: Burada Data bölümünde kullanılmayan 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-node 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 süper blok 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ı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 itibaren geçen saniye sayısı cinsindedir. + + i_block ve i_blocks: Bu elemanlar izleyen paragraflarda daha 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 elemanı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. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 179. Ders 10/11/2024 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 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 blok grubu disk bölümüne ilişkin bir bölümü belirtir. + Bir blok grubu aşağıdaki gibi bir yapıya sahiptir: + + + + + + + + + İşte "blok grup betimleyici tablosu (block group descriptor table)" blok grupları hakkında bilgi veren bir bölümdür. Bir + kaç blok bilginin yer aldığı süper bloğ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ı süper bloktaki s_inodes_per_group elemanıyla + belirtilmektedir. Yani aslında ext2 dosya sisteminde aşağıdaki gibi bir organizasyon 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 süper bloğun tek bir kopyası yoktur. Her blok grupta süper 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 betimleyici 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. + Blok grup betimleyici tablosu blok grup betimleyicilerinden oluşan bir dizi gibidir: + + Blok 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 | Inode haritasının başlangıç adresi | + | bg_inode_table | 4 | Inode 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 blok 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 süper bloktan 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 blok 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ın başlangıç bloğu da dolaylı bir biçimde hesaplanmaktadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 180. Ders 17/11/2024 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 181. Ders 22/11/2024 - Cuma +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 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 numarası 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 blok 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'ıncı blok grubu eğer blok uzunluğu 1K'dan fazla ise diskin 0'ıncı bloğundan başlamaktadır. (Tabii 0'ıncı bloğun 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 durumda 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ı 256 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. Bunları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 olmayan eleman)> + ... + + Reserve edilmiş ilk i-node elemanlarının anlamları şöyledir: + + +----------------------+-------+-------------------------------+ + | İsim | Değer | Anlamı | + +----------------------+-------+-------------------------------+ + | EXT2_BAD_INO | 1 | Bad blocks i-node | + | EXT2_ROOT_INO | 2 | Root directory i-node | + | EXT2_ACL_IDX_INO | 3 | ACL index inode (deprecated?) | + | EXT2_ACL_DATA_INO | 4 | ACL data i-node (deprecated?) | + | EXT2_BOOT_LOADER_INO | 5 | Boot loader i-node | + | EXT2_UNDEL_DIR_INO | 6 | Undelete directory i-node | + +----------------------+-------+-------------------------------+ + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 182. Ders 24/11/2024 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 183. Ders 01/12/2024 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi 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 teriminin 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ın içerisinde tutulmaktadır. i-node elemanlarının + genel olarak 128 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. Pekiyi o zaman dosyanın blok numaraları i-node elemanında nasıl tutulmaktadır? + + İşte i-node elemanında dosyanın hangi bloklarda olduğu "doğrudan (direct)", "dolaylı (indirect)", "çift dolaylı (double + indirect)" ve "üç dolaylı (triple indirect)"" bloklarda tutulmaktadır. i-node elemanının decimal 40'ıncı offset'inde (i-node + elemanının 0x28 offset'indeki "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 dizinin ilk 12 + elemanından doğrudan elde edilmektedir. Eğer dosya 12 bloktan büyükse bu durumda bu dizinin 12'nci indeksindeki elemanda + yazan blok numarası dosyanın diğer bloklarının blok numaralarını tutan bloğun numarasıdır. Yani dizinin 12'nci elemanında + belirtilen bloğa gidildiğinde bu bloğun içerisinde blok numaraları vardır. Bu blok numaraları da dosyanın 12'nci 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 1036 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 tutulduğu blok numaralarıdır. Eğer dosya çift dolaylı bloklara da sığmıyorsa üç dolaylı + bloğa başvurulmaktadır. Üç dolaylı blokta belirtilen blok numarasında çift dolaylı blokların numaraları vardır. Çift dolaylı + blokların içerisinde dolaylı blokların numaraları vardır. Nihayet dolaylı blokların içerisinde de asıl blokların numaraları + vardır. + + Pekiyi her bloğun 4K uzunluğunda olduğu bir sistemde bir dosyanın i-node elemanında belirtilen maksimum uzunluğu ne olabilir? + İşte bu uzunluk aşağıdaki değerlerin toplamıyla elde edilebilir: + + 12 tane doğrudan blok = 12 * 4096 + 1 tane dolaylı blok = 1024 * 4096 + 1 tane çift dolaylı blok = 1024 * 1024 * 4096 + 1 tane üç dolaylı blok = 1024 * 1024 * 1024 * 4096 + + Toplam = 12 * 4096 + 1024 * 4096 + 1024 * 12024 * 4096 + 1024 * 1024 * 1024 * 4096 = 4448483065856 = 4 TB civarı. + + Şimdi aşağıdaki i-node elemanına bakıp dosya bloklarının yerlerini tespit edelim: + + 00010b00 a4 81 00 00 c8 79 00 00 87 5c 2e 67 87 5c 2e 67 |.....y...\.g.\.g| + 00010b10 87 5c 2e 67 00 00 00 00 00 00 01 00 40 00 00 00 |.\.g........@...| + 00010b20 00 00 00 00 01 00 00 00 00 08 00 00 01 08 00 00 |................| + 00010b30 02 08 00 00 03 08 00 00 04 08 00 00 05 08 00 00 |................| + 00010b40 06 08 00 00 07 08 00 00 00 00 00 00 00 00 00 00 |................| + 00010b50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010b60 00 00 00 00 2e db 09 7c 00 00 00 00 00 00 00 00 |.......|........| + 00010b70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010b80 20 00 00 00 c8 76 3d ba c8 76 3d ba c8 76 3d ba | ....v=..v=..v=.| + 00010b90 87 5c 2e 67 c8 76 3d ba 00 00 00 00 00 00 00 00 |.\.g.v=.........| + 00010ba0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010bb0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010bc0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010bd0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010be0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010bf0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + + Burada dosyanın tüm blokları 0x28'inci offset'teki doğrudan bloklarda belirtilmektedir: + + 00 08 00 00 => 0x800 + 01 08 00 00 => 0x801 + 02 08 00 00 => 0x802 + 03 08 00 00 => 0x803 + 04 08 00 00 => 0x804 + 05 08 00 00 => 0x805 + 06 08 00 00 => 0x806 + 07 08 00 00 => 0x807 + + Dosya 0x4 offset'inde belirtilen 0x79C8 = 31176 byte uzunluğundadır. Bu sistemde bir blok 4K olduğuna göre toplam dosyanın + parçalarının 8 blok olması gerekmektedir. İşte burada söz konusu dosyanın blokları disk bölümünün başından itibaren + 0x800, 0x801, 0x802, 0x803, 0x804, 0x805, 0x806 ve 0x807'inci bloklardadır. Söz konusu sistemde bir blok 4096 byte + olduğuna göre dosyanın ilk bloğunun offset numarası 0x800 * 0x1000 = 0x800000 biçimindedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Şimdi de ext2 dosya sisteminde dizin organizasyonu üzerinde duralım. Tıpkı FAT dosya sistemlerinde olduğu gibi ext2 + dosya sisteminde de dizinler birer dosya gibi organize edilmiştir. (Anımsanacağı gibi dizin dosyalarından biz opendir, + readdir, closedir fonksiyonlarıyla okuma yapabiliyorduk.) Yani dizinler aslında birer dosya gibidir. Dizin dosyaları + "dizin girişleri (directory entries)" denilen girişlerden oluşmaktadır. + + + + + + + ... + + Bir dizin girişin format şöyledir: + + +----------------+--------------+-------------------------+ + | Offset (bytes) | Size (bytes) | Açıklama | + +----------------+--------------+-------------------------+ + | 0 | DWORD | i-node numarası | + | 4 | WORD | Girişin toplam uzunluğu | + | 6 | BYTE | Dosya isminin uzunluğu | + | 7 | BYTE | Dosyanın türü | + | 8 | 0-255 Bytes | Dosya ismi | + +----------------+--------------+-------------------------+ + + Aslında buradaki bilgiler Linux'taki readdir POSIX fonksiyonu ile de alınabilmektedir. readdir fonksiyonu POSIX standartlarına + göre en az iki elemana sahip olmak zorundadır. Bunlar d_ino ve d_name elemanlarıdır. Ancak Linux'taki read bize daha fazla + bilgi vermektedir. Linux'taki dirent yapısı şöyledir: + + struct dirent { + ino_t d_ino; /* Inode number */ + off_t d_off; /* Not an offset; see below */ + unsigned short d_reclen; /* Length of this record */ + unsigned char d_type; /* Type of file; not supported by all filesystem types */ + char d_name[256]; /* Null-terminated filename */ + }; + + Dizin girişleri FAT dosya sistemindeki gibi eşit uzunlukta girişlerden oluşmamaktadır. Bunun nedeni dosya isimlerinin + 0 ile 255 karakter arasında değişebilmesidir. Dizin girişlerinin hemen başında DWORD bir alanda dosyanın i-node numarası + belirtilmektedir. Dizinler değişken uzunlukta olduğu için ilgili girişin toplam kaç byte uzunlukta olduğu sonraki WORD + elemanda tutulmaktadır. Girişteki dosya isminin uzunluğu ise sonraki BYTE elemanında tutulmaktadır. Dosyanın türü + hiç i-node elemanına erişmeden elde edilebilsin diye dizin girişlerinde de tutulmaktadır. Dosya türlerini belirten değerler + şöyledir: + + +------------------+-------+-------------------+ + | İsim | Değer | Anlamı | + +------------------+-------+-------------------+ + | EXT2_FT_UNKNOWN | 0 | Unknown File Type | + | EXT2_FT_REG_FILE | 1 | Regular File | + | EXT2_FT_DIR | 2 | Directory File | + | EXT2_FT_CHRDEV | 3 | Character Device | + | EXT2_FT_BLKDEV | 4 | Block Device | + | EXT2_FT_FIFO | 5 | Buffer File | + | EXT2_FT_SOCK | 6 | Socket File | + | EXT2_FT_SYMLINK | 7 | Symbolic Link | + +------------------+-------+-------------------+ + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 184. Ders 06/12/2024 - Cuma +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi ext2 dosya sisteminde işletim sistemi bir yol ifadesini nasıl çözümlemektedir? Örneğin "/a/b/c.txt" gibi bir yol + ifadesinde "c.txt" dosyasının i-node elemanına nasıl erişmektedir? İşte kök dizin dosyasının bilgileri 2 numaralı i-node + elemanındadır. İşletim sistemi önce kök dizinin i-node elemanını elde eder. Oradan kök dizinin bloklarına erişir. O bloklar + içerisinde ilgili girişi arar. İşlemlerini bu biçimde devam ettirir. Örneğin "/a/b/c.txt" dosyasının i-node elemanına + erişmek için önce kök dizinde "a" girişini arar. Sonra "a" girişinin dizin olduğunu doğrular. Sonra "a" dizininde "b" + girişini arar. "b" girişinin de dosya olduğunu doğrular. Sonra "b" girişinin içerisinde "c.txt" arar ve hedef dosyanın + i-node bilgilerine erişir. + + Şimdi adım adım elimizdeki disk bölümünde "/a/b/c.txt" dosyasının yerini bulmaya çalışalım. Tabii buradaki kök dizin + aslında mount edilmiş dosya sisteminin köküdür. Biz bu dosya sistemini kursumuzda aşağıdaki noktaya mount ettik: + + "/home/kaan/Study/UnixLinux-SysProg/DiskIO-FileSystems" + + Kök dizinin i-node elemanı (2 numaralı i-node elemanı) aşağıda verilmiştir: + + 00010100 ed 41 00 00 00 10 00 00 a8 69 4c 67 a7 69 4c 67 |.A.......iLg.iLg| + 00010110 a7 69 4c 67 00 00 00 00 00 00 04 00 08 00 00 00 |.iLg............| + 00010120 00 00 00 00 04 00 00 00 2b 06 00 00 00 00 00 00 |........+.......| + 00010130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00010180 20 00 00 00 a0 15 ed 2d a0 15 ed 2d 1c 7e f4 e6 | ......-...-.~..| + 00010190 42 5c 2e 67 00 00 00 00 00 00 00 00 00 00 00 00 |B\.g............| + 000101a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000101b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000101c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000101d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000101e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000101f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + + Burada 0x28'inci offset'teki i_blocks elemanının yalnızca ilkinin dolu olduğunu görüyoruz. Demek ki kök dizin tek bir + bloktan oluşmaktadır. Kök dizinin blok numarası 0x62B'dir. Şimdi 0x62b bloğunun offset'ini hesaplayalım. Bunun için + bu değeri 0x1000 (4096) ile çarpmamız gerekir: + + 0x62B * 0x1000 = 0x62B000 (6467584) + + Diskin bu offset'indeki değerler şöyledir: + + 0062b000 02 00 00 00 0c 00 01 02 2e 00 00 00 02 00 00 00 |................| + 0062b010 0c 00 02 02 2e 2e 00 00 0b 00 00 00 14 00 0a 02 |................| + 0062b020 6c 6f 73 74 2b 66 6f 75 6e 64 00 00 0c 00 00 00 |lost+found......| + 0062b030 10 00 07 01 73 74 64 69 6f 2e 68 00 b2 61 00 00 |....stdio.h..a..| + 0062b040 c4 0f 01 02 61 00 00 00 00 00 00 00 00 00 00 00 |....a...........| + 0062b050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0062b060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0062b070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0062b080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0062b090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0062b0a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0062b0b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + + Buradaki dizin girişlerini çözelim. Dizin giriş formatını aşağıda yeniden veriyoruz: + + +----------------+--------------+-------------------------+ + | Offset (bytes) | Size (bytes) | Açıklama | + +----------------+--------------+-------------------------+ + | 0 | DWORD | i-node numarası | + | 4 | WORD | Girişin toplam uzunluğu | + | 6 | BYTE | Dosya isminin uzunluğu | + | 7 | BYTE | Dosyanın türü | + | 8 | 0-255 Bytes | Dosya ismi | + +----------------+--------------+-------------------------+ + + İlk dizin girişinin i-node numarası 2'dir. Bu girişin uzunluğu 0x0C = 12'dir. O halde bu dizin girişi şöyledir: + + 0062b000 02 00 00 00 0c 00 01 02 2e 00 00 00 02 + + Burada dosya ismi 1 karakter uzunluktadır. Dosya ismi yalnızca 0x2E karakterinde oluşmaktadır. Bu karakter de "." karakteridir. + Sonraki dizin girişinin i-node numarası yine 2'dir. Girişin uzunluğu yine 0xC = 12'dir. O halde giriş şöyledir: + + 0062b000 02 00 00 00 |................| + 0062b010 0c 00 02 02 2e 2e 00 00 |................| + + Buradaki dosya uzunluğunun 2 olduğu görülmektedir. Dosya ismi de 0x2E 0x2E karakterinden oluşmaktadır. Bu da ".." ismidir. + Her dizinin ilk iki elemanının bu biçimde olduğunu anımsayınız. Sonraki giriş ise şöyledir: + + 0062b010 0b 00 00 00 14 00 0a 02 |................| + 0062b020 6c 6f 73 74 2b 66 6f 75 6e 64 00 00 |lost+found......| + + Burada dosya i-node numarası 0x0b = 11'dir. Dizin girişinin uzunluğu 0x14 = 20'dir. Dosya isminin uzunluğu 0xA = 10'dur. + Dosya ismi "lost+found" biçimindedir. Sonraki giriş ise şöyledir: + + 0062b020 0c 00 00 00 |lost+found......| + 0062b030 10 00 07 01 73 74 64 69 6f 2e 68 00 |....stdio.h..a..| + + Buaraki girişin i-node numarası 0xC = 12'dir. Girişin toplam uzunluğu 0x10 = 16'dır. Dosyanın isminin uzunluğu 7'dir. + Dosya ismi "stdio.h" biçimindedir. Sonraki giriş ise şöyledir: + + 0062b030 b2 61 00 00 |....stdio.h..a..| + 0062b040 c4 0f 01 02 61 00 00 00 00 00 00 00 00 00 00 00 |....a...........| + + Burada dosyanın i-node numarası 0x61B2 = 25010'dur. Girişin uzunluğu 0xC4 = 196'dır. (Bu değerin çok uzun olması önemli değildir. + Çünkü bu dizindeki son dosyadır.) Dosya isminin uzunluğu 1'dir. Dosya türü 0x02'dir. Yani bu giriş bir dizin belirtmektedir. + Dosya ismi "a" biçimindedir. + + İşte işletim sistemi 0x61B2 = 25010'ıncı i-node elemanında bu dizinin bilgilerinin olduğunu tespit eder ve o i-node elemanını + okur. Bu i-node elemanı aşağıdaki gibidir: + + 08010100 ed 41 00 00 00 10 00 00 f8 2b 53 67 a7 69 4c 67 |.A.......+Sg.iLg| + 08010110 a7 69 4c 67 00 00 00 00 00 00 03 00 08 00 00 00 |.iLg............| + 08010120 00 00 00 00 02 00 00 00 2b 86 00 00 00 00 00 00 |........+.......| + 08010130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 08010140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 08010150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 08010160 00 00 00 00 f9 65 fb 3c 00 00 00 00 00 00 00 00 |.....e.<........| + 08010170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 08010180 20 00 00 00 a0 15 ed 2d a0 15 ed 2d 04 7d e1 7b | ......-...-.}.{| + 08010190 a7 69 4c 67 a0 15 ed 2d 00 00 00 00 00 00 00 00 |.iLg...-........| + 080101a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 080101b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 080101c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 080101d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 080101e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 080101f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + + "a" dizinine ilişkin i-node elemanının 0x28'inci offset'teki blokları bir tanedir ve blok numarası 0x862B = 34547'dir. + Bu bloğun offset'i de 34547 * 4096 = 140685312'dir. Dizine ilişkin dizin bloğunun içeriği şöyledir: + + 0862b000 b2 61 00 00 0c 00 01 02 2e 00 00 00 02 00 00 00 |.a..............| + 0862b010 0c 00 02 02 2e 2e 00 00 b3 61 00 00 e8 0f 01 02 |.........a......| + 0862b020 62 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |b...............| + 0862b030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0862b040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0862b050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0862b060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0862b070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0862b080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0862b090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0862b0a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + + Bu dizin girişlerine bakıldığında "b" isimli girişin bulunduğu görülmektedir. İşte yol ifadesi bu aşamalardan geçilerek + çözümlenmektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi bir dosya oluşturulurken boş bloklar nasıl tespit edilmektedir? İşte her blok grup betimleyicisi kendi blok grubundaki + boş blokları "block bitmap" denilen tabloda bit düzeyinde tutmaktadır. Blok bitmap tablosu her biti bir bloğun boş mu dolu + mu olduğunu tutmaktadır. Blok grup betimleyicisinde yalnızca blok grubunun yeri tutulur. Bunun blok uzunluğu ilgili blok + gruplarındaki blok sayısına bakılarak tespit edilmelidir. Her blok grubunda eşit sayıda blok bulunur. Bu sayı süper blok + içerisindeki s_blocks_per_group elemanında saklanmaktadır. Aşağıda bir grup betimleyicisinin blok bitmap tablosunun bir + bölümünü görüyorsunuz: + + 0000e000 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e010 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e020 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e030 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e040 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e050 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e060 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e070 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e080 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e090 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e0a0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e0b0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| + 0000e0c0 ff ff ff ff ff ff 01 00 00 00 00 00 00 00 00 00 |................| + 0000e0d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000e0e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000e0f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000e100 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + + Buradaki FF byte'larına dikkat ediniz. FF byte'ı aslında ikilik sistemde 1111 1111 bitlerine karşılık gelmektedir. Yani + bu bloklar tamamen tahsis edilmiştir. 00 olan byte'lara ilişkin bloklar tahsis edilmemiş durumdadır. + + i-node elemanlarının tahsis edilip edilmediğine yönelik de benzer bir tablo tutulmaktadır. Buna "i-node bitmap" tablosu + denilmektedir. Her blok grubunda bir i-node bitmap tablosu bulunur. Bu tablo da bitlerden oluşmaktadır. Her bit ilgili + i-node elemanının boş mu dolu mu olduğunu belirtir. i-node bitmap tablosunun yeri de yine blok grup betimleyicisinde + tutulmaktadır. Bu tablonun uzunluğu da yine süper bloktaki "bir grup bloğundaki i-node elemanlarının sayısı" dikkate + alınarak tespit edilmektedir. Aşağıda örnek bir i-node bitmap tablosunun bir kısmını görüyorsunuz: + + 0000f000 ff 0f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000f010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000f020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000f030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000f040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000f050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000f060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000f070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 0000f080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + + Bu blok grubunda toplam 12 i-node elemanı tahsis edilmiş durumdadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Biz yukarıdaki örneklerde dosya sistemini tanıyabilmek için manuel işlemler yaptık. Pekiyi bu işlemleri programlama yoluyla + nasıl yapabiliriz? Yukarıda açıkladığımız dosya sistemi alanlarına ilişkin yapılar çeşitli kütüphanelerin içerisinde hazır + bir biçimde bulunmaktadır. Örneğin "libext2fs" kütüphanesi kurulduğunda dosyasında tüm yapı bildirimleri + bulunacaktır. Kütüphanenin kurulumunu şöyle yapabilirsiniz: + + $ sudo apt-get install libext2fs-dev + + Aslında bu kütüphane ve başlık dosyası yalnızca ext2 dosya sistemine ilişkin değil, ext2 ve ext4 dosya + sistemine ilişkin de yapıları ve fonksiyonları bulundurmaktadır. + + Örneğin başlık dosyası içerisindeki süper blok yapısı aşağıdaki gibi bildirilmiştir: + + struct ext2_super_block { + /*000*/ __u32 s_inodes_count; /* Inodes count */ + __u32 s_blocks_count; /* Blocks count */ + __u32 s_r_blocks_count; /* Reserved blocks count */ + __u32 s_free_blocks_count; /* Free blocks count */ + /*010*/ __u32 s_free_inodes_count; /* Free inodes count */ + __u32 s_first_data_block; /* First Data Block */ + __u32 s_log_block_size; /* Block size */ + __u32 s_log_cluster_size; /* Allocation cluster size */ + /*020*/ __u32 s_blocks_per_group; /* # Blocks per group */ + __u32 s_clusters_per_group; /* # Fragments per group */ + __u32 s_inodes_per_group; /* # Inodes per group */ + __u32 s_mtime; /* Mount time */ + /*030*/ __u32 s_wtime; /* Write time */ + __u16 s_mnt_count; /* Mount count */ + __s16 s_max_mnt_count; /* Maximal mount count */ + __u16 s_magic; /* Magic signature */ + __u16 s_state; /* File system state */ + __u16 s_errors; /* Behaviour when detecting errors */ + __u16 s_minor_rev_level; /* minor revision level */ + /*040*/ __u32 s_lastcheck; /* time of last check */ + __u32 s_checkinterval; /* max. time between checks */ + __u32 s_creator_os; /* OS */ + __u32 s_rev_level; /* Revision level */ + /*050*/ __u16 s_def_resuid; /* Default uid for reserved blocks */ + __u16 s_def_resgid; /* Default gid for reserved blocks */ + /* + * These fields are for EXT2_DYNAMIC_REV superblocks only. + * + * Note: the difference between the compatible feature set and + * the incompatible feature set is that if there is a bit set + * in the incompatible feature set that the kernel doesn't + * know about, it should refuse to mount the filesystem. + * + * e2fsck's requirements are more strict; if it doesn't know + * about a feature in either the compatible or incompatible + * feature set, it must abort and not try to meddle with + * things it doesn't understand... + */ + __u32 s_first_ino; /* First non-reserved inode */ + __u16 s_inode_size; /* size of inode structure */ + __u16 s_block_group_nr; /* block group # of this superblock */ + __u32 s_feature_compat; /* compatible feature set */ + /*060*/ __u32 s_feature_incompat; /* incompatible feature set */ + __u32 s_feature_ro_compat; /* readonly-compatible feature set */ + /*068*/ __u8 s_uuid[16] __nonstring; /* 128-bit uuid for volume */ + /*078*/ __u8 s_volume_name[EXT2_LABEL_LEN] __nonstring; /* volume name, no NUL? */ + /*088*/ __u8 s_last_mounted[64] __nonstring; /* directory last mounted on, no NUL? */ + /*0c8*/ __u32 s_algorithm_usage_bitmap; /* For compression */ + /* + * Performance hints. Directory preallocation should only + * happen if the EXT2_FEATURE_COMPAT_DIR_PREALLOC flag is on. + */ + __u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/ + __u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */ + __u16 s_reserved_gdt_blocks; /* Per group table for online growth */ + /* + * Journaling support valid if EXT2_FEATURE_COMPAT_HAS_JOURNAL set. + */ + /*0d0*/ __u8 s_journal_uuid[16] __nonstring; /* uuid of journal superblock */ + /*0e0*/ __u32 s_journal_inum; /* inode number of journal file */ + __u32 s_journal_dev; /* device number of journal file */ + __u32 s_last_orphan; /* start of list of inodes to delete */ + /*0ec*/ __u32 s_hash_seed[4]; /* HTREE hash seed */ + /*0fc*/ __u8 s_def_hash_version; /* Default hash version to use */ + __u8 s_jnl_backup_type; /* Default type of journal backup */ + __u16 s_desc_size; /* Group desc. size: INCOMPAT_64BIT */ + /*100*/ __u32 s_default_mount_opts; /* default EXT2_MOUNT_* flags used */ + __u32 s_first_meta_bg; /* First metablock group */ + __u32 s_mkfs_time; /* When the filesystem was created */ + /*10c*/ __u32 s_jnl_blocks[17]; /* Backup of the journal inode */ + /*150*/ __u32 s_blocks_count_hi; /* Blocks count high 32bits */ + __u32 s_r_blocks_count_hi; /* Reserved blocks count high 32 bits*/ + __u32 s_free_blocks_hi; /* Free blocks count */ + __u16 s_min_extra_isize; /* All inodes have at least # bytes */ + __u16 s_want_extra_isize; /* New inodes should reserve # bytes */ + /*160*/ __u32 s_flags; /* Miscellaneous flags */ + __u16 s_raid_stride; /* RAID stride in blocks */ + __u16 s_mmp_update_interval; /* # seconds to wait in MMP checking */ + __u64 s_mmp_block; /* Block for multi-mount protection */ + /*170*/ __u32 s_raid_stripe_width; /* blocks on all data disks (N*stride)*/ + __u8 s_log_groups_per_flex; /* FLEX_BG group size */ + __u8 s_checksum_type; /* metadata checksum algorithm */ + __u8 s_encryption_level; /* versioning level for encryption */ + __u8 s_reserved_pad; /* Padding to next 32bits */ + __u64 s_kbytes_written; /* nr of lifetime kilobytes written */ + /*180*/ __u32 s_snapshot_inum; /* Inode number of active snapshot */ + __u32 s_snapshot_id; /* sequential ID of active snapshot */ + __u64 s_snapshot_r_blocks_count; /* active snapshot reserved blocks */ + /*190*/ __u32 s_snapshot_list; /* inode number of disk snapshot list */ + #define EXT4_S_ERR_START ext4_offsetof(struct ext2_super_block, s_error_count) + __u32 s_error_count; /* number of fs errors */ + __u32 s_first_error_time; /* first time an error happened */ + __u32 s_first_error_ino; /* inode involved in first error */ + /*1a0*/ __u64 s_first_error_block; /* block involved in first error */ + __u8 s_first_error_func[32] __nonstring; /* function where error hit, no NUL? */ + /*1c8*/ __u32 s_first_error_line; /* line number where error happened */ + __u32 s_last_error_time; /* most recent time of an error */ + /*1d0*/ __u32 s_last_error_ino; /* inode involved in last error */ + __u32 s_last_error_line; /* line number where error happened */ + __u64 s_last_error_block; /* block involved of last error */ + /*1e0*/ __u8 s_last_error_func[32] __nonstring; /* function where error hit, no NUL? */ + #define EXT4_S_ERR_END ext4_offsetof(struct ext2_super_block, s_mount_opts) + /*200*/ __u8 s_mount_opts[64] __nonstring; /* default mount options, no NUL? */ + /*240*/ __u32 s_usr_quota_inum; /* inode number of user quota file */ + __u32 s_grp_quota_inum; /* inode number of group quota file */ + __u32 s_overhead_clusters; /* overhead blocks/clusters in fs */ + /*24c*/ __u32 s_backup_bgs[2]; /* If sparse_super2 enabled */ + /*254*/ __u8 s_encrypt_algos[4]; /* Encryption algorithms in use */ + /*258*/ __u8 s_encrypt_pw_salt[16]; /* Salt used for string2key algorithm */ + /*268*/ __le32 s_lpf_ino; /* Location of the lost+found inode */ + __le32 s_prj_quota_inum; /* inode for tracking project quota */ + /*270*/ __le32 s_checksum_seed; /* crc32c(orig_uuid) if csum_seed set */ + /*274*/ __u8 s_wtime_hi; + __u8 s_mtime_hi; + __u8 s_mkfs_time_hi; + __u8 s_lastcheck_hi; + __u8 s_first_error_time_hi; + __u8 s_last_error_time_hi; + __u8 s_first_error_errcode; + __u8 s_last_error_errcode; + /*27c*/ __le16 s_encoding; /* Filename charset encoding */ + __le16 s_encoding_flags; /* Filename charset encoding flags */ + __le32 s_reserved[95]; /* Padding to the end of the block */ + /*3fc*/ __u32 s_checksum; /* crc32c(superblock) */ + }; + + Kütüphane aşağıdaki bağlantıdan indirebileceğiniz pdf dosyasında dokümante edilmiştir: + + https://www.dubeyko.com/development/FileSystems/ext2fs/libext2fs.pdf + + Ayrıca "e2fsprogs" isimli pakette de ext2, ext3 ve ext4 dosya sistemlerine ilişkin pek çok yardımcı program bulunmaktadır. + Örneğin bizim daha önce kullandığımız "dumpe2fs" programı bu paketin bir parçasıdır. Buradaki programlar "libext2fs" + kütüphanesi kulanılarak yazılmıştır. Paket pek çok Linux dağıtımında default biçimde kurulu durumdadır. Eğer dağıtımınızda + kurulu değilse bu paketi aşağıdaki gibi kurabilirsiniz: + + $ sudo apt-get install e2fsprogs +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 185. Ders 08/12/2024 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Şimdi "libext2fs" kütüphanesinin basit bir kullanımı üzerinde duracağız. Kütüphanenin temel başlık dosyası + isimli dosyadır. Bu dosyanın include edilmesi gerekir. + + #include + + Kütüphaneyi kullanan programlar derlenirken link aşamasında "-lext2fs" seçeneğinin bulundurulması gerekir. Örneğin: + + $ gcc -o sample sample.c -lext2fs + + Kütüphane içerisindeki fonksiyonların çoğunun geri dönüş değerleri errcode_t türündendir. errcode_t türü long biçimde + typedef edilmiştir. Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda hata ile ilgili bir değere geri + dönmektedir. Hatayı yazdırmak için error_message isimli fonksiyon bulunmaktadır. Bu fonksiyona errcode_t değeri parametre + olarak verilir, fonksiyon da hata yazısına ilişkin static bir dizinin başlangıç adresine geri döner. Örneğin: + + ext2_filsys fs; + errcode_t err; + + if ((err = ext2fs_open("/dev/loop0", 0 /* EXT2_FLAG_RW */, 0, 0, unix_io_manager, &fs)) != 0) { + fprintf(stderr, "%s\n", error_message(err)); + exit(EXIT_FAILURE); + } + + Bu kütüphane kullanılarak yazılmış programlar genel olarak aygıtlara eriştiği için "sudo" ile çalıştırılması gerekir. + Aksi takdirde "Permission denied (EACCESS)" hatası ortaya çıkacaktır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Kütüphanenin kullanılması için ilk yapılacak işlem dosya sisteminin ext2fs_open fonksiyonu ile açılmasıdır. Fonksiyonun + prototipi şöyledir: + + errcode_t ext2fs open (const char *name, int args, int superblock, int block size, io_manager manager, ext2_filsys *ret fs); + + Fonksiyonun birinci parametresi dosya sisteminin bulunduğu dosyanın ya da aygıt dosyasının yol ifadesini almaktadır. + Parametrelerin çoğu default 0 geçilebilir. Ancak io_manager parametresi için unix_io_manager argümanının girilmesi + gerekmektedir. Fonksiyon ext2fil_sys türünden handle belirten bir nesne vermektedir. Bu tür şöyle typedef edilmiştir: + + typedef struct struct_ext2_filsys *ext2_filsys; + + Dosya sistemine ilişkin tüm bilgiler bu struct_ext2_filsys yapısının içerisindedir. Bu yapı şöyle bildirilmiştir: + + struct struct_ext2_filsys { + errcode_t magic; + io_channel io; + int flags; + char * device_name; + struct ext2_super_block * super; + unsigned int blocksize; + int fragsize; + dgrp_t group_desc_count; + unsigned long desc_blocks; + struct opaque_ext2_group_desc * group_desc; + unsigned int inode_blocks_per_group; + ext2fs_inode_bitmap inode_map; + ext2fs_block_bitmap block_map; + ... + }; + + Örneğin bu yapının super elemanı süper blok bilgilerinin bulunduğu ext2_super_block isimli yapı nesnesinin adresini + vermektedir. + + Örneğin: + + ext2_filsys fs; + errcode_t err; + + if ((err = ext2fs_open("/dev/loop0", 0 /* EXT2_FLAG_RW */, 0, 0, unix_io_manager, &fs)) != 0) { + fprintf(stderr, "cannot open file system!..\n"); + exit(EXIT_FAILURE); + } + + Açılan dosya sisteminin işlem bitince ext2fs_close fonksiyonu ile kapatılması gerekir. Fonksiyonun prototipi şöyledir: + + errcode_t ext2fs close (ext2_filsys fs); + + Örneğin: + + ext2_filsys fs; + errcode_t err; + + if ((err = ext2fs_open("/dev/loop0", 0 /* EXT2_FLAG_RW */, 0, 0, unix_io_manager, &fs)) != 0) { + fprintf(stderr, "cannot open file system!..\n"); + exit(EXIT_FAILURE); + } + + /* ... */ + + ext2fs_close(fs); +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include + +int main(void) +{ + ext2_filsys fs; + errcode_t err; + + if ((err = ext2fs_open("/dev/loop0", 0 /* EXT2_FLAG_RW */, 0, 0, unix_io_manager, &fs)) != 0) { + fprintf(stderr, "cannot open file system!..\n"); + exit(EXIT_FAILURE); + } + + printf("Number of i-node: %lu\n", (unsigned long)fs->super->s_inodes_count); + printf("Total Block: %lu\n", (unsigned long)fs->super->s_blocks_count); + /* ... */ + + ext2fs_close(fs); + + return 0; +} + +/*-------------------------------------------------------------------------------------------------------------------------- + Belli bir numaraya sahip i-node elemanını elde edebilmek için ext2fs_read_inode fonksiyonu kullanılmaktadır. Fonksiyonun + prototipi şöyledir: + + errcode_t ext2fs_read_inode(ext2_filsys fs, ext2_ino_t ino, struct ext2_inode *inode); + + Fonksiyonun birinci parametresi dosya sistemini temsil eden handle değeridir. İkinci parametre bilgileri elde edilecek + i-node elemanın numarasını belirtir. Üçüncü parametre de i-node bilgilerinin yerleştirileceği yapının adresini almaktadır. + Örneğin: + + if ((err = ext2fs_read_inode(fs, 13, &inode)) != 0) { + fprintf(stderr, "cannot read inode!..\n"); + exit(EXIT_FAILURE); + } +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 186. Ders 15/12/2024 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir diske birden fazla bağımsız dosya sisteminin (ve belki de işletim sisteminin) yüklenebilmesi için diskin mantıksal + bakımdan parçalara ayrılması gerekmektedir. Diskin mantıksal bakımdan parçalara ayrılmasına ise "disk bölümlemesi" + denilmektedir. Disk bölümlemesi aslında disk bölümlerinin hangi sektörden başlayıp kaç sektör uzunluğunda olduğunun + belirlenmesi anlamına gelmektedir. Böylece her dosya sistemi başkasının alanına müdahale etmeden yalnızca o disk bölümünü + kullanmaktadır. + + Diskteki disk bölümleri hakkında bilgileri barındıran tabloya "disk bölümleme tablosu (disk partition table)" denilmektedir. + Bugün için iki disk bölümleme tablo formatı kullanılmaktadır: + + 1) Klasik (legacy) MBR Disk Bölümleme Tablo Formatı + 2) Modern UEFI BIOS Sistemlerinin Kullandığı GPT (Guid Partition Table) Formatı + + UEFI BIOS'lar GPT disk bölümleme tablosu kullanırken eski sistemler ve gömülü sistemler genel olarak klasik MBR disk + bölümleme tablosunu kullanmaktadır. Gömülü sistemler için oluşturduğumuz SD kartlar'daki disk bölümleme tablosu klasik + (legacy) disk bölümleme tablosudur. Ancak bugünkü büyük çaplı UEFI BIOS'lar önce GPT disk bölümleme tablosuna bakmakta + eğer onu bulamazsa klasik disk bölümleme tablosunu aramaktadır. Yani geçmişe doğru uyum korunmaya çalışılmıştır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Daha önceden de belirttiğimiz gibi disk sistemlerine okuma ve yazma işlemleri blok aygıt sürücüleri tarafından yapılmaktadır. + Dolayısıyla bir UNIX türevi sistemde her disk için "/dev" dizininin altında bir blok aygıt dosyası bulunmaktadır. Aynı zamanda + her disk bölümü için de bir blok aygıt dosyası bulunur. Böylece bir diskin bütünü üzerinde de yalnızca onun belli bir bölümü + üzerinde de çalışılabilir. Daha önceden de kullanmış olduğumuz "lsblk" komutu bize sistemimizdeki diskler ve onların bölümleri + hakkında bilgiler vermektedir. Örneğin çalıştığımız sistemde "lsblk" komutunu uyguladığımızda şöyle bir çıktı ile karşılaşmaktayız: + + $ lsblk + NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS + loop0 + 7:0 0 195,3M 0 loop /home/kaan/Study/UnixLinux-SysProg/DiskIO-FileSystems/ext2 + sda 8:0 0 60G 0 disk + ├─sda1 + │ 8:1 0 1M 0 part + ├─sda2 + │ 8:2 0 513M 0 part /boot/efi + └─sda3 + 8:3 0 59,5G 0 part / + sr0 11:0 1 1024M 0 rom + + Buradaki "sda" aygıtı bir diski bütün olarak ele almak için kullanılmaktadır. Bu diskin 3 tane disk bölüme vardır. Bu + disk bölümleri de "sda1", "sda2" ve "sda3" aygıtlarıdır. Bu aygıtlara ilişkin "/dev" dizini altında aygıt dosyaları + bulunmaktadır. Örneğin: + + $ ls -l /dev/sda* + brw-rw---- 1 root disk 8, 0 Ara 6 15:08 /dev/sda + brw-rw---- 1 root disk 8, 1 Ara 6 15:08 /dev/sda1 + brw-rw---- 1 root disk 8, 2 Ara 6 15:08 /dev/sda2 + brw-rw---- 1 root disk 8, 3 Ara 6 15:08 /dev/sda3 + + Diskleri temsil eden isimler o diskin türüne göre değişebilmektedir. Normal hard diskler için isimlendirme "sda", "sdb", + "sdc" biçiminde yapılmaktadır. Bunların bölümleri de "sda1", "sda2", "sdb1, "sdb2" biçiminde yapılır. MicroSD kartlar + "mmcblk0", "mmcblk1", "mmcblk2" biçiminde isimlendirilmektedir. Bunların bölümleri de "mmcblk0p1", "mmcblk0p2", "mmcblk1p1", + "mmcblk1p2" biçiminde yapılmaktadır. Eğer disk bir loop aygıtı biçiminde oluşturulmuşsa disk bölümleri "loop0p1", "loop0p2", + "loop1p1", "loop1p2" biçiminde isimlendirilmektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Klasik MBR (legacy) disk bölümlendirmesinde diskin ilk sektörüne (0 numaralı sektörüne) MBR (Master Boot Record) sektörü + denilmektedir. MBR sektörünün sonundaki 2 byte MBR'nin bilinçli olarak oluşturulduğunu belirten sihirli bir sayıdan (magic + number) oluşmaktadır. Bu sihirli sayı hex olarak 55 AA biçimindedir. Aşağıda "loop0" aygıtı üzerinde oluşturulmuş bir MBR + sektörü görülmektedir: + + $ sudo hexdump /dev/loop0 -C -v -n 512 + 00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000000e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000180 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 00000190 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000001a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000001b0 00 00 00 00 00 00 00 00 03 74 de d6 00 00 80 01 |.........t......| + 000001c0 01 00 83 20 0d 13 3f 00 00 00 01 b0 04 00 00 20 |... ..?........ | + 000001d0 0e 13 83 3e 18 26 40 b0 04 00 c0 af 04 00 00 00 |...>.&@.........| + 000001e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| + + Sektörün sonunun 55 AA ile bittiğine dikkat ediniz. + + Klasik MBR Disk Bölümlemesi'nde MBR sektörünün sonundaki 64 byte'a "Disk Bölümleme Tablosu (Disk Partition Table)" denilmektedir. + Tabii sektörün sonunda hex olarak 55 AA bulunduğu için disk bölümleme tablosu da bu 55 AA byte'larının hemen gerisindeki + 64 byte'tadır. O halde MBR sektörünün sonu aşağıdaki gibidir: + + ... <64 byte (Disk Bölümleme Tablosu)> 55 AA + + Yukarıdaki MBR sektörünün son 64 byte'ı ve 55 AA değerleri aşağıda verilmiştir: + + 80 01 |.........t......| + 000001c0 01 00 83 20 0d 13 3f 00 00 00 01 b0 04 00 00 20 |... ..?........ | + 000001d0 0e 13 83 3e 18 26 40 b0 04 00 c0 af 04 00 00 00 |...>.&@.........| + 000001e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| + + Başka bir deyişle Disk Bölümleme Tablosu MBR sektörünün 0x1BE (446) offset'inden başlayıp 64 byte sürmektedir. Disk Bölümleme + Tablosu'ndaki her disk bölümü 16 byte ile betimlenmektedir. Dolayısıyla klasik Disk Bölümleme Tablosu 4 disk bölümünü barındırmaktadır. + Pekiyi bu durumda 4'ten fazla disk bölümü oluşturulamaz mı? İşte "Genişletilmiş Disk Bölümü (Extended Disk Partition)" kavramı + ile bu durum mümkün hale getirilmiştir. Yukarıdaki Disk Bölümleme Tablosu'nun 16 byte'lık disk bölümleri aşağıda verilmiştir: + + 80 01 01 00 83 20 0d 13 3f 00 00 00 01 b0 04 00 + 00 20 0e 13 83 3e 18 26 40 b0 04 00 c0 af 04 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + Disk Bölümleme Tablosu'ndaki 16 byte'lık disk bölümünün içeriği şöyledir: + + +--------------+----------------+---------------------------------------------------------------------+ + | Offset (Hex) | Uzunluk | Anlamı | + +--------------+----------------+---------------------------------------------------------------------+ + | 0 | 1 BYTE | Disk Bölümünün Aktif Olup Olmadığı Bilgisi | + | 1 | 3 BYTE | Disk Bölümünün Eski Sistemdeki (CHS Sistemindeki) Başlangıç Sektörü | + | 4 | 1 BYTE | Sistem ID Değeri | + | 5 | 3 BYTE | Disk Bölümünün Eski Sistemdeki (CHS Sistemindeki) Bitiş Sektörü | + | 8 | 4 BYTE (DWORD) | Disk Bölümünün LBA Sistemindeki Başlangıç Sektörü | + | C | 4 BYTE (DWORD) | Disk Bölümündeki Sektör Sayısı (Disk Bölümünün Uzunluğu) | + +--------------+----------------+---------------------------------------------------------------------+ + + - 4 disk bölümünden yalnızca bir tanesi aktif olabilmektedir. Sistem aktif disk bölümünden boot edilmektedir. Aktif disk + bölümü için 0x80 değeri, aktif olmayan disk bölümü için 0x00 değeri kullanılmaktadır. + + - Eskiden diskteki bir sektörün yeri "hangi yüzde (her yüzü bir kafa okuduğu için, hangi kafada), hangi track'te (track'e + silindir (cylinder) de denilmektedir) ve hangi sektör diliminde olduğu bilgisiyle ifade ediliyordu. Bu koordinat sistemine + CHS (Cylinder-Head-Sector) koordinat sistemi deniyordu. Sonra bu koordinat sisteminden vazgeçildi. Sektörün yeri ilk sektör + 0 olmak üzere tek bir sayıyla temsil edilmeye başlandı. + + - Her disk bölümünde farklı bir işletim sisteminin kullandığı dosya sistemi bulunuyor olabilir. "Sistem ID Değeri" o disk + bölümünde hangi işletim sistemine ilişkin bir dosya sisteminin bulunduğunu belirtmektedir. Böylece Disk Bölümleme Tablosu'nu + inceleyen kişiler disk bölümlerinin hangi işletim sistemi için oluşturulduğunu anlayabilmektedir. Tüm Sistem ID Değerleri + için bunların listelendiği dokümanlara başvurabilirsiniz. Biz burada birkaç System ID değerini verelim: + + 0C: Windows FAT32 Sistemi + 0E: Windows FAT Sistemi + 0F: Genişletilmiş Disk Bölümü + 83: Linux Dosya Sistemlerinden Birisi + 82: Linux İçin Swap Alanı Olarak Kullanılacak Disk Bölümü + + - Bir disk bölümü için en önemli iki bilgi onun diskin hangi sektöründen başlayıp kaç sektör uzunlukta olduğudur. Yani disk + bölümünün başlangıç sektör numarası ve toplam sektör sayısıdır. İşletim sistemleri böylece kendileri için belirlenmiş olan + disk bölümlerinin dışına erişmezler. Yani disk bölümleri adeta disk içerisindeki disklerin yerlerini belirtmektedir. + + - 90'lı yıllarla birlikte diskteki sektörlerin adreslenmesi için CHS sistemi yavaş yavaş bırakılmaya başlanmış LBA (Logical + Block Address) denilen sisteme geçilmiştir. Bu sistemde diskin ilk sektörü 0 olmak üzere her sektöre artan sırada bir tamsayı + karşılık düşürülmüştür. İşte bu koordinat sistemine LBA denilmektedir. Artık MBR Disk Bölümleri'nde disk bölümünün başlangıç + sektörü LBA sistemine göre belirtilmektedir. + + - LBA sisteminde bir disk bölümünde en fazla 2^32 tane sektör bulunabilir. Bir sektör 2^9 (512) byte olduğuna göre MBR + Disk Bölümleme Tablosu en fazla 2^41 = 2TB diskleri destekleyebilmektedir. Gömülü sistemlerde henüz bu büyüklükte diskler + kullanılmadığı için klasik MBR Disk Bölümleme Tablosu iş görmektedir. Ancak masaüstü sistemlerde artık bu sınır aşılmaktadır. + İşte UEFI BIOS'lar tarafından kullanılan "GUID Disk Bölümlemesi (GPT)" bu sınırı çok daha ötelere taşımaktadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Disk Bölümleme Tablosu manuel bir biçimde oluşturulabilir. Ancak Disk Bölümleme Tablosu üzerinde işlem yapan çeşitli + araçlar da vardır. Linux sistemlerinde en yaygın kullanılan iki araç "fdisk" ve "gparted" isimli araçlardır. fdisk komut + satırından kullanılabilen basit bir programdır. "gparted" ise GUI arayüzü ile görsel bir biçimde aynı işlemleri yapmaktadır. + "fdisk" temel bir araçtır. Dolayısıyla Linux sistemleri kurulduğunda büyük olasılıkla zaten kurulmuş olarak sisteminizde + bulunuyor olacaktır. Ancak "gparted" programını Debian tabanlı sistemlerde aşağıdaki gibi siz kurmalısınız: + + $ sudo apt-get install gparted +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 187. Ders 22/12/2024 - Cuma +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bu noktada Linux sistemlerindeki fdisk programının kullanımı üzerinde duracağız. Bu tür denemeleri loop aygıtları üzerinde + yapmalısınız. fdisk kullanımını maddeler halinde açıklayalım: + + 1) Hangi disk üzerinde işlem yapılacaksa o diske ilişkin aygıt dosyası fdisk programına komut satırı argümanı olarak + verilmelidir. Tabii disk aygıt dosyaları "root" kullanıcısına ilişkin olduğu için fdisk programı da genellikle "sudo" + ile çalıştırılır. Örneğin önce blok aygıt sürücülerimize bakalım: + + ****************************************************** + $ lsblk + NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT + sda 8:0 0 80G 0 disk + ├─sda1 8:1 0 512M 0 part /boot/efi + ├─sda2 8:2 0 1K 0 part + └─sda5 8:5 0 79,5G 0 part / + sr0 11:0 1 1024M 0 rom + ****************************************************** + + Burada "sda" diski bir bütün olarak gösteren aygıt sürücüsüdür. Bu aygıt sürücüye ilişkin aygıt dosyası "/dev/sda" biçimindedir. + "sda1", "sda2" ve "sda5" disk üzerindeki disk bölümleridir. Bizim bölümlendirme için diski bir bütün olarak ele almamız gerekir. + Bu nedenle "sda" diski için fdisk programı şöyle çalıştırılmalıdır: + + $ sudo fdisk /dev/sda + + Tabi biz örneğimizde loop aygıtı kullanacağız. Bu durumda loop aygıtını şöyle kullanıma hazır hale getirebiliriz: + + ****************************************************** + $ dd if=/dev/zero of=disk.img bs=300M count=1 + 1+0 kayıt girdi + 1+0 kayıt çıktı + 314572800 bytes (315 MB, 300 MiB) copied, 1,23241 s, 255 MB/s + ****************************************************** + + Buradan diski temsil eden içi sıfırlarla dolu 300MB'lik bir dosya oluşturduk. Şimdi bu dosyayı "/dev/loop0" aygıt dosyası + ile bir blok aygıtı gibi gösterelim: + + $ sudo losetup /dev/loop0 disk.img + + Artık "/dev/loop0" aygıt dosyası sanki bir disk gibi kullanılabilecektir. Bu aygıt üzerinde işlem yaptığımızda işlemden + "disk.img" dosyası etkilenecektir. + + Artık blok aygıt sürücülerine baktığımızda "loop0" aygıtını göreceğiz: + + ****************************************************** + $ lsblk + NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT + loop0 7:0 0 300M 0 loop + sda 8:0 0 80G 0 disk + ├─sda1 8:1 0 512M 0 part /boot/efi + ├─sda2 8:2 0 1K 0 part + └─sda5 8:5 0 79,5G 0 part / + sr0 11:0 1 1024M 0 rom + ***************************************************** + + Artık fdisk programını bu aygıt üzerinde kullanabiliriz: + + $ sudo fdisk /dev/loop0 + + 2) Artık interaktif bir biçimde disk bölümlendirme işlemleri yapılabilir. Burada tek harfli çeşitli komutlar girildiğinde + interaktif bir biçimde işlemler yapılmaktadır. Bu komutlardan önemli olanlarını açıklamak istiyoruz: + + - "n" (new) komutu yeni bir disk bölümü oluşturmak için kullanılmaktadır. Bu komut verildiğinde yaratılacak disk bölümünün + "primary" bölüm mü "extended" bölüm mü olduğu sorulmaktadır. Primary disk bölümü ana 4'lük girişteki bölümlerdir. Dolayısıyla + burada genellikle "p" komutu ile "primary" disk bölümü oluşturulur. Sonra bize 4 girişten hangisinin disk bölümü olarak + oluşturulacağı sorulmaktadır. Bu durumda sıradaki numarayı vermek (disk tamamen ilk kez bölümlendiriliyorsa 1) uygun olur. + Sonra da bize ilgili disk bölümünün hangi sektörden başlatılacağı ve ne uzunlukta olacağı sorumaktadır. Aşağıda bir örnek + görüyorsunuz: + + ************************************************************************************** + Komut (yardım için m): n + Disk bölümü tipi + p birincil (0 birincil, 0 genişletilmiş, 4 boş) + e genişletilmiş (mantıksal disk bölümleri için konteyner) + Seç (varsayılan p): p + Disk bölümü numarası (1-4, varsayılan 1): 1 + İlk sektör (2048-614399, varsayılan 2048): + Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-614399, varsayılan 614399): +50M + + Yeni bir disk bölümü 1, 'Linux' tipinde ve 50 MiB boyutunda oluşturuldu. + *************************************************************************************** + + - "p" (print) komutu oluşturulmuş olan disk bölümlerini görüntülemektedir. Örneğin: + + ************************************************************************************** + Komut (yardım için m): p + Disk /dev/loop0: 300 MiB, 314572800 bayt, 614400 sektör + Birimler: sektör'i 1 * 512 = 512 baytın + Sektör boyutu (montıksal/fiziksel): 512 bayt / 512 bayt + G/Ç boyutu (en düşük/en uygun): 512 bayt / 512 bayt + Disketikeri tipi: dos + Disk belirleyicisi: 0x267a62e0 + + Aygıt Açılış Başlangıç Son Sektör Boyut ld Türü + /dev/loop0p1 2048 104447 102400 50M 83 Linux + ************************************************************************************** + + - fdisk yaratılan disk bölümlerinin ID'sini default olarak 0x83 (Linux) yapmaktadır. Eğer disk bölümüne FAT dosya sistemi + yerleştirilecekse "t" (type) komutu ile bölüm ID'si değitirilmelidir. Örneğin: + + ************************************************************************************** + Komut (yardım için m): t + Seçilen disk bölümü 1 + Hex kod (bütün kodlar için L tuşlayın): c + 'Linux' disk bölümünün tipini 'W95 FAT32 (LBA)' olarak değiştirin. + + Komut (yardım için m): p + Disk /dev/loop0: 300 MiB, 314572800 bayt, 614400 sektör + Birimler: sektör'i 1 * 512 = 512 baytın + Sektör boyutu (montıksal/fiziksel): 512 bayt / 512 bayt + G/Ç boyutu (en düşük/en uygun): 512 bayt / 512 bayt + Disketikeri tipi: dos + Disk belirleyicisi: 0x267a62e0 + + Aygıt Açılış Başlangıç Son Sektör Boyut ld Türü + /dev/loop0p1 2048 104447 102400 50M c W95 FAT32 (LBA) + ************************************************************************************** + + Şu anda biz bir FAT disk bölümü yaratmış olduk. Şimdi ikinci Linux dosya sistemleri için ikinci bölümünü de yaratalım: + + ************************************************************************************** + Komut (yardım için m): n + Disk bölümü tipi + p birincil (1 birincil, 0 genişletilmiş, 3 boş) + e genişletilmiş (mantıksal disk bölümleri için konteyner) + Seç (varsayılan p): p + Disk bölümü numarası (2-4, varsayılan 2): + İlk sektör (104448-614399, varsayılan 104448): + Last sector, +/-sectors or +/-size{K,M,G,T,P} (104448-614399, varsayılan 614399): + + Yeni bir disk bölümü 2, 'Linux' tipinde ve 249 MiB boyutunda oluşturuldu. + + Komut (yardım için m): p + Disk /dev/loop0: 300 MiB, 314572800 bayt, 614400 sektör + Birimler: sektör'i 1 * 512 = 512 baytın + Sektör boyutu (montıksal/fiziksel): 512 bayt / 512 bayt + G/Ç boyutu (en düşük/en uygun): 512 bayt / 512 bayt + Disketikeri tipi: dos + Disk belirleyicisi: 0x267a62e0 + + Aygıt Açılış Başlangıç Son Sektör Boyut ld Türü + /dev/loop0p1 2048 104447 102400 50M c W95 FAT32 (LBA) + /dev/loop0p2 104448 614399 509952 249M 83 Linux + ************************************************************************************** + + Artık diskimizde iki disk bölümü vardır. + + - Bir disk bölümünü aktive etmek için "a" komutu (activate) kullanılmaktadır. Örneğin biz FAT32 disk bölümünü aktif disk + bölümü haline getirelim: + + ************************************************************************************** + Komut (yardım için m): a + Disk bölümü numarası (1,2, varsayılan 2): 1 + + Disk bölümü 1'de önyüklenebilir bayrağı artık etkin. + + Komut (yardım için m): p + Disk /dev/loop0: 300 MiB, 314572800 bayt, 614400 sektör + Birimler: sektör'i 1 * 512 = 512 baytın + Sektör boyutu (montıksal/fiziksel): 512 bayt / 512 bayt + G/Ç boyutu (en düşük/en uygun): 512 bayt / 512 bayt + Disketikeri tipi: dos + Disk belirleyicisi: 0x267a62e0 + + Aygıt Açılış Başlangıç Son Sektör Boyut ld Türü + /dev/loop0p1 * 2048 104447 102400 50M c W95 FAT32 (LBA) + /dev/loop0p2 104448 614399 509952 249M 83 Linux + ************************************************************************************** + + - fdisk önce yazılacakları kendi içerisinde biriktirmekte sonra bunları diske yazmaktadır. Biriktirilenlerin diske yazılması + için "w" (write) komutu kullanılmaktadır. Örneğin: + + ************************************************************************************** + Komut (yardım için m): w + Disk bölümleme tablosu değiştirildi. + Disk bölüm tablosunu yeniden okumak için ioctl() çağrılıyor. + Disk bölümü tablosu yeniden okunamadı.: Geçersiz bağımsız değişken + + Çekirdek hala eski tabloyu kullanıyor. Yeni tablo bir sonraki yeniden başlatma işleminden sonra ya da partprobe(8) veya + kpartx(8)'i çalıştırdığınızda kullanılacak. + ************************************************************************************** + + - Bir disk bölümünü silmek için "d" komutu kullanılmaktadır. Disk bölümlerini silerken dikkat ediniz. + + 3) Disk bölümlerini oluşturduktan sonra çekirdeğin onları o anda görmesi için "partprobe" komutu kullanılmalıdır. Örneğin: + + ************************************************************************************** + $ sudo partprobe /dev/loop0 + [sudo] kaan için parola: + $ lsblk + NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT + loop0 7:0 0 300M 0 loop + ├─loop0p1 259:0 0 50M 0 part + └─loop0p2 259:1 0 249M 0 part + sda 8:0 0 80G 0 disk + ├─sda1 8:1 0 512M 0 part /boot/efi + ├─sda2 8:2 0 1K 0 part + └─sda5 8:5 0 79,5G 0 part / + sr0 11:0 1 1024M 0 rom + ************************************************************************************** + + Aslında yukarıda yapılan işlemlerin sonucu olarak Disk Bölümleme Tablosu'ndaki iki giriş (32 byte) güncellenmiştir. + + 5) fdisk programının başka komutları da vardır. Örneğin disk bölümlendirmesi yapıldıktan sonra bu bölümlendirme bilgileri + "O" komutu ile bir dosyaya aktarılabilir. Sonra "I" komutu ile bu dosyadan yükleme yapılabilir. Böylece farklı diskler + için aynı işlemlerin daha kolay yapılması sağlanabilmektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Biz modern Linux sistemlerinde yetki gerektiren programları "sudo" komutuyla ("sudo" bir programdır) çalıştırmaktayız. + Bir programı "sudo" ile çalıştırdığımızda yaratılan prosesin etkin kullanıcı id'si 0 ve etkin grup id'si, gerçek kullanıcı + id'si ve gerçek grup id'si 0 olur. Kullanıcı id'si sıfır olan kullanıcı genellikle "root" ismiyle bulundurulmaktadır. Pek + çok UNIX türevi sistemde aynı zamanda 0 numaralı bir grup da bulunmaktadır. Bu da genellikle "root" biçiminde isimlendirilmektedir. + Yani "root" isminde hem bir kullanıcı hem de bir grup bulunmaktadır. Anımsanacağı gibi prosesin etkin grup id'sinin 0 olması + hiçbir erişim ayrıcalığı oluşturmamaktadır. Asıl olan "etkin kullanıcı id'sinin" 0 olmasıdır. Anımsanacağı gibi Linux sistemleri + install edilirken kurulum sırasında kullanıcının girdiği isimle aynı olan bir kullanıcı ismi ve grup ismi oluşturulmaktadır. + Kullanıcı ve grup id'lerinin farklı isim alanlarında olduğunu anımsayınız. Örneğin 1000 numaralı bir kullanıcı id'si de olabilir, + bir grup id'si de olabilir. Ancak aynı id'ye sahip birden fazla kullanıcı ya da grup olmamalıdır. (Tabii bu durum aslında + yasak değildir. Yani biz örneğin etkin kullanıcı id'si 0 olan başka bir kullanıcı da yaratabiliriz. Çekirdek, isimleri + değil numaraları işleme sokmaktadır.) +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir programı "sudo" ile çalıştırdığımızda çalıştırılan programa ilişkin prosesin etkin ve gerçek kullanıcı id'sinin ve grup + id'sinin 0 olduğunu belirtmiştik. Yani "sudo" ile bir programı çalıştırdığımızda adeta program "root" kullanıcısı tarafından + çalıştırılıyormuş gibi bir etki oluşmaktadır. Linux dağıtımlarının bir bölümünde "root" kullanıcısı ile "login" olma güvenlik + gerekçesiyle engellenmiştir. Ancak bu dağıtımlarda "root" olarak "bash" komut satırına düşmek istiyorsanız "sudo" ile "bash" + programını çalıştırabilirsiniz. Örneğin: + + $ sudo bash + + Bir programı "sudo" ile çalıştırmak istediğimizde bizden önce kendi kullanıcımıza ilişkin parola istenmektedir. Ancak bir + süre içerisinde artık "sudo" yapıldığında parola istenmeyecektir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Her kullanıcı "sudo" yapamamaktadır. "sudo" yapabilen kullanıcılara "sudoer" denilmektedir. Sistem kurulurken kurulum + programı tarafından yaratılan kullanıcı "sudoer" durumundadır. Bir kullanıcıyı "sudoer" yapabilmek için en basit yol o + kullanıcıyı "/etc/group" dosyasında "sudo" grubuna eklemektir. Örneğin kursun yapıldığı makinedeki "/etc/group" dosyasının + "sudo" grup satırı şöyledir: + + sudo:x:27:kaan + + Buradan "sudo" grubunun grup id'sinin 27 olduğu ve "kaan" kullanıcısının bu gruba ek grup (supplemantary group) biçiminde + dahil olduğu görülmektedir. Şimdi biz "ali" kullanıcısının da "sudo" yapabilmesini istiyorsak "ali" kullanıcı ismini de + aşağıdaki gibi satıra eklemeliyiz: + + sudo:x:27:kaan,ali + + Aslında bu işlem komut satırında "usermod" komutu ile "-a" ve "-G" seçenekleri kullanılarak da yapılabilmektedir. Örneğin: + + $ sudo usermod -a -G sudo student + + Tabii bu komutu şöyle de uygulayabilirdik: + + $ sudo usermod -aG sudo student + + Kullanıcıya "sudo" yeteneği verebilmenin diğer bir yolu da "/etc/sudoers" dosyasına yeni bir giriş eklemektir. Biz burada + bu yöntem üzerinde durmayacağız. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir programı "sudo" ile çalıştırdığımızda üst prosesin çevre değişkenleri default durumda çalıştırılan programa aktarılmamaktadır. + Örneğin biz kabukta bazı çevre değişkenleri tanımlamış olalım. "sudo" ile bir program çalıştırdığımızda bu çevre değişkenleri + default durumda yaratılan prosese aktarılmayacaktır. Bunu basit bir biçimde deneyimleyebiliriz. Bunun için önce komut + satırında aşağıdaki gibi çevre değişkenleri oluşturabiliriz: + + $ export XXX=100 + $ export YYY=100 + + Şimdi "env" komutunu uygularsak bu çevre değişkenlerinin yaratılmış olduğunu görürüz. Ancak "sudo env" komutunu uyguladığımızda + bu çevre değişkenleri alt prosese aktarılmayacağı için onları göremeyeceğiz. Ancak kabuğun çevre değişkenleri çalıştırılan + programa ilişkin prosese "-E" seçeneği kullanılarak aktarılabilmektedir. Örneğin: + + $ sudo -E env + + Ancak "-E" seçeneği de güvenlik gerekçesiyle PATH çevre değişkenini yaratılan prosese geçirmemektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 188. Ders 27/12/2024 - Cuma +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + sudo işlemi konusunda "/etc/sudoers" dosya da etkilidir. Bu dosyaya girişler eklenerek belli bir proses sudoer yapılabilmektedir. + Bu dosyanın başındaki aşağıdaki satırlar önemlidir: + + Defaults env_reset + Defaults mail_badpass + Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" + Defaults use_pty + + Buradaki "env_reset" satırında "env_reset" yerine "env_keep" getirilirse defaut olarak burada belirtilen çevre değişkenleri + sudo ile çalıştırılan proseslere aktarılmaktadır. Örneğin: + + Defaults env_keep += "XX YY" + + Burada sudo ile bir program çalıştırıldığında alt prosese XX ve YY çevre değişkenleri aktarılacaktır. env_keep += "*" + tüm çevre değişkenlerinin aktarılacağı anlamına gelir. Ancak yine PATH çevre değişkeni bunun dışındadır. Örneğin: + + Defaults env_keep += "*" + + Eğer PATH çevre değişkeninin de yaratılan prosese aktarılması isteniyorsa secure_path satırı kaldırılmalı ya da # ile + yorum satırı içerisine alınmalıdır. Örneğin: + + Defaults env_reset + Defaults mail_badpass + # Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" + Defaults use_pty +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Klasik UNIX tasarımında erişim bakımından "ya hep ya hiç" sistemi uygulanmaktadır. Yani sıradan kullanıcılar sistemle + ilgili önemli dosyalara erişemezler ve sistemde davranış değişikliklerine yol açabilecek sistem fonksiyonlarını çağıramazlar. + Ancak prosesin kullanıcı id'si 0 ise (yani proses "root" hakkına sahipse) her şeyi yapabilirler. Bazı UNIX türevi sistemlerde + bu "ya hep ya hiç" sistemi "yeteneklilik (capability)" denilen kavramla yumuşatılmıştır. Bu sistemlerde çeşitli yetenekler + vardır. Bir proses root hakkına sahip olmasa bile bu yeteneklerden bazılarına sahip olabilir. Bu durumda bu yetenekleri + gerektiren bazı işlemleri yapabilir. + + Yeteneklilik (capability) konusu POSIX standartlarına sokulmamıştır. POSIX standartları genel olarak "uygun öncelik + (appropriate priviledge)" terimini kullanmaktadır. Burada uygun öncelik prosesin root önceliğinde olması ya da ilgili + işlemi yapacak yeteneğe sahip olması anlamına gelmektedir. + + Yeteneklilik konusu POSIX tarafından standardize edilmiş bir konu olmadığı için farklı UNIX türevi sistemlerde farklı + biçimlerde gerçekleştirilmiştir. Ancak bunlar arasında en geniş tasarıma sahip olan Linux sistemleridir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir thread'in sahip olabileceği yetenekler proses kontrol bloğunda bitsel bir biçimde tutulmaktadır. Linux'taki yetenekler + şunlardır: + + CAP_CHOWN: Dosya sahibi ve grup değiştirme yeteneği. + CAP_DAC_OVERRIDE: Dosya erişim kontrollerini geçme yeteneği (okuma/yazma/çalıştırma). + CAP_DAC_READ_SEARCH: Dosya okuma ve dizin arama izinlerini geçme yeteneği. + CAP_FOWNER: Dosya üzerinde, sahibi tarafından yapılan işlemleri yapma yeteneği (örneğin dosyanın erişim özelliklerinin + değiştirilmesi gibi) + CAP_FSETID: Dosyanın UID veya GID’sini değiştirme yeteneği. + CAP_KILL: Diğer proseslere sinyal gönderme yeteneği. + CAP_SETGID: Kullanıcı grubunu değiştirme yeteneği. + CAP_SETUID: Kullanıcı kimliğini değiştirme yeteneği. + CAP_SETPCAP: Process yetenekliliğini değiştirme yeteneği. + CAP_SETFCAP: Bir dosyanın yeteneklerini değiştirme yeteneği + CAP_LINUX_IMMUTABLE: Bir dosyanın değiştirilemez (immutable) hale getirilmesini sağlama yeteneği. + CAP_NET_BIND_SERVICE: Düşük numaralı portlara (1024'ten küçük) bağlanma yeteneği. + CAP_NET_BROADCAST: Ağda broadcast (yayın) yapma yeteneği. + CAP_NET_ADMIN: Ağ ayarlarını yönetme, ağ arayüzlerini yapılandırma yeteneği. + CAP_NET_RAW: Düşük seviyeli ağ işlemleri yapabilme yeteneği. + CAP_IPC_LOCK: Bellek üzerinde kilit (lock) işlemleri yapma yeteneği. + CAP_IPC_OWNER: IPC (Inter-process communication) kaynaklarının sahipliğini değiştirme yeteneği. + CAP_SYS_MODULE: Linux modüllerini yükleme ve kaldırma yeteneği. + CAP_SYS_RAWIO: Donanımsal I/O portlarına erişme yeteneği + CAP_SYS_CHROOT: chroot (root dosya sistemi değiştirme) işlemi yapma yeteneği. + CAP_SYS_PTRACE: Diğer süreçleri izleme ve kontrol etme yeteneği (örneğin, hata ayıklama). + CAP_SYS_PACCT: Process accounting verilerini okuma veya yazma yeteneği. + CAP_SYS_ADMIN: Sistem yöneticisi yetkileri (modül yükleme, dosya sistemi değiştirme vb.). + CAP_SYS_BOOT: Sistemi yeniden başlatma veya açma yeteneği. + CAP_SYS_NICE: Diğer proseslerin nice değerini değiştirme yeteneği. + CAP_SYS_RESOURCE: Sistem kaynaklarını yönetme ve limitlerini ayarlama yeteneği. + CAP_SYS_TIME: Sistemin saatini değiştirme yeteneği. + CAP_SYS_TTY_CONFIG: TTY (terminal) ayarlarını değiştirme yeteneği. + CAP_MKNOD: Aygıt dosyaları oluşturma yeteneği. + CAP_LEASE: Dosya kiralama (lease) mekanizmasını kullanma yeteneği. + CAP_AUDIT_WRITE: Audit (denetim) sistemine yazma yeteneği. + CAP_AUDIT_CONTROL: Audit sistemi yapılandırmalarını değiştirme yeteneği. + CAP_SETFCAP: Dosya yetenekiliğini (capabilities) ayarlama yeteneği. + CAP_MAC_OVERRIDE: MAC (Mandatory Access Control) politikalarını geçme yeteneği. + CAP_MAC_ADMIN: MAC politikalarını yönetme yeteneği. + CAP_SYSLOG: Sistem günlüklerini (log) okuma veya yazma yeteneği. + CAP_WAKE_ALARM: Sistemi uyanma alarmıyla uyandırma yeteneği. + CAP_BLOCK_SUSPEND: Sistemin uykuya geçmesini engelleme yeteneği. + CAP_AUDIT_READ: Audit verilerini okuma yeteneği. + CAP_PERFMON: Performans izleme (profiling) yeteneği. + CAP_BPF: BPF (Berkeley Packet Filter) programlarını yükleme ve çalıştırma yeteneği. + CAP_CHECKPOINT_RESTORE: Bir süreç için checkpoint alma ve geri yükleme yeteneği. + + Örneğin biz kill fonksiyonu kabaca yalnızca kendimizin oluşturduğu proseslere sinyal gönderebiliriz. Ancak eğer prosesimizin + CAP_KILL yetenekliliği varsa bu durumda tıpkı root prosesi gibi başka proseslere de sinyal gönderebiliriz. + + Linux sistemlerindeki yeteneklilik (capability) konusu maalesef biraz karmaşık bir konudur. İzleyen paragraflarda konunun + ayrıntılarına gireceğiz. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Aslında Linux sistemlerinde yeteneklilik proses temelinde değil thread temelinde uygulanmaktadır. Yani prosesin farklı + thread'leri farklı yeteneklere sahip olabilmektedir. Ancak pratikte prosesin tüm thread'leri genellikle aynı yeteneklere + sahip olur. Yetenekler proses kontrol bloğunda bitsel düzeyde tutulmaktadır. Yani yukarıdaki listedeki CAP_XXXX sembolik + sabitleri aslında tüm bitleri 0 olan yalnızca tek biti 1 olan sembolik sabitlerdir. Eskiden proses kontrol bloğunda yetenekleri + tutan nesneler 32 bitlik nesnelerdi. Sonra çeşitli yetenekler de sisteme eklenince bunlar 64 bite yükseltildiler. Bugünkü + Linux çekirdeklerinde therad'e ilişkin yetenekler task_struct yapısının cred isimli elemanının gösterdiği struct cred + yapısında tutulmaktadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir thread'in dört grup yetenekleri vardır: Etkin yetenekler (effective capabilities), İzin verilen yetenekler (permitted + capabilities), aktarılan yetenekler (inheritable capabilities) ve aarmalayan yetenekler (bounding capabilities). Test + işlemlerine etkin yetenekler sokulmaktadır. Örneğin thread'imizin başka bir prosese sinyal gönderebilmesi için thread'imizin + etkin yetenekleri arasında CAP_KILL bulunuyor olması gerekir. İzin verilen yetenekler (permitted capabilities) etkin yetenekler + için bir üst küme oluşturmaktadır. Yani bir thread'in kendisinin etkin yetenekleri ancak izin verilen yetenekleri kadar olabilir. + Thread'in bir etkin yetenek kümesinde bir yetenek yoksa ancak izin verilen kümesinde varsa proses izin verilen kümesi içerisindeki + o yeteneği etkin kümesine taşıyabilir. Ancak izin verilen kümesini genişletemez. Başka bir deyişle thread'in etkin yetenek + kümesi izin verilen yetenek kümesinin bir alt kümesi biçimindedir. Aktarılabilen yetenek kümesi thread bir programı çalıştırdığında + onun izin verilen kümesine dahil edilebilecek yetenekleri belirtmektedir. Benzer biçimde thread'in sarmalayan yetenekleri + de izin verilen yeteneklerin belirlenmesi sırasında etki göstermektedir. Bu konu izleyen paragraflarda ele alınacaktır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Biz yukarıda Linux çekirdeğinin yetenekleri thread temelinde işleme soktuğunu belirttik. Yetenekler fork işlemi sırasında + ya da pthread_create işlemi sırasında (bu da bir çeşit fork işlemi gibidir) üst thread'ten alt thread'e aktarılmaktadır. + Yani aslında default durumda bir prosesin tüm thread'leri aynı yeteneklere sahiptir. Fakat biz istersek prosesin belli bir + thread'inin yeteneklerini diğer thread'lere dokunmadan değiştirebiliriz. Üst proses fork uyguladığında onun "etkin", "izin + verilen" ve "aktarılan" yeteneklerinin hepsi alt prosese aktarılmaktadır. + + Herhangi bir önceliğe sahip olmayan sıradan proseslerin etkin, izin verilen ve aktarılan yetenek kümesinde hiçbir yetenek + yoktur. Örneğin biz bir terminal açıp orada çalışan kabuk programının (bash) yetenek kümelerine baksak bu üç yetenek kümesinin + de boş küme olduğunu görürüz. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Linux sistemlerinde thread'in yeteneklerini elde etmek için ve onu set etmek için sys_getcap ve sys_setcap isimli iki + sistem fonksiyonu bulundurulmuştur. Ancak libc kütüphanesinde bu sistem fonksiyonlarını çağıran fonksiyonlar bulunmamaktadır. + Eğer bu sistem fonksiyonlarını çağıracaksanız bunların numaralarını belirterek syscall isimli fonksiyonla çağırabilirsiniz. + Ancak bu sistem fonksiyonlarının kullanımı biraz zahmetlidir. Bu fonksiyonlar yerine bu sistem fonksiyonları çağrılarak + yazılmış olan "libcap" isimli bir kütüphane de bulunmaktadır. Sistem programcıları genellikle sys_getcap ve sys_setcap + fonksiyonlarını kullanmak yerine zaten bunları kullanan "libcap" kütüphanesini kullanmayı tercih etmektedir. Ancak bu "libcap" + kütüphanesi default durumda genellikle yüklü olmaz. Bu kütüphaneyi Debian türevi sistemlerde aşağıdaki gibi yükleyebilirsiniz. + + $ sudo apt install libcap2 libcap-dev +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir prosesin yetenek kümeleri "/proc//status" dosyasından elde edilebilir. Buradaki yeteneklere ilişkin bilgiler + aşağıdaki gibi gözükecektir: + + CapInh: 0000000000000000 + CapPrm: 0000000000000000 + CapEff: 0000000000000000 + CapBnd: 000001ffffffffff + + Bu örnekte prosesin bütün yetenek kümesi boştur. Yani proses hiçbir yeteneğe sahip değildir. Anımsanacağı gibi aslında + "proc" dosya sisteminde her için bir dizin yaratılmaktadır. Yani "/proc//status" yol ifadesindeki pid aslında ilgili + thread'in gettid fonksiyonu ile elde edilen pid numarasıdır. (POSIX standartlarında thread'lerin pid değerinin olmadığını + ancak Linux sistemlerinde Linux çekirdeğinin thread'leri prosesler gibi oluşturduğu için thread'lerin de pid değerlerinin + olduğunu anımsayınız. Yine anımsanacağı gibi proc dosya sisteminde ana thread'e ilişkin "task" dizininde prosesin thread'lerinin + pid numaraları bulunmaktadır.) +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir thread'in yetenekleri programlama yoluyla "libcap" kütüphanesindeki cap_get_proc fonksiyonu ile elde edilebilir. Fonksiyon + kendisini çağıran thread'in yetenek bilgilerini temsil eden cap_t türünden bir handle değeri ile geri döner. + + #include + + cap_t cap_get_proc(void); + + cap_t türü aşağıdaki gibi bildirilmiştir: + + typedef struct __user_cap_header_struct { + unsigned int version; // Yapı versiyonu + pid_t pid; // İlgili işlem ID'si + } __user_cap_header_t; + + typedef struct __user_cap_data_struct { + unsigned int effective; // Etkin yetenekler + unsigned int permitted; // İzin verilen yetenekler + unsigned int inheritable; // Miras alınan yetenekler + } __user_cap_data_t; + + typedef struct __user_cap_struct { + __user_cap_header_t header; // Başlık bilgisi + __user_cap_data_t data[2]; // Yetenek verisi (genellikle iki set) + } *cap_t; + + Örneğin: + + cap_t caps; + + if ((caps = cap_get_proc()) == NULL) + exit_sys("get_cap_proc"); + + cap_get_pid fonksiyonu ise pid değeri verilen prosesin (ya da thread'in) yeteneklerini elde etmektedir. Fonksiyonun prototipi + şöyledir: + + cap_t cap_get_pid(pid_t pid); + + cap_t türünün bir yapı adresini belirttiğine dikkat ediniz. Bu fonksiyonlar kendi içerisinde cap_t türünden bir yapı nesnesini + dinamik olarak tahsis edip onun adresiyle geri dönemktedir. Bu dinamik alaın boşaltımı cap_free fonksiyonu ile yapılmalıdır: + + #include + + int cap_free(void *obj_d); + + Aslında cap_free fonksiyonu yalnızca free fonksiyonunu çağırmaktadır. + + Örneğin: + + if ((caps = cap_get_proc()) == NULL) + exit_sys("get_cap_proc"); + + /* ... */ + + cap_free(caps); + + Her ne kadar cap_get_pid fonksiyonu sanki proses id alıyor gibiyse de yukarıda da belirttiğimiz gibi yetenekler aslında + prosese değil thread'e özgüdür. Biz bu fonksiyona bir prosesin pid değerini geçirdiğimizde o prosesin ana thread'ine ilişkin + yetenekleri elde etmiş oluruz. Belli bir thread'in bilgileri elde edilecekse doğrudan gettid fonksiyonu ile thread'in + pid değeri elde edilebilir. cap_get_proc fonksiyonu ise o anda çalışmakta olan thread'in yeteneklerini elde etmekte + kullanılmaktadır. + + Thread'in yetenekleri elde edildiğinde onların yazdırılması da bir sorundur. Bu nedenle "libcap" kütüphanesinde cap_to_text + isimli bir fonksiyon bulundurulmuştur. Fonksiyonun prototipi şöyledir: + + #include + + char *cap_to_text(cap_t caps, ssize_t *length_p); + + Fonksiyonun birinci parametresi cap_t türünden handle değerini, ikinci parametresi verilen yazının uzunluğunun yerleştirileceği + nesnenin adresini belirtir. İkinci parametre NULL adres geçilebilir. Bu durumda fonksiyon böyle bir yerleştirme yapmaz. + Fonksiyon oluşturulan yazının adresine geri dönmektedir. Yine bu adresin de cap_free kullanımdan sonra boşaltılması gerekir. + Eğer cap_to_text fonksiyonunda yalnızca "=" biçiminde bir yazı elde ediliyorsa bu durum thread'in herhangi bir yeteneğe + sahip olmadığı anlamına gelmektedir. + + Aşağıda o anda çalışmakta olan thread'in yetenekleri ekrana yazdırılmıştır. +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include +#include + +void exit_sys(const char *msg); + +int main(void) +{ + cap_t caps; + char *captext; + + if ((caps = cap_get_proc()) == NULL) + exit_sys("cap_get_proc"); + + if ((captext = cap_to_text(caps, NULL)) == NULL) { + cap_free(caps); + exit_sys("cap_to_text"); + } + + printf("%s\n", captext); + + cap_free(captext); + cap_free(caps); + + return 0; +} + +void exit_sys(const char *msg) +{ + perror(msg); + + exit(EXIT_FAILURE); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir thread'in yetenekleri onu yaratan thread'ten aktarılmaktadır. Yani genel olarak bir proses bir thread yarattığında + ya da fork işlemi yaptığında yaratılan thread ya da proses bu işlemi uygulayan thread ile aynı yeteneklere sahip olur. + Sıradan bir prosesin ya da onun thread'lerinin yeteneklerine baktığımızda tipik olarak şöyle olduğunu görürüz: + + CapInh: 0000000000000000 + CapPrm: 0000000000000000 + CapEff: 0000000000000000 + CapBnd: 000001ffffffffff + + Buradan çıkan sonuç şudur: Sıradan bir prosesin ya da thread'in aktarılan, izin verilen ve etkin yetenek kümesi boş + kümedir. Yani bu küme hiçbir yeteneği kapsamamaktadır. Ancak sarmalayan yeteneklerin (bounding capabilities) bütün bitlerinin + 1 olduğunu görüyorsunuz. (Her ne kadar burada yüksek anlamlı bitlerin bazılarını 0 görüyor olsanız da zaten Linux o bitleri + kullanmamaktadır.) +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Etkin kullanıcı id'si 0 olan proseslerin hiçbir engele takılmadığını anımsayınız. Linux çekirdeği önce prosesin etkin + kullanıcı id'sini kontrol etmektedir. Eğer prosesin etkin kullanıcı id'si 0 ise zaten herhangi bir yetenek kontrolü + yapmasına gerek kalmamaktadır. Yani yetenekler ancak etkin kullanıcı id'si 0 olmayan prosesler için anlamlı bir özelliktir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi sıradan bir prosesin tüm thread'lerinin yetenekleri boş küme olduğuna göre bu prosesler ve thread'ler nasıl yetenek + kazanmaktadır? İşte bunun iki yolu olabilir: + + 1) Yetenek kazandırabilecek yeteneğe sahip ya da etkin kullanıcı id'si 0 olan bir proses bir alt proses yaratıp onun + yeteneklerini oluşturup sonra kullanıcı id'sini 0 olmaktan çıkartabilir. + + 2) Aslında Linux sistemlerinde çalıştırılabilen dosyalara da yetenekler atanabilmektedir. Bir çalıştırılabilen dosya exec + fonksiyonlarıyla çalıştırıldığında izleyen paragraflarda açıklanacağı gibi otomatik olarak proses bazı yeteneklere sahip + olabilmektedir. Bu durum aslında daha önce gördüğümüz çalıştırılabilen dosyaların "set-user-id" ve "set-grup-id" bayraklarına + benzemektedir. Örneğin thread'imizin yetenek kümeleri boş küme olsun. Ama biz "xxx" isimli programı exec fonksiyonlarıyla + çalıştırmak isteyelim. Bir kez fork yapıp alt proseste exec yaptığımızda artık alt prosesimizin yetenekleri bu "xxx" + dosyasında belirtilen yeteneklere sahip olabilmektedir. Yani prosesleirn ve thread'lerin yetenekleri aslında genellikle + exec işlemi sırasında boş küme olmaktan çıkmaktadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi thread'imize belli bir yeteneği nasıl kazandırabiliriz? Bir thread etkin yeteneklerini ancak izin verilen yetenekler + kadar artırabilir. O halde önce thread'imizin izin verilen yeteneklerini artırması gerekir. Bunun için sırasıyla şu işlemler + yapılmalıdır: + + 1) Önce içi boş bir yetenek nesnesi cap_init fonksiyonuyla oluşturulur. cap_init fonksiyonunun prototipi şöyledir: + + #include + + cap_t cap_init(void); + + cap_init fonksiyonu başarısızlık durumunda NULL adrese geri dönmektedir. + + Örneğin: + + cap_t newcaps; + + if ((newcaps = cap_init()) == NULL) + exit_sys("cap_init"); + + 2) Bu yetenek nesnesine cap_set_flag fonksiyonu ile çeşitli bayraklar eklenir. Fonksiyonun prototipi şöyledir: + + #include + + int cap_set_flag(cap_t cap_p, cap_flag_t flag, int ncap, const cap_value_t *caps, cap_flag_value_t value); + + Fonksiyonun birinci parametresi yeteneklerin set edileceği nesneyi belirtmektedir. İkinci parametre hangi yetenekler + üzerinde işlem yapılacağını belirtir. Bu parametre aşağıdaki sembolik sabitlerden yalnızca birini içerebilir: + + CAP_EFFECTIVE + CAP_INHERITABLE + CAP_PERMITTED + + Fonksiyon tarafından set edilecek yetenekler dördüncü parametresi ile belirtilen dizinden elde edilmektedir. Yani programcı + cap_value_t türünden bir dizi oluşturup bu dizinin içerisine yenetekleri yerleştirir. Fonksiyonun üçün paraöetresine de + bu dizinin uzunluğunu geçirir. Fonksiyoun son parametresi aşağıdaki iki değerdne biri olarak girilir: + + CAP_CLEAR + CAP_SET + + Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönemtedir. Örneğin: + + cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL}; + ... + + if (cap_set_flag(newcaps, CAP_PERMITTED, 2, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_flag(newcaps, CAP_EFEFCTIVE, 2, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + 3) Oluşturulan yetenekler cap_set_proc fonksiyonu ile o anda çalışan thread'e aktarılır. cap_Set_proc fonksiyonun + prototipi şöyledir: + + #include + + int cap_set_proc(cap_t cap_p); + + Fonksiyon hazırlanan capt_t nesnesini parametre olarak alır. Thread'in yeteneklerini set eder. Başarı durumunda 0 değerine + başarısızlık durumunda -1 değerine geri döner. Örneğin: + + if (cap_set_proc(newcaps) == -1) { + cap_free(newcaps); + exit_sys("cap_set_proc"); + } + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 190. Ders 10/01/2025 - Cuma +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Aşağıda bir thread'in yeteneklerini değiştiren örnek bir program verilmiştir. Tabii cap_set_proc fonksiyonun da kullanılması + için prosesin uygun önceliğe sahip olması gerekir. (Yani prosesin etkin kullanıcı id'si 0 olmalı ya da ilgili thread yetenek + değiştirme yeteneği olan CAP_SYS_ADMIN yeteneğine sahip olmalıdır.) +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include +#include + +void exit_sys(const char *msg); + +int main(void) +{ + cap_t newcaps; + cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL}; + + if ((newcaps = cap_init()) == NULL) + exit_sys("cap_init"); + + + if (cap_set_flag(newcaps, CAP_PERMITTED, 2, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_flag(newcaps, CAP_EFFECTIVE, 2, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_proc(newcaps) == -1) { + cap_free(newcaps); + exit_sys("cap_set_proc"); + } + + cap_free(newcaps); + + printf("Ok\n"); + + getchar(); + + return 0; +} + +void exit_sys(const char *msg) +{ + perror(msg); + + exit(EXIT_FAILURE); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + Aşağıda önce thread'in izin verilen ve etkin yetenekleri değiştirilmiş sonra da bu yenekler elde edilip yazdırılmıştır. + Programın çalıştırılması sonucunda aşağdıaki gibi bir çıktı elde edilecektir: + + cap_chown,cap_kill=ep +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include +#include + +void exit_sys(const char *msg); + +int main(void) +{ + cap_t newcaps, caps; + cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL}; + char *captext; + + if ((newcaps = cap_init()) == NULL) + exit_sys("cap_init"); + + if (cap_set_flag(newcaps, CAP_PERMITTED, 2, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_flag(newcaps, CAP_EFFECTIVE, 2, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_proc(newcaps) == -1) { + cap_free(newcaps); + exit_sys("cap_set_proc"); + } + + cap_free(newcaps); + + if ((caps = cap_get_proc()) == NULL) + exit_sys("cap_get_proc"); + + if ((captext = cap_to_text(caps, NULL)) == NULL) { + cap_free(caps); + exit_sys("cap_to_text"); + } + + printf("%s\n", captext); + + cap_free(captext); + cap_free(caps); + + return 0; +} + +void exit_sys(const char *msg) +{ + perror(msg); + + exit(EXIT_FAILURE); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + Proses root olarak (etkin kullanıcı id'si 0 olarak) çalışıyor olsun. Prosesin etkin kullanıcı id'si seteuid fonksiyonuyla + değiştirildiğinde prosesin tüm thread'lerinin etkin yenetenleri sıfırlanmaktadır. Ancak izin verilen yeteneklerine dokunulmamaktadır. + Ancak prsesin gerçek kullanıcı id'si ile etkin kullanıcı id'si setuid fonksiyonu ile değiştirildiğinde prosesin tüm + thread'lerinin hem izin verilen hem de etkin yetenekleri sıfırlanmaktadır. Bu konudaki ayrıntılar için "capabilities(7)" + man sayfasına başvurabilirsiniz. + + Aşağıdaki örnekte seteuid fonksiyonundna sonra thread'in etkin yeteneklerinin düşürüldüğüne ilişkin bir örnek verilmiştir. + + Program çalıştırıldığında ekranda şunlar görünecektir: + + cap_chown,cap_kill,cap_setuid=ep + cap_chown,cap_kill,cap_setuid=p +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include +#include +#include + +void exit_sys(const char *msg); + +void disp_capability(void) +{ + cap_t caps; + char *captext; + + if ((caps = cap_get_proc()) == NULL) + exit_sys("cap_get_proc"); + + if ((captext = cap_to_text(caps, NULL)) == NULL) { + cap_free(caps); + exit_sys("cap_to_text"); + } + + printf("%s\n", captext); + + cap_free(captext); + cap_free(caps); +} + + +int main(void) +{ + cap_t newcaps; + cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL, CAP_SETUID}; + + if ((newcaps = cap_init()) == NULL) + exit_sys("cap_init"); + + if (cap_set_flag(newcaps, CAP_PERMITTED, 3, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_flag(newcaps, CAP_EFFECTIVE, 3, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_proc(newcaps) == -1) { + cap_free(newcaps); + exit_sys("cap_set_proc"); + } + + cap_free(newcaps); + + disp_capability(); + + if (seteuid(1000) == -1) + exit_sys("setuid"); + + disp_capability(); + + return 0; +} + +void exit_sys(const char *msg) +{ + perror(msg); + + exit(EXIT_FAILURE); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + Linux çekirdek kendi içerisinde aslında hep yetenek kontrolü yapmaktadır. Yani bir prosesin root önceliğinde olması + (yani etkin kullanıcı id'sinin 0 olması) aslında izimn verilen ve etkin yeteneklerin hepsinin set edildiği anlamına + gelmektedir. Dolayısıyla biz bir programı sudo ile çalıştırdıktan sonra onun etkin yeteneklerini değiştirirsek artık + proses'in etkin kullanıcı id'si 0 olsa bile proses yetenek kaybedecektir. + + open fonksiyonu tarafından uygulanan erişim kontrollerinin thread yetenekleriyle bir ilgisi yoktur. Yani etkin user id'si + 0 olan bir root proses yenek kümesi düşürülse bile yine open fonksiyonunda başarısız olmaz. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Linux sistemlerinde yalnızca thread'lerin değil çalıştırılabilen dosyaların da yetenekleri vardır. Biz daha önce çalıştırılabilen + bir dosyanın "set-user-id" ve "set-group-id" biçiminde isimlendirilen bayraklarının işlevlerini incelemiştik. Çalıştırılabilen + bir dosyanın "set-user-id" bayrağı set edilmişse o programı exec yapan prosesin etkin kullanıcı id'si dosyanın kullanıcı + id'si haline getiriliyordu. Aynı durum "set-group-id" bayrağı için de benzer biçimde işletiliyordu. Çalıştırılabilen bir + dosyanın "set-group-id" bayrağı set edilmişse bu dosya exe yapıldığında prosesin etkin grup id'si dosyanın grup id'si + oluyordu. İşte bu mekanizmanın benzeri yetenek temelinde de oluşturulmuştur. Çalıştırılabilen dosyaların da yetenekleri + set edilmiş olabilir. Bu durumda bu dosyalar exec yapıldığında proses otomatik olarak o yeteneğe sahip hale gelmektedir. + Örneğin bir program dosyasının CAP_KILL yeteneğinin set edilmiş olduğunu varsayalım. Ancak bu dosyanın etkin kullanıcı id'si + root olmasın. Bu dosya çalıştırıldığında ilgili thread CAP_KILL yeteneğine sahip olacaktır. (Program birden fazla thread'e + sahipse exec işlemiyle yalnızca exec yapan thread'in yaşamına devam ettiğini anımsayınız.) Ancak dosya yetenekleri konusunun + da bazı ayrıntıları vardır. Dosya yetenekleri i-node elemanlarının içerisinde tutulmaktadır. Bu nedenle dosya sisteminin + de dosya yeteneklerini tutma özelliğine sahip olması gerekir. Örneğin FAT dosya sistemlerinde böyle bir alan bulunmamaktadır. + + Yalnızca çalıştırılabilen dosyalar için yetenek kavramı söz konusudur. Tabii bir text dosyanın içerisinde bir script kodu + da bulunabilir. Bu durumda bu dosyalar da çalıştırılabilen dosya olarak ele alınabilmektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Dosyaların yeteneklerini görüntülemek için "libcap" paketinde "getcap" isimli bir yardımcı program da bulunmaktadır. + Örneğin "ping" programının CAP_NET_RAW yeteneği vardır. "getcap" ile bu dosyanın yeteneklerini aşağıdaki gibi görüntüleyebiliriz: + + $ /usr/bin/ping cap_net_raw=ep + /usr/bin/ping cap_net_raw=ep + + Tıpkı thread'ler gibi dosyaların da "izin verilen (permitted)", etkin (effective) ve aktarılan (inheritable) yetenekleri + vardır. Dosyaların yetenekleri "setcap" isimli programla değiştirilebilmektedir. Programın örnek kullanımı şöyledir: + + $ sudo setcap "cap_net_bind_service=pei cap_net_raw=ep" sample + + Burada sample programına çeşitli yetenekler set edilmiştir. Bu yeneklerin tek bir komut satırı argümanı biçiminde verildiğine + dolayısıyla da tırnak içeeisine alındığına dikkat edniz. Eğer dosyadan tüm yetenekler silinecekse komut aşağıdaki gibi + kullanılabilir: + + $ sudo setcap = sample + + setcap programının kullanımına ilişkin ayrıntılar için man sayfalarına başvurabilirsiniz. + + Örneğin "sample" isimli programımıza izin verilen ve etkin olarak CAP_KILL yeteneğini eklemek isteyelim. Bu işlemi şöyle + yapabiliriz: + + $ sudo setcap "cap_kill=ep" sample + + Şimdi dosyanın yeteneğini "getcap" komutu ile görüntüleyelim: + + $ getcap sample + sample cap_kill=ep + + Tabii bir dosyanın yeteneğini değiştirebilmek için ilgili thread'in de CAP_SETPCAP yeneğine sahip olması gerekir. root + prosesleri tüm yeneklere sahipmiş gibi düşünmeliyiz. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 191. Ders 12/01/2025 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Tabii aslında dosyaların yeteneklerini elde etmek için ve onlara yetenek iliştirmek için Linux çekirdeğinde sistem + fonksiyonları bulunmaktadır. "getcap" ve "setcap" programları libcap kütüphanesindeki cap_set_file ve cap_get_file + fonksiyonları kullanılarak yazılmıştır. Bu fonksiyonlar da sys_setxattr, sys_fsetxattr, sys_lsetxattr ve sys_getxattr, + sys_fgetxattr ve sys_lgetxattr sistem fonksiyonları çağrılarak yazılmıştır. Bu sistem fonksiyonları için "libc" kütüphanesinde + sarma fonksiyonlar bulunmaktadır. Ancak biz burada bu işlemleri "libcap" kütüphanesindeki cap_set_file ve cap_get_file + fonksiyonlarını kullanarak yapacağız. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + cap_get_file fonksiyonun prototipi şöyledir: + + #include + + cap_t cap_get_file(const char *path_p); + + Fonksiyon dosyanın yol ifadesini alır ve başarı durumunda dosyanın yetenek bilgilerine ilişkin cap_t nesnesine geri döner. + Bu nesnenin kullanım bittikten sonra cap_free fonksiyonu ile boşaltılması gerekmektedir. Fonksiyon başarısızlık durumunda + NULL adrese gerei dönmektedir. Örneğin: + + cap_t caps; + + if ((caps = cap_get_file("/usr/bin/ping")) == NULL) + exit_sys("cap_get_file"); + + Aşağıda fonksiyonun kullanımına ilişkin örnek bir program verilmiştir. +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include +#include + +void exit_sys(const char *msg); + +void disp_cap(cap_t caps) +{ + char *captext; + + if ((captext = cap_to_text(caps, NULL)) == NULL) { + cap_free(caps); + exit_sys("cap_to_text"); + } + + printf("%s\n", captext); + + cap_free(captext); +} + +int main(int argc, char *argv[]) +{ + cap_t caps; + + if (argc != 2) { + fprintf(stderr, "wrong number of arguments!..\n"); + exit(EXIT_FAILURE); + } + + if ((caps = cap_get_file(argv[1])) == NULL) + exit_sys("cap_get_file"); + + disp_cap(caps); + + return 0; +} + +void exit_sys(const char *msg) +{ + perror(msg); + + exit(EXIT_FAILURE); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + cap_set_file fonksiyonun prototipi de şöyledir: + + #include + + int cap_set_file(const char *path_p, cap_t cap_p); + + Fonksiyonun birinci parametresi dosyanın yol ifadesini, ikinci parametresi dosyaya iliştirilecek yetenekleri belirtmektedir. + Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri dönmektedir. Tabii bir dosyaya yetenek set + edebilmek için ya prosesin root olması (etkin kullanıcı id'sinin 0 olması) ya da prosesin CAP_SETFCAP yeteneğine sahip olması + gerekmektedir. Örneğin: + + cap_t caps, newcaps; + cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL, CAP_SETUID}; + + if (argc != 2) { + fprintf(stderr, "wrong number of arguments!..\n"); + exit(EXIT_FAILURE); + } + + if ((newcaps = cap_init()) == NULL) + exit_sys("cap_init"); + + if (cap_set_flag(newcaps, CAP_PERMITTED, 3, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_flag(newcaps, CAP_EFFECTIVE, 3, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_file(argv[1], newcaps) == -1) { + cap_free(newcaps); + exit_sys("cap_set_proc"); + } + + cap_free(newcaps); + + Burada CAP_CHOWN, CAP_KILL, CAP_SETUID yenekleri "izin verilen" ve "etkin" yetenekler olarak dosyaya iliştirilmiştir. +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include +#include + +void exit_sys(const char *msg); + +void disp_cap(cap_t caps) +{ + char *captext; + + if ((captext = cap_to_text(caps, NULL)) == NULL) { + cap_free(caps); + exit_sys("cap_to_text"); + } + + printf("%s\n", captext); + + cap_free(captext); +} + +int main(int argc, char *argv[]) +{ + cap_t caps, newcaps; + cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL, CAP_SETUID}; + + if (argc != 2) { + fprintf(stderr, "wrong number of arguments!..\n"); + exit(EXIT_FAILURE); + } + + if ((newcaps = cap_init()) == NULL) + exit_sys("cap_init"); + + if (cap_set_flag(newcaps, CAP_PERMITTED, 3, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_flag(newcaps, CAP_EFFECTIVE, 3, caparray, CAP_SET) == -1) { + cap_free(newcaps); + exit_sys("cap_set_flag"); + } + + if (cap_set_file(argv[1], newcaps) == -1) { + cap_free(newcaps); + exit_sys("cap_set_proc"); + } + + cap_free(newcaps); + + if ((caps = cap_get_file(argv[1])) == NULL) { + cap_free(newcaps); + exit_sys("cap_get_file"); + } + + disp_cap(caps); + + cap_free(caps); + cap_free(newcaps); + + return 0; +} + +void exit_sys(const char *msg) +{ + perror(msg); + + exit(EXIT_FAILURE); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + Ayrıca açık bir dosya için yenekleri alan ve set eden aşağıdaki fonksiyonlar da bulunmaktadır: + + #include + + cap_t cap_get_fd(int fd); + int cap_set_fd(int fd, cap_t caps); +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 192. Ders 19/01/2025 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi çeşitli yetenekere sahip bir program dosyası exec yapıldığında ne olur? Bu süreç biraz karşıktır. Normal olarak + set-user-id ve set-group-id bayraklarında olduğu gibi prosesin dosyada belirtillen yeteneklere sahip olması gerekir. Ancak + sürecin açıklayacağımız bazı ayrıntıları vardır. + + Öncelikle aşağıdaki gibi bir deneme yapalım. Sistem zamanını öğrenmek ve ayarlamak için kullanılan "date" programını + kendi dizinimize kopyalayalım: + + $ whereis date + date: /usr/bin/date /usr/share/man/man1/date.1.gz /usr/share/man/man1/date.1posix.gz + + $ cp /usr/bin/date date + + Sistem tarihini değiştirmeye çalışalım: + + $ ./date -s "2025-01-12" + ./date: tarih ayarlanamadı: İşleme izin verilmedi + Paz 12 Oca 2025 00:00:00 +03 + + Görüldüğü gibi bu işlem için "sudo" gerekmektedir. Şimdi de dosyaya sistem tarihini değiştirmek için gereken cap_sys_time yeteneğini + iliştirelim: + + $ sudo setcap cap_sys_time=pe date + + İşlemimizi doğrulayalım: + + $ sudo getcap date + date cap_sys_time=ep + + Şimdi sistem tarihini değiştirmeye çalışalım: + + $ ./date -s "2025-01-12" + Paz 12 Oca 2025 00:00:00 +03 + + Görüldüğü gibi biz programı çalıştırdığımızda artık prosesimiz dosyada belirtilen CAP_SYS_TIME yeteneğini kazanmıştır. + Ancak konunun bazı ayrıntıları vardır. İzleyen paragraflarda bu ayrıntılar üzerinde duracağız. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bir thread (proses de diyebiliriz) çalıştırılabilen bir dosyayı exec fonksiyonlarıyla çalıştırdığında thread'in yeetenekleri + aşağıdaki biçimde değişime uğramaktadır (The Linux Programming Interface kitabından alınma): + + P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset) + P'(effective) = F(effective) & P'(permitted) + P'(inheritable) = P(inheritable) + + Burada P prosesin exec öncesindeki yeteneklerini P' exec sonrasındaki yeteneklerini, F dosyanın yeteneklerini, cap_bset + ise prosesin çevreleyen yeteneklerini (process bounding capabilitiees) belirtmektedir. + + Dosyanın etkin yetenekleri eskiden her yetenek için farklı değildi. Bütün yenekler için tek bir bayrak kullanılıyordu. + Ancak bir süredir bu durum değiştirilmiştir. Artık her yetenek için ayrı bir etkinlik bayrağı turulmaktadır. Prosesin + çevreleyen yetenekleri (bounding capabilites) her yetenek için 0/1 biçiminde bitlerden oluşmaktadır. Genellikle prosesin + çeevreleyen yeteneklerinin bütün bitleri 1 durumdadır. Örneğin sıradan bir programın yeeneklerine "proc" dosya sisteminden + baktığımızda şunları görmekteyiz: + + CapInh: 0000000000000000 + CapPrm: 0000000000000000 + CapEff: 0000000000000000 + CapBnd: 000001ffffffffff + + Görüldüğü gibi çevreleyen yeteneklerin tüm bitleri 1 durumundadır. (Yüksek anlamlı 0 olan hex digitlere ilişkin bitlerin + zaten kullanılmadığına dikkat ediniz.) + + Buradaki durumu madde madde şöyle açıklayabiliriz: + + 1) exec yapıldıktan sonra prosesin yeni izin verilen yetenekleri şu biçimde oluşturulmaktadır: + + P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset) + + Burada bit OR operatörünün solundaki ifade şu anlama gelmektedir: "Prosesin exec öncesindeki aktarılan yeteenekleri eğer + aynı zamanda dosyanın aktarılan yetenekleri içerisinde varsa bu yetenekler alınmaktadır". Bit Or operatörünün sağ tarafındaki + ifade ise şu anlama gelmektedir: "Dosyanın izin verilen yetenekleri eğer prosesin çavreleyen yeteneklerinde varsa bu + yetenekler alınmaktadır. Biz yukarıda prosesin çevreleyen yeteneklerinin genellikle tüm bitlerinin 1 olduğunu beelirtmiştik. + Bu durumda dosyanın izin verilen yeteneklerinin hepsi alınacaktır. Örneğin bizim dizine kopyaladığımız "date" programının + yetenekleri şöyledir: + + $ getcap date + date cap_sys_time=ep + + Yani dosyanın etkin ve izin verilen yenekleri "cap_sys_time" yeteeneğie sahiptir. O halde yukarıda satır dikkat alındığında + exec sonrasında prosesin izin verilen yeetenekleri "cap_sys_time" içerecektir. + + 2) exec sonrasında prosesin etkin yetenekleri exec sonrasındaki izin verilen yeteneklerinin dosyanın etkin yetenekleriyle + maskelenmesi sonucunda elde edilmektedir: + + P'(effective) = F(effective) & P'(permitted) + + Dosyanın etkin yeteneklerinin bir şalter görevi gördüğüne dikkat ediniz. Eğer dosyanın etkin yeteneklerindeki bir yetenek + 0 ise bu durumda bu yetenek exec sonrasındaki prosesin etkin yeteneklerine yansıtılmayacaktır. + + Proseesin exec öncesindeki aktarılan yetenekleri ile exec sonrasındaki aktarılan yeetenekleri arasında bir farklılık yoktur: + + P'(inheritable) = P(inheritable) +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi Linux'taki yetenek konusu neden bu kadar karışık hale getirilmiştir? Tasarımın bu biçimi aslında fazlaca esnek + duruma izin vermektedir. Ancak bu esneklikten faydalanma duurmu pratikte çokça karşımıza çıkmamaktadır. Bura aslında temel + kullanım için şunlar söylenebilir: + + 1) Bir programı sudo ile çalıştırdığımızda ya da programımızın etkin kullanıcı id'si 0 ise bu durum adeta "tüm etkin + yetenekleri set edilmiş" bir proses anlamına gelmektedir. + + 2) Çekirdek içerisinde özel bazı durumlarda genel olarak etkin kullanıcı id'sinin 0 olup olmadığı biçiminde değil thread'in + ilgili yeteneğe sahip olup olmadığı biçiminde kontroller yapılmaktadır. + + 3) Biz çalıştırılabilen bir dosyanın belli bir etkin yeteneğini ve izin verilen yeeteneğini set edersek o programı + exec ile çalıştırrdığımızda o yeneke exec sonrasında prosesimizin izin verilen yeteneklerine ve etkin yetenelerine otomatik + bir biçimde geçer. Tersten gidersek biz bir programın çalıştırılması ile ilgili prosesin bir yeetenek kazanmasını istiyorsak + program dosyası için ilgili yeteneği etkin ve izin verilen biçimde set etmeeliyiz. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Bu bölümde Linux'taki sistem fonksiyonlarının nasıl çağrılacağı üzerinde duracağız. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Linux'ta her sistem fonksiyonunun bir numarası vardır. Linux çekirdeği sistem fonksiyonlarının başlangıç adreslerini + bir gösterici dizisinde tutar. Bir sistem fonksiyonu çağrıldığında bu dizinin ilgi numaralı indeksindeki fonksiyonu + çağırır. Tabii akış sistem fonksiyonuna geçirilmeden thread'in akışı da user moddan kernel moda geçirilmektedir. Intel + sisteminde user moddan kernel moda geçiş 80H kesmesi yoluyla yapılmaktadır. Değişik sistemlerde bu işlemler değişik makine + komutlarıyla yapıldığı için "libc" kütüphanesi syscall isimli bir sarma fonksiyon bulundurmuştur. Bu fonksiyona programcı + sistem fonksiyonunun numarasını ve parametrelerini girer. Fonksiyon sembolik makine dilinde yazılmış makine komutlarından + oluşmaktadır. Böylece ilgili sistemde çağrım için gereken işlemler taşınabilir bir biçimde yapılabilmektedir. syscall + fonksiyonun prototipi şöyledir: + + #include + + long syscall(long number, ...); + + Fonksiyonun birinci parametresi sistem fonksiyonun numarasını almaktadır. Diğer parametreler ilgili sistem fonksiyonuna + geçirilecek argümanları almaktadır. Fonksiyonun geri dönüş değeri sistem fonksiyonuna bağlı olarak değişebilmektedir. + Pek çok sistem fonksiyonunda 0 başarı için kullanılmaktadır. Sistem fonksiyonlarının numaralarını Internet'te pek çok + yerde bulabilirsiniz. Aynı zamanda bu numaralar başlık dosyasında SYS_xxx biçiminde define edilmiştir. + Örneğin prosesi sonlandıran sys_exit isimli sistem fonksiyonun numarası 60'tır. Ancak biz bu numarayı kullanmak yerine + SYS_exit ismini de kullanabiliriz. SYS_exit aşağıdaki gibi define edilmiştir: + + #define SYS_exit 60 + + Aşağıda sys_exit sistem fonksiyonunun syscall fonksiyonuyla çağrılmasına bir örnek verilmiştir. +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include +#include + +int main(void) +{ + printf("running...\n"); + + syscall(SYS_exit, 0); + + return 0; +} + +/*-------------------------------------------------------------------------------------------------------------------------- + Daha önceden de belirttiğimiz gibi bazı POSIX fonksiyonları Linux'ta doğrudan belli bir sistem fonksiyonunu çağırmaktadır. + Örneğin open POSIX fonksiyonu Linux'ta doğrudan sys_open isimli sistem fonksiyonunu, read, write ve close POSIX fonksiyonları + doğrudan sys_read, sys_write ve sys_close sistem fonksiyonlarını, exit POSIX fonksiyonu sys_exit sistem fonksiyonunu çağırmaktadır. + GNU "libc" kütüphanesinde bazı sistem fonksiyonları için POSIX standartlarında olmayan sarma fonksiyonlar da bulundurulmuştur. + Ancak bazı sistem fonksiyonlarını çağıran POSIX fonksiyonları da sarma fonksiyonlar da yoktur. Bu tür durumlarda ilgili + sistem fonksiyonu numara belirtilerek syscall fonksiyonuyla çağrılabilir. + + Aşağıdaki örnekte openi read ve close POSIX fonkssiyonları yerine doğrudan Linux'taki sistem fonksiyonları çağrılmıştır. +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include +#include +#include +#include + +int main(void) +{ + int fd; + char buf[10]; + long result; + + if ((fd = syscall(SYS_open, "sample.c", O_RDONLY)) == -1) { + perror("SYS_open"); + exit(EXIT_FAILURE); + } + + if ((result = syscall(SYS_read, fd, buf, 10)) == -1) { + perror("SYS_read"); + exit(EXIT_FAILURE); + } + buf[result] = '\0'; + puts(buf); + + if ((result = syscall(SYS_close, fd)) == -1) { + perror("SYS_read"); + exit(EXIT_FAILURE); + } + + return 0; +} + +/*-------------------------------------------------------------------------------------------------------------------------- + Bu bölümde Linux kaynak kodlarının derlenmesi ve sistemin yeni çekirdekle açılması üzerinde duracağız. Çekirdek kodlarının + derlenmesi tüm Linux sistemleri için aynı biçimde yapılmaktadır. Ancak sistemin yeni çekirdekle açılması kullanılan "boot + loader" programın ayarlarıyla ilgilidir. Bugün masaüstü bilgisayarlarında ağırlıklı olarak GRUB isimli boot lader + kullanılmaktadır. Biz burada bu nedenle sürecin GRUB'ta nasıl yürütüleceğini ele alacağız. Gömülü sistemlerde ise ağırlıklı + olarak U-Boot denilen boot loader kullanılmaktadır. Biz kursumuzda U-Boot hakkında bir açıklama yapmayacağız. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 193. Ders 26/01/2025 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + İşletim sistemlerinin çekirdeklerini belleğe yükleyip kontrolün çekirdek kodlarına bırakılmasını sağlayan araçlara + "önyükleyici (boot loader)" denilmektedir. Microsoft Windows sistemlerinde kendi önyükleyici programını kullanmaktadır. + Buna "Windows Boot Manager" ya da kısaca "bootmgr" de denilmektedir. UNIX/Linux dünyasında çeşitli önyükleyici programlar + kullanılmıştır. Halen en yaygın kullanılan önyükleyici program "grub" isimli programdır. Tabii "grub" aynı zamanda Windows + işletim sistemini de yükleyebilmektedir. Grub önyükleyicisinden önce Linux sistemlerinde uzun bir süre "lilo" isimli + önyükleyici kullanılmıştır. Gömülü sistemlerde de çeşitli önyükleyiciler kullanılabilmektedir. Bazı gömülü sistemlerde + o gömülü sistemi üreten kurum tarafından oluşturulmuş olan önyükleyiciler kullanılmaktadır. Ancak gömülü sistemlerde en + çok kullanılan önyükleyici "U-Boot" isimli önyükleyicidir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Nasıl C'deki main fonksiyonuna komut satırı argümanları geçiriliyorsa işletim sistemi çekirdeklerine de çeşitli biçimlerde + parametreler geçirilebilmektedir. Böylece çekirdek belli bir konfigürasyonla işlev görecek biçimde başlatılabilmektedir. + Linux çekirdeğini önyükleyici yüklediğine göre çekirdek parametreleri de önyükleyici tarafından çekirdeğe aktarılmaktadır. + Linux'ta bu parametreler "grub" önyükleyicisinin başvurduğu dosyalarda belirtilmektedir. Grub önyükleyicisinin kullanımı + biraz ayrıntılıdır. Ancak biz burada grub işlemlerini daha basit ve görsel biçimde yapabilmek için "grub-customizer" + isimli bir programdan faydalanacağız. Bu programı Debian türevi sistemlerde aşağıdaki gibi yükleyebilirsiniz: + + $ sudo add-apt-repository ppa:danielrichter2007/grub-customizer + $ sudo apt-get update + $ sudo apt-get install grub-customizer + + Pekiyi neden işletim sistemini yüklemek için ayrı bir programa gereksinim duyulmuştur? Eskiden işletim sistemleri doğrudan + BIOS kodları tarafındna yüklenebiliyordu. Ancak zamanla işletim sistemleri parametreler alacak biçimde geliştirildi. + Önyükleyiciler birden fazla çekirdeğin bulunduğu durumlarda basit ayarlarla sistem yöneticisinin istediği çekirdekle + boot işlemini yapabilmektedir. Diskte birden fazla işletim sisteminin bulunduğu durumlarda sistemin istenilen bir işletim + sistemi tarafından boot edilmesini sağlayabilmektedir. Örneğin makinemizde hem Windows hem de Linux aynı anda bulunuyor + olabilir. Önyükleyicimiz bize bir menü çıkartıp hangi işletim sistemi ile boot işlemini yapmak istediğimizi sorabilir. + Eskiden nispeten basit olan boot prosedürleri zamanla daha karmaşık hale gelmiştir. Önyükleyici programlara gereksinim + duyulmaya başlanmıştır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi neden biz Linux çekirdeğini kaynak kodlardan yeniden derlemek isteyebiliriz? İşte bunun tipik nedenleri şunlar + olabilir: + + - Bazı çekirdek modüllerinin ve aygıt sürücülerinin çekirdek imajından çıkartılması ve dolayısıyla çekirdeğin küçültülmesi + için. + - Yeni birtakım modüllerin ve aygıt sürücülerin çekirdek imajına eklenmesi için. + - Çekirdeğe tamamen başka özelliklerin eklenmesi için. + - Çekirdek üzerinde çekirdek parametreleriyle sağlanamayacak bazı konfigürasyon değişikliklerinin yapılabilmesi için. + - Çekirdek kodlarında yapılan değişikliklerin etkin hale getirilmesi için. + - Çekirdeğe yama (patch) yapılması için. + - Yeni çıkan çekirdek kodlarının kullanılabilir hale getirilmesi için. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Çekirdeğin derlenmesi için öncelikle çekirdek kaynak kodlarının derlemenin yapılacağı bilgisayara indirilmesi gerekir. Pek + çok dağıtım default durumda çekirdeğin kaynak kodlarını kurulum sırasında makineye çekmemektedir. Çekirdek kodları "kernel.org" + sitesinde bulundurulmaktadır. Tarayıcdan "kernel.org" sitesine girerek "pub/linux/kernel" dizinine geçtiğinizde tüm yayınlanmış + çekirdek kodlarını göreceksiniz. İndirmeyi tarayıcıdan doğrudan yapabilirsiniz. Eğer indirmeyi komut satırından "wget" + programıyla yapmak istiyorsanız aşağıdaki URL'yi kullanabilirsiniz: + + https://cdn.kernel.org/pub/linux/kernel/v[MAJOR_VERSION].x/linux-[VERSION].tar.xz + + Buradaki MAJOR_VERSION "3", "4", "5" gibi tek bir sayıyı belirtmektedir. VERSION ise çekirdek büyük ve küçük numaralarını + belirtmektedir. Örneğin biz çekirdeğin 5.15.12 versiyonunu şöyle indirebiliriz: + + $ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.12.tar.xz + + Örneğin çekirdeğin 6.8.1 versiyonunu da şöyle indirebiliriz: + + $ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.8.1.tar.xz + + Çekirdek kodları indirildikten sonra onun açılması gerekir. Açma işlemi "tar" komutuyla aşağıdaki gibi yapılabilir: + + $ tar -xvJf linux-5.15.12.tar.xz + + Debian tabanlı sistemlerde o anda makinede yüklü olan mevcut çekirdeğin kaynak kodlarını indirmek için aşağıdaki komutu + kullanabilirsiniz: + + $ sudo apt-get install linux-source + + Burada yükleme "/usr/src" dizinine yapılacaktır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Linux kaynak kodlarının versiyonlanması eskiden daha farklıydı. Cekirdeğin 2.6 versiyonundan sonra versiyon numaralandırma + sistemi değiştirilmiştir. Eskiden (2.6 ve öncesinde) versiyon numaraları çok yavaş ilerletiliyordu. 2.6 sonrasındaki yeni + versiyonlamada versiyon numaraları daha hızlı ilerletilmeye başlanmıştır. Bugün kullanılan Linux versiyonları nokta ile + ayrılmış üç sayıdan oluşmaktadır: + + Majör.Minör.Patch-Extra (-rcX, -stable, -custom, -generic) + + Buradaki "majör numara" büyük ilerlemeleri "minör numara" ise küçük ilerlemeleri belirtmektedir. Eskiden (2.6 ve öncesinde) + tek sayı olan minör numaralar "geliştirme versiyonlarını (ya da beta versiyonlarını)", çift olanlar ise stabil hale ggetirilmiş + dağıtılan versiyonları belirtiyordu. Ancak 2.6 sonrasında artık tek ve çift minör numaralar arasında böyle bir farklılık + kalmamıştır. Patch numarası birtakım böceklerin ya da çok küçük yeniliklerin çekirdeğe dahil edildiği versiyonları belirtmektedir. + Bu bağlamda minör numaralardan daha küçük bir ilerlemenin söz konusu olduğunu anlatmaktadır. Burada Extra ile temsil edilen + alanda "rcX (X burada bir sayı belirtir) "stable", "custom", "generic", "realtime" gibi sözcükler de bulunmaktadır. "rc" + harfleri "release candidate" sözcüklerin kısaltmadır. Satabil sürümün öncesindeki son geliştirme sürümlerini belirtmektedir. + "stable" sözcüğü dağıtılan kararlı sürümü belirtir. Eğer sistem programcısı çekirdekte kendisi birtakım değişiklikler yapmışsa + genellikle bunun sonuna "custom" sözcüğünü getirir. Tabii bu "custom" sözcüğünü ayrıca "-" biçiminde + numaralar da izleyebilir. Buradaki numaralar sistem programcısının kendi özelleştirmesine ilişkin numaralardır. "generic" + sözcüğü ise genel kullanım için yapılandırılmış bir çekirdek olduğunu belirtmektedir. "realtime" yapılandırmanın gerçek + zamanlı sistem özelliği kazandırmak için yapıldığını belirtmektedir. "generic" ve "realtime" sözcüklerinin öncesinde "-N-" + biçiminde bir sayı da bulunabilmektedir. Bu sayı "dağıtıma özgü yama ya da derleme numarasını belirtmektedir. + + Çalışmakta olan Linux sistemi hakkında bilgiler "uname -a" komutu ile elde edilebilir. Örneğin: + + $ uname -a + inux kaan-virtual-machine 5.15.0-91-generic #101-Ubuntu SMP Tue Nov 14 13:30:08 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux + + Bu bilgi içerisindeki çekirdek versiyonu "uname -r" ile elde edilebilir: + + $ uname -r + 5.15.0-91-generic + + Buradan biz çekirdeğin "5.15.0" sürümünün kullanıldığını anlıyoruz. Burada genel yapılandırılmış bir çekirdek söz konusudur. + 91 sayısı dağıtıma özgü yama ya da derleme numarasını belirtir. + + Aslında "uname" komutu bu bilgileri "/proc" dosya sisteminin içerisinde almaktadır. Örneğin: + + $ cat /proc/version + Linux version 5.15.0-91-generic (buildd@lcy02-amd64-045) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld + (GNU Binutils for Ubuntu) 2.38) #101-Ubuntu SMP Tue Nov 14 13:30:08 UTC 2023 +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Çekirdeğin derlenmesi için zaten çekirdek kodlarında bir "build sistemi" oluşturulmuştur. Buna "KConfig sistemi" ya da + "KBuild sistemi" denilmektedir. Biz önce çekirdek derleme işleminin hangi adımlardan geçilerek yapılacağını göreceğiz. + Sonra çekirdeğin önemli konfigürasyon parametreleri üzerinde biraz duracağız. Sonra da çekirdekte bazı değişiklikler yapıp + değiştirilmiş çekirdeği kullanacağız. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Linux'ta çekirdeğin davranışını değiştirmek için farklı olanaklara sahip 5 yöntem kullanılabilmektedir: + + 1) Çekirdeğin boot parametreleri yoluyla davranışının değiştirilmesi. Bunun için çekirdeğin yeniden derlenmesi gerekmez. + + 2) Kernel mode aygıt sürücüsü yazmak yoluyla çekirdeğin davranışının değiştirilmesi. Bunun çekirdek kodlarının yeniden + derlenmesi gerekmez. + + 3) Çekirdeğin konfigürasyon parametrelerinin değiştirilmesiyle davranışının değiştirilmesi. Bunun için çekirdeğin yeniden + derlenmesi gerekir. + + 4) Çekirdeğin kodlarının değiştirilmesiyle davranışının değiştirilmesi. Bunun için de çekirdeğin yeniden derlenmesi gerekir. + + 5) Çekirdeğin bazı özellikleri "proc" dosya sistemindeki bazı dosyalara birtakım değerler yazarak da değiştirilebilmektedir. + Aslında bu tür değişiklikler "systemd" init sisteminde "systemctl" komutuyla da yapılabilmektedir. Örneğin sistem çalışırken + bir prosesin açabileceği dosya sayısını "proc" dosya sistemi yoluyla şöyle değiştirebiliriz: + + $ echo 2048 | sudo tee /proc/sys/fs/file-max +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Linux'ta çekirdek derlemesi tipik olarak aşağıdaki aşamalardan geçilerek gerçekleştirilmektedir: + + 1) Derleme öncesinde derlemenin yapılacağı makinede bazı programların yüklenmiş olması gerekmektedir. Gerekebilecek tipik + programlar aşağıda verilmiştir: + + $ sudo apt update + $ sudo apt install build-essential libncurses-dev bison flex libssl-dev wget gcc-arm-linux-gnueabihf \ + binutils-arm-linux-gnueabihf libelf-dev dwarves + + 2) Çekirdek kodları indirilerek açılır. Biz bu konuyu yukarıda ele almıştık. İndirmeyi şöyle yapanbiliriz: + + $ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.9.2.tar.xz + + Bu işlemden sonra "linux-6.9.2.tar.xz" isimli dosya indirilmiş durumdadır. Onu aşağıdaki gibi açabiliriz: + + $ tar -xvJf linux-6.9.2.tar.xz + + Bu işlemden sonra "linux-6.9.2" isminde bir dizin oluşturulacaktır. + + Ayıca ek bir bilgi olarak eğer Ubuntu türevi bir dağıtımda çalışıyorsanız istediğniz bir çekirdeği aşağıdaki gibi indirip + kurabilirsiniz: + + sudo apt install linux-image-<çekirdek_sürümü> + + Örneğin: + + $ sudo apt install linux-image-5.15.0-91-generic + +/*-------------------------------------------------------------------------------------------------------------------------- + 194. Ders 31/01/2025 - Cuma +---------------------------------------------------------------------------------------------------------------------------*/ + 3) Çekirdek derlenmeden önce konfigüre edilmelidir. Çekirdeğin konfigüre edilmesi birtakım çekirdek özelliklerin belirlenmesi + anlamına gelmektedir. Konfigürasyon bilgileri çekirdek kaynak kod ağacının kök dizininde (örneğimizde "linux-6.9.2" dizini) + ".config" ismiyle bulunmalıdır. Bu ".config" dosyası default durumda kaynak dosyaların kök dizininde bulunmamaktadır. Bunun + çekirdeği derleyen kişi tarafından oluşturulması gerekmektedir. Çekirdek konfigürasyon parametreleri oldukça fazladır. Biz + izleyen paragraflarda önemli çekirdek konfigürasyon parametrelerini göreceğiz. Çekirdek konfigürasyon parametreleri çok + fazla olduğu için bunlar bazı genel amaçları karşılayacak biçimde default değerlerle önceden oluşturulmuş durumdadır. Bu + önceden oluşturulmuş default konfigürasyon dosyaları "arch//configs" dizininin içerisinde bulunmaktadır. Örneğin + Intel X86 mimarisi için bu default konfigürasyon dosyaları şöyledir: + + $ ls arch/x86/configs + hardening.config i386_defconfig tiny.config x86_64_defconfig xen.config + + Burada biz 64 bit Linux sistemleri için "x86_64_defconfig" dosyasını kullanabiliriz. O halde bu dosyayı kaynak dosyaların + bulunduğu dizininin kök dizinine ".config" ismiyle kopyalayabiliriz: + + $ cp arch/x86/configs/x86_64_defconfig .config + + Biz bütün işlemlerde çekirdek kaynak kodlarının kök dizininde bulunduğumuzu (current working directory) varsayacağız. Ancak + burada bir noktaya dikkatinizi çekmek istiyoruz. Linux kaynak kodlarındaki default konfigürasyon dosyaları minimal biçimde + konfigüre edilmiştir. Bu nedenle pek çok modül bu default konfigürasyon dosyalarında işaretlenmiş değildir. Bu default + konfigürasyon dosyalarını kullanarak derleme yaptığınızda bazı çekirdek modüllerinin seçilmemiş olması nedeniyle sisteminiz + açılmayabilir. Bu tür denemeleri zaten var olan konfigürasyon dosyalarını kullanarak yaparsanız daha fazla modül dosyası + oluşturulabilir ancak daha az zahmet çekebilirsiniz. Linux sistemlerinde genel olarak "/boot" dizini içerisinde + "configs-<çekirdek_sürümü>" ismi altında mevcut çekirdeğe ilişkin konfigürasyon dosyası bulundurulmaktadır. + + Burada bir noktaya dikkatinizi çekmek istiyoruz. Çekirdek kaynak kodlarındaki "arch//configs/x86_64_defconfig" + dizinindeki konfigürasyon dosyası ".config" ismiyle kopyalandıktan sonra ayrıca "make menuconfig" gibi bir işlemle onun + satırlarına bazı default değerlerin de eklenmesi gerekir. Bu default değerler "arch/" dizinindeki "Kconfig" dosyasından + gelmektedir. Bu nedenle bu default konfigürasyon dosyalarını kaynak kök dizine ".config" ismiyle kopyaladıktan sonra + aşağıda belirtildiği gibi "make menuconfig" yapmalısınız. + + Aslında ".config" dosyasını oluşturmanın başka alternatif yolları da vardır: + + make defconfig: Bu komut çalıştığımız sisteme uygun olan konfigürasyon dosyasını temel alarak mevcut donanım bileşenlerini + de gözden geçirerek sistemin açılması için gerekli minimal bir konfigürasyon dosyasını ".config" ismiyle oluşturmaktadır. Örneğin + biz 64 bit Intel sistemine ilişkin bir bilgisayarda çalışıyorsak "make defconfig" dediğimizde "arch/x86/configs/x86_64_defconfig" + dosyası temel alınarak o anda çalışılmakta olan çekirdek donanımları da dikkate alınarak nispeten minimal olan bir konfigürasyon + dosyası oluşturmaktadır. + + make oldconfig: Bu seçeneği kullanmak için kaynak kök dizinde bir ".config" dosyasının bulunyor olması gerekir. Ancak bu + seçenek KConfig dosyasındaki ve kaynak dosya ağacındaki diğer değişiklikleri de göz önüne alarak bu eski ".config" dosyasını + eğer söz konusu mimaride birtakım değişiklikler söz konusu ise o değişikliklere uyumlandırmaktadır. Yani örneğin biz eski + bir ".config" dosyasını kullanıyor olabiliriz. Ancak çekirdeğin yeni versiyonlarında ek birtakım başka konfigürasyon parametreleri + de eklenmiş olabilir. Bu durumda "make oldconfig" bize bu eklenenler hakkında da bazı sorular sorup bunların dikkate alınmasını + sağlayacaktır. + + make _defconfig: Bu seçenek belli bir platformun default konfig dosyasını ".config" dosyası olarak save etmektedir. + Örneğin biz Intel makinelerinde çalışıyor olabiliriz ancak BBB için default konfigürasyon dosyası oluşturmak isteyebiliriz. + Eğer biz "make defconfig" yaparsak Intel tabanlı bulunduğumuz platform dikkate alınarak ".config" dosyası oluşturulur. Ancak + biz burada örneğin "make bb.org_defconfig" komutunu uygularsak bu durumda Intel mimarisinde çalışıyor olsak da "bb.org_defconfig" + konfigürasyon dosyası ".config" olarak save edilir. Tabii bu durumda biz aslında yine ilgili platformun konfigürasyon dosyasını + manuel olarak ".config" biçiminde de kopyalayabiliriz. + + make modules: Bu seçenek ile yalnızca modüller derlenir. Yani bu seçenek ".config" dosyasında belirtilen aygıt sürücü dosyalarını + derler ancak çekirdek derlemesi yapmaz. Yalnızca "make" işlemi zaten aynı zamanda bu işlemi de yapmaktadır. + + Aşağıdaki ilave konfig seçenekleri ise seyrek kullanılmaktadır: + + make allnoconfig: Tüm seçenekleri hayır (no) olarak ayarlar (minimal yapılandırma). + make allyesconfig: Tüm seçenekleri evet (yes) olarak ayarlar (maksimum özellikler). + make allmodconfig: Tüm aygıt sürücülerin çekirdeğin dışında modül (module) biçiminde derleneceğini belirtir. + make localmodconfig: Sistemde o anda yüklü modüllere dayalı bir yapılandırma dosyası (".config" dosyası) oluşturur. + make silentoldconfig: Yeni seçenekler için onları görmezden gelir ve o yeni özellikler ".config" dosyasına yansıtılmaz. + make dtbs: Kaynak kod ağacında "/arch/platform/boot/dts" dizininideki aygıt ağacı kaynak dosyalarını derler ve "dtb" + dosyalarını elde eder. Gömülü sistemlerde bu işlemin yapılması ve her çekirdek versiyonuyla o versiyonun "dtb" dosyasının + kullanılması tavsiye edilir. + make headers_install: Aygıt sürücüler için gereken başlık dosyalarının kurulumu sağlanır. Default kurulum yeri "/usr/include/linux" + dizinidir. Ancak INSTALL_HDR_PATH çevre değişkeni ile değiştirilebilir. + + Yukarıda da belirttiğimiz gibi aslında pek çok dağıtım o anda yüklü olan çekirdeğe ilişkin konfigürasyon dosyasını "/boot" + dizini içerisinde "config-$(uname -r)" ismiyle bulundurmaktadır. Örneğin kursun yapılmakta olduğu Mint datıtımında "/boot" + dizinin içeriği şöyledir: + + $ ls /boot + config-5.15.0-91-generic grub initrd.img-5.15.0-91-generic vmlinuz + efi initrd.img System.map-5.15.0-91-generic vmlinuz-5.15.0-91-generic + + Buradaki "config-5.15.0-91-generic" dosyası çalışmakta olduğumuz çekirdekte kullanılan konfigürasyon dosyasıdır. Benzer biçimde + BBB'deki built-in eMMC içerisinde bulunan çekirdekteki "/boot" dizininin içeriği de şöyledir: + + SOC.sh dtbs System.map-5.10.168-ti-r71 + initrd.img-5.10.168-ti-r71 uboot config-5.10.168-ti-r71 + vmlinuz-5.10.168-ti-r71 + + Buradaki konfigürasyon dosyası da "config-5.10.168-ti-r71" biçimindedir. + + Eğer çalışılan sistemdeki konfigürasyon dosyasını temel alacaksanız bu dosyayı Linux kaynak kodlarının bulunduğu kök dizine + ".config" ismiyle kopyalayabilirsiniz. Örneğin: + + $ cp /boot/config-$(uname -r) .config + + Fakat eski bir konfigürasyon dosyasını yemni bir öçekirdekle kullanmak için ayrıca "make oldconfig" işleminin de yapılması + gerekmektedir. + + 4) Şimdi elimizde pek çok değerin set edilmiş olduğu ".config" isimli bir konfigürasyon dosyası vardır. Artık bu konfigürasyon + dosyasından hareketle yalnızca istediğimiz bazı özellikleri değiştirebiliriz. Bunun için "make menuconfig" komutunu kullanabiliriz: + + $ make menuconfig + + Bu komut ile birlikte grafik ekranda konfigürasyon seçenekleri listelenecektir. Tabii buradaki seçenekler default değerler + almış durumdadır. Bunların üzerinde değişiklikler yaparak ".config" dosyasını save edebiliriz. Aslında "make menuconfig" + işlemi hiç ".config" dosyası oluşturulmadan doğrudan da yapılabilmektedir. Bu durumda hangi sistemde çalışılıyorsa o sisteme + özgü default config dosyası temel alınmaktadır. Biz en azından "General stup/Local version - append to kernel release" + seçeneğine "-custom" gibi bir sonek girmenizi böylece yeni çekirdeğe "-custom" soneki iliştirmenizi tavsiye ederiz. + + ".config" dosyası elde edildiğinde çekirdek imzalamasını ortadan kaldırmak için dosyayı açıp aşağıdaki özellikleri belirtildiği + gibi değiştirebilirsiniz (bunların bazıları zaten default durumda aşağıdaki gibi dde olabilir): + + CONFIG_SYSTEM_TRUSTED_KEYS="" + CONFIG_SYSTEM_REVOCATION_KEYS="" + CONFIG_SYSTEM_TRUSTED_KEYRING=n + CONFIG_SECONDARY_TRUSTED_KEYRING=n + + CONFIG_MODULE_SIG=n + CONFIG_MODULE_SIG_ALL=n + CONFIG_MODULE_SIG_KEY="" + + Çekirdek imzalaması konusu daha ileride ele alınacaktır. + + Yukarıda a belirttiğimiz gibi derlenecek çekirdeklere yerel bir versiyon numarası da atanabilmektedir. Bu işlem Bu işlem + "make menuconfig" menüsünde "General Setup/Local version - append custom release" seçeneği kullanılarak ya da ".config" + dosyasında "CONFIG_LOCALVERSION" kullanılarak yapılabilir. Örneğin: + + CONFIG_LOCALVERSION="-custom" + + Artık çekirdek sürümüne "-custom" sonekini eklemiş olduk. + + 5) Derleme işlemi için "make" komutu kullanılmaktadır. Örneğin: + + $ make + + Eğer derleme işleminin birden fazla CPU ile yapılmasını istiyorsanız "-j" seçeneğini komuta dahil edebilirsiniz. + Çalışılan sistemdeki CPU sayısının "nproc" komutuyla elde edildiğini anımsayınız: + + $ make -j$(nproc) + + Derleme işlemi bittiğinde ürün olarak biz "çekirdek imajını", "çekirdek tarafından yüklenecek olan modül dosyalarını (aygıt + sürücü dosyalarını)" ve diğer bazı dosyaları elde etmiş oluruz. Derleme işleminden sonra elde oluşturulan dosyalar ve + onların yerleri şöyledir (buradaki <çekirdek_sürümü> "uname -r" ile elde edilecek değeri belirtiyor): + + - Sıkıştırılmış Çekirdek Imajı: "arch//boot" dizininde "bzImage" ismiyle oluşturulmaktadır. Denemeyi yaptığımız Intel makinede + dosyanın yol ifadesi "arch/x86_64/boot/bzImage" biçimindedir. + + - Çekirdeğin Sıkıştırılmamış ELF İmajı: Kaynak kök dizininde "vmlinux" isminde dosya biçiminde ooluşturulur. + + - Çekirdek Modülleri (Aygıt Sürücü Dosyaları): "drivers" dizininin altındaki dizinlerde ve "fs" dizininin altındaki dizinlerde + ve "net" dizininin altındaki dizinlerde. Ancak "make modules_install" ile bunların hepsi belirli bir dizine çekilebilir. + + Çekirdek Sembol Tablosu: Kaynak kök dizininde "System.map" ismiyle bulunuyor. + + Çekirdeğin derlemesi ne kadar zaman almaktadır? Şüphesiz bu derlemenin yapıldığı makineye göre değişebilir. Ancak derleme sürecinin + uzamasına yol açan en önemli etken çekirdek konfigüre edilirken çok fazla modülün seçilmesidir. Pek çok dağıtım "belki lazım + olur" gerekçesiyle konfigürasyon dosyalarında pek çok modülü dahil etmektedir. Bir dağıtımın konfigürasyon dosyasını kullandığınız + zaman çekirdek derlemesi uzayacaktır. Ayrıca çekirdek konfigüre edilirken çok fazla modülün dahil edilmesi modüllerin çok fazla + yer kaplamasına da yol açabilmektedir. Çekirdek kodlarındaki platforma özgü default konf,gürasyon dosyaları daha minimalist + bir biçimde oluşturulmuş durumdadır. + + 6) Derleme sonrasında farklı dizinlerde oluşturulmuş olan aygıt sürücü dosyalarını (modülleri) belli bir dizine kopyalamak + için "make modules_install" komutu kullanılmaktadır. Bu komut seçeneksiz kullanılırsa default olarak "/lib/modules/<çekirdek_sürümü>" + dizinine kopyalama yapar. Her ne kadar bu komut pek çok ".ko" uzantılı aygıt sürücü dosyasını hedef dizine kopyalıyorsa + da bunların hepsi çekirdek tarafından belleğe yüklenmemektedir. Çekirdek gerektiği zaman gereken aygıt sürücüleri bu dizinden + alarak yüklemektedir. Örneğin: + + $ sudo make modules_install + + Aslında "make modules_install" komutunun modül dosyalarını (aygıt sürücü dosyalarını) istediğimiz bir dizine kopyalamasını + da sağlayabiliriz. Bunun için INSTALL_MOD_PATH komut satırı argümanı kullanılmaktadır. Örneğin: + + $ sudo INSTALL_MOD_PATH=modules make modules_install + + Burada aygıt sürücü dosyaları "/lib/modules/<çekirdek_sürümü>" dizinine değil bulunulan yerdeki "modules" dizinine + kopyalanacaktır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi "make modules_install" komutu yalnızca modül dosyalarını mı hedef dizine kopyalıyor? Hayır aslında bu komut modül + dosyalarının kopyalanması dışında bazı dosyaları da oluşturup onları da hedef dizine kopyalamaktadır. Bu komut sırasıyla + şunları yapmaktadır: + + - Modül dosyalarını "/lib/modules/<çekirdek_sürümü>" dizinine kopyalar. + - "modules.dep" isimli dosyayı oluşturur ve bunu "/lib/modules/<çekirdek_sürümü>" dizinine kopyalar. + - "modules.alias" isimli dosyayı oluşturur ve bunu "/lib/modules/<çekirdek_sürümü>" dizinine kopyalar. + - "modules.order" isimli dosyayı oluşturur ve "/lib/modules/<çekirdek_sürümü>" dizinine kopyalar. + - "modules.builtin" isimli dosyayı "/lib/modules/<çekirdek_sürümü>" dizinine kopyalar. + + Aslında burada oluşturulan dosyaların bazıları mutlak anlamda bulunmak zorunda değildir. Ancak sistemin öngörüldüğü gibi + işlev göstermesi için bu dosyaların ilgili dizinde bulunması uygundur. + + Bir aygıt sürücü başka bir aygıt sürücüleri de kullanıyor olabilir. Bu durumda bu aygıt sürücü yüklenirken onun kullandığı + tüm sürücülerin özyinelemeli olarak yüklenmesi gerekir. İşte "modules.dep" dosyası bir aygıt sürücünün yüklenmesi için + başka hangi sürücülerin yüklenmesi gerektiği bilgisini tutmaktadır. Aslında "modules.dep" bir text dosyadır. Bu text dosya" + satırlardan oluşmaktadır. Satırların içeriği şöyledir: + + : ... + + Dosyanın içeriğine şöyle örnek verebiliriz: + + ... + kernel/arch/x86/crypto/nhpoly1305-sse2.ko.zst: kernel/crypto/nhpoly1305.ko.zst kernel/lib/crypto/libpoly1305.ko.zst + kernel/arch/x86/crypto/nhpoly1305-avx2.ko.zst: kernel/crypto/nhpoly1305.ko.zst kernel/lib/crypto/libpoly1305.ko.zst + kernel/arch/x86/crypto/curve25519-x86_64.ko.zst: kernel/lib/crypto/libcurve25519-generic.ko.zst + ... + + Eğer bu "modules.dep" dosyası olmazsa bu durumda "modeprob" komutu çalışmaz ve çekirdek modülleri yüklenirken eksik + yükleme yapılabilir. Dolayısıyla sistem düzgün bir biçimde açılmayabilir. Eğer bu dosya elimizde yoksa ya da bir biçimde + silinmişse bu dosyayı yeniden oluşturabiliriz. Bunun için "dempmod -a" komutu kullanılmaktadır. Komut doğrudan kullanıldığında + o anda çekirdek sürümü için "modules.dep" dosyasını oluşturmaktadır. Örneğin: + + $ sudo depmod -a + + Ancak siz yüklü olan başka bir çekirdek sürümü için "modules.dep" dosyasını oluşturmak istiyorsanız bu durumda çekirdek + sürümünü de komut satırı argümanı olarak aşağıdaki gibi komuta vermelisiniz: + + $ sudo depmod -a <çekirdek sürümü> + + Tabii depmod komutunun çalışabilmesi için "/lib/modules/<çekirdek_sürümü> dizininde modül dosyalarının bulunuyor olması gerekir. + Çünkü bu komut bu dizindeki modül dosyalarını tek tek bulup ELF formatının ilgili bölümlerine bakarak modülün hangi modülleri + kullandığını tespit ederek "modules.dep" dosyasını oluşturur. + + "modules.alias" dosyası belli bir isim ya da id ile aygıt sürücü dosyasını eşleştiren bir text dosyadır. Bu dosyanın + bulunmaması bazı durumlarda sorunlara yol açmayabilir. Ancak örneğin USB port'a bir aygıt takıldığında bu aygıta ilişkin + aygıt sürücünün hangisi olduğu bilgisi bu dosyada tutulmaktadır. Bu durumda bu dosyanın olmayışı aygıt sürücünün yüklenememesine + neden olabilir. Dosyanın içeriği aşağıdaki formata uygun satırlardan oluşmaktadır: + + alias + + Örnek bir içerik şöyle olabilir: + + ... + alias usb:v05ACp*d*dc*dsc*dp*ic*isc*ip*in* apple_mfi_fastcharge + alias usb:v8086p0B63d*dc*dsc*dp*ic*isc*ip*in* usb_ljca + alias usb:v0681p0010d*dc*dsc*dp*ic*isc*ip*in* idmouse + alias usb:v0681p0005d*dc*dsc*dp*ic*isc*ip*in* idmouse + alias usb:v07C0p1506d*dc*dsc*dp*ic*isc*ip*in* iowarrior + alias usb:v07C0p1505d*dc*dsc*dp*ic*isc*ip*in* iowarrior + ... + + Bu dosya bir biçimde silinirse yine "depmod" komutu ile oluşturulabilir. (Yani depmod komutu yalnızca "modules.dep" dosyasını + değil bu dosyayı da oluşturmaktadır.) + + "modules.order" dosyası aygıt sürücü dosyalarının yüklenme sırasını barındıran bir text dosyadır. Bu dosyanın her satırında + bir çekirdek aygıt ssürücüsünün dosya yol ifadesi bulunur. Daha önce yazılmış aygıt sürücüler daha sonra yazılanlardan + daha önce yüklenir. Bu dosyanın olmaması genellikle bir soruna yol açmaz. Ancak modüllerin belli sırada yüklenmemesi + bozukluklara da neden olabilmektedir. Bu dosyanın da silinmesi durumunda yine bu dosya da "depmod" komutuyla oluşturulabilmektedir. + + 7) Eğer gömülü sistemler için derleme yapıyorsanız kaynak kod ağacındaki "arch//boot/dts" dizini içerisindeki aygıt + ağacı kaynak dosyalarını da derlemelisiniz. Tabii elinizde zaten o versiyona özgü aygıt dosyası bulunuyor olabilir. Bu durumda + bu işlemi hiç yapmayabilirsiniz. Aygıt ağacı kaynak dpsyalarını derlemek için "make dtbs" komutunu kullanabilirsiniz: + + $ make dtbs + + Derlenmiş aygıt ağacı dosyaları "arch//boot/dts" dizininde oluşturulacaktır. + + 8) Bizim çekirdek imajını, geçici kök dosya sistemine ilişkin dosyayı ve aygıt ağacı dosyasını uygun yere yerleştirmemiz + gerekir. Bu dosyalar "/boot" dizini içerisinde bulunmalıdır. Ancak aslında bu işlem de "make install" komutuyla otomatik + olarak yapılabilmektedir. "make install" komutu aynı zamanda "grub" isimli bootloder programın konfigürasyon dosyalarında da + güncelleme yapıp yeni çekirdeğin "grub" menüsü içerisinde görünmesini de sağlamaktadır. Komut şöyle kullanılabilir: + + $ sudo make install + + Bu komut ile sırasıyla yapılanlar şunlardır: + + - Çekirdek imaj dosyası "arch//boot/bzImage" hedef "/boot" dizinine "vmlinuz-<çekirdek_sürümü>" + ismiyle kopyalanır. + - "System.map" dosyası hedef "/boot" dizinine "System.map-<çekirdek_sürümü>" ismiyle kopyalanır. + - ".config" dosyası "/boot" dizinine "config-<çekirdek_sürümü>" ismiyle kopyalanır. + - "Geçici kök dosya sistemi dosyası oluşturulur ve hedef "/boot" dizinine "initrd.img-<çekirdek_sürümü>" ismiyle kopyalanır. + - Eğer "grub" boot loader kullanılıyorsa "grub" konfigürasyonu güncellenir ve "grub"" menüsüne yeni girişler eklenir. Böylece + sistemin otomatik olarak yeni çekirdekle açılması sağlanır. + + Yukarıda da belirttiğimiz gibi derleme işlemi sonucunda elde edilmiş olan dosyaların hedef sistemde bazı dizinlerde bulunuyor + olması gerekir. Bu yerleri bir kez daha belirtmek istiyoruz: + + - Çekirdek Imajı ---> "/boot" dizinine + - Çekirdek Sembol Tablosu ---> "/boot" dizinine + - Modül Dosyaları ---> "/lib/modules/<çekirdek_sürümü>/kernel" dizinin altında + + Ancak yukarıdaki dosyalar dışında isteğe bağlı olarak aşağıdaki dosyalar da hedef sisteme konuşlandırılabilir: + + - Konfigürasyon Dosyası ---> "/boot" dizini + - Geçici Kök Dosya Sistemi Dosyası ---> "/boot" dizinine + - Modüllere İlişkin Bazı Dosyalar ---> "/lib/modules/<çekirdek_sürümü>" dizinine + + Pekiyi yukarıda belirttiğimiz dosyalar hedef sistemdeki ilgili dizinlere hangi isimlerle kopyalanmalıdır? İşte tipik + isimlendirme şöyle olmalıdır (buradaki <çekirdek_sürümü> "uname -r" komutuyla elde edilecek olan yazıdır): + + - Çekirdek İmajı: "/boot/vmlinuz-<çekirdek_sürümü>". Örneğin "vmlinuz-6.9.2-custom" gibi. + - Çekirdek Sembol Tablosu: "/boot/System.map-<çekirdek_sürümü>". Örneğin "System.map-6.9.2-custom" gibi. + - Modüllere İlişkin Dosyalar: Bunlar yukarıda da belirttiğimiz gibi "/lib/modules/<çekirdek_sürümü>" dizininin içerisine + kopyalanmalıdır. + - Konfigürasyon Dosyası: "/boot/config-<çekirdek_sürümü>". Örneğin "config-6.9.2-custom" gibi. + - Geçici Kök Dosya Sistemine İlişkin Dosya: "/boot/initrd.img-<çekirdek_sürümü>". Örneğin "initrd.img-6.9.2-custom" gibi. + + Ayrıca bazı dağıtımlarda "/boot" dizini içerisindeki "vmlinuz" dosyası default olan "vmlinuz-<çekirdek_sürümü>" dosyasına, + "inird.img" dosyası da "/boot/initrd.img-<çekirdek_sürümü>" dosyasına sembolik link yapılmış durumda olabilir. Ancak bu sembolik + bağlantıları "grub" kullanmamaktadır. Aşağıda Intel sistemindeki "/boot" dizinin default içeriğini görüyorsunuz: + + $ ls -l + total 141168 + -rw-r--r-- 1 root root 261963 Kas 14 2023 config-5.15.0-91-generic + drwx------ 3 root root 4096 Oca 1 1970 efi + drwxr-xr-x 7 root root 4096 Ara 5 19:02 grub + lrwxrwxrwx 1 root root 28 Ara 5 20:28 initrd.img -> initrd.img-5.15.0-91-generic + -rw-r--r-- 1 root root 126391088 Tem 11 20:19 initrd.img-5.15.0-91-generic + -rw------- 1 root root 6273869 Kas 14 2023 System.map-5.15.0-91-generic + lrwxrwxrwx 1 root root 25 Ara 5 20:28 vmlinuz -> vmlinuz-5.15.0-91-generic + -rw-r--r-- 1 root root 11615272 Kas 14 2023 vmlinuz-5.15.0-91-generic + + Pekiyi derleme sonucunda elde ettiğimiz dosyaları manuel isimlendirirken çekirdek sürüm yazısını nasıl bileceğiz? + Bunun için "uname -r" komutunu kullanamayız. Çünkü bu komut bize o anda çalışmakta olan çekirdeğin sürüm yazısını verir. + Biz yukarıdaki denemede Linux'un "6.9.2" sürümünü derledik. Bunun sonuna da "-custom" getirirsek sürüm yazısının + "6.9.2-custom" olmasını bekleriz. Ancak bu sürüm yazısı aslında manuel olarak isim değiştirmekle oluşturulamamaktadır. + Bu sürüm yazısı çekirdek imajının içerisine yazılmaktadır ve bizim bazı dosyayalara verdiğimiz isimlerin çekirdek içerisindeki + bu yazıyla uyumlu olması gerekir. Default olarak "kernel.org" sitesinden indirilen kaynak kodlar derlendiğinde çekirdek sürümü + "6.9.2" gibi üç haneli bir sayı olmaktadır. Yani yazının sonunda "-generic" gibi "-custom" gibi bir sonek yoktur. İşte çekirdeği + derlemeden önce daha önceden de belirttiğimiz gibi ".config" dosyasında "CONFIG_LOCALVERSION" özelliğine bu sürüm numarasından + sonra eklenecek bilgiyi girebilirsiniz. Örneğin: + + CONFIG_LOCALVERSION="-custom" + + Anımsayacağınız gibi bu işlem "make menuconfig" menüsünde "General Setup/Local version - append custom release" seçeneği kullanılarak + da yapılabilmektedir. Biz buradaki örneğimizde bu işlemi yaparak çekirdeği derledik. Dolayısıyla bizim derlediğimiz çekirdekte + çekirdek imajı içerisinde yazan sürüm ismi "6.9.2-custom" biçimindedir. Pekiyi biz bu ismi unutsaydık nasıl öğrenebilirdik. Bunun + basit bir yolu sıkıştırılmamış çekirdek dosyası içerisindeki (kaynak kök dizindeki "vmlinux" dosyası) string tablosunda "Linux version" + yazısını aramaktır. Örneğin: + + $ strings vmlinux | grep "Linux version" + Linux version 6.9.2-custom (kaan@kaan-virtual-machine) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld (GNU Binutils for + Ubuntu) 2.38) # SMP PREEMPT_DYNAMIC + Linux version 6.9.2-custom (kaan@kaan-virtual-machine) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld (GNU Binutils for + Ubuntu) 2.38) #2 SMP PREEMPT_DYNAMIC Thu Dec 5 17:55:14 +03 2024 + + Buradan sürüm yazısının "6.9.2-custom" olduğu görülmektedir. O halde bizim derleme sonucunda elde ettiğimiz dosyaları + manuel biçimde kopyalarken sürüm bilgisi olarak "6.9.2-custom" yazısını kullanmalıyız. Çekirdek imajının "/boot" dizinine + manuel kopyalanması işlemi şöyle yapılabilir (kaynak kök dizinde bulunduğumuzu varsayıyoruz): + + $ sudo cp arch/x86_64/boot/bzImage /boot/vmlinuz-6.9.2-custom + + Konfigürasyon dosyasını da şöyle kopyalayabiliriz: + + $ sudo cp .config /boot/config-6.9.2-custom + + Tabii bizim çekirdek modüllerini de "/lib/modules/6.9.2-custom/kernel" dizinine kopyalamamız gerekir. Ayrıca bir de + geçici kök dosya sistemine ilişkin dosyayı da kopyalamamız gerekir. Çekirdek modüllerinin kopyalanması biraz zahmetli bir + işlemdir. Çünkü bunlar derlediğimiz çekirdekte farklı dizinlerde bulunmaktadır. Bu kopyalamanın en etkin yolu "make modules_install" + komutunu kullanmaktır. Benzer biçimde çekirdek dosyalarının ve gerekli diğer dosyaların uygun yerlere kopyalanması için + en etkin yöntem "make install" komutudur. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Normal olarak biz "make install" yaptığımızda eğer sistemimizde "grub" önyükleyicisi varsa komut "grub" konfigürasyon + dosyalarında güncellemeler yaparak sistemin yeni çekirdekle açılmasını sağlamaktadır. Ancak kullanıcı bir menü yoluyla + sistemin kendi istediği çekirdekle açılmasını sağlayabilir. Grub menüsü otomatik olarak görüntülenmemektedir. Boot işlemi + sırasında ESC tuşuna basılırsa menü görüntülenir. Eğer "grub" menüsünün her zaman görüntülenmesi isteniyorsa "/etc/default/grub" + dosyasındaki iki satır aşağıdaki gibi değiştirilmelidir: + + GRUB_TIMEOUT_STYLE=menu + GRUB_TIMEOUT=5 + + Buradaki GRUB_TIMEOUT satırı eğer menünün müdahale yapılmamışsa en fazla 5 saniye görüntüleneceğini belirtmektedir. + + Bu işlemden sonra "update-grub" programı da çalıştırılmalıdır: + + $ sudo update-grub + + Bu tür denemeler yapılırken "grub" menüleri bozulabilmektedir. Düzeltme işlemleri bazı konfigürasyon dosyalarının edit + edilmesiyle manuel biçimde yapılabilir. Konfigürasyon dosyaları güncelleendikten sonra "update-grub" programı mutlaka + çalıştırılmalıdır. Ancak eğer "grub" konfigürasyon dosyaları konusunda yeterli bilgiye sahip değilseniz "grub" işlemlerini + görsel bir biçimde "grub-customizer" isimli programla da yapabilirsiniz. Bu program "debian depolarında" olmadığı için + önce aşağıdaki gibi programın bulunduğu yerin "apt" kayıtlarına eklenmesi gerekmektedir: + + $ sudo add-apt-repository ppa:danielrichter2007/grub-customizer + $ sudo apt-get update + + Bu işlemden sonra kurulum yapılabilir: + + $ sudo apt-get install grub-customizer +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Biz yukarıda çekirdek derleme ve yeni çekirdeği kurma sürecini maddeler halinde açıkladık. Şimdi yukarıdaki adımları özet + hale getirelim: + + 1) Çekirdek derlemesi için gerekli olan araçlar indirilir. + + 2) Çekirdek kodları indirilir ve açılır. + + 3) Zaten hazır olan konfigürasyon dosyası ".config" biçiminde kaynak kök dizine save edilir. + + 4) Konfigrasyon dosyası üzerinde "make menuconfig" komutu ile değişiklikler yapılır. + + 5) Çekirdek derlemesi "make -j$(nproc)" komutu ile gereçekleştirilir. + + 6) Modüller ve ilgili dosyalar hedefe "sudo make modules_install" komututu ile konuşlandırılır. + + 7) Çekirdek imajı ve ilgili dosyalar "sudo make install" komutu ile hedefe konuşlandırılır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi yeni çekirdeği derleyip sisteme dahil ettikten sonra nasıl onu sistemden tamamen çıkartabiliriz? Tabii yapılan + işlemlerin tersini yapmak gerekir. Bu işlem manuel biçimde şöyle yapılabilir: + + - "/lib/modules/<çekirdek_sürümü>" dizini tamamen silinebilir. + - "/boot" dizinindeki çekirdek sürümüne ilişkin dosyalar silinmelidir. + - "/boot" dizininden çekirdek sürümüne ilişkin dosyalar silindikten sonra "update-grub" programı sudo ile çalıştırılmalıdır. + Bu program "/boot" dininini inceleyip otomatik olarak ilgili girişleri "grub" menüsünden siler. Yani aslında "grub" + konfigürasyon dosyaları üzerinde manuel değişiklik yapmaya gerek yoktur. "grub" işlemleri için diğer bir alternatif ise + "grub-customizer" programı ile görsel silme yapmaktır. Ancak bu program "/boot" dizini içerisindeki dosyaları ve modül + dosyalarını silmez. Yalnızca ilgili girişleri "grub" menüsündne çıkartmaktadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 195. Ders 02/02/2025 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi biz Intel sisteminde çalışırken ARM için çekirdek derlemesini nasıl yapabiliriz? Bir platformda çalışırken başka + bir platform için derleme yapılabilir. Ancak hedef platforma ilişkin ismine "araç zinciri (toolchain)" denilen bir paketin + yüklenmiş olması gerekir. Araç zincirleri yalnızca derleyicilerden değil sistem programlama için gerekli olan çeşitli + programları barındıran paketlerdir. Örneğin ARM platformu için çeşitli araç zincirleri bulunmaktadır. ARM platformu için + en yaygın kullanılan araç zincirleri aşağıdaki bağlantıdan indirilebilir: + + https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads + + Örneğin Beaglebone Black (BBB) için Windows'ta çalışan araç zinciri bu sitede aşağıdaki bağlantıya tıklanarak indirilebilir: + + arm-gnu-toolchain-14.2.rel1-mingw-w64-i686-arm-none-linux-gnueabihf.zip + + Genel olark araç zincirleri kullanılmadan önce birkaç çevre değişkeninin set edilmesi gerekmektedir: + + - CROSS_COMPILE isimli çevre değişkeni araç zincirinin öneki ile set edilmelidir. Örneğin: + + $ export CROSS_COMPILE=arm-none-linux-gnueabihf- + + - PATH çevre değişkenine araç zincirine ilişkin "bin" dizininin eklenmesi gerekir: + + $ PATH=$PATH:/home/kaan/Study/UnixLinux-SysProg/arm-gnu-toolchain-13.3.rel1-x86_64-arm-none-linux-gnueabihf/bin + + - ARCH çevre değişkeninin hedef platformu belirten bir yazı ile set edilmesi gerekir. ARM platformu için bu yazı "arm" + biçimindedir: + + $ export ARCH=arm + + Bundan sonra çekirdeğin kaynak kodları yukarıda belirtildiği gibi derlenebilir. Burada bu işlemin ayrıntısı üzerinde + durmayacağız. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Şimdi de çekirdek kodlarının değiştirilip derlenmesine bir örnek verelim. Çekirdek kodlarında değişiklik yapmanın birkaç + yolu olabilir: + + 1) Çekirdek kodlarındaki bir dosya içerisinde bulunan fonksiyon kodlarında değişiklik yapılması. + 2) Çekirdek kodlarındaki bir dosya içerisine yeni bir fonksiyon eklenmesi. + 3) Çekirdek kodlarındaki bir dizin içerisine yeni bir C kaynak dosyası eklenmesi. + 4) Çekirdek kodlarındaki bir dizin içerisine yeni bir dizin ve bu dizinin içerisinde çok sayıda C kaynak dosyalarının + eklenmesi. + + Eğer biz birinci maddedeki ve ikinci maddedeki gibi çekirdek kodlarına yeni bir dosya eklemiyorsak çekirdeğin derlenmesini + sağlayan make dosyalarında bir değişiklik yapmamıza gerek yoktur. Ancak çekirdeğe yeni bir kaynak dosya ya da dizin ekleyeceksek + bu eklemeyi yaptığımız dizindeki make dosyasında bu ekleme izleyen paragraflarda açıklayacağımız biçimde belirtilmelidir. + Böylece çekirdek yeniden derlendiğinde bu dosyalar da çekirdek imajının içerisine eklenmiş olacaktır. Eğer kaynak kod + ağacında bir dizinin altına yeni bir dizin eklemek istersek bu durumda o dizini yine ana dizine ilişkin make dosyasında + belirtmemiz ve o dizinde ayrı bir Makefile oluşturmamız gerekmektedir. + + Pekiyi çekirdek kodlarındaki bir dosya içerisindeki bir fonksiyonda değişiklik yaptığımızda çekirdek modüllerini yeniden + hedef makineye aktarmamız gerekir mi? İşte genel olarak bu tür basit değişikliklerde çekirdek modüllerinin güncellenmesi + gerekmemektedir. Ancak ne olursa olsun bu durum yapılan değişikliklere de bağlıdır. Bu nedenle çekirdek modüllerinin de + yeniden "make modules_install" komutu hedef makineye çekilmesi önerilir. + + Örneğin biz çekirdek kaynak kod ağacında "fs/open.c" içerisinde "chdir" sistem fonksiyonun aşağıdaki gibi bir satır ekleyelim: + + SYSCALL_DEFINE1(chdir, const char __user *, filename) + { + struct path path; + int error; + unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY; + + printk(KERN_INFO "directory is changing...\n"); + + retry: + error = user_path_at(AT_FDCWD, filename, lookup_flags, &path); + if (error) + goto out; + + error = path_permission(&path, MAY_EXEC | MAY_CHDIR); + if (error) + goto dput_and_out; + + set_fs_pwd(current->fs, &path); + + dput_and_out: + path_put(&path); + if (retry_estale(error, lookup_flags)) { + lookup_flags |= LOOKUP_REVAL; + goto retry; + } + out: + return error; + } + + Bu işlemden sonra sırasıyla aşağıdakiler yapıp sisytemi yeni ekirdekle açabiliriz: + + make -j$(nproc) + make modules_install + make install + + Bu yeni çekirdekte ne zaman bir dizin değiştirilse bir log yazısı oluşturulmaktadır. Bu yazıları "dmesg" komutuyla + görebilirsiniz. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Pekiyi biz çekirdeğin kaynak kod ağacına yeni bir ".c" dosyası eklemek istersek ne yapacağız? İşte bu durumda çekirdeğin + make dosyalarında bu eklemenin belirtilmesi gerekmektedir. Çekirdek kodlarında her kaynak kod dizininde ayrı bir Makefile + dosyası bulunmaktadır. Programcı yeni kaynak dosyayı hangi dizine ekliyorsa o dizine ilişkin Makefile içerisine aşağıdaki + gibi bir satır eklemesi gerekir: + + obj-y += dosya_ismi.o + + Böylece artık make işlem yapıldığında bu dosya da derlenip çekirdek imajına dahil edilecektir. Buradaki += operatörü obj-y + isimli hedefe ekleme yapma anlamına gelmektedir. "obj" sözcüğünün yanındaki "-y" harfi ilgili dosyanın çekirdeğin bir + parçası biçiminde çekirdek imajının içerisine gömüleceğini belirtmektedir. Make dosyalarının bazı satırlarında "obj-y" + yerine "obj-m" de görebilirsiniz. Bu da ilgili dosyanın ayrı bir modül biçiminde derleneceği anlamına gelmektedir. + Eklemeler genellikle çekirdek imajının içine yapıldığı için biz de "obj-y" kullanırız. Eğer bir dosyayı biz çekirdek + imajının içine gömmek yerine ayrı bir çekirdek modülü olarak derlemek istiyorsak bu durumda dosyayı yerleştirdiğimiz + dizinin "Makefile" dosyasına aşağıdaki gibi bir ekleme yaparız: + + obj-m += dosya_ismi.o + + Eğer çekirdek kaynak kodlarına tümden bir dizin eklemek istiyorsak bu durumda o dizini oluşturduğumuz dizindeki "Makefile" + dosyasına aşağıdaki gibi bir ekleme yaparız: + + obj-y += dizin_ismi/ + + Burada dizin isminden sonra '/' karakterini unutmayınız. Tabii bu ekleme bir modül biçiminde de olabilirdi: + + obj-m += dizin_ismi/ + + Fakat bu ekleme yapıldıktan sonra bizim ayrıca yarattığımız dizinde "Makefile" isimli bir dosya oluşturmamız ve o dosyanın + içerisinde o dizinde çekirdek kodlarına ekleyeceğimiz dosyaları belirtmemiz gerekir. Örneğin biz "drivers" dizininin altına + "mydriver" isimli bir dizin oluşturup onun da içerisine "a.c" "b.c" ve "c.c" dosyalarını eklemiş olalım. Bu durumda önce + "drivers" dizini içerisindeki Makefile dosyasına aşağıdaki gibi bir satır ekleriz: + + obj-y += mydriver/ + + Sonra da "mydriver" dizini içerisinde "Makefile" isimli bir dosya oluşturup bu dosyanın içerisinde de bu dizin içerisindeki + dosyaları belirtiriz. Örneğin: + + obj-y += a.o + obj-y += b.o + obj-y += c.o +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Örneğin biz kaynak kod ağacında "drivers" dizinin altında "mydriver" isimli dizin yaratıp onun içerisine "mydrive.c" + dosyasını yerleştirmek isteyelim. Sırasıyla şunları yapmamız gerekir: + + 1) "drivers" dizini altında "mydriver" dizini yaratırız. + + 2) "drivers" dizini içerisindeki Makefile dosyasına aşağıdaki satır ekleriz: + + obj-y += mydriver/ + + 3) "drivers/mydriver" dizini içerisinde "mydriver.c" dosyasını oluştururz. Dosyanın içeriği şöyle olabilir: + + #include + #include + + static int __init helloworld_init(void) + { + printk(KERN_INFO "Hello World...\n"); + + return 0; + } + + static void __exit helloworld_exit(void) + { + printk(KERN_INFO "Goodbye World...\n"); + } + + module_init(helloworld_init); + module_exit(helloworld_exit); + + 4) "drivers/mydriver" dizini içerisinde "Makefile" isimli dosya oluştururz ve içine aşağıdaki satır ekleriz: + + obj-y += mydriver.o + + 5) Çekirdek kod dizinin kök dizinine gelip ve sırasıyla aşağıdaki komutları uygularız: + + make -j$(nproc) + make modules_install + make install + + Böylece sistem yeni çekirdekle açılabilir. Aygıt sürücünün çekirdeğe dahil edildiğini geçmiş dmesg mesajlarına bakarak + aşağıdaki gibi anlayabilirssiniz: + + $ dmesg | grep -i "Hello World..." + [ 0.949515] Hello World... +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 196. Ders 07/02/2025 - Cuma +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Şimdi de yeni bir sistem fonksiyonunu çekirdeğe eklemek isteyelim. Linux çekirdeğinde sistem fonksiyonlarının adresleri + bir fonksiyon gösterici dizisinde tutulmaktadır. Bu gösterici dizisinin her elemanı bir sistem fonksiyonun adresini içerir. + O halde çekirdeğe bir sistem fonksiyonu ekleyebilmek için sistem fonksiyonunu bir dosya içerisine yazmak ve bu tabloya + o fonksiyonu gösteren bir giriş eklemek gerekir. Bunun yapılış biçimi Linux'un çeşitli versiyonlarında değiştirilmiştir. + Aşağıda güncel bir versiyonda bu işlemin nasıl yapıldığına ilişkin bir örnek vereceğiz: + + 1) Sistem fonksiyonumuz "mysyscall" biçiminde isimlendirmiş olalım. Önce yine çekirdek kaynak kod ağacında uygun bir dizine + yine bir dosya eklemek gerekir. Bunun için en uygun dizin "kernel" dizinidir. Bu durumda sistem fonksiyonumuzu "kernel" + dizini içerisinde "mysyscall.c" ismiyle yazabiliriz: + + /* mysyscall.c */ + + #include + #include + #include + + SYSCALL_DEFINE0(mysyscall) + { + printk(KERN_INFO "My system call\n"); + + return 0; + } + + Bundan sonra kernel dizini içerisindeki "Makefile" dosyasına aşağıdaki satırı ekleriz: + + obj-y += mysyscall.o + + 2) Sistem fonksiyon tablosuna ilgili sistem fonksiyonu bir eleman olarak girilir. Sistem fonksiyon tablosu + "arch//syscall/xxx.tbl" dosyasında belirtilmektedir. 64 bit Linux sistemleri için bu dosya + "arch/x86/entry/syscalls/syscall_64.tbl" biçimindedir. Ekleme bu dosyanın sonuna aşağıdaki gibi yapılabilir: + + ...... + 544 x32 io_submit compat_sys_io_submit + 545 x32 execveat compat_sys_execveat + 546 x32 preadv2 compat_sys_preadv64v2 + 547 x32 pwritev2 compat_sys_pwritev64v2 + # This is the end of the legacy x32 range. Numbers 548 and above are + # not special and are not to be used for x32-specific syscalls. + 548 common mysyscall sys_mysyscall + + 3) Artık çekirdek aaşağıdaki gibi derlenebilir: + + $ sudo make -j$(nproc) + + 4) Çekirdek modüllerini aşağıdaki gibi install edebiliriz: + + $ sudo make modules_install + + 5) Çekirdeğin kendisini de şöyle install edebiliriz: + + $ sudo make install +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Sistem fonksiyonunu çekişrdeğe yerleştirip yeni çekirdekle makinemizi açtıktan sonra fonksiyonun tesitini aşağıdaki gibi + yapabiliriz: + + #include + #include + #include + + #define SYS_mysyscall 548 + + int main(void) + { + printf("running...\n"); + + syscall(SYS_mysyscall); + + return 0; + } + + Bu programı derleyip çalıştırdıktan sonra "dmesg" yaptığımızda aşağıdaki gibi bir çıktı elde etmeliyiz: + + .... + file uses a different sequence number ID, rotating. + [ 144.816248] warning: `ThreadPoolForeg' uses wireless extensions which will stop working for Wi-Fi 7 hardware; use nl80211 + [ 487.365691] My system call +---------------------------------------------------------------------------------------------------------------------------*/ + +/*----------------------------------------------------------------------------------------------------------------------------- + Her ne kadar sistem programlamanın doğrudan veritabanlarıyla bir ilgisi yoksa da C programcılarının yine de sistem programcıların + bazı durumlarda veritabanları oluşturup onları kullanması gerekebilmektedir. Bu bölümde C'den SQL kullanarak veritabanlarıyla + nasıl işlem yapılacağı üzerinde duracağız. +-----------------------------------------------------------------------------------------------------------------------------*/ + +/*----------------------------------------------------------------------------------------------------------------------------- + Eskiden veritabanı işlemleri kütüphanelerle yapılıyordu. Daha sonra veritabanı işlemleri için özel programlar geliştirildi. + Veritabanı işlemlerini ayrıntılı ve etkin bir biçimde gerçekleştiren yazılımlara "Veritabanı Yönetim Sistemleri (DBMS)" + denilmektedir. Günümüzde çeşitli firmalar ve kurumlar tarafından geliştirilmiş pek çok VTYS vardır. Bunların bazıları + kapalı ve ücretli yazılımlardır. Bazıları ise açık kaynak kodlu ve ücretsiz yazılımlardır. En çok kullanılan VTYS yazılımları + şunlardır: + + - IBM DB2 (Dünyanın ilk VTYS'sidir.) + - Oracle (Oracle firmasının en önemli ürünü.) + - SQL Server (Microsoft firmaının VTYS'si.) + - MySQL (Açık kaynak kodlu, ancak Oracle firması satın aldı ve gelecekteki durumu tartışmalı.) + - MariaDB (Açık kaynak kodlu, MySQL Oracle yarafındna satın alınınca kapatılma tehlikesine karşı MySQL varyantı olarak + devam ettirlmektedir.) + - PostgreSQL (Açık kaynak kodlu, son yıllarda geniş kesim tarafından kullanılan VTYS.) + - SQLite (Gerçek anlamda bir VTYS değil, VTYS'yi taklit eden mini bir kütüphane gibi. Bu tür yazılımlara "gömülü VTYS" de + denilmektedir.) + - Microsoft Access Jet Motoru (Bu da Microsoft'un gömülü bir VTYS sistemidir. Micsofot Access tarafından da kullanılmaktadır.) + + Bir yazılımın VTYS olabilmesi için onun bazı gelişmiş özelliklere sahip olması gerekir: + + 1) VTYS'ler kullanıcılarına yüksek seviyeli bir çalışma modeli sunmaktadır. + + 2) VTYS'ler genellikle dış dünyadan istekleri "SQL (Structured Query Language)" denilen dekleratif bir dille almaktadır. + Yani programcı VTYS'ye iş yaptırmak için SQL denilen bir yüksek seviyeli dekleratif bir dil kullanmaktadır. VTYS SQL + komutlarını alıp onları parse eder ve C ve C++ gibi dillerde yazılmış olan motor kısmı (engine) tarafından işlemler yapılır. + SQL veritabanı işlemlerini yapan bir dil değildir. Programcı ile VTYS arasında yüksek seviyeli iletişim için kullanılan + bir dildir. VTYS'lerin motor ısımları genellikle C ve C++ gibi sistem programlama dilleriyle yazılmaktadır. + + 3) VTYS'ler pek çok yüksek seviyeli araçlara da sahiptir. Örneğin backup ve restore işlemlerini yapan araçlar her VTYS'de + bulunmaktadır. + + 4) VTYS'ler genellikle birden fazla kullanıcıya aynı anda hizmet verecek biçimde client-server mimarisine uygun biçimde + yazılmaktadır. Bunlara uzaktan erişilebilmekte ve aynı anda pek çok kullanıcı bunlara iş yaptırabilmektedir. + + 5) VTYS'ler ileri derece güvenlik sunmaktadır. Bir kullanıcı başka bir kullanıcının bilgilerine erişememektedir. + + 6) VTYS'ler yüksek miktarda kayıtlardan oluşan veritabanarı üzerinde etkin bir biçimde işlemler yapabilmektedir. +-----------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Sistem programlama uygulamalarında bazen küçük veritabanalrının oluşturulması gerekebilmektedir. Bu tür durumlarda kapasiteli + VTYS'ler yerine tek bir dosyadan oluşan adeta bir kütüphane biçiminde yazılmış olan gömülü VTYS'lerden (embedded DBMS) + faydalanılmaktadır. Bunlar en çok kullanılanı SQLite denilen gömülü VTYS'dir. Örneğin bir tarayıcı yazdığımızı düşünelim. + Son ziyaret edilen Web sayfalarının bir biçimde tarayıcdan çıkıldıktan sonra saklanması gerekir. İşte bu tür durumlarda + SQLite gibi basit yapıda VTYS'ler tercih edilebilmektedir. Internet bağlantısı olmayan mobil cihazlarda da SQLite gibi + gömülü VTYS'ler çokça kullanılmaktadır. Örneğin biz bir soket uygulaması yazmış olalım. Bu uygulama bir log tutacak olsun. + Burada SQLServer, MySQL gibi büyük çaplı VTYS'ler yerine SQLite gibi bir gömülü VTYS'yi tercih edebiliriz. Gömülü VTYS'ler + büyük çağlı veritabanlarında iyi bir performans gösterememektedir. Bunlar daha ok küçük ve orta çaplı veritabanlarında + kullanılmaktadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Veritabanı işlemleri için C'den ziyade yüksek seviyeli diller tercih edilmektedir. Örneğin Java, C#, Python gibi diller + veritabanı işlemlerinde oldukça yaygın kullanılmaktadır. Benzer biçimde JavaScript de Web uygulamalarında veritabanları + üzerinde işlem yapmak için kullanılan dillerdendir. Günümüzde veritabanlarının en çok kullanıldığı uygulamalar Web + uygulamalarıdır. Bir Web mağasına girdiğinizde oradaki bütün ürünler veritabanlarında tutulmaktadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + VTYS'lerin client-server mimarisine uygun bir biçimde yazıldığını belirtmiştik. VTYS'lerle tipik çalışma şu biçimdedir: + + 1) Programcı "kullanıcı ismi" ve "parola" ile VTYS'ye bağlanır. Bu durumda programcı client durumunda VTYS ise server + dumundadır. + + 2) Programcı VTYS'ye yaptırmak istediği şeyleri SQL dilinde oluşturur ve VTYS'ye SQL komutlarını gönderir. + + 3) VTYS bu SQL komutlarını parse eder ve istenilen işlemleri yapar, programcıya işlemin sonuçlarını iletir. + + 4) Programcı işi bittiğinde bağlantıyı kapatır. + + Her ne kadar SQLite gibi Microsoft Access Jet Motoru gibi VTYS'ler aslında client-server çalışmıyor olsa da bunlar + çalışma biçimi olarak geniş kapasiteli VTYS'leri taklit etmektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Veritabanları tasarım bakımından birkaç gruba ayrılmaktadır. Günümüzde en çok kullanılan veritabanı mimarisine "İlişkisel + Veritabanı (Relational Database) Mimarisi" denilmektedir. İlişkisel veritabanları "tablolardan (tables)" tablolar da + sütun ve satırlardna oluşmaktadır. Örneğin biz öğrencilerin bilgilerini tutmak için bir veritabanı tablosu oluştururz. + Bu tablo aşağıdaki görünümde olur: + + Adı Soyadı Numarası Sınıfı + --------------------------------------- + Ali Serçe 1234 3B + Güray Sönmez 6745 2C + Ayşe Er 6234 2B + .... + + Tablolardaki sütunlara "alan (field)" satırlara ise "satır (row)" ya da "kayıt (record)" denilmektedir. MySQL, SQLServer, + Oracle, SQLite gibi VTYS'ler ilişkissel veritabanı mimarisini kullanmaktadır. + + Hiyerarşik bilgileri (örneğin bir ağaç yapısını) tutmak için hiyerarşik veritabanı mimarileri kullanılabilmektedir. Son 15 + senedir ismine "nosql" denilen ilişkisel olmayan ve özellikle metin tabanlı bilgiler üzerinde işlem yapan veritabanı + mimarileri sıkça kullanılır hale gelmiştir. Ancak en yaygın kullanılan mimari ilişkisel veritabanı mimarisidir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + MyQL'i kurmak için tek yapılacak şey server programı http://dev.mysql.com/downloads/ sitesnden indirip yüklemektir. Kurulum + oldukça basittir. Birtakım sorular default değerlerle geçilebilir. Ancak kurulum sırasında MySQL kurulum programı bizden + “root” isimli yetkili kullanıcının parolasını istenecektir. Diğer Bu parola yetkili olarak VTYS'ye bağlanmak için gerekir. + Server programın yanı sıra bir yönetim ekranı elde etmek için ayrıca "MySql Workbench" programı da kurulabilir. + + MySQL Linux sistemlerinde Debian paket yöneticisi ile aşağıdaki gibi basit bir biçimde kurulabilir: + + $ sudo apt-get install mysql-server + + Kütüphane dosyaları da şöyle indirilebilir: + + sudo apt-get install libmysqlclient21 + + MySQL Workbench ise komut satırı yerine Web Sayfasından indirilerek kurululabilir. + + Yukarıda da belirttiğimiz gibi MySQL kısmen paralı hale getirilince bunun MariDB isimli bir klonu oluşturuldu. MariDB'nin + uzun vadede açık kaynak kod güvencesi olduğu için tercih edebilirsiniz. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + SQL paralı bir üründür. Fakat bunun da "Express Edition" isminde bedava bir sürümü vardır. Bu sürüm Microsoft'un sayfasından + indirilip kurulabilir. Tıpkı MySQL'de olduğu gibi SQL Server'da da yönetim konsol programı da vardır. Buna "SQL Server + Management Studio" denilmektedir. Bunun da indirilip kurulması tavsiye edilir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + SQLite zaten tek bir DLL'den oluşmaktadır. Dolayısıyla aslında kurulumu diye bir durum söz konusu değildir. Fakat biz + burada C için örnekler yaparken SQLite başlık dosyalarına ve SQLite DLL’inin import kütüphanesine sahip olmak zorundayız. + Bunların nasıl elde edileceği sonraki konularda ele alınacaktır. SQLite yönetim konsolu olarak pek çok alternatif vardır. + Bunlardan biri "FireFox Add On olarak çalışmaktadır. Diğer seçenekler ise “SQLite Studio” ve "SQLite Browseer" programlarıdır. + Cross Platform olan u araç ilgili web sayfasından indirilerek kurulabilir. Ya da daha genel "DBeaver" da tercih edilebilir. + + SQLite'ı Windows için aşağıdaki bağlantıdan indirebilirsiniz: + + https://www.sqlite.org/download.html + + Buradan indirilen zip dosyasının içerisinde bir tane ".DLL" dosyası bir ".DEF" dosyası bulunacaktır. Bu DLL'i PATH dizinlerinin + içerisine ya da uygulama dizinin içerisine çekebilirsiniz. Linux'ta SQLite şöyle indirilebilir: + + $ sudo apt-get install sqlite +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + İlişkisel veritabanları tablolardan tablolar da sütunlardan (fields) oluşmaktadır. Tabii sütunların da veri türleri vardır. + SQL Standratlarında standart bazı veri türleri belirtilmiştir. Ancak SQL VTYS'den VTYS'ye değişiklik gösterebilmektedir. + Dolayısıyla her VTYS'nin SQL komutlarında bazı farklılıklar bulunabilmektedir. Biz burada bazı standart sütun türleri + üzerinde duracağız. Çalıştığınız VTYS'nin dokğmanlarından onlara özgü ayrıntıları elde edebilirsiniz. + + INTEGER: Tamsayısal bilgileri tutan bir türdür. İstenirse kaç digitlik sayıların tutulacağı da belirtilebilir. + + INT: Tipik olarak 4 byte uzunluğunda işaretli tamsayı türüdür. (Örneğin bu tür C’deki int türü ile temsil edilebilir.) + + SMALLINT: Tipik olarak 2 byte'lık işaretli tamsayı türüdür. (Örneğin bu tür C’deki short türü ile temsil edilebilir.) + + BIGINT: Tipik olarak 8 byte uzunluğunda işaretli tamsayı türüdür. (Örneğin bu tür C’deki long long türü ile temsil + edilebilir.) + + FLOAT: Tipik olarak 4 byte'lık gerçek sayı türüdür. (Örneğin bu tür C’deki float türü ile temsil edilebilir.) + + DOUBLE: Tipik olarak 8 byte'lık gerçek sayı türüdür. (Örneğin bu tür C’deki double türü ile temsil edilebilir.) + + TIME: Zaman bilgisini saklamak için kullanılan türdür. + + DATE: Tarih bilgisini saklamak için kullanılan türdür. + + CHAR(n): n karakterli yazıyı tutmak için kullanılan türdür. + + VARCHAR(n): En fazla n karakterli bir yazıyı tutmak için kullanılan türdür. + + TINYTEXT: Yazısal bilgileri tutmak için kullanılan türdür. (Tipik olarak 256 byte'a kadar) + + TEXT: Yazısal bilgileri tutmak için kullanılan türdür. (Tipik olarak 64K'ya kadar) + + LONGTEXT: (Tipik olarak 4GB'ye byte'a kadar) + + TINYBLOB: Binary bilgileri tutmak için kullanılan türdür. (Tipik olarak 256 byte'a kadar) + + BLOB: Binary bilgileri tutmak için kullanılan türdür. (Tipik olarak 64K'ya kadar) + + LONGBLOB: Binary bilgileri tutmak için kullanılan türdür. (Tipik olarak 4GB'ye byte'a kadar) + + Tablo sütunlarının türleri tablo yaratılırken belirlenmektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Şimdi de temel SQL komutlarını görelim. SQL bazı ayrıntıları olan dekleratif bir programlama dilidir. Komutlardan oluşmaktadır. + Biz burada temel SQL komutlarını ayrıntılarına girmeden ele alacağız. + + SQL büyük jarf küçük harf duyarlılığı olmayan (case insensitive) bir dildir. Ancak geleneksel olarak anahtar sözcüklerin + büyük harflerle yazılması tercih edilmektedir. SQL komutlarının sonunda sonlandırıcı olarak ';' karakteri bulundurulmaktadır. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + CREATE DATABASE Komutu: İlişkisel veritabanlarında “veritabanı” tablolardan oluşmaktadır. Bu nedenle önce bir veritabanının + yaratılması, sonra da onun içerisinde tabloların yaratılması gerekir. Veritabanlarını yaratmak için CREATE DATABASE komutu + kullanılır. Komutun genel biçimi şöyledir: + + CREATE DATABASE ; + + Örneğin: + + CREATE DATABASE student; + + USE Komutu: Belli bir veritabanı üzerinde işlemler yapmak için öncelikle onun seçilmesi gerekir. Bu işlem USE komutuyla + yapılır. Komutun genel biçimi şöyledir: + + USE ; + + SHOW DATABASES Komutu: Bu komut VTYS'de yaratılmış olarak bulunan veritabanlarını gösterir. Komutun genel biçimi şöyledir: + + SHOW DATABASES; + + CREATE TABLE Komutu: Bu komut veritabanı için bir tablo yaratmak amacıyla kullanılır. Komutun basit genel biçimi şöyledir: + + CREATE TABLE ( , , ... ); + + Aslında bu komutun bazı ayrıntıları vardır. Bu ayrıntılar ilgili dokümanlardan öğrenilebilir. + + Örneğin: + + CREATE TABLE student_info(student_id PRIMARY KEY AUTO_INCREMENT, student_name VARCHAR(45), student_no INTEGER); + + Bir tabloda tekrarlanması yasaklanmış olan sütunlara “birincil anahtar (primary key)” denilmektedir. Tablodaki kayıtların + hepsinin birincil anahtar sütunları farklı olmak zorundadır. Başka bir deyişle biz birtablya orada zaten var olan birincil + anahtar değerine ilişkin bir kayıt ekleyemeyiz. Her tabloda bir tane birincil anahtarın olması tavsiye edilmektedir. + Birincil anahtarın tablo yaratılırken CREATE TABLE komutunda belirtilme biçimi çeşitli VTYS’lerde farklı olabilmektedir. + + DROP TABLE Komutu: Bu komut tabloyu silmek için kullanılır. Genel biçimi şöyledir: + + DROP TABLE ; + + Örneğin: + + DROP TABLE person; + + INSERT INTO Komutu: Bu komut bir tabloya bir satır eklemek için kullanılır. Komutun temel genel biçimi şöyledir: + + INSERT INTO (sütun1, sütun2, sütun3,...) VALUES (değer1, değer2, değer3,...); + + Tabloya satır eklerken aslında her sütun bilgisinin belirtilmesi gerekmez. Bu durumda o sütun için tablo yaratılırken + (CREATE TABLE komutunda) belirlenmiş olan default değerler kullanılır. Komutun ayrıntılı genel biçimi için ilgili + dokümanlara başvurabilirsiniz. Örneğin: + + INSERT INTO student_info(student_name, student_no) VALUES('Güray Sönmez', 754); + + Değerler girilirken yazılar ve tarihler tek tırnak içerisinde belirtilmelidir. + + WHERE Cümleciği: Pek çok komut bir WHERE kısmı içermektedir. Where cümleciği koşul belirtmek için kullanılır. Koşullar + karşılaştırma operatörleriyle oluşturulur. Mantıksal operatörlerle birleştirilebilir. Örneğin: + + WHERE age > 20 AND birth_place = 'Eskişehir' + + LIKE operatörü joker karakterleri kullanılarak belli bir kalıba uyan yazı koşulu oluşturur. Örneğin: + + WHERE student_name LIKE 'A%' + + Burada student_name için 'a' ile başlayanlar koşulu verilmiştir. % karakteri "geri kalanı herhangi biçimde olabilir" + anlamına gelir. Örneğin: + + WHERE student_name LIKE '%an' + + Burada sonu 'an' ile bitenler koşulu verilmiştir. + + WHERE cümleciğinin bazı detayları vardır. Bu detaylar ilgili dokümanlardan öğrenilebilir. + + DELETE FROM Komutu: Bu komut bir tablodan satır silmek için kullanılır. Komutun Genel biçimi şöyledir: + + DELETE FROM ; + + UPDATE Komutu: Update komutu belli kayıtların alan bilgilerini değiştirmek amacıyla kullanılır. Örneğin ismi "Kağan" + olan bir kaydı "Kaan" olarak değiştirmek isteyebiliriz. Ya da bir müşterinin bakiyesini değiştirmek isteyebiliriz. Komutun + genel biçimi çöyledir: + + UPDATE SET alan1 = değer1, alan2 = değer2, ... WHERE ; + + Örneğin: + + UPDATE student_info SET student_name = 'Kaan Kaplan' WHERE student_name = 'Kaan Aslan' + + DELETE ve UPDATE komutlarını kullanrıken dikkat ediniz. Çünkü eğer koşul belirtmezseniz ya da koşulu yanlış belirtirseniz + yaptıpınız işlemden birden fazla kayıt etkilenir. Örneğin: + + UPDATE student_info SET student_name = 'Ali Ballı' WHERE student_name = 'Veli Ballı'; + + Burada koşul zayıf oluşturulmuştur. Bu durumda bütün "Veli Ballı" isimleri "Ali Ballı" olarak değiştirilir. Örneğin: + + UPDATE student_info SET student_name = 'Ali Ballı' WHERE student_name = 'Veli Ballı' AND student_no = 754; + + Artık burada ismi "Veli Ballı" olan ve numarası 754 olan satırın ismi "Ali Ballı" olarak dğeiştirilecektir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 199. Ders 21/02/2025 - Cuma +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + SELECT Komutu: Koşulu sağlayan kayıtların elde edilmesi SELECT komutuyla yapılmaktadır. SELECT komutunun genel biçimi + oldukça ayrıntılıdır. Çünkü komuta çeşitli cümlecikler monte edilebilmektedir. Komutun temel biçimi şöyledir: + + SELECT FROM [WHERE 600; + + Burada öğrenci numarası 600'den büyük olan öğrencilerin tüm sütun bilgileri elde edilmiştir. + + Eğer SELECT edilen kayıtlar belli bir sütuna göre sıralı biçimde elde edilmek istenirse ORDER BY cümleciği komuta eklenir. + Örneğin: + + SELECT * FROM student WHERE student_id > 600 ORDER BY stdent_name; + + Burada öğrenci numarası 600'den büyük olan öğrencilerin tüm sütun bilgileri elde edilmiştir. + + Eğer SELECT edilen kayıtlar belli bir sütuna göre sıralı biçimde elde edilmek istenirse ORDER BY cümleciği komuta eklenir. + Örneğin: + + SELECT * FROM student WHERE student_id > 600 ORDER BY stdent_name; + + ORDER BY default olarak kayıtları küçükten büyüğe (ASC) vermektedir. Ancak DESC ile büyükten küçüğe de sıralama yapılabilir. + Örneğin: + + SELECT * FROM student WHERE student_no > 600 ORDER BY student_name DESC; + + ORDER BY cümleciğinde birden fazla sütun belirtilebilir. Bu durumda ilk sütun değerleri aynıysa diğer sütunlar dikkate alınır. + Örneğin: + + SELECT * FROM student WHERE student_no > 600 ORDER BY student_name DESC, student_no ASC; + + Burada ismi aynı olanlar numaralarına göre küçükten büyüğe elde edilecektir. + + LIMIT cümleceği de SELECT cümlesiyle kullanılabilir. LIMIT anahtar sözcüğnün yanında bir sayı bulunur. Koşulu sağlayan + kayıtların belli sayıda miktarını elde etmek için kullanılır. Örneğin: + + SELECT * FROM student WHERE student_no > 600 ORDER BY student_name DESC, student_no ASC LIMIT 10; + + WHERE cümleciğinde built-in fonksiyonlar kullanılabilir. Örneğin: + + SELECT * FROM city WHERE char_length(city) = 6; +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + İlişkisel veritabanlarında tablolarda veri tekrarı istenmez. Örneğin bir öğrenci veritabanı oluşturacak olalım. Bir öğrencinin + çeşitli bilgilerinin yanı sıra onun okulu hakkında da bilgileri tutmak isteyelim. Aşağıdaki gibi bir tablo tasarımı uygun + değildir: + + Adı Soyadı No Okul Adı Okulun Bulunduğu Şehir Okulun Türü + -------------------------------------------------------------------------------------- + Ali Serçe 123 Tarsus Amerikan Lisesi Mersin Devlet Lisesi + Kaan Aslan 745 Eskişehir Atatürk Lisesi Eskişehir Devlet Lisesi + Hasan Bulur 734 Tarsus Amerikan Lisesi Mersin Devlet Lisesi + ... ... ... ... + + Burada Okul bilgileri gereksiz bir biçimde tekrarlanmaktadır. Bu tekrarı engellemek için iki tablo oluşturabiliriz. + + Öğrenci Tablosu + + Adı Soyadı No Okul ID'si + ---------------------------------- + Ali Serçe 123 100 + Kaan Aslan 745 235 + Hasan Bulur 734 100 + ... ... ... + + Okul Tablosu + + Okul Id'si Okul Adı Okulun Bulunduğu Şehir Okulun Türü + ------------------------------------------------------------------------------------ + ... ... ... ... + 100 Tarsus Amerikan Lisesi Mersin Devlet Lisesi + 150 Eskişehir Atatürk Lisesi Eskişehir Devlet Lisesi + ... ... ... ... + + Burada veri tekrarı oratadan kaldırılmıştır. Tabii bu tablolarda da Okul ID'si ortak bir sütundur. Bu ortak sütun tablolar + arasında ilişki kurmak için gerekmektedir. Bu tür sütunlara "foreign key" de denilmektedir. Ancak yukarıdaki gibi tekrarlar + engellendiğinde gerekli bilgiler artık tek bir tablodan değil çeşitli tabolardan çekilip alınacaktır. İşe çeşitli tabolardan + bilgilerin çekilip alınması işlemine "JOIN" işlemi denilmektedir. JOIN işleminin birkaç biçimi vardır (INNER JOIN, OUTER JOIN, + LEFT JOIN, RIGHT JOIN gibi). Ancak en fazla kullanılan JOIN işlemi "INNER JOIN" denilen işlemdir. JOIN denildiğinde zaten + default olarak INNER JOIN anlaşılır. INNER JOIN işleminde eğer iki tablo söz konusu ise önce iki tablonun kartezyen çarpımları + elde edilir. Her kaztezyen çarpım iki tablonun birleştirilmesi biçiminde ("join" ismi oradan geliyor) elde edilmektedir. + Sonra kartezyen çarpımlarda yalnızca belli koşulu sağlayan satırlar elde edilir. Böylece tablolar "ilişkisel (relational)" + biçimde birleştirilmiş olur. + + INNER JOIN sentaksı iki biçimde oluşturulabilmektedir. Birinci sentaks klasik eski tip sentakstır. İkinci sentaks daha modern + biçimdir. Klasik eski tip sentaks şöyledir: + + SELECT FROM INNER JOIN ON ; + + Örneğin: + + SELECT student.student_name, student.student_no, school.school_name FROM student INNER JOIN school ON + student.school_id = school.school_id WHERE stduent.student_no > 600; + + Sütun isimleri belirtilirken eğer çakışma yoksa yalnızca isimler yazılabilir. Ancak çakışma varsa tablo ismi ve nokta + operatörü ile sütunun hangi tabloya ilişkin olduğu belirtilmelidir. Bazı uygulamacılar çakışma olsa da olmasa da + niteliklendirme yaparlar. Bazı uygulamacılar yalnızca çakışan sütunlarda niteliklendirme yaparlar. Yukarıdaki örnekte + tüm sütunlar niteliklendirilerek belirtilmiştir. Bu örnek şöyle de yapılabilirdi: + + SELECT student_name, student_no, school_name FROM student INNER JOIN school ON student.school_id = school.school_id + WHERE student_no > 600; + + Modern INNER JOIN sentaksında SELECT komutunun FROM kısmında birden fazla tablo ismi belirtilir. Koşul da yine WHERE + cümleciğine taşınır. Örneğin: + + SELECT student_name, student_no, school_name FROM student, school WHERE student.school_id = school.school_id AND + student_no > 600; + + Daha çok bu modern biçim tercih edilmektedir. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + İşlemlere başlamadan önce SQLite’ın programalama kurulumunu yapmamız gerekir. SQLite yukarıda da belirtitğimiz gibi + çok küçük (tek bir dinamik kütüphaneden oluşan) bir VTYS’dir. Dolayısıyla onun kurulması Windows’ta bildiğimiz anlamda + bir setup işlemi ile yapılmaz. Tabii bizim C’den SQLite kütüphanesini kullanabilmemiz için ona ilişkin başlık ve kütüphane + dosyalarını elde etmemiz gerekir. Windows'ta SQLite'ın resmi indirme sitesi şöyledir: + + https://sqlite.org/download.html + + Buradan aşağıdaki iki indirme yapılır: + + 1) Precompiled Binaries for Windows (32 bit ya da 64 bit) + 2) SQLite Amalgamation + + Birinci indirmede tek bir DLL elde edilecektir. İkinci indirmede de "sqlite3.h" başlık dosyası ve kaynak dosyası elde + edilecektir. + + Ayrıca SQlite için "sqlite3" isminde komut satırından kullanılan bir program da bulundurulmuştur. Bu programın kullanımına + ilişkin bilgileri aşağıdaki bağlantıdan edinebilirsiniz: + + https://www.sqlite.org/cli.html + + Birinci indirmede Windows için gereken sqlite3.dll ve sqlite3.def dosyaları elde edilir. Buradaki “.def” dosyasına + “module definition file” denilmektedir. Bu dosya “DLL’in import kütüphanesi” gibi link aşamasına dahil edilebilir. + Ya da istenirse aşağıdaki komutla bu “.def” dosyasından “.lib” uzantılı “import kütüphanesi de oluşturulabilmektedir: + + LIB /DEF:sqlite3.def /machine:x86 + + Buradaki machine argümanı hedef sistemi belirtmektedir. Burada 32 bit Windows sistemleri için x86, 64 bit Windows + sistemleri için "x64" kullanılmalıdır. Örneğin: + + LIB /DEF:sqlite3.def /machine:x64 + + İkinci indirmeden biz SQLite’ın kaynak dosyalarını elde ederiz. Buradaki “sqlite3.h” dosyası SQLite fonksiyonları için + başlık dosyası niteliğindedir. + + Mac OS X için kurulum Windows’takine benzemektedir. Yine ilgili “.zip” dosyaları indirilip kurulum yapılabilir. Bu + sistemlerde derleme yaparken link aşamasında "-lsqlite3" lie kütüphane dosyasını belirtmeyi unutmayınız. +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + 200. Ders 23/02/2025 - Pazar +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + Kurulum sonrası her şeyin hazır oladuğunu anlamak için SQLite kütüphanesinin versiyon numarasını yazdıran aşağıdaki + gibi bir programla test işlemi yapabilirsiniz: + + #include + #include "sqlite3.h" + + int main(void) + { + printf("%s\n", sqlite3_libversion()); + + return 0; + } +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + C’de SQLite veritabanı ile işlem yapmak için önce o veritabanının sqlite3_open fonksiyonuyla açılması gerekir. Bu işlemden + sqlite3 türünden bir handle elde edilir. sqlite3_open fonksiyonun prototipi şöyledir: + + int sqlite3_open( + const char *filename, /* Database filename (UTF-8) */ + sqlite3 **ppDb /* OUT: SQLite db handle */ + ); + + Fonksiyonun birinci parametresi bizden sqlite dosyasının yol ifadesini alır. İkinci parametresi sqlite3 isimli yapı türünden + bir göstricinin adresini almaktadır. Fonksiyon handle alanını (yani sqlite yapı nesnesini) oluşturur. Onun adresini bu + göstericinin içerisine yerleştirir. Fonksiyonun geri dönüş değeri işlemin başarısını belirtmektedir. Fonksiyon başarılıysa + SQLITE_OK değerine geri döner. Fonksiyon başarısız olduğunda yine dosyanın sqlite3_close fonksiyonuyla kapatılması gerekir. + Hata nedeni de sqlite3_errmsg fonksiyonuyla yazdırılabilir. Bu durumda sqlite dosyasının açılması tipik olarak şöyle + yapılabilir: + + if (sqlite3_open("student.db", &db) != SQLITE_OK) { + fprintf(stderr, "sqlite3_open failed: %s\n", sqlite3_errmsg(db)); + sqlite3_close(db); + exit(EXIT_FAILURE); + } + + sqlite3_open fonksiyonu dosya varsa olanı açar, yoksa yeni bir SQLite DB dosyası yaratır. + + sqlite3_errmsg fonksiyonunun parametrik yapısı şöyledir: + + const char *sqlite3_errmsg(sqlite3 *db); + + sqlite3_close fonksiyonun prototipi ise şöyledir: + + int sqlite3_close(sqlite3*); + + Dosya kapatılırken başarı kontrolü yapmaya gerek yoktur. Başarısızlık durumlarında hata mesajını stderr dosyasına yazdırıp + programı sonlandıran bir sarma fonksiyon şöyle yazılabilir: + + void sqlite3_exit(const char *msg, sqlite3 *db) + { + fprintf(stderr, "%s failed => %s\n", msg, sqlite3_errmsg(db)); + sqlite3_close(db); + + exit(EXIT_FAILURE); + } + + Böylece biz hata durumlarını aşağıdaki gibi ele alabiliriz: + + if (sqlite3_open("studentxxx.db", &db) != SQLITE_OK) + sqlite3_exit("sqlite3_open", db); +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include +#include "sqlite3.h" + +void sqlite3_exit(const char *msg, sqlite3 *db); + +int main(void) +{ + sqlite3 *db; + + if (sqlite3_open("student.db", &db) != SQLITE_OK) + sqlite3_exit("sqlite3_open", db); + + printf("succes...\n"); + + sqlite3_close(db); + + return 0; +} + +void sqlite3_exit(const char *msg, sqlite3 *db) +{ + fprintf(stderr, "%s failed => %s\n", msg, sqlite3_errmsg(db)); + sqlite3_close(db); + + exit(EXIT_FAILURE); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + SQLite'a bir SQL cümlesi göndermek için sqlite3_exec fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: + + int sqlite3_exec( + sqlite3 *db, /* An open database */ + const char *sql, /* SQL to be evaluated */ + int (*callback)(void*,int,char**,char**), /* Callback function */ + void *param, /* 1st argument to callback */ + char **errmsg /* Error msg written here */ + ); + + Fonskiyonun birinci parametresi sqlite3_open fonksiyonundan elde edilen handle değeridir. İkinci parametre sql cümlesinin + yazısını alır. Üçüncü parametre işlemden sonra çağrılacak “callback” fonksiyonun adresini almaktadır. Bu parametre NULL + geçilebilir. Dördüncü parametre bu “callback” fonksiyona geçirilecek argümanı belirtir. Bu parametre de NULL geçilebilir. + Son parametre ise char * türünden bir göstericinin adresini almaktadır. Hata drumunda hata mesajının adresi bu göstericiye + yerleştirilir. Bu parametre NULL olarak da geçilebilir. Fonksiyonun geri dönüş değeri işlemin başarısını belirtir. Fonksiyon + başarılıysa SQLITE_OK değerine geri dönmektedir. Bu durumda biz hata mesajını yazdırdıktan sonra sqlite3_free fonksiyonu + ile tahsis edilen alanı serbest bırakabiliriz. Örneğin: + + if (sqlite3_exec(db, "INSERT INTO student_info(student_name, student_no) VALUES('Rasim Öztekin', 367)", + NULL, NULL, NULL) != SQLITE_OK) + sqlite3_exit("sqlite3_exec", db); + + Aşağıdaki örnekte student_info veri tabanına sqlite3_exec fonksiyonu ile bir kayıt eklenmiştir. +---------------------------------------------------------------------------------------------------------------------------*/ + +#include +#include +#include "sqlite3.h" + +void sqlite3_exit(const char *msg, sqlite3 *db); + +int main(void) +{ + sqlite3 *db; + + if (sqlite3_open("student.db", &db) != SQLITE_OK) + sqlite3_exit("sqlite3_open", db); + + if (sqlite3_exec(db, "INSERT INTO student_info(student_name, student_no) VALUES('Rasim Öztekin', 367)", NULL, NULL, NULL) != SQLITE_OK) + sqlite3_exit("sqlite3_exec", db); + + printf("Success...\n"); + + sqlite3_close(db); + + return 0; +} + +void sqlite3_exit(const char *msg, sqlite3 *db) +{ + fprintf(stderr, "%s failed => %s\n", msg, sqlite3_errmsg(db)); + sqlite3_close(db); + + exit(EXIT_FAILURE); +} + +/*-------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------------------------------------*/ + +/*-------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------------------------------------*/ +