/*--------------------------------------------------------------------------------------------------------------------------

                                                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 <stdio.h>

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 <unistd.h>

    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 <unistd.h> 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdbool.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <getopt.h>

    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 <arg> ya da --count <arg>
	--verbose
---------------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>

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 <arg>
    -c
    -h ya da --help
    --count <arg>
    --line[=<arg>]
---------------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>

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 <stdio.h>
#include <stdlib.h>
#include <getopt.h>

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 <stdio.h>
#include <stdlib.h>
#include <getopt.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <locale.h>

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 <stdio.h>

	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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>

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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <errno.h>
#include <fcntl.h>

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 <unistd.h>

	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) <limits.h> 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 <limits.h> dosyası içerisinde PATH_MAX 4096 olarak define edilmiştir. 
---------------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <unistd.h>

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 <unistd.h>

	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 <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <unistd.h>

#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 <fcntl.h>

	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 <sys/stat.h> 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ü 
    <sys/types.h> 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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

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 <unistd.h>

	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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

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 <fcntl.h>

	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 <unistd.h>

    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 <unistd.h> ve <sys/types.h> 
    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

void exit_sys(const char *msg);

int main(void)
{
	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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#define BUFFER_SIZE		4096

void exit_sys(const char *msg);

int main(int argc, char *argv[])
{
	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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#define BUFFER_SIZE		4096

void exit_sys(const char *msg);

int main(int argc, char *argv[])
{
	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 <unistd.h>

    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 <unistd.h>

    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 
    <unistd.h> ve <sys/types.h> 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 <unistd.h> ve <stdio.h> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

void exit_sys(const char *msg);

int main(void)
{
	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 <sys/stat.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <limits.h>
#include <unistd.h>

#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 <sys/stat.h>

    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ı <sys/stat.h> 
    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 <sys/stat.h> 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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>

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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>

#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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#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 <sys/limits.h> 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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>

#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 <stdio.h>

    int remove(const char *path);

    #include <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <sys/stat.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

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 <sys/stat.h>

    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 <unistd.h>

    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 <unisth.h> 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 <stdio.h>
	#include <unistd.h>

	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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <unistd.h>

    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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <sys/stat.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

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 <unistd.h>

    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 <pwd.h> 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 <pwd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pwd.h>

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 <pwd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pwd.h>

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 <pwd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pwd.h>

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 <grp.h>

    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ı <grp.h> 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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <grp.h>

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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <fcntl.h>

    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <dirent.h>

    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 <dirent.h>

    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 <dirent.h>

    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 <dirent.h>

    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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <dirent.h>

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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
#include <sys/stat.h>
#include <dirent.h>
#include <pwd.h>
#include <grp.h>

#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 <dirent.h>

    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 <sys/stat.h>

    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 <sys/stat.h> içerisinde değil
    <fcntl.h> 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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <dirent.h>
#include <pwd.h>
#include <grp.h>

#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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <dirent.h>

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 <dirent.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>

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 <dirent.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <dirent.h>

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 <ftw.h>

    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 <stdio.h>
#include <stdlib.h>
#include <ftw.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <fcntl.h>

    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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <fcntl.h>

    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 <unistd.h>

    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 <limits.h> 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <unistd.h>

    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 <fcntl.h>

    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <unistd.h> 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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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.

    <unistd.h> 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdio.h> 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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

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 <stdio.h>

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 <stdio.h>

	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 <stdio.h>
#include <stdlib.h>

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 <stdio.h>
#include <stdlib.h>

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 <stdio.h>

int main(void)
{
	for (int i = 0; i < 10; ++i)
		printf("%d\n", i);

	return 0;
}

/* b.c */

#include <stdio.h>

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 <stdio.h> 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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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üğü <stdio.h> 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 <stdio.h>

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 <stdio.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdio.h>

    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

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 <stdio.h> 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 <stdio.h>

    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 <stdio.h>
#include <stdlib.h>

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 <stdio.h>

    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 <stdio.h>
#include <stdlib.h>

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 <stdio.h> 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 <stdio.h>

    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 <stdio.h>

    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 <stdio.h>

    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 <stdio.h>

    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 <stdio.h>

    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 <stdio.h>

    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 <stdio.h>

    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 <stdio.h>

    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 <stdio.h>

    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 <stdio.h>

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 <stdio.h>

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 <stdio.h>

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 <stdio.h>

    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 <stdio.h>

    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 <sys/types.h> ve
    <unistd.h> 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 <unistd.h>

    pid_t getpid(void);

    Fonksiyon başarısız olamaz. 
---------------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdint.h>
#include <unistd.h>

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 <unistd.h>

    pid_t getppid(void);

    Fonksiyon başarısız olamamaktadır. 
---------------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdint.h>
#include <unistd.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdio.h>
#include <unistd.h>

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 <stdio.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <unistd.h>

    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 <stdlib.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <sys/wait.h>

    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 <sys/wait.h> 
    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

#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 <sys/wait.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

#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 <defunct>
    4 R  1000   10621    1618  0  80   0 -  3540 -      pts/0    00:00:00 ps
---------------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdlib.h>

    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 <stdio.h>
#include <stdlib.h>

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 <stdlib.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>

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 <stdlib.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>

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 <stdio.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>

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 <stdlib.h>

    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 <stdio.h>
#include <stdlib.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdio.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <unistd.h>

    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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <unistd.h>
#include <sys/wait.h>

#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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdarg.h>

#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 <stdio.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#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] <executable file mutlak yol ifadesi> [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 <stdio.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>

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 <stdlib.h>

    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 <stdio.h>
#include <stdlib.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>

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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

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 <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>

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 <unistd.h>

    uid_t getuid(void)
    uid_t geteuid(void)

    uid_t türü <unistd.h> ve <sys/types.h> 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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>

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 <unistd.h>

    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 <unistd.h>

    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 <unistd.h>

    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 <unistd.h>

    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 <unistd.h>

    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 <unistd.h>

    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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <pwd.h>

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 <unistd.h>

    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)
        <proses her şeyi yapabilir >
    else if (prosesin etkin kullanıcı id'si == dosyanın kullanıcı id'si)
        <dosyanın rwx owner hakları dikkate alınır>
    else if (prosesin etkin grup id'si ya da ek gruplarından birinin id'si == dosyanın grup id'si)
        <dosyanın rwx grup hakları dikkate alınır>
    else
        <dosyanın rwx other hakları dikkate alınır>

    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 <unistd.h>

    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 <limits.h> 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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <grp.h>

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 <grp.h>

    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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <grp.h>

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 <grp.h>

    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 <stdio.h>
#include <stdlib.h>
#include <time.h>

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 <stdio.h>
#include <stdlib.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#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 <stdio.h>

    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 <stdio.h>

    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 <stdio.h>
#include <stdlib.h>

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 <stdio.h>
#include <stdlib.h>

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 <sys/stat.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <dirent.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <dirent.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>

#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 <parametrik bilgiler>"

    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 <boru ismi>
    DISCONNECT_REQUEST
    DISCONNECT
    CMD <kabuk komutu>

    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 <komutun çıktı yazısı>
    DISCONNECT_ACCEPTED
    INVALID_COMMAND <hata yazısı>

    - 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>

/* 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 <fcntl.h>

    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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

#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 <sys/msg.h>

    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 <sys/stat.h> 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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/msg.h>

#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 <sys/msg.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/msg.h>

#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 <sys/msg.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/msg.h>

#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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/msg.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/msg.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/msg.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/msg.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/msg.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/msg.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/msg.h>

#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 <sys/ipc.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/msg.h>
#include <sys/ipc.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/msg.h>
#include <sys/ipc.h>

#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 <sys/msg.h>

    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ı <sys/msg.h> 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 <sys/msg.h> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/stat.h>
#include <sys/msg.h>
#include <sys/ipc.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/stat.h>
#include <sys/msg.h>
#include <sys/ipc.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/stat.h>
#include <sys/msg.h>
#include <sys/ipc.h>

#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 <mqueue.h>

    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) <mqueue.h> 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 <mqueue.h>

    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 <limits.h> 
    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 <mqueue.h>

    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 <mqueue.h>

    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 <mqueue.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <mqueue.h>

#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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <mqueue.h>

#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 <mqueue.h>

    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 <mqueue.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <mqueue.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <mqueue.h>

#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 <time.h>
    #include <mqueue.h>

    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ı <time.h> 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 <time.h>

    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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/stat.h>
#include <time.h>
#include <mqueue.h>

#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 <sys/shm.h>

    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 <sys/shm.h>

    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 <sys/shm.h> 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 <sys/shm.h>

    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 <sys/shm.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/shm.h>

#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 <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/shm.h>

#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 <sys/mman.h>

    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 <sys/mman.h>

    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 <sys/mman.h>

    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 <sys/mman.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/mman.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <sys/mman.h>

    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

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 <stdint.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

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 <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

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);
        <saati bas>
    }

    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 (;;) {
        <saati bas>
    }

    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 <pthread.h> dosyası içerisindedir. Dolayısıyla thread işlemleri yapacaksak 
    bizim bu dosyayı include etmemiz gerekmektedir:

    #include <pthread.h>

    - 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 <pthread.h>

    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ü <thread.h> ve <sys/types.h> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

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 <pthread.h>

    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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <thread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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.h>

    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ü <pthread.h> ve <sys/types.h> 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 <pthread.h>

    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 <pthread.h>

    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 <pthread.h>

    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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <pthread.h> ve <sys/types> 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 <pthread.h>

    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 <pthread.h>

    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);

    ...
    ...		<KRİTİK KOD>
    ...

    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 <pthread.h>

    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);

    ...
    ...		<KRİTİK KOD>
    ...

    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);

    ...
    ...		<KRİTİK KOD>
    ...

    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);
        <eleman ekleniyor>
        pthread_mutex_unlock(&g_llmutex);
    }

    remove_item(...)
    {
        pthread_mutex_lock(&g_llmutex);
        <eleman siliniyor>
        pthread_mutex_unlock(&g_llmutex);
    }

    search_item(...)
    {
        pthread_mutex_lock(&g_llmutex);
        <eleman aranıyor>
        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 <pthread.h>

    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) {
            <başka bir şeyler yap>
        }
        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 <pthread.h>

    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) {
            <fonksiyon zaman aşımından dolayı sonlandı>
        }
        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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>

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 <pthread.h>

    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 <pthread.h>

    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 <pthread.h>

    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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

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)
        <deyim>

    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)
        <deyim>

    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 <pthread.h>

    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 <pthread.h>

    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 <sys/types.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <pthread.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <pthread.h>
#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 <pthread.h> ve <sys/types.h> 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 <pthread.h>

    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 (<koşul sağlanmadığı sürece>)
        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 <pthread.h>

    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 <pthread.h>

    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 (<koşul_sağlanmadığı_sürece>)
        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 <başarı durumu>
    }

    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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

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 (<koşul sağlanmadığı sürece>)
        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ış.

        <Thread'lerden biri mutex'in sahipliğini alacak, yanlışlıkla uyanan diğerleri bu noktada bekleyecek>

        return <başarı durumu>
    }

    İş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 */

    <Artık bu noktada koşul değişkeninden yanlışlıkla uyanmış olan thread'ler koşul sağlanmadığı için yeniden uykuya dalacaklar>

    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);
    <koşulun sağlanıyor>
    pthread_cond_signal(&g_cond);
    pthread_mutex_unlock(&g_mutex);

    Diğer Bir Thread
    ----------------
    pthread_mutex_lock(&g_mutex);
    <koşulun saplanıyor>
    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

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 (;;) {
        <değeri elde et>

        pthread_mutex_lock(&g_mutex);

        while (g_flag == 1)
            pthread_cond_wait(&g_producer, &g_mutex);

        <değeri paylaşılan alana yerleştir>

        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);

        <paylaşılan bellek alanından bilgiyi alacak>

        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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

#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 <pthread.h>

    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 <pthread.h>

    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 <pthread.h>

    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 <pthread.h>

    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 <sys/types.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <pthread.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <pthread.h>
#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 kodun başı>
    ...
    ...
    ...		<KRİTİK KODDA MAKİNE ATAMASI YAPILIYOR>
    ...
    ...
    <kritik kodun sonu>

    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 <semaphore.h>

    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);
    ...
    ...		<KRİTİK KOD>
    ...
    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 <semaphore.h>

    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 <semaphore.h>

    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 <semaphore.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>

#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 (;;) {
        <değer elde ediliyor>
        sem_wait(&g_sem_producer);
        <değer paylaşılan alana yerleştiriliyor>
        sem_post(&g_sem_consumer);
    }

    TÜKETİCİ THREAD

    for (;;) {
        sem_wait(&g_sem_consumer);
        <paylaşılan bellek alanından bilgi alınyor>
        sem_post(&g_sem_producer);
        <alınan bilgi işleniyor>
    }

---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
												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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.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);

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 <stddef.h>
#include <semaphore.h>

/* 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 <stdio.h>
#include <stdlib.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#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 <semaphore.h>
#include <sys/types.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#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 <semaphore.h>

    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);
    ...
    ...		<KRİTİK KOD>
    ...
    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 <semaphore.h>

    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 <semaphore.h>

    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 <stddef.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>
#include <semaphore.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>
#include <semaphore.h>
#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 <sys/sem.h>

    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 <sys/sem.h>

    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 <sys/sem.h>

    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ı <sys/sem.h> 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);

    ...
    ...		<KRİTİK KOD>
    ...

    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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#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ü 
    <pthread.h> ve <sys/types.h> 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 <pthread.h>

    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 <pthread.h>

    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 <pthread.h>

    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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>

#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 <pthread.h> ve <sys/types.h> 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 <pthread.h>

    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 <pthread.h>

    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);
    ...
    ...		<KRİTİK KOD>
    ...
    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 <pthread.h>

    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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#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);
    ...
    ...		<READ İŞLEMİ>
    ...
    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 <thread.h> ve <sys/types.h> 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 <pthread.h>

    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);
    ...
    ...		<READ İŞLEMİ>
    ...
    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);
    ...
    ...		<WRITE İŞLEMİ>
    ...
    pthread_rwlock_unlock(&g_rwlock);

    Buradaki fonksiyonların prototipleri şöyledir:

    #include <pthread.h>

    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 <pthread.h>

    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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>

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 <atomic> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#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 <stdatomic.h> 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 <stdatomic.h> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#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>
    ...

    atomic<int> g_count;

    Yukarıdaki örneğin C++'ın atomic sınıfı ile gerçekleştirimi aşağıda verilmiştir.
---------------------------------------------------------------------------------------------------------------------------*/

/* atomic.cpp */

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <atomic>
#include <pthread.h>

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<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", (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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>

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);
        ...
        ...	<DİKKAT! Diğer thread burada fork yapmış olabilir>
        ...
        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 <sys/resource.h>

    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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/resource.h>

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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/resource.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/resource.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>

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 -<nice değeri> <program>

    Ö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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>

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 -<nice değeri> -p <proses id>

    Ö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 <unistd.h>

    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 (<unistd.h> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/resource.h>

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 <sched.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sched.h>

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 <sched.h>

    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 <sched.h>

    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 <sched.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sched.h>

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 <sched.h>

    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 <pthread.h>

    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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>

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 <sched.h>

    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 <sched.h> 
    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sched.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

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 <sys/sysinfo.h>

    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 <stdio.h>
#include <sys/sysinfo.h>

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 <sched.h>

    int sched_getcpu(void);

    Aşağıdaki örnekte bu fonksiyonun kullanımına örnek verilmiştir.
---------------------------------------------------------------------------------------------------------------------------*/

#define _GNU_SOURCE

#include <stdio.h>
#include <sched.h>

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;

        <result'ın içi dolduruluyor>

        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 <stdlib.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.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)
{
	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 <time.h>

    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 <sys/types.h>
    #include <pwd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pwd.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <pthread.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>

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:

    ...
    ...		<start-up code>
    ...
    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#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 <string.h>

    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 <string.h>

    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 <threads.h> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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 <threads.h> dosyası içerisindedir. Dolayısıyla bu dosyanın include edilmesi gerekmektedir. Bu dosyanın kendi içerisinde
    <time.h> 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 <threads.h>

    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 <threads.h>

    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 <threads.h>

    int thrd_detach(thrd_t thr);

    - Thread'i belli bir süre blokede bekletmek için thrd_sleep fonksiyonu kullanılmaktadır:

    #include <threads.h>

    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 <stdio.h>
#include <stdlib.h>
#include <threads.h>

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 <threads.h>

    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 <stdio.h>
#include <stdlib.h>
#include <threads.h>

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 <iostream>
#include <thread>
#include <chrono>

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 <iostream>
#include <thread>
#include <chrono>
#include <mutex>

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ı <signal.h> 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 <signal.h>

    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 <signal.h> 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 <stdio.h>
#include <stdlib.h>
#include <signal.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <signal.h>

    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ı <signal.h> 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 <signal.h>

    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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <signal.h>

    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 <signal.h> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <string.h>

    char *strsignal(int signum);

    Ayrıca glibc kütüphanesinde standart olmayan iki fonksiyon da vardır:

    #include <string.h>

    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 <string.h> 
    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

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 <signal.h>

    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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <signal.h>

    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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <signal.h>

    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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <signal.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>

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 <signal.h>

    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(<sinyalleri bloke et>);
    <birtakım işlemler>
    siprocmask(<sinyalleri aç>);
    ---> Problem var!
    pause();
    <bu kısma sinyal geldiğinde geçilmek istenmektedir>

    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 <signal.h>

    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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <stdio.h>
    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 <stdio.h> 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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdlib.h>

    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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <signal.h>

void exit_sys(const char *msg);

/* ./prog1 <realtime signal no> <process id> <count> */

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void exit_sys(const char *msg);
void sigrt_handler(int signo, siginfo_t *info, void *context);

/* ./prog2 <realtime signal no> */

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 <signal.h> 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 <limits.h> 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 <limits.h> 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 <limits.h> 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 <limits.h> 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <signal.h>

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 <signal.h>

    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 <değer> 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 <process id> <sig> <value> 

    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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

/* ./sq <process id> <sig> <value> */

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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <signal.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <signal.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

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 <signal.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

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 <setjmp.h>

    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 <stdio.h>
#include <setjmp.h>

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 <setjmp.h>

    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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>

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 <unistd.h>

    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 <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>

    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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/wait.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/wait.h>

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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

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 <unistd.h>

    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 <stdio.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>

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 = <undef>; eol2 = <undef>;
    swtch = <undef>; 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>

    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 <unistd.h>

    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 <unistd.h>

    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 <time.h>

    int nanosleep(const struct timespec *rqtp, struct timespec *rmtp);

    Buradaki timespec yapısını daha önce de kullanmıştık. Bu yapı <time.h> 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 <stdio.h>
#include <stdlib.h>
#include <time.h>

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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <errno.h>
#include <signal.h>

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 <time.h>

    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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <errno.h>
#include <signal.h>

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 <time.h> 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 <time.h>

    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 <time.h>

    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 <stdio.h>
#include <stdlib.h>
#include <time.h>

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 <time.h>

    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 <stdio.h>
#include <stdlib.h>
#include <time.h>

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 <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

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 <time.h>

    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 <sys/times.h>

    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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>
#include <sys/times.h>

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 <sys/time.h>

    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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/time.h>

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 <time.h>

    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 <signal.h>

    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 <time.h>

    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 <time.h>

    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 <time.h>

    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 <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>

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 <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>

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 <time.h>

    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 <limits.h> dosyası içerisinde sembolik sabitler biçiminde define edilmiştir. 
    (C standartlarında da bir <limits.h> dosyası vardır, ama POSIX limits.h dosyası çok daha geniş kapsamlıdır.) <limits.h> 
    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 <limits.h> 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.

    <limits.h> 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 <limits.h> 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 
    <limits.h> 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 <limits.h> içerisinde define 
    edilmemesi gerektiğini belirtmektedir. Yani aslında OPEN_MAX gibi, ARG_MAX gibi, CHILD_MAX gibi sembolik sabitler <limits.h> 
    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 <limits.h> 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
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
    <limits.h> 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 <limits.h> 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 <limits.h> içerisinde define edilmemişse bu 
    durumda onların değerleri pathconf ya da fpathconf POSIX fonksiyonuyla alınmalıdır.

    <limits.h> 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.

    <limits.h> içerisindeki "Numeric Limits" grubundaki sembolik sabitler aslında C'nin <limits.h> 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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>

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 <limits.h> 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 <unistd.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>

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 <sys/resource.h>

    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 <sys/resource.h> 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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/resource.h>

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 <sys/resource.h>

    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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/resource.h>

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 <sys/resource.h>

    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 <sys/file.h>

    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 <fcntl.h>

    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:

    <fcntl command code> <lock type> <starting offset> <length>

    Ö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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

#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, &region1);		/* F_WRLCK */
    fcntl(fd, F_SETLKW, &region2);		/* F_WRLCK */

    write(fd, region1, size1);
    write(fd, region2, size2);

    fcntl(fd, F_SETLKW, &region1);		/* F_UNLCK */
    fcntl(fd, F_SETLKW, &region2);		/* F_UNLCK */

    Şimdi belli bir kaydın bir elemanını değiştirmek isteyelim:

    fcntl(fd, F_SETLKW, &region);		/* F_WRLCK */

    read(fd, buf, elem_size);
    <update işlemi>
    write(fd, buf, elem_size);

    fcntl(fd, F_SETLKW, &region);		/* 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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>

#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 <device> <mount point>

    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 <dosya>

    Ö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 <dosya ismi> <r|w> <offset> <uzunluk>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

void exit_sys(const char *msg);

/* ./rwtest <dosya ismi> <r/w> <offset> <uzunluk> */

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 <unistd.h>

    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 <sys/select.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>

#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 <stdio.h>
#include <stdlib.h>
#include <sys/select.h>

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 <sys/select.h>

    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 <poll.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>

#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 <poll.h>

    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 <sys/epoll.h>

    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 <sys/epoll.h>

    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 <sys/epoll.h>

    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 <sys/epoll.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/epoll.h>

#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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>

#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 <aio.h> dosyası içerisinde bildirilmiştir.

    Asenkron IO işlemleri tipik olarak şu aşamalardan geçilerek yürütülmektedir:

    1) Önce <aio.h> 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 <aio.h>

    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 <signal.h>

    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 <signal.h>

    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 <aio.h>

    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 <aio.h>

    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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <aio.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <stdatomic.h>
#include <aio.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <stdatomic.h>
#include <aio.h>

#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 <aio.h>

    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 <aio.h>

    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 <sys/uio.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/uio.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/uio.h>

#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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>

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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>
#include <sys/file.h>

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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>
#include <sys/file.h>

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 <syslog.h>

    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 <syslog.h>

    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 <syslog.h>

    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 <stdio.h>
#include <syslog.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>
#include <sys/file.h>
#include <syslog.h>

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 <daemon_ismi>

    Buradaki <daemon_ismi> 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 <daemon_ismi>

    Ö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_ismi>

    - Daemon'ı durdurmak için "systemctl stop" komutu kullanılmalıdır. Komutun genel biçimi şöyledir:

    $ sudo systemctl stop <daemon_ismi>

    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 <daemon_ismi>

    Ö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ü <sys/socket.h> 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 <sys/socket.h>

    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 <netinet/in.h> 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 <sys/socket.h>

    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ı <netinet/in.h> dosyası içerisinde aşağıdaki gibi bildirilmiştir:

    #include <netinet/in.h>

    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ı <netint/in.h> dosyası içerisinde şöyle bildirilmiştir:

    #include <netinet/in.h>

    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 <arpa/inet.h>

    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 <sys/socket.h>

    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 <sys/socket.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <netinet/in.h>

    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 <arpa/inet.h>

    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 <arpa/inet.h>

    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 <netdb.h>

    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 <netdb.h> 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 <netdb.h>

    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 <sys/socket.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <sys/socket.h>

    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 <sys/socket.h>

    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 <sys/socket.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <winsock2.h> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <winsock2.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#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 <server_ismi> -p <server_port> -b <client_port>

    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 <port_numarası>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <arpa/inet.h>

    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 <arpa/inet.h>

    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 <netdb.h>

    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 <netdb.h>

    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 <netdb.h>

    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 <netdb.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <sys/socket.h>

    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 <sys/socket.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <aio.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <sys/socket.h>

    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 <sys/socket.h>

    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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <getopt.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>

#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 (<belli bir sokete bilgi geldi>) {
            <thread oluştur ve işlemi thread'e devret>
        }
    }

    Benzer biçimde UDP server uygulamasında da işlem şöyle yapılabilir:

    for (;;) {
        recvfrom(...)
        <thread oluştur ve işlemi thread'e devret>
    }

    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(...)
        <mesajı işle>
    }

    // 2'inci thread

    for (;;) {
        recvfrom(...)
        <mesajı işle>
    }
    ...

    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(...)
        <mesajı kuyruğa yerleştir>
    }
    ...

    // 1'inci thread

    for (;;) {
        <kuyruktan mesajı al işle>
    }

    // 2'inci thread

    for (;;) {
        <kuyruktan mesajı al işle>
    }
    ...

    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);

    <msg_xxx'in içi dolduruluyor>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <user_name> <password>\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 <math.h> içerisinde olan standart C fonksiyonları libc kütüphanesinde değildir. libm isimli ayrı bir 
    kütüphanededir. Maalesef gcc otomatik olarak bu kütüphaneyi link aşamasına dahil etmemektedir. Bu nedenle matematiksel 
    fonksiyonları kullanırken linker için -lm seçeneğinin bulundurulması gerekmektedir.

    Client program (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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <username> <password> [-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 <kullanıcı_adı> <parola>\r\n"
    "SEND_MESSAGE <mesaj metni>\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 <kullanıcı adı>\r\n"
    "USER_LOGGEDOUT <kullanıcı_adı>\r\n"
    "LOGOUT_ACCEPTED\r\n"
    "DISTRIBUTE_MESSAGE <mesaj metni>\r\n"
    "LOGOUT_ACCEPTED\r\n"
    "ERROR <hata_mesajı>"

    Ç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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

#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 <user name> CR/LF"
    "PASS <password> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <pthread.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <sys/socket.h>

    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 <sys/socket.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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ı <sys/un.h> dosyası içerisinde bildirilmiştir ve en azından şu elemanlara 
    sahip olmak zorundadır:

    #include <sys/un.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#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 <sys/socket.h>

    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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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<isim> 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

    <dosyanın içeriği>

    #endif

    Buradaki SOME_NAME dosya isminden hareketle uydurulmuş olan herhangi bir isim olabilir. Örneğin:

    #ifndef MYUTIL_H_
    #define MYUTIL_H_

    <dosyanın içeriği>

    #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 <yol ifadeleri>" 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 <isim>" 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:

    <dosya_ismi>.so.<majör_numara>.<minör_yüksek_numara>.<minör_alçak_numara>

    Ö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 <dlfcn.h>

    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 <dlfcn.h>

    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 <dlfcn.h>

    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 <stdio.h>

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 <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

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 <stdio.h>

void bar(void);

void foo(void)
{
	bar();
}

/* app.c */

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

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 <stdio.h>

void __attribute__((visibility("hidden"))) bar(void)
{
	printf("bar\n");
}

void foo(void)
{
	printf("foo\n");

	bar();
}

/* app.c */

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

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 <stdio.h>

    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 <stdio.h>

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 <stdio.h>

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 <stdio.h>

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 <stdio.h>
#include "util.h"

void foo(void)
{
	printf("foo\n");
}

void bar(void)
{
	printf("bar\n");
}

/* app.c */

#include <stdio.h>
#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 <iostream>
#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 <iostream>
#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
    <TAB>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 <unistd.h>
#include <stdint.h>
#include <asm/unistd.h>

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 <linux/module.h>
    #include <linux/kernel.h>

    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 <linux/module.h>
#include <linux/kernel.h>

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 <linux/module.h>
    #include <linux/kernel.h>

    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 <linux/module.h> 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 <linux/kernel.h> 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 <linux/module.h>
    #include <linux/kernel.h>

    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 <linux/module.h>
#include <linux/kernel.h>

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 <linux/module.h>
#include <linux/kernel.h>

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 <linux/init.h> dosyası içerisindedir. Bu dosya da <linux/module.h> 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 <linux/module.h>
#include <linux/kernel.h>

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/<modül ismi>" 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 <linux/module.h>
#include <linux/kernel.h>

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 <sys/stat.h> 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 <sys/stat.h>

    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 <sys/sysmacros.h>

    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 <linux/fs.h>

    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 <fcntl.h>

    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 <erişim hakları>] <path> <c ya da b> <majör numara> <minör numara>

    Ö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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <getopt.h>
#include <sys/sysmacros.h>

bool ismode_correct(const char *mode);
void exit_sys(const char *msg);

int main(int argc, char *argv[])		/* ./mymknod [-m <mode>] <path> <type> <major> <minor> */
{
	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 <erişim hakları>] <dosya ismi> <c ya da b> <majör numara> <minör numara>

    Ö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 <linux/fs.h>

    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 <linux/fs.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>

#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 <linux/fs.h>

    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 <linux/cdev.h>

    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 <linux/cdev.h>

    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 <linux/cdev.h>

    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 <linux/cdev.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <linux/uaccess.h>

    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 <linux/uaccess.h>

    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 <linux/uaccess.h>

    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 <linux/uaccess.h>

    __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 <linux/uaccess.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <linux/mutex.h>

    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
    <linux/mutex.h> 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 <linux/mutex.h>

    void mutex_lock(struct mutex *lock);

    Mutex'in kilitli olup olmadığı ise mutex_trylock fonksiyonuyla kontrol edilebilir:

    #include <linux/mutex.h>

    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 <linux/mutex.h>

    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;
    ...
    ... <KRİTİK KOD>
    ...
    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <linux/semaphore.h>

    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 <linux/semaphore.h>

    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);
    ...
    ... <KRİTİK KOD>
    ...
    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/semaphore.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <linux/spinlock.h>

    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 <linux/spinlock.h>

    void spin_lock_init(spinlock_t *lock);

    2) Kritik koda giriş için aşağıdaki fonksiyonlar kullanılmaktadır:

    #include <linux/spinlock.h>

    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 <linux/spinlock.h>

    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);
    ...
    ... <KRİTİK KOD>
    ...
    spin_unlock(&g_spinlock);

    Ya da örneğin:

    ...
    unsigned long irqstate;
    ...

    spin_lock_irqsave(&g_spinlock, irqstate);
    ...
    ... <KRİTİK KOD>
    ...
    spin_unlock_irqrestore(&g_spinlock, irqstate);

    Yine kernel spinlock nesnelerinde de try'lı lock fonksiyonları bulunmaktadır:

    #include <linux/spinlock.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <linux/rwlock.h>

    void rwlock_init(rwlock_t *lock);

    reader/writer spinlock nesneleri ile ilgili diğer çekirdek fonksiyonları şunlardır:

    #include <linux/rwlock.h>

    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 <asm/atomic.h>

    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 <linux/wait.h>

    static DECLARE_WAIT_QUEUE_HEAD(g_wq);

    Ya da nesne tanımlanıp init_waitqueue_head fonksiyonuyla da ilk değerlenebilir:

    #include <linux/wait.h>

    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) {
        <thread'i bekleme kuyruğuna ekle>
        <thread_uyandırıldı>
    }

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
 #include <asm/atomic.h>
#include <linux/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>
#include <linux/wait.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.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;

	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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <linux/kernel.h>

    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 <linux/wait.h>

    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 <linux/wait.h>

    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 <linux/kernel.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>
#include <linux/wait.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>
#include <linux/wait.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

#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 <linux/slab.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>
#include <linux/wait.h>
#include <linux/slab.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.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;

	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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <linux/slab.h>

    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 <linux/slab.h>

    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 <linux/slab.h>

    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 <linux/slab.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>
#include <linux/wait.h>
#include <linux/slab.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.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;

	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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>
#include <linux/wait.h>
#include <linux/slab.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.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;

	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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>

#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 <sys/ioctl.h>

    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 <stddef.h>
    #include <linux/ioctl.h>

    #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 <stddef.h>
#include <linux/ioctl.h>

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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>
#include <linux/wait.h>
#include <linux/slab.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#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 <linux/proc_fs.h>

    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 <linux/proc_fs.h>

    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 <linux/proc_fs.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/proc_fs.h>

#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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/proc_fs.h>

#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 <linux/proc_fs.h>

    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 <linux/proc_fs.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>

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 <stddef.h>
#include <linux/ioctl.h>

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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>
#include <linux/wait.h>
#include <linux/slab.h>
#include <linux/proc_fs.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#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 <linux/jiffies.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>

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 <linux/jiffies.h>

    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 <linux/time.h>

    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 <linux/time.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/time.h>

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 <linux/sched.h>

    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 <linux/delay.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/time.h>
#include <linux/delay.h>

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 <linux/time.h>

    #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 <linux/time.h>

    #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 <linux/time.h>

    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 <linux/time.h>

    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 <linux/time.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/time.h>

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 <linux/kthread.h>

    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 <linux/kthread.h>

    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 <linux/kthread.h>

    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 <linux/kthread.h>

    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 <linux/kthread.h>

    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 <linux/kernel.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/timer.h>
#include <linux/delay.h>

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 <linux/interrupt.h>

    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 <linux/interrupt.h>

    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 <linux/interrupt.h>

    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:

    <A'nın scan kodu> <F0> <A'nın scan kodu>

    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:

    <Ctrl scan kodu> <A'nın scan kodu> <F0> <A'nın scan kodu> <F0> <Ctrl scan kodu>

    İş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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/interrupt.h>

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 <asm/io.h>

    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 <asm/io.h>

    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 <asm/io.h>

    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <linux/ioport.h>

    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 <linux/ioport.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/interrupt.h>
#include <asm/io.h>

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 <linux/ioctl.h>

#define KEYBOARD_MAGIC		'k'
#define IOC_GETKEY			_IOR(KEYBOARD_MAGIC, 0, int)

#endif

/* irq-driver.c */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/interrupt.h>
#include <asm/io.h>
#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#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 <linux/ioctl.h>

#define KEYBOARD_MAGIC		'k'
#define IOC_GETKEY		_IOR(KEYBOARD_MAGIC, 0, int)

#endif

/* irq-driver.c */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/interrupt.h>
#include <asm/io.h>
#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 <linux/ioctl.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#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 <asm/system.h>

    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 <linux/ioport.h>

    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 <linux/ioport.h>

    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 <asm/io.h>

    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 <asm/io.h>

    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 <asm/io.h>

    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 <asm/io.h>

    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 <asm/io.h>

    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 <asm/io.h>

    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 <asm/io.h>

    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 <linux/blkdev.h>

    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 <linux/blkdev.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/blkdev.h>

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 <linux/genhd.h> dosyası da çekirdeğin 5.18 versiyonunda kaldırılmış buradaki fonksiyonların 
    prototipleri <linux/blkkdev.h> dosyasına taşınmıştır.

    #include <linux/genhd.h>

    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 <linux/blkdev.h>

    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 <linux/blkdev.h>

    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 <linux/genhd.h>

    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 <linux/genhd.h>

    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 <linux/genhd.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>

#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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/blkdev.h>

#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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#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 <linux/bvec.h>

    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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/blkdev.h>

#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 <linux/blk-mq.h>

    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 <linux/blk-mq.h>

    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.
    <DEVAM EDECEK>
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
    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 <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

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 <sys/mount.h>

    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 <blok_aygıt_dosyası> <mount_edilecek_dizin>

    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 <dosya_sisteminin_türü> 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 <sys/mount.h>

    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 <mount_dizini ya da blok_aygıt_dosyası>

    Ö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 <stdio.h>
#include <stdlib.h>
#include <sys/mount.h>

void exit_sys(const char *msg);

/* mymount <device> <mount_point> <filesystem_type> */

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 <stdint.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#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 <stdint.h>

#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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#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 <stdint.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#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
    <FFF8> <FFFF> <0000> <FFFF> <0005> <0006> <0007> <0008> <0009> <000A> <000B> <000C> <000D> <FFFF>

    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 <stdint.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#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    <DIR>          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ş>
    <dosyanın 8+3'lük kısa ismi>

    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 <stdint.h>

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#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 <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <unistd.h>
#include <fcntl.h>
#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 block>>
    <super block>
    <i-node block>
    <data block>

    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:

    <boot block>                    (1024 byte)
    <super block>
    <i-node block>
    <data block>

    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:

    <boot block>                      (1024 byte)
    <block group>
    <block group>
    <block group>

    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:

    <super block>
    <block group descriptor table>
    <block bitmap>
    <i-node bitmap>
    <i-node table>
    <data block>

    İş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:

    <boot block>

    <super block>
    <block group desciptor table>     (1024 byte)
    <block bitmap>                    (1024 byte)
    <i-node bitmap>                   (1024 byte)
    <i-node table>
    <data block>

    <super block>
    <block group desciptor table>     (1024 byte)
    <block bitmap>                    (1024 byte)
    <i-node bitmap >                  (1024 byte)
    <i-node table>
    <data block>

    <super block>
    <block group desciptor table>     (1024 byte)
    <block bitmap>                    (1024 byte)
    <i-node bitmap >                  (1024 byte)
    <i-node table>
    <data block>
    ...

    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

    <block group descriptor>
    <block group descriptor>
    <block group descriptor>
    ...
    <block group descriptor>
    <block group descriptor>

    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 <dosyanın 0'ıncı bloğunun numarası>
    1 <dosyanın 1'inci bloğunun numarası>
    2 <dosyanın 2'nci bloğunun numarası>
    3 <dosyanın 3'üncü bloğunun numarası>
    4 <dosyanın 4'üncü bloğunun numarası>
    5 <dosyanın 5'inci bloğunun numarası>
    6 <dosyanın 6'ıncı bloğunun numarası>
    7 <dosyanın 7'inci bloğunun numarası>
    8 <dosyanın 8'inci bloğunun numarası>
    9 <dosyanın 9'uncu bloğunun numarası>
    10 <dosyanın 10'uncu bloğunun numarası>
    11 <dosyanın 11'inci bloğunun numarası>
    12 <dolaylı blok numarası>
    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.

    <dizin_girişi>
    <dizin_girişi>
    <dizin_girişi>
    <dizin_girişi>
    <dizin_girişi>
    ...

    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 <extfs/ext2_fs.h> 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 <extfs/ext2_fs.h> 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 <extfs/ext2_fs.h> 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ı <extefs/extfs.h> 
    isimli dosyadır. Bu dosyanın include edilmesi gerekir.

    #include <ext2fs/ext2fs.h>

    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 <stdio.h>
#include <ext2fs/ext2fs.h>

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/<pid>/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/<pid>/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 <sys/capability.h>

    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 <sys/capability.h>

    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 <sys/capability.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sys/capability.h>

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 <sys/capability.h>

    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 <sys/capability.h>

    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 <sys/capability.h>
    
    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 <stdio.h>
#include <stdlib.h>
#include <sys/capability.h>

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 <stdio.h>
#include <stdlib.h>
#include <sys/capability.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/capability.h>

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 <sys/capability.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sys/capability.h>

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 <sys/capability.h>

    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 <stdio.h>
#include <stdlib.h>
#include <sys/capability.h>

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 <sys/capability.h>

    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 <unistd.h>

    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 <sys/syscall.h> 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 <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <unistd.h>

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 "-<custom_version_number>" 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/<mimari>/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/<paltform>/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/<platform>" 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 <platform>_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<cpu_sayısı>" 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/<platform>/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:

    <modül_yolu>: <bağımlılık> <bağımlılık2> ...

    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 <tanımlayıcı> <modül_adı>

    Ö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/<platform>/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/<platform>/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/<platform>/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 <linux/module.h>
    #include <linux/kernel.h>

    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 <linux/kernel.h>
    #include <linux/syscalls.h>
    #include <linux/uaccess.h> 

    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/<platform>/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 <stdio.h>
    #include <sys/syscall.h>
    #include <unistd.h>

    #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 <isim>;

    Ö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 <isim>;

    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 <isim> (<isim> <tür>, <isim> <tür>, <isim> <tür>... );

    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 <isim>;
	
    Ö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 <tablo ismi> (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 <tablo ismi> <WHERE cümleciği>;

    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 <tablo ismi> SET alan1 = değer1, alan2 = değer2, ... WHERE <koşul>;

    Ö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 <sütun_listesi> FROM <tablo_ismi> [WHERE <koşul];

    Eğer WHERE cümleciği kullanılmazsa tablodaki tüm kayıtlar elde edilir. Sütun listesi sütunların isimlerinden oluşmaktadır. 
    Uygulamacı yalnızca bazı sütun bilgilerini elde edebilir. Sütun istesi yerine * kullanılırsa "tüm sütunlar" anlamına gelmektedir. 
    Örneğin:

    SELECT * FROM student WHERE student_no > 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 <iki tablodaki istenen sütunların listesi> FROM <birinci tablo ismi> INNER JOIN <ikinci tablo ismi> ON <koşul>;

    Ö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 <stdio.h>
    #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 <stdio.h>
#include <stdlib.h>
#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 <stdio.h>
#include <stdlib.h>
#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);
}

/*--------------------------------------------------------------------------------------------------------------------------
   
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
   
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
   
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
   
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
   
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
   
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
   
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
   
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
   
---------------------------------------------------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------------------------------------------------
   
---------------------------------------------------------------------------------------------------------------------------*/

