CSDKursNotlari/SysProg-1-OzetNotlar-Ornekler.txt

44700 lines
1.7 MiB
Text
Raw Permalink Normal View History

/*------------------------------------------------------------------------------------------------------------------------------------------
C ve Sistem Programcıları Derneği
Sistem Programlama ve İleri C Uygulamalar - I Kursu
Sınıfta 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ı okurken editörünüzün "Line Wrapping" özelliğini pasif hale getiriniz.)
2024-06-09 22:42:45 +03:00
Son Güncelleme: 09/06/2024 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
1. Ders 27/05/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
2. Ders 03/06/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
3. Ders 04/06/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
4.Ders 10/06/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
strtok fonsiyonun prototipi şöyledir:
#include <string.h>
char *strtok(char *str, const char *delims);
Fonksiyon basit yazıları parse etmek için yani onları belirli ayıraç karakterlerinden ayırmak için kullanılmaktadır. Fonksiyonun birinci
parametresi ayrıştırılacak olan yazının adresini almaktadır. Bu parametrenin const olmayan bir gösterici olduğuna dikkat ediniz. Fonksiyonun
ikinci parametresi ayrıştırmada kullanılacak ayrıştırma karakterlerinin (delimiters) bulunduğu dizinin adresini almaktadır.
Fonksiyon şöyle çalışır: Fonksiyon önce birinci parametresiyle belirtilen adresten ilerleyerek ilk ayıraç karakteri olmayan karakterin
yerini elde eder. Sonra ayıraç karakteri olmayana kadar ilerlemeye devam eder. İlk ayıraç karakterini gördüğünde oraya null karakter yerleştirip
o kısmın başlangıç adresiyle geri döner. strtok fonksiyonu genellikle bir kez çağrılmaz. Bir döngü içerisinde çağrılır. Çünkü fonksiyon bir kez
çağrıldığında yalnızca ayıraçların ayırdığı ilk kısım elde edilir. İşte fonksiyonun kaldığı yerden devam etmesi için sonraki çağrılarda
birinci parametre yerine NULL adres girilmelidir. Fonksiyon en sonunda yazı ayrıştırılacak yazı bittiğinde NULL adrese geri dönmektedir.
Fonksiyonun tipik kullanuım kalını şöyledir:
char *str;
for (str = strtok(s, delims); str != NULL; str = strtok(NULL, delims))
puts(str);
Tabii aynı kalıp while döngüüs ile de gerçekleştirilebilir:
str = strtok(s, delimes);
while (str != NULL) {
puts(str);
str = strtok(NULL, delims);
}
Fonksiyonun birinci parametresiyle belirtilen dizi içerisindeki yazıyı bozduğuna dikkat ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
int main(void)
{
char s[] = " Ali, Veli, Selami ";
char *pstr;
pstr = strtok(s, ", ");
while (pstr != NULL) {
puts(pstr);
pstr = strtok(NULL, ", ");
}
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
strtok Fonksiyonun Kullanımına örnek.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
int main(void)
{
char s[] = "...,,,,,ali,...,,,veli,....,selami..,,,,,,hakan";
char *str;
for (str = strtok(s, ",."); str != NULL; str = strtok(NULL, ".,"))
puts(str);
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
strtok fonksiyonun kullanımı örnek.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
int main(void)
{
char s[] = " Ali, Veli, Selami ";
char *pstr;
for (pstr = strtok(s, ", "); pstr != NULL; pstr = strtok(NULL, ", "))
puts(pstr);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
strtok fonksiyonun kullanımına örnek.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
int main(void)
{
char s[] = "12/11/2009";
char *pstr;
for (pstr = strtok(s, "/"); pstr != NULL; pstr = strtok(NULL, "/"))
puts(pstr);
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
strtok Fonksiyonunun birinci parametresiyle aldığı adreste değişiklik yaptığına dikkat ediniz. Bu nedenle aşağıdaki kod
tanımsız davranışa yol açacaktır:
char *s = "12/11/2009";
char *pstr;
for (pstr = strtok(s, "/"); pstr != NULL; pstr = strtok(NULL, "/"))
puts(pstr);
Burada s föstericisi artık bir iki tırnak ile tahsis edilen string'i göstermektedir. C'de iki tırnak ile tahsis edilen string'lerin
güncellenmesi "tanımsız davranışa (undefined behavior)" yol açmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
int main(void)
{
char *s = "12/11/2009";
char *pstr;
for (pstr = strtok(s, "/"); pstr != NULL; pstr = strtok(NULL, "/"))
puts(pstr);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
İleride görüleceği gibi static yerel değişken ve global değişken kullanan fonksiyonlar thread güvenli (thread safe) değildir.
Bunların thread güvenli versiyonları birer "eklenti (extension)" bulundurulabilmektedir. Örneğin Microsoft strtok fonksiyonunun
thread güvenli biçimini strtok_s ismiyle bir eklenti biçiminde bulundurmuştur. Fonksiyonun prototipi şöyledir:
#include <string.h>
char *strtok_s(char *str, const char *delims, char **context);
Fonksiyonun strtok fonksiyonundna farkı kalınan yeri statik yerel bir nesnede saklamak yerine bir göstericide saklamasıdır. Fonksiyon
bu göstericiye kalınan yerin adresini saklar ve adresi oradan kullanır.
Microsoft derleyicilerinde strtok fonksiyonun strtok_s ismiyle thread güvenli bir versiyonu da vardır. Microsoft'un strtok_s fonksiyonun
eşdeğeri GNU C kütüphanesinde strtok_r ismiyle bulunmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
int main(void)
{
char s[] = "12/11/2009";
char *pstr;
char *pstrl;
for (pstr = strtok_s(s, "/", &pstrl); pstr != NULL; pstr = strtok_s(NULL, "/", &pstrl))
puts(pstr);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
strtok fonksiyonun gerçekleştirimi aşağıdaki gibi yapılabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
char *mystrtok(char *str, const cha *delim)
{
static char *pos;
char *beg;
if (str != NULL)
pos = str;
while (*pos != '\0' && strchr(delim, *pos) != NULL)
++pos;
if (*pos == '\0')
return NULL;
beg = pos;
while (*pos != '\0' && strchr(delim, *pos) == NULL)
++pos;
if (*pos != '\0')
*pos++ = '\0';
return beg;
}
int main(void)
{
char s[] = "";
char *pstr;
for (pstr = mystrtok(s, ","); pstr != NULL; pstr = mystrtok(NULL, ","))
puts(pstr);
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
strtok fonksiyonun birinci parametresinin const olan (yani birinci parametresindeki yazıyı değiştirmeyen) versyionu aşağıdaki gibi yazılabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
char *mystrtok(const char *str, const char *delim, char *dest)
{
static const char *pos;
const char *beg;
if (str != NULL)
pos = str;
while (*pos != '\0' && strchr(delim, *pos) != NULL)
++pos;
if (*pos == '\0')
return NULL;
beg = pos;
while (*pos != '\0' && strchr(delim, *pos) == NULL)
++pos;
strncpy(dest, beg, pos - beg);
dest[pos - beg] = '\0';
if (*pos != '\0') /* bu kontrol kaldırılabilir */
++pos;
return dest;
}
int main(void)
{
char text[] = "10/12/2009";
char str[100];
char *pstr;
for (pstr = mystrtok(text, "/", str); pstr != NULL; pstr = mystrtok(NULL, "/", str))
puts(pstr);
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
strtok fonksiyonun dinamik bir alan tahsis edip o alana geri dönen versyionu. Ancak burada fonksiyon kullanan kişinin bu alanı kendisinin
free etmesi gerekir. Ayrıca aşağıdaki örnekte yazının sonuna gelindiğinden dolayı mı yoksa bellek tahsisatının yapılamadığından dolayı mı
fonksiyonun NULL adrese geri döndüğü anlaşılamamaktadır. Bu tür durumlarda UNIX/Linux sistemlerinde errno değişkeninden faydalanılabilmektedir.
Aslında errno değişkeni C'de de bu amaçla kullanılabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *mystrtok(const char *str, const char *delim)
{
static const char *pos;
const char *beg;
char *dstr;
if (str != NULL)
pos = str;
while (*pos != '\0' && strchr(delim, *pos) != NULL)
++pos;
if (*pos == '\0')
return NULL;
beg = pos;
while (*pos != '\0' && strchr(delim, *pos) == NULL)
++pos;
if ((dstr = (char *)malloc(pos - beg + 1)) == NULL)
return NULL;
strncpy(dstr, beg, pos - beg);
dstr[pos - beg] = '\0';
return dstr;
}
int main(void)
{
char text[] = "10/12/2009";
char *pstr;
for (pstr = mystrtok(text, "/"); pstr != NULL; pstr = mystrtok(NULL, "/")) {
puts(pstr);
free(pstr);
}
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
Bir dosyayı satır satır okuyarak her satırı strtok fonksiyonuyla atomlarına ayrıştırabiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_LINE 4096
int isall_space(const char *str)
{
while (*str != '\0' && isspace(*str))
++str;
return *str == '\0';
}
int main(void)
{
FILE *f;
char line[MAX_LINE];
char *str;
if ((f = fopen("test.csv", "r")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
while (fgets(line, MAX_LINE, f) != NULL) {
if (isall_space(line))
continue;
if ((str = strchr(line, '\n')) != NULL)
*str = '\0';
for (str = strtok(line, ","); str != NULL; str = strtok(NULL, ","))
printf("%s\n", str);
printf("-----------------\n");
}
fclose(f);
return 0;
}
/* test.csv */
Ali Serçe, 100, 200, 300
Sacit Bulut, 12.3, 56.3, 78.2
Hakan Akyıldız, 12.3, 67.3
Hüsnü Biter, 12, 34, 34
/* ------------------------------------------------------------------------------------------------------------------------------------------
remove standart bir C fonksiyonudur. Yol ifadesi ile belirtilen dosyayı silmek için kullanılır. Fonksiyonun prototipi şöyledir:
#include <stdio.h>
int remove(const char *filename);
Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda sıfır dışı bir değere geri döner. Bir dosyanın silinememesi için çeşitli nedenler
söz konusu olabilir. Örneğin dosya yoktur bu nedenle silinemez. Ya da örneğin silinmek istenen dosyaya prosesin silme yetkisi olmayabilir.
Bu nedenle fonksiyonun başarısı kontrol edilmelidir.
Bir dosya remove fonksiyonuyla silindiğinde dosya "geri dönüş kutusunda (recycle bin)" saklanmaz. Geri dönüşüm kutusu yüksek seviyeli bir
kabuk organizasyonudur. Geri dönüş kutusuyla işletim sisteminin çekirdeğinin bir ilgisi yoktur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
if (remove("test.txt") != 0) {
fprintf(stderr, "cannot remove file!..\n");
exit(EXIT_FAILURE);
}
printf("success..\n");
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir dosya bir program tarafında açılmışsa dosyanın silinebilirliği sistemden sisteme değişebilmektedir.UNIX / Linux sistemlerinde genel olarak
ık dosyalar silinebilmektedir.Ancak silme işlemi gerçek anlamda silinen dosyayının kapatılması durumunda yapılmaktadır.Aynı durum macOS
sistemlerinde de böyledir.Windows sistemlerinde ise açık dosyaların silinebilirliği dosyanın açış moduna bağlı olarak değişebilmektedir.
Aşağıdaki programı Windows sistemlerinde ve UNIX/Linux sistemlerinde deneyiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *f;
if ((f = fopen("test.txt", "r")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
printf("File successfully opened...\n");
if (remove("test.txt")) {
fprintf(stderr, "cannot remove file!..\n");
exit(EXIT_FAILURE);
}
printf("file removed...\n");
fclose(f);
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
rename isimli standart C fonksiyonu dosyanın ismini değiştirmek için kullanılır. Tabii bir dosyanın ismi başka dizinde de değiştirilebilir.
Bu da taşıma etkisi yaratmaktadır. rename fonksiyonunun prototipi şöyledir:
#include <stdio.h>
int rename(const char *old, const char *new);
Fonksiyonun birinci parametresi ismi değiştirilecek dosyanın yol ifadesini ikinci parametresi ise yeni isimli yol ifadesini almaktadır.
Yukarıda da belirttiğimiz gibi bir dosyanın ismini başka bir dizindeki dosya ismi olarak dğeiştirebiliriz. Bu zaten bir taşıma gibi
işlev görmektedir. Tabii rename işlemi sırasında aslında dosyanın disk üzerindeki içeriği taşınmamaktadır. rename başarı durumunda 0
değerine başarısızlık durumunda -1 değerine geri dönmektedir.
Yine rename işlemi çeşitli nedenelrden dolayı yapılamayabilir. Örneğin yeni isimle zaten bir dosya bulunyor olabilir. Ya da proses bu işlemi
yapmaya yetkili olmayabilir. Bu nedenle fonksiyonun başarısı mutlaka kontrol edilmelidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
if (rename("test.txt", "mest.txt") == -1) {
fprintf(stderr, "cannot rename file!..\n");
exit(EXIT_FAILURE);
}
printf("Success...\n");
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
system fonksiyonu ilgili sistemdeki "komut yorumlayıcı (command interpreter)" programını çalıştıran ve ona bizim fonksiyona argüman olarak
verdiğimiz komutu işlettiren standart bir C fonksiyonudur. Fonksiyonun prototipi şöyledir:
#include <stdlib.h>
int system(const char *string);
Fonksiyonun geri dönüş değerinin anlamı derleyeiciyi yazanların isteğine bırakılmıştır. Ancak Windows, macOS ve UNIX/Linux sistemlerinde
fonksiyon aşarı durumunda 0, başarısızlık durumunda sıfır dışı bir değere geri dönmektedir. Fonksiyonun çeşitli ayrıntılaarı vardır. Bu konu
ileride daha ayrıntılı biçimde ele alınacaktır.
C derleyicisinin olduğu bir sistemde komut yorumlayıcı olmak zorunda değildir. Örneğin işletim sistemi olmayan bir mikrodenetleyici sisteminde
komut yorumlayıcı da olmayacaktır. İşte programcı ilgili sistemde bir komut yorumlayıcının olup olmadığını anlayabilmek için fonksiyonu NULL
adres ile çağırabilir. Bu durumda fonksiyon sıfır dışı herhangi bir değere geri dönerse ilgili sistemde komut yorumlayıcı vardır. Sıfır değerine
geri dönerse ilgili sistemd ekomut yorumlayıcı yoktur.
Komut yorumlayıcılardaki komutlar o işletim sistemine hatta o komut yorumlayıya bağlı olarak dğeişebilmektedir. Dolsyayıyla system fonksiyonunda
vereceğiniz komutlar belli bir sistemdeki belli komut yaorumlayıcıları için anlamlı ve geçerli olmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
system("ls -l");
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
Programın çalışması sırasında belli bir amaçla yaratılan o amaç gerçekleştirildikten sonra silinen kısa ömürlü dosyalara "geçici dosyalar
(temporary files)" denilmektedir. Geçici dosyaları oluştururken önemli bir sorun bu dosyaların verilecek isimlerin tesadüfen var olan dosyalarla
çakışmasıdır.
tmpfile isimli standart C fonksiyonu olmayan bir dosya ismi ile bir dosyayı "w+b" modunda yaratır ve dosyaya ilişkin dosya bilgi göstericisiyle
geri döner. Böyle dosyalar kapatıldığında diskteki dosya da otomatik olarak silinecektir. Tabii tmpfile fonksiyonu da çeşitli nedenlerden
dolayı başarısız olabilir. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir. Programcı fonksiyonun başarısını kontrol etmelidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *f;
int i, val;
if ((f = tmpfile()) == NULL) {
fprintf(stderr, "cannot create temporary file!..\n");
exit(EXIT_FAILURE);
}
for (i = 0; i < 100; ++i)
if (fwrite(&i, sizeof(int), 1, f) != 1) {
fprintf(stderr, "cannot write file!..\n");
exit(EXIT_FAILURE);
}
fseek(f, 0, SEEK_SET);
while (fread(&val, sizeof(int), 1, f) > 0)
printf("%d\n", val);
if (ferror(f)) {
fprintf(stderr, "cannot read file!..\n");
exit(EXIT_FAILURE);
}
fclose(f);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
tmpfile fonksiyonu ile biz açılmış bir geçici dosya elde ederiz. Ancak o geçici dosyanın ismini bilmeyiz. Oysa bazı uygulamalarda programcı
bir isimle geçici dosya oluşturmak isteyebilir. Örneğin bir text dosyadaki bütün "ankara" yazılarını "istanbul" yapmak isteyeim. Böyle bir
işlemi dosya üzerinde yapmak hem yavaşlığa hem de karmaşıklığa yol açar. Bunun kolay bir yolu bir geçici dosya oluşturmak asıl dosyadaki karakterleri
bu geçici dosyaya kopyalamak, ancak bu sırada "ankara" yazıları yerine "istanbul" yazılarını bu geçici dosyaya yazmak ve işlem birtince de
asıl dosyayı silip geçici dosyanın ismini değiştirmektir. İşte bunun için bizim geçici dosyanın ismini biliyor olmamız gerekir.
tmpnam isimli standart C fonksiyonu geçici dosyayı açmaz bize bir geçici dosyaya ilişkin yol ifadesi verir. Bu yol ifadesi ile belirtilen dosyayı
açmak programcının yapacağı bir iştir. tmpnam fonksiyonunun prototipi şöyledir:
#include <stdio.h>
char *tmpnam(char *s);
Fonksiyonun parametresi geçici dosya isminin yerleştirileceği dizinin adresini almaktadır. Ancak fonksiyon çağrılırken argüman olarak NULL
adres de geçilebilir. Bu durumda fonksiyon geçici dosya ismini static yerel bir dizinin içerisine yerleştirir ve o dizinin
adresiyle geri döner. Eğer argüman olarak NULL adres geçilmezse fonksiyon argüman olarak geçilmiş olan adresin aynısıyla geri dönmektedir.
Fonksiyonun en az TMP_MAX (<stdio.h> içerisinde define edilmiştir) dosya ismi üretebileceği garanti edilmiştir. Ancak makul bir limit aşıldığında
fonksiyon başarısız olabilr. Bu duurmda NULL adrese geri döner. Geçici dosyanın yerleştirileceği dizinin maksimum uzunluğu için <stdio.h>
içerisinde L_tmpnam isimli bir sembolik sabit de bulundurulmuştur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* ------------------------------------------------------------------------------------------------------------------------------------------
Bir dosyadaki # ile başlayan satırları silen program örneği. Programın tek bir komut satırı argümanı vardır. O da #'lerden arındırılacak dosyanın
yol ifadesini belirtir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#define MAX_LINE 8192
int issharp(const char *line);
int main(int argc, char *argv[])
{
FILE *f, *ftemp;
char line[MAX_LINE];
char *path;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((path = tmpnam(NULL)) == NULL) {
fprintf(stderr, "cannot get temporary file!..\n");
exit(EXIT_FAILURE);
}
if ((f = fopen(argv[1], "r")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
if ((ftemp = fopen(path, "w")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
while (fgets(line, MAX_LINE, f) != NULL) {
if (!issharp(line))
if (fputs(line, ftemp) == EOF) {
fprintf(stderr, "cannot write file temporary file!..\n");
goto FAILURE;
}
}
if (ferror(f)) {
fprintf(stderr, "cannot read file: %s\n", argv[1]);
goto FAILURE;
}
fclose(f);
fclose(ftemp);
if (remove(argv[1]) != 0) {
fprintf(stderr, "cannot remove file!..\n");
goto FAILURE;
}
if (rename(path, argv[1]) != 0) {
fprintf(stderr, "cannot rename file!..\n");
exit(EXIT_FAILURE);
}
printf("sucess...\n");
exit(EXIT_SUCCESS);
FAILURE:
fclose(ftemp);
if (remove(path) != 0)
fprintf(stderr, "cannot remove file!..\n");
exit(EXIT_FAILURE);
return 0;
}
int issharp(const char *line)
{
while (isspace(*line))
++line;
return *line == '#';
}
/*------------------------------------------------------------------------------------------------------------------------------------------
6.Ders 17/06/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir programı komut satırında çalıştırırken program isminin yaznına yazılan yazılara programın "komut satırı argümanları (command line arguments)"
denilmektedir. Örneğin:
prog ali veli selami
Burada çalıştırılmak istenen program "prog" isimli programdır. "prog", "ali", "veli", "selami" yazıları ise programın komut satırı argümanlarını
oluşturmaktadır. Komut satırı argümanlarının amacı programı çalıştırırken programın yapacağı işler konusund etkili olmaktır.
C'de programın programın komut satırı argümanları main fonksiyonundan alınmaktadır. Yani programın komut satırı argümanları tipik olarak
kabuk (shell) programı tarafındna komut satırından alınır ve main fonksiyonuna argüman olarak geçirilir. C standratlarına göre main fonksiyonunun
parametrik yapısı iki biçimde olabilmektedir:
int main(void)
int main(int argc, char *argv[])
Tabii burada argc va argv isimleri gelenekseldir. Bu isimlerin kullanılması zorunlu değildir. C standartları main fonksiyonun geri dönüş değerinin
int türden olmasını zorunlu tutmaktadır. Ancak standartlar main fonksiyonun derleyiciye özgü bir biçimde başka parametrelere de sahip olabileceğini
belirtmektedir. Anımsanacağı gibi fonksiyonun parametre parantezindeki köşeli parantezler aslında "gösterici" anlamına gelmektedir. Bu durumda
main fonksiyonu şöyle de yazılabilir:
int main(int argc, char **argv)
Buradaki argc komut satırındaki argümanların program ismi dahil olmak üzere toplam sayısını belirtmektedir. argv ise programın ismi dahil olmak
üzere komut satırı argüman yazılarının başlangıç adreslerini tutan gösterici dizisini göstermektedir. Standartlar bu dizinin son elemanında
NULL adresin bulunucağını (yani argv[argc] değerinin NULL adres olacağını) garanti etmektedir.
Burada önemli noktalardan biri bu argc ve argv parametrelerinin main fonksiyonuna kim tarafından ve nasıl aktarıldığıdır. Biz programı komut
satırından çalıştırdığımızda bu kabuk programı komut satırında girdiğimiz yazıları boşluklaradan ayırır ve bunlardan bir gösterici dizisi
oluşturur. Bu gösteri dizisinin adresini main fonksiyonuna geçirir. Tabii sürecin bazı ayrıntıları vardır. İleride bu ayrıntılar üzerinde d
duracağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Klavyeden (stdin dosyasından) girilen bir komut satırı yazısından argc ve argv parametrelerini oluşturan basit program aşağıdaki
yazılabilir. Burada bazı kontroller yapılmamıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#define MAX_CMDLINE 4096
#define MAX_ARGV 1024
int main(void)
{
char cmdline[MAX_CMDLINE];
char *argv[MAX_ARGV];
char *str;
int argc;
fgets(cmdline, MAX_CMDLINE, stdin);
argc = 0;
for (str = strtok(cmdline, " \t"); str != NULL; str = strtok(NULL, " \t"))
argv[argc++] = str;
argv[argc] = NULL;
printf("-------------\n");
for (int i = 0; i < argc; ++i)
puts(argv[i]);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Komut satırı argümanlarında "seçenek (option)" denilen bir kavram vardır. Programın yaptığı iş üzerinde bazı belirlemeleri oluşturmak için
seçenek argümanlardan faydalanılmaktadır. Yani seçenekler bir şim çeşitli varyasyonlarını belirltmek için kullanılan komut satırı argümanlarıdır.
Seçenek belirtme işlemi UNIX/Linux dünyasında "-" karakteri ile Windows dünyasında "/" karakteri ile yapılmaktadır. Örneğin:
gcc -c sample.c
cl /c sample.c
Burada UNIX/Linux sistemlerindeki gcc derleyicisinin -c seçeneği "derle fakat link etme" anlamına gelmektedir. Benzer biçimde Windows
sistemlerinde Microsoft C derleyicilerindeki /c seçeneği de aynı anlama gelmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux dünyasında komut satırı argümanlarının oluşturulması için geniş bir kesim tarafındna 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 belirtilesi 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çebek 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ıyrodu. 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çenkler de arümanlı ve arümansız olabilmektedir. Yukarıdaki örnekte "--count" arü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çeneklerde 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
Genel olarak bugün 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 standratları uzun seçenekleri desteklememektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda ele aldığımız komut satırı argüman sentaksı her program tarafından yukarıda açıklandığı gibi uygulanmayabilmektedir. Özellikle
henüz bu biçimin tam oturmadığı eski bazı programlarda burada ele aldığımız stil tam olarak uygulanmamıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux dünyasında kullanılan komut satırı argümanlarını parse etmek için getopt ve getopt_long isimli iki fonksiyon bulunudurlmuş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 paramteresi main fonksiyonunun argc ve argv parametreleri gibidir. Yani programcı main fonksiyonunun bu parametrelerini
getopt fonksiyonuna geçirir. Fonksiyon üçü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 ':' karakteri onun 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 dnmektedir. O halde fonksiyonun
çağrılma kalıbı şöyle olmaldı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 almamaı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ü
ekleyerel 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 dğeişken vardır. Bu global değişkenler kütüphanenin içeisinde tanımlanmıştır. Bunları biz
extern bildirimi ile kullanabiliriz. Ancak bunların extern bildirimleri zaten <unistd.h> dosyası içierisinde 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
hata 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ı stder 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ığıı 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 eç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ümnalrı 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 getirmekltedir:
./sample -a -b veli -c ali selami
Şimdi burada optind indeksi artık "ali" argümanının bşaldağı indeksi belirtecektir. Onun ötesindeki türm argümanlar seçeneksiz argümanlardır.
Bu argümanları while dönü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 fonksiyonunun kullanımına bir örnek. Burada -a, -b ve -c seçenekleri vardır. -z ve -c seçenekleri argümansız seçenek -b seçeneği
ise argümanlı bir seçenektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int result;
int a_flag, b_flag, c_flag;
char *b_arg;
a_flag = b_flag = c_flag = 0;
opterr = 0;
while ((result = getopt(argc, argv, "ab:c")) != -1) {
switch (result) {
case 'a':
a_flag = 1;
break;
case 'b':
b_flag = 1;
b_arg = optarg;
break;
case 'c':
c_flag = 1;
break;
case '?':
if (optopt == 'b')
fprintf(stderr, "-b option given without argument!..\n");
else
fprintf(stderr, "invalid option: -%c\n", optopt);
break;
}
}
if (a_flag)
printf("-a given...\n");
if (b_flag)
printf("-b given with argument \"%s\"...\n", b_arg);
if (c_flag)
printf("-a given...\n");
if (argc - optind != 0)
printf("argument without options:\n");
for (int i = optind; i < argc; ++i)
puts(argv[i]);
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
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şkenindne 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, err_flag;
char *b_arg;
a_flag = b_flag = c_flag = err_flag = 0;
opterr = 0;
while ((result = getopt(argc, argv, "ab:c")) != -1) {
switch (result) {
case 'a':
a_flag = 1;
break;
case 'b':
b_flag = 1;
b_arg = optarg;
break;
case 'c':
c_flag = 1;
break;
case '?':
if (optopt == 'b')
fprintf(stderr, "-b option given without argument!..\n");
else
fprintf(stderr, "invalid option: -%c\n", optopt);
err_flag = 0;
break;
}
}
if (err_flag)
exit(EXIT_FAILURE);
if (a_flag)
printf("-a given...\n");
if (b_flag)
printf("-b given with argument \"%s\"...\n", b_arg);
if (c_flag)
printf("-a given...\n");
if (argc - optind != 0)
printf("argument without options:\n");
for (int i = optind; i < argc; ++i)
puts(argv[i]);
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
7.Ders 18/06/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
getopt fonksiyonun kullanımına bir örnek. Bu örnekte disp isimli program şu klomut 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.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* disp.c */
#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, "canno 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;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
getopt kullanımına bir örnek. Aşağıdaki mycalc programı iki komut satırı argümanı üzerinde dört işlem uygulamaktadır.
Uygulanacak işlemler seçeneklerle belirtmiştir. Seçenekler şunlardır:
-a (addition)
-s (subtraction)
-m (multiplication)
-d (division)
Programın kullanımı şöyledir:
./mycalc [-asmd] <number1> <number2>
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* mycalc.h */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int result;
int a_flag, s_flag, m_flag, d_flag;
int err_flag;
double arg1, arg2, val_result;
opterr = 0;
a_flag = s_flag = m_flag = d_flag = 0;
err_flag = 0;
while ((result = getopt(argc, argv, "asmd")) != -1) {
switch (result) {
case 'a':
a_flag = 1;
break;
case 's':
s_flag = 1;
break;
case 'm':
m_flag = 1;
break;
case 'd':
d_flag = 1;
break;
case '?':
fprintf(stderr, "invalid option: -%c\n", optopt);
err_flag = 1;
}
}
if (err_flag)
exit(EXIT_FAILURE);
if (a_flag + s_flag + m_flag + d_flag != 1) {
fprintf(stderr, "exactly one option must be specified!..\n");
exit(EXIT_FAILURE);
}
if (argc - optind != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
arg1 = strtod(argv[optind], NULL);
arg2 = strtod(argv[optind + 1], NULL);
if (a_flag)
val_result = arg1 + arg2;
else if (s_flag)
val_result = arg1 - arg2;
else if (m_flag)
val_result = arg1 * arg2;
else
val_result = arg1 / arg2;
printf("%f\n", val_result);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
8. Ders 01/07/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
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 senek eşdeğeir 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ılmakatdı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ı biaz 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 iğse bu udurmda 0 değeri ile geri döner. Ancak bu flag
göstewricisine 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 ndeks 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ğerler 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 gerö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çerisz 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şışığını
optopt değişkenine yerleştirir.
2) Eğer fonksiyon argümanlı bir uzunseç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 duurmda 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 duurmda optopt değişkenine 0 değeri yerleştirilmektedir.
Maalesef getopt_long olmayan bir uzun seçenek girildiğinde bunubize 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.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
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;
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte bir ya da birden fazla dosyanın içeriği görüntülenmektedir. Buradaki seçenekler şunlardır:
-x: Hex olarak görünler
-o: Octal olarak görüntüler
-t: Yazı olarak görüntüler
-h ya da --header: Her dosyanın başına dosya ismini de yazar
--line[=<arg>]: Bu argüman hiç belirtilezse tüm dosya görüntülenir. İsteğe bağlı argüman girilmezse 10 satır görüntülenir.
İsteğe bağlı argüman girilirse girildiği kadar satır görüntülenir.
myod [--top --header -x -o -t -h] <dosya listesi>
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* myod.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
/* Symbolic Constans */
#define DEF_LINE 10
#define HEX_OCTAL_LINE_LEN 16
/* Function Prorotypes */
int print_text(FILE *f, int nline);
int print_hex_octal(FILE *f, int nline, int hexflag);
int main(int argc, char *argv[])
{
int result, err_flag = 0;
int x_flag = 0, o_flag = 0, t_flag = 0, top_flag = 0, header_flag = 0;
char *top_arg;
struct option options[] = {
{"top", optional_argument, NULL, 1},
{"header", no_argument, NULL, 'h'},
{0, 0, 0, 0}
};
FILE *f;
int i, nline = -1;
opterr = 0;
while ((result = getopt_long(argc, argv, "xoth", options, NULL)) != -1) {
switch (result) {
case 'x':
x_flag = 1;
break;
case 'o':
o_flag = 1;
break;
case 't':
t_flag = 1;
break;
case 'h':
header_flag = 1;
break;
case 1:
top_flag = 1;
top_arg = optarg;
break;
case '?':
if (optopt != 0)
fprintf(stderr, "invalid switch: -%c\n", optopt);
else
fprintf(stderr, "invalid switch: %s\n", argv[optind - 1]); /* argv[optind - 1] dokümante edilmemiş */
err_flag = 1;
}
}
if (err_flag)
exit(EXIT_FAILURE);
if (x_flag + o_flag + t_flag > 1) {
fprintf(stderr, "only one option must be specified from -o, -t, -x\n");
exit(EXIT_FAILURE);
}
if (x_flag + o_flag + t_flag == 0)
t_flag = 1;
if (top_flag)
nline = top_arg != NULL ? (int)strtol(top_arg, NULL, 10) : DEF_LINE;
if (optind == argc) {
fprintf(stderr, "at least one file must be specified!..\n");
exit(EXIT_FAILURE);
}
for (i = optind; i < argc; ++i) {
if ((f = fopen(argv[i], "rb")) == NULL) {
fprintf(stderr, "cannot open file: %s\n", argv[i]);
continue;
}
if (header_flag)
printf("%s\n\n", argv[i]);
if (t_flag)
result = print_text(f, nline);
else if (x_flag)
result = print_hex_octal(f, nline, 1);
else
result = print_hex_octal(f, nline, 0);
if (i != argc - 1)
putchar('\n');
if (!result)
fprintf(stderr, "cannot read file: %s\n", argv[i]);
fclose(f);
}
return 0;
}
int print_text(FILE *f, int nline)
{
int ch;
int count;
if (nline == -1)
while ((ch = fgetc(f)) != EOF)
putchar(ch);
else {
count = 0;
while ((ch = fgetc(f)) != EOF && count < nline) {
putchar(ch);
if (ch == '\n')
++count;
}
}
return !ferror(f);
}
int print_hex_octal(FILE *f, int nline, int hexflag)
{
int ch, i, count;
const char *off_str, *ch_str;
off_str = hexflag ? "%07X " : "%012o";
ch_str = hexflag ? "%02X%c" : "%03o%c";
if (nline == -1)
for (i = 0; (ch = fgetc(f)) != EOF; ++i) {
if (i % HEX_OCTAL_LINE_LEN == 0)
printf(off_str, i);
printf(ch_str, ch, i % HEX_OCTAL_LINE_LEN == HEX_OCTAL_LINE_LEN - 1 ? '\n' : ' ');
}
else {
count = 0;
for (i = 0; (ch = fgetc(f)) != EOF && count < nline; ++i) {
if (i % HEX_OCTAL_LINE_LEN == 0)
printf(off_str, i);
printf(ch_str, ch, i % HEX_OCTAL_LINE_LEN == HEX_OCTAL_LINE_LEN - 1 ? '\n' : ' ');
if (ch == '\n')
++count;
}
}
if (i % HEX_OCTAL_LINE_LEN != 0)
putchar('\n');
return !ferror(f);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
İşletim sistemlerinin çekirdekleri C Programlama Dili kullanılarak yazılmıştır. (Çekirdek kodlamasında C++ kullanılmamaktadır. Yaygın
işletim sistemlerinin bütün çekirdek kodları C'de yazılmış durumdadır.) Dolayısıyla işletim sistemlerinin çekirdekleri
"nesne yönelimli (object oriented)" değil "prosedürel teknikle" yazılmaktadır. Örneğin Linux çekirdeğinde binlerce fonksiyon bulunmaktadır.
İşletim sistemlerinin çekirdeklerindeki bir grup fonksiyon dışarıdan da uygulama programcıları tarafından çağrılabilmektedir. Bu fonksiyonlara
"işletim sisteminin sistem fonksiyonları (system call)" denilmektedir. İşte dosya açma, kapama, dosyadan okuma yazma yapma yapma gibi,
dosya silme gibi, başka bir programı çalıştırma gibi işlemler aslında ilgili işletim sisteminin sistem fonksiyonları ile yapılmaktadır.
Biz bir dosyasyı C'de fopen isimli standart C fonksiyonu ile açarız. Ancak aslında dosya açma işinin asıl sorumlusu işletim sistemidir.
Yani fopen fonksiyonu aslında dosyayı kendisi açmamaktadır. İşletim sisteminin dosya açmakta kullanılan sistem fonksiyonunu çağırmaktadır.
Uygulama programcıları işletim sisteminin içerisindeki diğer fonksiyonları çağıramamaktadır. Yalnızca "sistem fonksiyonu" biçiminde yazılmış
olan fonksiyonları çağırabilmektedir.
İşletim sistemlerinin sistem fonksiyonlarının isimleri, parametrik yapıları işletim sisteminden işletim sistemine hatta aynı işletim sisteminde
versiyondan versiyona değişebilmektedir. Dolaysıyla sistem fonksiyonlarının taşınabilirliği yoktur. Linux işletim sisteminde sistem fonksyionlarının
isimleri sys_xxxx öneki ile başlatılarak isimlendirilmiştir.
UNIX türevi sistemlerdeki ortak C fonksiyonlarına POSIX fonksiyonları denilmektedir. POSIX kütüphanesinin amacı mimari olarak birbirine benzeyen
ancak çekirdek kodlaması bakımından birbirinden farklı olan UNIX türevi sistemlerde kulalnılabilecek ortak taşınabilir fonksiyonların
oluşturulmasıdır. POSIX fonksiyolarının bazıları doğrudna işletim sistemindeki sistem fonksiyonlarını bire bir çağırıyor olabilir. Bazı
POSIX fonksiyonları ise işletim sisteminin hiçbir sistem fonksiyonunu çağırtmıyor olabilir. Bazı POSIX fonksiyonları birden fazla sistem fonksiyonu
çağrılarak gerçekleştirilmiş de olabilir.
Windows API fonksiyonları da Windows sistemleri üzerinde taşınabilir bir kütüphane oluşturmaktadır. Yani nasıl POSIX kütüphanesai UNIX
türevi sistemlerin ortak fonksiyonlarını barındırıyorsa Windows API fonksiyonları da Windows sistemlerinde kullanılabilen ortak fonksiyonları
barındırmaktadır. Başka bir deyişle düzey v eişlev olarak POSIX fonksiyonlarıyla Windows API fonksiyonları benzerdir. Yine bazı Windows API
fonksiyonları Windows'un sistem fonksiyonlarını çağırabilmekte bazıları ise bir sistem çağrısı yapmayabilmektedir.
Standart C fonksiyonları her C derleyicisinde bulunan ortak fonksiyonlardır. Tabii bunların bazıları UNIX türevi sistemlerde POSIX fonksiyonları
çağrılarak Windows sistemlerinde ise Windows API fonksiyonu çağrılarak yazılmış durumdadır. Örneğin fopen fonksiyonu Linux sistemlerinde
open isimi POSIX fonksiyonunu çağırmakta, open POSIX fonksiyonu ise sys_open sistem fonksiyonunu çağırmaktadır:
fopen ---> open ---> sys_open
Aynı fopen fonksiyonu Windows sistemlerinde CreateFile isimli API fonksiyonunu çağırmakta CreateFile Windows'un sistem fonksiyonunu çağırmaktadır.
fopen ---> CreateFile ---> NtCreeateFile
Tabii fopen fonksiyonu UNIX türevi sistemlerde yalnızca open fonksiyonunu, Windows sistemlerinde CreateFile fonksiyonunu çağırmamaktadır.
Başka şeyler de yapmaktadır. Ancak asıl dosyanın açılması yukarıda belirrtiğimiz çağrılarla işletim sistemi tarafındna yapılmaktadır.
Java, C# ve Python gibi dillerde de tamamen bu tür işlemler benzer biçimde gerçekleştirilmektedir. Örneğin Python ile Linux sistemlerinde
bir dosya açılmak istendiğinde dosya open isimli fonksiyonla açılmaktadır. Aslında bu open fonksiyonu fopen fonksiyonunu çağırmaktadır.
open (Python) ---> fopen --> open ---> sys_open
Pekiyi bir C programcısının doğrudan işletim sisteminin sistem fonksiyonlarını çağırmasına gerek duyulmakta mıdır? Aslında çoğu zaman
C programcıları UNUX/Linux sistemlerinde POSIX fonksiyonlarını Windows sistemlerinde ise Windows API fonksiyonlarını kulalnmaktadır.
Ancak çok seyrek de olsa bazen C prograöcısının doğrudan ilgili sistem fonksiyonunu çağırması gerekebilmektedir. Örneğin Linux sistemlerinde
bazı sistem fonksiyonları hiçbir POSIX fonksiyonu tarafından çağrılmamaktadır. Programcı mecburen gerektiği zaman onları kendisi çağırır.
Ancak böylesi gereksinimler oldukça seyrektir.
Pekiyi bir C programcısının işletim sistemiyle ilgili en aşağı etkişelmi nasıl olmaktadır? İşte C programcısı en aşağı düzeyde aslında
sistem fonksiyonlarını kendisi çağırarak işini yapabilir. Ancak genellikle bunun yerine C programcıları UNIX/Linux sistemlerinde POSIX
fonksiyonlarını çağırarak, Windows sistemlerinde de Windows API fonksiyonlarını çağırarak aşağı seviyeli işlemleri yaparlar. Aslında C
programcısının işletim sistemi ile en yakın olabildiği çalışma biçimi "aygıt sürücü (device driver)" ya da "çekirdek modülü (kernel module)"
yazımı sırasındadır. Aygıt sürücüler ve çekirdek modülleri adeta çekirdeğin içerisine bir modül eklemek anlamına geşmektedir.
Bu durumda eğer biz bir işlemi etkin bir biçimde standart C fonksiyonları ile yapabiliyorsak onları çağırarak yaparız. Eğer standart C fonksiyonları
bu işleme yetmiyorsa bu durumda UNIX/Linux sistemlerinde POSIX fonksiyonlarını Windows sistemlerinde de Windows API fonksiyonlarını kullanmalıyız.
Eğer hala bunlar yetersiz kalıyorsa işletim sisteminin sistem fonksiyonlarını doğrudan çağırabiliriz.
Örneğin bir dizin (directory) yaratmak istediğimizi düşünelim. Bu işi yapacak bir standart C fonksiyonu yoktur. O halde biz bu işlem için
UNIX/Linux sistemlerinde mkdir isimli POSIX fonksiyonunu Windows sistemlerinde de CreateDirectory isimli API fonksiyonunu kullanırız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
9. Ders - 02/07/2023-Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows'un API fonksiyonlarının prototipleri çeşitli başlık dosyalarının içerisinde bulundurulmuştur. Ancak en genel başlık dosyası
(bu dosya başka başlık dosyalarını da kendi içerisinde include etmektedir) <windows.h> isimli dosyadır. Dolayısıyla bir Windows API
fonksiyonu kullanılacaksa bu dosyanın include edilmesi gerekir.
Windows API fonksiyonlarının yazı parametresi alanları ASCII ve UNICODE karakter kümeleri için iki farklı fonksiyon biçiminde yazılmıştır.
Örneğin yazı parametresi alan API fonksiyonun isminin xxxxx olduğunu varsayalım. Aslında xxxxx isimli bir fonksiyon yoktur. xxxxxA ve
xxxxxW biçiminde fonksiyonlar vardır. Ancak biz bu fonksiyonu kullanırken xxxxx ismini kullanırız. Önişlemci bu xxxxx ismini "yapılmış olan
ayara göre" xxxxxA ya da xxxxxW haline getirmektedir. Biz kursumuzda bu API fonksiyonlaarın ASCII versiyonlarını kullanacağız. Ancak bir proje
yaratıldığında Visual Stduio default ayarı UNICODE yapmaktadır. Bu nedenle bir proje yarattıktan sonra proje seçeneklerinden Advanced
sekmesinden "Cahatacter Set" seçeneği "Not Set" haline getirilmelidir. Aksi takdirde bizim bu API fonksiyonlarına UNICODE yazılar girmemiz
gerekir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows dünyasında çalışırken Windows sistemleri arasındaki taşınabilirliğin sağlanması ve okunabilirliğin artırılması için Microsoft
tarafından çeşitli typedef isimleri oluşturulmuştur. Windows API programcısının bu typedef isimlerine aşina olması gerekmektedir. Bu typedef
isimlerinin hepsi büyük harflerle oluşturulmuştur. Programcı da kendi fonksiyonlarını yazarken mümkün olduğunca bu typedef isimlerini
kullanmalıdır. Bu typedef bildirimleri <windows.h> içerisinde bulunmaktadır. Önemli olanları şunlardır:
BYTE: 1 byte uzunlupunda işaretsiz tamsayı türü (tipik olarak unsigned char)
WORD: 2 byte uzunlupunda işaretsiz tamsayı türü (tipik olarak unsigned short)
DWORD: 4 byte uzunlupunda işaretsiz tamsayı türü (tipik olarak unsigned int)
QWORD: 8 byte uzunlupunda işaretsiz tamsayı türü (tipik olarak unsigned long long int)
Adres türleri başına P ya da LP öneki getirilerek isimlendirilmiştir. 16 bit Windows sistemlerinde P öneki "near pointer", LP öneki
"far pointer" için kullanılıyordu. Ancak daha sonra 32 Windows sistemlerine geçildiğinde artık göstericiler arasında "yakın" ve "uzak"
ayrımı kalkmıştır. Dolayısıyla P öneki ile LP öneki arasında bir fark kalmamıştır. Ancak geleneksel olarak programcılar P öneki yerine LP
önekini tercih etmektedir. Bir göstericinin gösterdiği yerin const olduğu P ya da LP önekinden sonraki C ekiyle belirtmiştir. Örneğin
PC ya da LPC önekleri "gösterdiği yer const olan" göstericiler için kullanılmaktadır. Göstericilerin türü de bu öneki izlemektedir.
Örneğin LPCDWORD tür ismi "const DWORD *" anlamına gelmektedir. Ancak LPDWORD tür ismi "DWORD *" anlamına gelmektedir. void türü VOID olarak
da typedef edilmiştir. Bu duurmda örneğin LPVOID tür ismi "void *" anlamına gelir. LPCVOID tür ismi ise "const void *" anlamına gelmektedir.
char türden adresler PSTR ya da LPSTR ismiyle typedef edilmiştir. Bunlar const biçimleri de PCSTR ya da LPCSTR biçiminde isimlendirilmiştir.
HANDLE tür ismi genel olarak "handle" belirtir. Bu tür ismi "void *" olarak typedef edilmiştir.
PSTR, PCSTR, LPSTR ve LPCSTR gibi yazıları gösteren adres türlerinin T'li biçimleri de vardır. Örneğin LPSTR türünün yanı sıra LPTSTR türü gibi,
LPCSTR türünün yanı sıra LPCTSTR türü gibi. Bu T'li türler seçilen konfigürasyona göre ASCII ya da UNICODE olabilen türleri temsil etmektedir.
C'nin standart türleri de (her ne kadar gerekmiyorsa da) büyük harflerle typedef edilmiştir. Örneğin INT, LONG, CHAR, DOUBLE gibi typedef
isimleri orijinal C türlerine karşılık gelmektedir.
BOOL türü int olarak typedef edilmiştir. Bir API fonksiyonun geri dönüş değerinde BOOL gördüğümüzde bu geri dönüş değerinin başarı/balarısızlık
biçiminde bir değer verdiğini anlarız. Örneğin:
BOOL SomeFunc(void);
Yapılar büyük harflerle isimlendirilmiştir. Yapı isimlerinin başına "tag" öneki getirilmiştir. Ancak typedef isimlerinde bu "tag" öneki yoktur.
Örneğin:
typedef struct tagRECT {
...
} RECT;
Yine yapılar türünden adresler de bu yapı isimlerinin başına P, LP, PC, LPC gibi önekler getirilerek isimlendirilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows API programlamada kullanılan isimlendirme sistemine "Macar Notasyonu (Hungarian Notation)" denilmekteir. Windows API programcılarının
bu Macar notasyonu hakkında bilgi sahibi olması gerekmektedir. Macar notasyonun en belirgin özelliklerinden biri değişken isimlerinin başında
değişkenlerin türlerini belirten öneklerin bulunmasıdır. Öneklerin bazıları şunlardrı:
b BYTE, BOOL
w WORD
dw DWORD
p, lp Adres
sz CHAR türden yazı belirten diziler
psz, lpsz CHAR türden yazı belirten adres
pc CHAR türden adres
h HANDLE
l LONG
u UNSIGNED INT
lu UNSIGNED LONG
Yapı türünden değişkenlerde o yapıya ilişkin küçük önekler kullanılmaktadır. Örneğin rect öneki dikdörtgen belirten RECT yapısı türünden
değişkenlerde kullanılır.
Macar Notasyonunda bir değişken ismi onun türünü belirtne önekten sonra her sözcüğün ilk harfi büyük yazılarak isimlendirilmektedir. Örneğin:
DWORD dwNumberOfSectors;
LONG lStudentNumber;
char szName[64];
DWORD dwWordCount;
LPSTR lpszFileName;
Macar notasyonundaki önek getirme bazı programcılar tarafından daha gevşek bir biçimde yapılabilmektedir. Örneğin Microsoft da int türü
için özel bir önek kullanmamaktadır. Değişkenler için önek kullanılmıyor olsa da değişken harflendirilmesi "deve notasyonuna (camel sasting)"
uygun yapılmalıdır. Örneğin:
int numberOfStudents;
Deve notasyonunda ilk sözcük tamamen küçük harflerle sonraki sözcüklerin yalnızca ilk harflari büyük harflerle yazılmaktadır. Tabii değişken
ismi zaten tek sözcükten oluşyorsa sözcüğün tamamı küçük harflerle yazılır. Örneğin:
int i;
int count;
Tabii değişken türe ilişkin bir önek içeriyorsa zaten bu önek deve notasyonundaki ilk sözcüğün yerini tutmaktadır.
Macar Notasyonunda değişken isimleri biraz uzun olma eğilimindedir. Bu notasyon programcılar için yorucu olabilmektdir.
Macar notasyonunda fonksiyon isimleri "Pascal tarzı harflendirmeyle (Pascal casting)" oluşturulmaktadır. Pascal notasyonunda her
sözcüğün (ilk sözcük de dahil) ilk harfi büyük yazılmaktadır. Fonksiyon isimlerinde önce eylem belirten sözcük sonra o eylemin nesnesine
ilişkin sözcükler kullanılmaktadır. Örneğin:
CreateFile
ReadFile
GetWindowLong
SetWindowText
Windows sistemlerinde Microsoft'un C ve C++ derleyicileri zaten default olarak API fonksiyonlarının bulunduğu kütüphane dosyalarına
link aşamasında referans etmektedir. Dolayısıyla API fonksiyonlarının kullanılması için porgramcının özel bir şey yapması gerekmemektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux dünyasında POSIX fonksiyonlarında Windows'ta olduğu gibi özel bir yazım notasyonu kullanılmamıştır. POSIX fonksiyonları
klasik C isimlendirme tekniği ile isimlendirilmiştir. Fonksiyon isimlerinde hiç büyük harf kullanılmamıştır. Fonksiyon ismi uzunsa
sözcükleri ayırmak için alt tire kullanılmıştır. Genellik POSIX fonksiyonlarının isimleri kısa olma eğilimindedir. POSIX fonksiyonlarında
taşınabilirliği sağlamak için bazı typedef türleri de oluşturulmuştur. POSIX'in bu typedef türlerinin hemen hepsi <sys/types.h> dosyası
içerisinde bildirilmiştir. Fonksiyonların prototipileri ve bunlara ilişkin sembolik sabitler çeşitli başlık dosyalarında topalnmıştır.
Pek çok temel fonskyionun prototipi <unistd.h> isimli bir başlık dosyasında bulunmaktadır. POSIX sistemlerinde typedef isimleri xxxx_t biçiminde
"_t" soneki ile isimlendirilmiştir. Örneğin pid_t, pthread_mutex_t gibi. POSIX fonksiyonlarının kullanımı için programcının genellikle
link aşamasında özel bir kütüphaneye referans etmesi gerekmemektedir. Çünkü POSIX fonksiyonlarının çoğu standart C fonksiyonlarıyla aynı kütüphane
içerisinde (bunu "glibc" kütüphanesi denilmektdir) bulunmaktadır. Ancak bazı POSIX fonksiyonları başka kütüphanelerin içerisinde olduğu için
onlar kullanılırken o kütüphanelere referans etmek gerekmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Sistem programlamada yapılan işlemlerde çeşitli sorunlar ortaya çıkabilmektedir. Bu sorunların kaynağı programcının dışında da olabilmektedir.
Dolayısıyla sistem programcısının çağırdığı fonksiyonların başarısını kontrol etmesi ve sorunlar karşısında düzgün bir biçimde sorunun kaynağını
belirten hata mesajlarını oluşturması gerekir. Pekiyi het rürlü fonksiyonun başarısı kontrol edilmeli midir? Bu bakımdan fonksiyonları üç
gruba ayırabiliriz:
1) Her çağrıda başarı kontrolünün yapılması gerektiği fonksiyonlar: Bu fonksiyonlarda başarısızlık çok değişik faktörlere ve sistemin o anda
içinde bulunduğu koşullara bağlı olabilmektedir. Bu nedenle bu tür fonksiyonların başarılarının mutlaka programcı tarafından kontrol edilmesi
gerekir. Örneğin fopen gibi malloc gibi fonksiyonlar bu gruba örnek verilebilir.
2) Eğer programcı her şeyi doğru yapmışsa başarısız olma olasılığı olmayan fonksiyonlar: Bu tür fonksiyonlarda hata kontrolü yapılmaayabilir.
Çünkü bu tür fonksiyonlar "biz her şeyi düzgün yapmışsak başarısız olma olasılığı olmayan" fonksiyonlardır. Tabii programcı birtakım hatalar
yapmış olabilir. Bu nedenle bu bruptaki fonksiyonlar başarısız olmuş olabilir. Bu tür durumlarda hata kontrolleri projenin "debug" versiyonunda
yapılabilir "release" versiyonunda yapılmayabilir. Biz kursumuzda bu grup fonksiyonların başarısını kontrol etmeyeceğiz. fclose fonksiyonunu
bu gruba örnek verebiliriz. Biz her şeyi doğru yapmışsa fclose fonksiyonun başarısız olma olasılığı yoktur.
3) Başarısz olma olasılığının olmadığı ya da bunun tespit olanağının olmadığı fonksiyonlar: Bazı fonksiyonların başarısız olma olasılığı yoktur.
Bazı fonksiyonların ise başarısız olma olasılığı olsa da bu başarısızlığın tespit edilmesi olanağı yoktur. Örneğin free fonksiyonu dinamik alanının
geçerli alan olup olmadığını tespit edememektedir. Dolayısıyla free fonksiyonu bize zaten başarı-balarısızlık ile ilgili bir bilgi de
vermemektedir. Örneğin getpid POSIX fonksiyonu o anda çalışmakta olan prosesin id değerini bize verir. Bu fonksiyon başarısız olamaz.
Bu fonksiyonun başarısız olması işletim sisteminni kendisini inkar etmesi anlamına gelmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
10. Ders - 08/07/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyonların başarısının tespit edilmesinin yanı sıra onların neden başarısız olduğunun da belirlenmesi gerekebilmektedir. Biz başarısız
olan bir fonksiyon için başarısızlığın nedenini belirten bir hata mesajı vermek isteyebiliriz. Böylece kullanıcı başarısızlığın nedenini anlayıp
onu telafi edici işlemler yapabilir. Bu bölümde biz Windows sistemlerinde ve UNIX/Linux sistemlerinde başarısızlığın nedeninin nasıl
elde edileceği üzerinde duracağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde bir API fonksiyonu başarısız olmuşsa neden başarısız olduğu GetLastError isimli fonksiyonla tespit edilir.
Bu fonksiyonun prototipi şöyledir:
DWORD GetLastError(void);
Fonksiyon her farklı error gerekçesi için değişik bir değer vermektedir. Bu değerler aynı zamanda <windows.h> içerisinde
ERROR_XXXX biçiminde sembolik sabitlerle define edilmiştir. Tabii GetLastError fonksiyonından elde edilen değeri özel durumlar dışında
kontrol etmek zordur ve oldukça zahmetlidir. Windows sistmlerinde belli bir API fonksiyonunun başarısız olması durumunda "last error"
değerinin hangi seçeneklerden biri olacağı belirtilmemiştir. (Halbuki POSIX fonksiyonlarında başarısızlığın tüm olası nedenleri dokğmanlarda
listelenmiştir.) Dolayısıyla Windows programcıları "last error" değerine ilişkin tam bir switch kontrolü yapamamaktadır.
Microsoft API fonksiyonlarına ilişkin bütün hata kodlarını ve onlara karşı gelen sembolik sabitleri listelemiştir. Bu listeye aşağıdaki
bağlantıdan erişebilirsiniz:
https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes
Programcı API fonksiyonu başarısız olduğunda "last error" değerini yazdırabilir. Kullanıcı da bu error değerinin açıklamasını yukarıdaki
bağlantıdan görebilir. Örneğin:
if (!SomeAPIFunc(...)) {
fprintf(setderr, "SomeAPIFunc failed: %lu\n", GetLastError());
exit(EXIT_FAILURE);
}
Bir API fonksiyonu başarılı olduğunda "last error" değerini özel bir değere çekmek zorunda değildir. Dolayısıyla GetLastError fonksiyonu
yalnızca başarısızlık durumunda (bazı istisnalar vardır) çağrılmalıdır.
Windows'ta GetLastError fonksiyonun verdiği "last error" kodu thread'e özgüdür. Yani her thread'in ayrı bir "last error" kodu vardır.
Thread'ler konusu ileride ele alınacaktır.
Hata nedenine ilişkin "last error" kodunun kullanıcılar için anlaşılması zordur. Bunun yerine ilgili hata koduna ilşkin bir hata yazısının
rapor edilmesi daha uygundur. FormatMessage isimli API fonksiyonu "last error" değerini alarak bize o hata koduna ilişkin bir hata yazısı
vermektedir. FormatMessage fonksiyonunun kullanımı biraz zordur. Biz kursumuzda ExitSys isimli bir fonksiyon kullanacağız. Bu fonksiyon bu fonksiyon
"last error" değerinden hareketle hata yazısını stderr dosyasına basıp prosesi exit fonksiyonuyla sonlandırmaktadır:
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
Fonksiyon bizden bir yazıyı argüman olarak alır. Önce o yazıyı bastırır. Sonra ':' karakteri ve bir boşluk karakteri bastırıp
"last error" değerine karşılık gelen hata mesajını yazar. Örneğin:
if ((hFile = CreateFile("xxxxx.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
if (CreateSemaphore(NULL, -10, 1, "") == NULL)
ExitSys("CreateSemaphore");
printf("Ok\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Programcı isterse last error değerini kendisi de belli biğerle set edebilir. Bunun için SetLastError isimli API fonksiyonu kullanılmaktadır.
Fonksiyonun prototipi şöyledir:
void SetLastError(DWORD dwErrCode);
Örneğin:
BOOL Foo(LPCSTR lpszName)
{
if (strlen(lpszName) == 0) {
SetLastError(ERROR_BAD_LENGTH);
return FALSE;
}
...
return TRUE;
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows API fonksiyonlarında çok sayıda fonksiyonun geri dönüş değeri BOOL türündedir. Böyle bir API fonksiyonu gördüğünüzde "fonksiyon
başarılıysa sıfır dışında herhangi bir değere, başarısızsa sıfır değerine geri döndüğünü" düşünmelisiniz. <windows.h> dosyası içerisinde aşağıdaki
gibi iki sembolik de bulunmaktadır:
#define FALSE 0
#define TRUE 1
Geri dönüş değeri BOOL türden olan API fonksiyonlarında başarı kontrolünün aşağıdaki gibi yapılması hatalıdır:
if (SomeAPIFunc() == TRUE) {
...
}
Çünkü fonksiyon başarılıyken 1 değerine değil sıfır dışı herhangi bir değere geri dönebilmektedir. Tabii başarı kontrolünü aşağıdaki
gibi yapabiliriz:
if (SomeAPIFunc()) {
...
}
Başarısızlık kontrolünün aşağıdaki gibi yapılmasında sakınca yoktur:
if (SomeAPIFunc() == FALSE) {
...
}
Fakat pek çok programcı aşağıdaki gibi bir kontrolü tercih etmektedir:
if (!SomeAPIFunc()) {
...
}
------------------------------------------------------------------------------------------------------------------------------------------ */
/*------------------------------------------------------------------------------------------------------------------------------------------
POSIX fonksiyonlarının önemli bir bölümünün geri dönüş değeri int türdendir. Bu fonksiyonlar genellikle başarı durumunda 0 değerine,
başarısızlık durumunda -1 değerine geri dönerler. (Windows sistemlerinde 0 değeri başarısızlığı belirtirken POSIX sistemlerinde tam tersine
başarıyı belirtmektedir.) Bu duurmda geri dönüş değeri int olan bir POSIX fonksiyonunun başarısızlığı şöyle kontrol edilebilir:
if (some_posix_func() == -1) {
...
}
Bazı programcılar başarısızlığı şöyel kontrol etmektedir:
if (some_posix_func() < 0) {
...
}
Böyle bir kontrolün uygulandığını görürseniz fonksiyonun başarısızlık durumunda negatif bir değere geri döndüğü yönünde yanlış bir
izlenime kapılmayınız. Biz kursumuzda başarısızlığı -1 ile karşılaştırarak tespit etmeye çalışacağız.
Bazı POSIX fonksiyonları bir adres değerine geri dönmektedir. Böyle POSIX fonksiyonları başarısızlık durumunda NULL adrese geri dönmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde bir POSIX fonksiyonu başarısız olduğunda başarısızlığın nedeni errno isimli int türden global bir değişkene
sayısal olarak kodlanmaktadır. Dolayısıyla bir POSIX fonksiyonu başarısız olduğunda biz errno değişkenine balkarak başarısızlığın nedenini
tespit edebiliriz. Tıpkı Windows sistemlerinde olduğu gibi errno değişkenine başarısızlık durumunda başvurulmalıdır. POSIX fonksiyonları
başarı durumunda bile bu değeri değiştirebilmektedir. POSIX standartlarında hiçbir POSIX fonksiyonunun errno değişkenini 0'a çekmeyeceği
belirtilmiştir. errno değişkenine değer de atanabilmektedir. errno değişkeninin bildirimi <errno.h> dosyası içerisindedir. Dolayısıyla errno
değişkeninin kullanabilmek için bu dosyanın include edilmesi gerekmektedir. POSIX standartlarına göre errno değişkeni bir makro biçiminde de
bildirilmiş olabilir. Bu nedenle programcının kendi yerel değişkenlerine ya da parametre değişkenlerine errno ismini vermemesi gerekir.
POSIX standartlarında errno değerleri <errno.h> başlık dosyası içerisinde EXXXX biçiminde sembolik sabitlerle define edilmiştir. POSIX
standartlarında hataları ilişkin errno sayıları standardize edilmemiştir. Yalnızca sembolik sabitler standardize edilmiştir. Yani bu durumda
örneğin bir sistemdeki EACCESS error değeri ile diğer sistemdeki EACCESS eror değerinin sayısal değerleri aynı olmak zorunda değildir.
Programcının bu sayısal değerleri değil sembolik sabitleri kullanması gerekir.
POSIX standartlarında bir POSIX fonksiyonunun başarısız olması durumunda errno değişkenine hangi errno değerlerinin yerletirilebileceğine
ilişkin kesin bir liste verilmiştir. Yani biz bir POSIX fonksiyonunun başarısız olması durumunda onun bütün başarısızlık nednelerini
dokümanlardan elde edebilmekteyiz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
POSIX fonksiyonun başarısızlık nedeninin rapor edilmesi nasıl yapılmalıdır? errno değerinin sayısal karşılığının rapor edilmesi yanlıştır.
Çünkü bu sayısal değerler sistemden sisteme değişebilmektedir. (Windows sistemlerinde GetLastError fonksiyonunun verdiği sayısal değerin
rapor edilmesi hatalı değildir. Çünkü Microsoft bu "last error" kodlarını değiştirmeyeceğini belirtmiştir.) errno değişkeninin switch
içerisine alıp her case kısmında hataları yazı olarak yazdırmak da çok zordur. Örneğin:
if ((fd = open("test.dat", O_RDONLY)) == -1) {
switch (errno) {
case EACCESS:
...
case EISDIR:
...
...
}
}
Hatanın nedenini rapor etmek için errno değerini yazıya dönüştüren strerror fonksiyonun faydalanılabilmektedir. strerror fonksiyonunun
prototipi şöyledir:
#include <string.h>
char *strerror(int errnum);
Fonksiyon errno değerini parametre olarak alır ve hataya ilişkin hata yazsının adresine geri döner. Fonksiyonun geri döndürdüğü adres
static bir dizinin adresidir. Öneğin:
if ((fd = open("test.dat", O_RDONLY)) == -1) {
fprintf(stderr, "open failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
Aslında strerror fonksiyonu aynı zamanda standart bir C fonksiyonudur. Ancak errno kavramı C standartlarında olsa da kullanımı çok
sınırlıdır.
strerror fonksiyonu default durumda hata mesajlarını İngilizce (default locale) olarak vermektedir. Eğer hata mesajı Türkçe
elde edilmek isteniyorsa setlocale standart C fonksiyonu ile Türkçe locale'e geçilmelidir. strerror fonksiyonu LS_MESSGAES locale değerini
kullanmaktadır. Ancak değiştrme işleminde konsol penceresinin code page'i için LC_ALL locale değerini kullanabilirisniz. Örneğin:
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 <fcntl.h>
#include <errno.h>
int main(void)
{
int result;
if ((result = open("test.dat", O_RDONLY)) == -1) {
fprintf(stderr, "Open failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("success...\n");
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde perror fonksiyonu (aynı zamanda perror bir standart C fonksiyonudur) önce programcının belirlediği mesajı,
sonra ':' ve SPACE karakterini, sonra da sonra da errno değişkeninin değerinin yazısal karşılığını stderr dosyasına yazdırmaktadır.
#include <stdio.h>
void perror(const char *msg);
Programcılar genellikle başarısızlık durumunda başarısızlığa yol açan POSIX fonksiyonunun ismini perror fonksiyonuna argüman olarak vermektedir.
Örneğin:
if ((fd = open("test.dat, O_RDONLY)) == -1) {
perror("open");
exit(EXIT_FAILURE);
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main(void)
{
int result;
if ((result = open("test.dat", O_RDONLY)) == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
printf("success...\n");
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Genellikle UNIX/Linux sistemlerinde de bir POSIX fonksiyonu başarısız olduğunda programcı hata yazısını ekrana basıp programı sonlandırmaktadır.
Bu işlem çok yapıldığından dolayı bir sarma fonksiyona (wrapper function) devredilebilir. Biz de kursumuzda exit_sys isimli bir sarma
fonksiyon kullanacağız:
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
void exit_sys(const char *msg);
int main(void)
{
int result;
if ((result = open("test.dat", O_RDONLY)) == -1)
exit_sys("open");
printf("success...\n");
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Sistem programalamda bir grup bilgiye erişmek içinkullanılan anahtar niteliğindeki tekil bilgilere "handle" denilmektedir. Handle kavramı
tekil bir bilgi belirtir. Ancak handle sayesinde konu ile ilgili gerekli bilgilere erişilebilmektedir. Handle'lar çeşitli biçimlerde karşımıza
çıkabilir. Örneğin handle bazen int bir değer olabilir. Bu duurmda muhtemelen bu int değer bir yapı dizisinde indeks belirtebilir. Bu değer
sayesinde diğer bilgilere erişilebilir. Bazen handle bir adres biçimindedir. Doğrudan veri yapsının adresini tutabilir. Handle değerleri bazen
bir bozulmuş bir adres gibi de olabilir. Bu durumda biz handle değerini ilgili fonksiyona verdiğimizde fonksiyonu düzeltip erişimi sağlıyor
olabilir. Handle kavramına pek çok örnek verilebilir. Örneğin C'nin dosya fonksiyonları handle kavramını kullanmaktadır. fopen fonksiyonu
dosyayı başarılı bir biçimde açınca dosya bilgilerini FILE isimli bir yapı nesnesinin içine doldurur. Bize de bu FILE besbesinin adresni verir.
Bu FILE adresi (stream) bu anlamda bir handle belirtmektedir. Biz bu adresi diğer dosya fonksiyonlarına paremetre olarak geçiririz. O fonksiyonlar da
bu dosya bilgilerine erişirler. Örneğin:
FILE *f;
...
f = fopen(...);
...
ch = fgetc(f);
...
Bir handle sisteminde genel olarak üç tür fonksiyon bulunur:
1) Handle sistemini yaratan ya da açan fonksiyonlar: Bu fonksiyonlar handle alanını (yani ilgili bilgilerin saklanacağı yeri) tahsis ederler.
O bilgilere ilkdeğerlerini verirler ve handle değeri ile geri dönerler. Örneğin fopen fonksiyonu FILE yapısını tahsis eder. Onun içini doldurur
ve onun adresini handle olarak verir. Handle sisteminin yaratan ya da açan fonksiyonlar genellikle createxxx ya da openxxx biçiminde
isimlendirilirler.
2) Handle sistemini kullanan fonksiyonlar: Bu fonksiyonlar handle değerini parametre olarak alır. Gerekli bilgilere erişerek faydalı
işlemler yaparlar. Örneğin fgetc, fputs gibi fonksiyonlar handle değerini alıp işlem yapmaktadır.
3) Handle sistemini kapatan ya da yok eden fonksiyonlar: Bunlar handle değerini parametre olarak alıp handle alanını tamamen yok ederler
ve handle sistemi yaratılırken ya da açılarak yapılmış olan bazı işlemleri de geri alırlar. Örneğin fclose fonksiyonu handle sistemini
yok eden ya da kapatan fonksiyona örnek olarak verilebilir. Handle sistemini yok eden ya da kapatan fonksiyonlar genellikle DestroyXXX
ya da CloseXXX biçiminde isimlendirilmektedir.
Windows API programalamada başı H harfi ile başlayan tüm tyepdef isimleri void * türündendir ve birer handle belirtmektedir. Her ne kadar
Windows API sisteminde handle türleri void * olarak typedef edilmişse de aslında bunun amacı handle alanının gizlenmesidir. Yani aslında
Windows API sisteminde söz konusu handle değerleri bir grup bilginin bulunduğu alanların adreslerini belirtir. Ancak sanki programcıya
orada bir şey yokmuş gibi void bir adres biçiminde handle'lar verilmektedir. Macar Notasyonunda handle türlerini tutacak değişkenler
h öneki ile isimlendirilmektgedir. Örneğin:
HBITMAP hBitmap;
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
11. Ders 09/07/ 2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Handle sistemine bir örnek
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
typedef int DATATYPE;
typedef struct tagMATRIX {
size_t rowsize;
size_t colsize;
DATATYPE *matrix;
} MATRIX, *HMATRIX;
HMATRIX CreateMatrix(size_t rowsize, size_t colsize);
void CloseMatrix(HMATRIX hMatrix);
void SetElem(HMATRIX hMatrix, size_t row, size_t col, DATATYPE val);
DATATYPE GetElem(HMATRIX hMatrix, size_t row, size_t col);
void DispMatrix(HMATRIX hMatrix);
int main(void)
{
HMATRIX hMatrix1, hMatrix2;
DATATYPE i;
if ((hMatrix1 = CreateMatrix(3, 3)) == NULL) {
fprintf(stderr, "cannot create matrix!..\n");
exit(EXIT_FAILURE);
}
if ((hMatrix2 = CreateMatrix(2, 2)) == NULL) {
fprintf(stderr, "cannot create matrix!..\n");
exit(EXIT_FAILURE);
}
i = 0;
for (int row = 0; row < 3; ++row)
for (int col = 0; col < 3; ++col)
SetElem(hMatrix1, row, col, i++);
i = 0;
for (int row = 0; row < 2; ++row)
for (int col = 0; col < 2; ++col)
SetElem(hMatrix2, row, col, i++);
DispMatrix(hMatrix1);
printf("------------------\n");
DispMatrix(hMatrix2);
CloseMatrix(hMatrix2);
CloseMatrix(hMatrix1);
return 0;
}
HMATRIX CreateMatrix(size_t rowsize, size_t colsize)
{
HMATRIX hMatrix;
if ((hMatrix = (HMATRIX)malloc(sizeof(MATRIX))) == NULL)
return NULL;
hMatrix->rowsize = rowsize;
hMatrix->colsize = rowsize;
if ((hMatrix->matrix = (DATATYPE *)malloc(sizeof(DATATYPE) * rowsize * colsize)) == NULL) {
free(hMatrix);
return NULL;
}
return hMatrix;
}
void CloseMatrix(HMATRIX hMatrix)
{
free(hMatrix->matrix);
free(hMatrix);
}
void SetElem(HMATRIX hMatrix, size_t row, size_t col, DATATYPE val)
{
size_t index;
index = row * hMatrix->colsize + col;
hMatrix->matrix[index] = val;
}
DATATYPE GetElem(HMATRIX hMatrix, size_t row, size_t col)
{
size_t index;
index = row * hMatrix->colsize + col;
return hMatrix->matrix[index];
}
void DispMatrix(HMATRIX hMatrix)
{
for (size_t i = 0; i < hMatrix->rowsize * hMatrix->colsize; ++i)
printf("%d%c", hMatrix->matrix[i], i % hMatrix->colsize == hMatrix->colsize - 1 ? '\n' : ' ');
/*
for (size_t row = 0; row < hMatrix->rowsize; ++row) {
for (size_t col = 0; col < hMatrix->colsize; ++col)
printf("%d ", hMatrix->matrix[row * hMatrix->colsize + col]);
printf("\n");
}
*/
}
/* ------------------------------------------------------------------------------------------------------------------------------------------
Windows API fonksiyonlarında olduğu gibi baen handle alanı içerisindeki bilgilerin kullanıcı gizlenmesi tercih edilebilmektedir.
Bu sayede handle sistemini kullanacak kişilerin kafası karışmaz ve aynı zamanda dokümante edilmemiş handle alanı bilgilerine kişilerin
erişme olanağı da ortadan kaldırılmış olur.
Aşağıdaki örnekte Matrix.c dosyasında matris sisteminin gerçekleştirimi bulunmaktadır. Bu sistemi kullanacak olanlara Matrix.h dosyası ve
matris gerçekeleştiriminin bulunduğu kütüpane dosyası verilecektir. Sistemi kullanacak kişiler Matrix.h başlık dosyasına baksalar bile
orada handle alanına ilişkin bir bilgi göremeyeceklerdir. Böylece handle alanı kullanıcıdan tamamen gizlenmiş olacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* Matrix.c */
#include <stdio.h>
#include <stdlib.h>
/* Type Declarations */
typedef int DATATYPE;
typedef struct tagMATRIX {
size_t rowsize;
size_t colsize;
DATATYPE *matrix;
} MATRIX;
typedef void *HMATRIX;
/* Function Prototypes */
HMATRIX CreateMatrix(size_t rowsize, size_t colsize);
void CloseMatrix(HMATRIX hMatrix);
void SetElem(HMATRIX hMatrix, size_t row, size_t col, DATATYPE val);
DATATYPE GetElem(HMATRIX hMatrix, size_t row, size_t col);
void DispMatrix(HMATRIX hMatrix);
/* Function Definitions */
HMATRIX CreateMatrix(size_t rowsize, size_t colsize)
{
MATRIX *pMatrix;
if ((pMatrix = (HMATRIX)malloc(sizeof(MATRIX))) == NULL)
return NULL;
pMatrix->rowsize = rowsize;
pMatrix->colsize = rowsize;
if ((pMatrix->matrix = (DATATYPE *)malloc(sizeof(DATATYPE) * rowsize * colsize)) == NULL) {
free(pMatrix);
return NULL;
}
return pMatrix;
}
void CloseMatrix(HMATRIX hMatrix)
{
MATRIX *pMatrix = (MATRIX *)hMatrix;
free(pMatrix->matrix);
free(pMatrix);
}
void SetElem(HMATRIX hMatrix, size_t row, size_t col, DATATYPE val)
{
MATRIX *pMatrix = (MATRIX *)hMatrix;
size_t index;
index = row * pMatrix->colsize + col;
pMatrix->matrix[index] = val;
}
DATATYPE GetElem(HMATRIX hMatrix, size_t row, size_t col)
{
MATRIX *pMatrix = (MATRIX *)hMatrix;
size_t index;
index = row * pMatrix->colsize + col;
return pMatrix->matrix[index];
}
void DispMatrix(HMATRIX hMatrix)
{
MATRIX *pMatrix = (MATRIX *)hMatrix;
for (size_t i = 0; i < pMatrix->rowsize * pMatrix->colsize; ++i)
printf("%d%c", pMatrix->matrix[i], i % pMatrix->colsize == pMatrix->colsize - 1 ? '\n' : ' ');
}
/* Matrix.h */
#ifndef MATRIX_H_
#define MATRIX_H_
/* Type Declarations */
typedef int DATATYPE;
typedef void *HMATRIX;
/* Function Prototypes */
HMATRIX CreateMatrix(size_t rowsize, size_t colsize);
void CloseMatrix(HMATRIX hMatrix);
void SetElem(HMATRIX hMatrix, size_t row, size_t col, DATATYPE val);
DATATYPE GetElem(HMATRIX hMatrix, size_t row, size_t col);
void DispMatrix(HMATRIX hMatrix);
#endif
/* App.c */
#include <stdio.h>
#include <stdlib.h>
#include "Matrix.h"
int main(void)
{
HMATRIX hMatrix1, hMatrix2;
DATATYPE i;
if ((hMatrix1 = CreateMatrix(3, 3)) == NULL) {
fprintf(stderr, "cannot create matrix!..\n");
exit(EXIT_FAILURE);
}
if ((hMatrix2 = CreateMatrix(2, 2)) == NULL) {
fprintf(stderr, "cannot create matrix!..\n");
exit(EXIT_FAILURE);
}
i = 0;
for (int row = 0; row < 3; ++row)
for (int col = 0; col < 3; ++col)
SetElem(hMatrix1, row, col, i++);
i = 0;
for (int row = 0; row < 2; ++row)
for (int col = 0; col < 2; ++col)
SetElem(hMatrix2, row, col, i++);
DispMatrix(hMatrix1);
printf("------------------\n");
DispMatrix(hMatrix2);
CloseMatrix(hMatrix2);
CloseMatrix(hMatrix1);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir fonksiyon içerisinde çok sayıda tahsisatın yapıldığını düşünelim. Ancak bunlardan biri başarısız olduğunda önceki tahsisatların da
geri bırakılması gereksin. Buradaki kod tekrarını nasıl engelleyebiliriz? Örneğin:
bool foo(void)
{
...
if ((p1 = malloc(size1)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n);
return false;
}
if ((p2 = malloc(size2)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n);
free(p1);
return false;
}
if ((p3 = malloc(size3)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n);
free(p1);
free(p2);
return false;
}
if ((p4 = malloc(size3)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n);
free(p1);
free(p2);
free(p3);
return false;
}
....
return true;
}
Tabii buradaki genel durumun bir tahsisat işlemi olması gerekmez. Örneğin bir dizi dosyanın açılması gibi bu duruma pek çok örnek verilebilir.
Buradaki ana tema bir grup işlemde bunların başarısız olduğunda fonksiyondan ya da programdan çıkmadan önce önceki işlemlerin geri alınmasıdır.
Yukarıda uygulamış olduğumuz yöntemde kod tekrarı vardır. Burada tipik uygulanması gereken teknik goro ile ters sırada boşaltım tekniğidir.
Bu teknik aşağıdaki gibi kullanılmaktadır:
bool foo(void)
{
bool status;
...
status = false;
if ((p1 = malloc(size1)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n);
goto EXIT1;
}
if ((p2 = malloc(size2)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n);
goto EXIT2;
}
if ((p3 = malloc(size3)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n);
goto EXIT3;
}
if ((p4 = malloc(size3)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n);
goto EXIT4;
}
....
status = true;
EXIT4:
free(p3);
EXIT3:
free(p2);
EXIT2:
free(p1);
EXIT1:
return status;
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Bilindiği gibi C'de char türünün dışındaki türlerin kaç byte yer kaplayacağı bazı minimum kısıtlar konularak derleyicileri yazanların
isteğine bırakılmıştır. Windows'ta 32 bit ve 64 bit C ve C++ derleyicilerinde int türü 4 byte uzunluğundadır. Aynı biçimde 32 bit ve 64 bit
Linux sistemlerinde de int türü 4 byte uzunluktadır. 32 bir ve 64 bit Windows derleyicilerinde long türü de 4 byte uzunluktadır. Ancak
64 bit Linux sistemlerinde long türü 8 byte uzunluğundadır. C11 ile C'ye eklenen long long türü tüm sistemlerde 8 byte uzunlupundadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
İşletim sistemlerinin dosya işlemleri ile ilgili alt sistemlerine "dosya sistemi (file system)" denilmektedir. İşletim sistemlerinin
dosya sistemleri dosyaların ikincil belleklerde organizasyonları ve onların kullanımı ile ilgili temel işlemleri yerine getirmektedir.
İşletim sistemleri programcıların dosya işlemlerini yapabilmesi için bir grup sistem fonksiyonu bulundurmaktadır. Bu sistem fonklsiyonşarının
bazıları dosyadan okuma ve yazma yapılması için kullanılırken bazıları dosya silmek, dosyanın ismini değiştirmek gibi bütünsel işlemlerin
yapılmasında kullanılmaktadır.
İşletim sistemlerinde açık dosya üzerinde işlem yapmak için beş temel sistem fonksiyonu bulundurulmaktadır:
1) Dosyayı açan sistem fonksiyonu
2) Dosyayı kapatn sistem fonksiyonu
3) Dosyadan okuma yapan sistem fonksiyonu
4) Dosyaya yazma yapan sistem fonksiyonu
5) Dosya göstericisini konumlandıran sistem fonksiyonu
Dosya işlemlerinden tamamen işletim sisteminin sorumlu olduğuna programa dillerinin ya da kütüphanelerinin sorumlu olmadığına dikkat ediniz.
Bi zhangi programlama dilinde ve hangi kütüphanede çalışıyor olursak olalım dosya işlemleri eninde sonunda işletim sisteminin yukarıda
belirttiğimiz beş fonksiyonu çağrılarak gerçekleştirilmektedir. Öte yandan sistem fonksiyonlarının taşınabilir olmadığını da belirtmiştik.
Bu nedenle biz temel dosya işlemleri için işletim sisteminin sistem fonksiyonlarını doğrudan çağırmak yerine Windows sistemlerinde
ilgili Windows API fonksiyonlarını UNIX/Linux sistemlerinde ise ilgili POSIX fonksiyonlarını çağırmayı tercih ederiz. Zaten örneğin
Linux sistemlerinde dosyayı açan, dosyayı kapatani, dosyadan okuma yapan, dosyaya yazma yapan, dosya göstericisini konumlandıran sistem
POSIX fonksiyonları doğrudan ilgili sistem fonksiyonlarını çağırmaktadır. Windows sistemlerinde de durum benzerdir.
Programlama dillerindeki o dile özgü dosya fonksiyonları maksimum taşnabilirlik için her sistemde söz konusu olan ortak özellekler
dikkate alınarak tasarlanmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
12.Ders 16/07/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde aşağı seviyeli olarak bir dosyayı açmak (ya da yaratmak) için CreateFile API fonksiyonu,
kapatmak için ise CloseHandle isimli API fonksiyonu kullanılmaktadır. CreateFile fonksiyonunun prototipi şöyledir:
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDistribution,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
CreateFile fonksiyonunun birinci parametresi açılacak dosyaya ilişkin "yol ifadesini (path name)" almaktadır. Fonksiyonun ikinci parametresi
ış modunu belirtmektedir. Bu parametre GENERIC_READ, GENERİC_WRITE ya da GENERIC_READ|GENERIC_WRITE biçiminde girilebilir. Fonksiyonunb üçüncü
parametresi başka bir prosesin dosyayı ne yapmak için açabileceği üzerinde etkili olmaktadır. Bu parametre aşağıdaki sembolik sabitlerin
bir düzeyinde OR işlemiyle birleştirilmesiyle oluşturulabilir.
FILE_SHARE_DELETE: Başka bir proses biizm açmış olduğumuz dosyayı silebilir
FILE_SHARE_READ: Başka bir proses de dosyayı okuma amacıyla açabilir.
FILE_SHARE_WRITE: Başka bir proses de dosyayı yazma yapma amacıyla açabilir.
Bu parametre 0 girilirse başka bir proses dosyayı açamaz.
Fonksiyonun dördüncü parametresi olan lpSecurityAttributes kernel nesnesinin güvenlik (security) bilgilerini belirlemek için kullanılmaktadır.
Windwos sistemlerinde biraz ayrıntılı bir güvenlik mekanizaması vardır. Biz kursumuzda bu konuya girmeyeceğiz. Bu konunun ayrıntıları
"Windows Sistem Programalama" kursunda ele alınmaktadır. Örneğin biz bu paramere sayesinde belirli kullanıcıların bu dosyayı açabilmesini
ancak diğerlerinin dosyayı açamamasını sağlayabiliriz. Bu parametre NULL girilirse nesne default güvenlik bilgisi ile yaratılır. Dosya erişim
işlemleri söz konusu olduğunda özel bir güvenlik mekanizması uygulanmaz.
Fonksiyonun beşinci parametresi (dwCreationDistribution) açılmak istenen dosyanın var ya da yok olması durumunda fonksiyonun nasıl davranacağını
belirtmektedir. Bu parametreler için şu değerlerden biri girilebilir:
OPEN_EXISTING: Ancak zaten var olan bir dosya açılabilir. Eğer dosya yokse fonksiyon başarısız olacaktır.
CREATE_NEW: Olmayan dosyanın yaratılmasını sağlar. Eğer dosya varsa fonksiyon başarısız olur. Yani dosyanın açılabilmesi için var olmaması
gerekir. Bu durumda fonksiyon dosyayı yaratacak ve açacaktır.
OPEN_ALWAYS: Bu durumda dosya varsa olan açılır, yoksa yaratılır ve açılır.
CREATE_ALWAYS: Bu duurmda dosya yoksa yaratılır ve açılır. Dosya varsa sıfırlanır ve açılır.
TRUNCATE_EXISTING: Dosya varsa sıfırlanır ve açılır, dosya yoksa fonksiyon başarısız olur.
Fonksiyonun altıncı parametresi (dwFlagsAndAttributes) dosya yaratılacaksa onun özellik bilgilerini içerir. Eğer olan dosya
ılacaksa dosya açımına ilişkin bazı ayrınları belirlemekte kullanılmaktadır. Eğer dosyayı sıfırdan yaratacaksanız bu parametreye
FILE_ATTRIBUTE_NORMAL değerini girebilrsiniz. Eğer zaten olan dosyayı açacaksanız bu parametreye 0 değeri girebilirsiniz.
Fonksiyonun son parametresine (hTemplateFile) daha önce açılmış olan bir dosyanın hadnle değeri girilebilir. Bu durumda dosya açım işleminde
yukarıda belirttiğimiz bazı özellikler zaten açılmış olan bu dosya referans alınarak kullanılacaktır. Böyle bir durum söz konusu değilse
bu parametre NULL adres olarak geçilebilir.
CreateFile fonksiyonu başarı durumunda açılmış olan dosyaya ilişkin handle değerine, başarısızlık durumunda INVALID_HANDLE_VALUE biçiminde
temsil edilen özel bir değere geri dönmektedir. HANDLE türünün void * biçiminde typedef edildiğini anımsayınız.
Örneğin:
HANDLE hFile;
...
if ((hFile = CreateFile("sample.c", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
Dosyayı kapatmak için CloseHandle isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL CloseHandle(
HANDLE hObject
);
Fonksiyon açık dosyanın handle değerini paranetre olarak alır ve dosyayı kapatır. Başarı durumunda durumunda sıfır dışı bir değere,
başarısızlık durumunda sıfır değerine geri dönmektedir. Örneğin:
CloseHandle(hFile);
Aşağıdaki örnekte dosya açılmış sonra da kapatılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFile;
if ((hFile = CreateFile("test.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
printf("success...\n");
CloseHandle(hFile);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-----------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde dosyadan okuma yapmak için ReadFile API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
Fonksiyonun birinci parametresi açılmış dosyanın handle değerini almaktadır. İkinci parametre okunan bilgilerin yerleştirileceği adresi
belirtmektedir. Üçüncü parametre dosya göstericisinin gösterdiği yerden itibaren okunacak byte sayısını belirtmektedir. Fonksiyon okunan
byte sayısını dördüncü parametresiyle belirtilen DWORD nesnenin içerisine yerleştirir. Dosya göstericisi EOF durumundaysa dosyadan herhangi
bir byte okunamayacaktır. Bu durumda fonksiyon başarı ile geri döner ve bu nesneye 0 değeri yerleştirilir. Fonksiyonun son parametresi asenkron
IO işlemleri iiçin kullanılmaktadır. Bu parametre NULL geçilebilir. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık
durumunda sıfır değerine geri dönemktedir.
Aşağıda bir text dosyanın içeriğini ReadFile çağrıları ile okuyup stdout dosyasına bastıran bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define BUFFER_SIZE 512
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFile;
char buf[BUFFER_SIZE + 1];
DWORD dwRead;
BOOL bResult;
if ((hFile = CreateFile("test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
while ((bResult = ReadFile(hFile, buf, BUFFER_SIZE, &dwRead, NULL)) != 0 && dwRead > 0) {
buf[dwRead] = '\0';
printf("%s", buf);
}
if (!bResult)
ExitSys("ReadFile");
CloseHandle(hFile);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde açık bir dosyaya yazma yapmak için WriteFile isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
Aslında WriteFile fonksiyonunun parametreleri tamamen ReadFile fonksiyonunda olduğu gibidir. Ancak transfer adrsini belirten ikinci
parametre const bir göstericidir. Normal durumda talep edilen miktarda byte dosya yazılır. Ancak diskin dolması gibi uç bazı durumlarda
talep edilen miktardan adah az byte değeri de dosyaya yazılabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte bir döngü içerisinde kaynak dosyadan bir grup byte okunarak hedef dosyaya yazılmak suretiyle dosya kopyaşaması
yapılmıştır. Burada kullanılan tampon büyüklüğünün işletim sisteminin disk blok büyüklüğü ile hizalanması genellikle performansı artırabilmektedir.
Aslında Windows sistemlerinde dosyayı açmadan disk üzerinde alçak seviyeli işlemlerle daha etkin dosya kopyalaması yapan CopyFile isimli
bir API fonksiyonu da bulunmaktadır. Bu fonksiyonun benzeri Linux sistemlerine de eklenmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define BUFFER_SIZE 8192
void ExitSys(LPCSTR lpszMsg);
int main(int argc, char *argv[])
{
HANDLE hFileSource, hFileDest;
char buf[BUFFER_SIZE];
DWORD dwRead, dwWritten;
BOOL bResult;
if (argc != 3) {
fprintf(stderr, "Wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((hFileSource = CreateFile(argv[1], GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
if ((hFileDest = CreateFile(argv[2], GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
while ((bResult = ReadFile(hFileSource, buf, BUFFER_SIZE, &dwRead, NULL)) != 0 && dwRead > 0) {
if (!WriteFile(hFileDest, buf, dwRead, &dwWritten, NULL))
ExitSys("WriteFile");
if (dwWritten != dwRead) {
fprintf(stderr, "Partial write Error!..\n");
exit(EXIT_FAILURE);
}
}
if (!bResult)
ExitSys("ReadFile");
CloseHandle(hFileDest);
CloseHandle(hFileSource);
printf("1 file copied...\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde dosya göstericisinin konumlandırılması için SetFilePointer isimli bir API fonksiyonu bulundurulmuştur. Fonksiyonun
prototipi şöyledir:
DWORD SetFilePointer(
HANDLE hFile,
LONG lDistanceToMove,
PLONG lpDistanceToMoveHigh,
DWORD dwMoveMethod
);
Fonksiyonun birinci parametresi açılmış dosyanın handle değerini belirtir. İkinci ve üçüncü parametreler konumlandırmaya ilişkin offset'i
belirtir. İkinci parametre konumlandırma offset'inin düşük anlamlı 32 bitlik değerini belirtmektedir. Eğer üçüncü parametre NULL adres
geçilirse konumlandırma ancak 4 byte'lık offset temelinde yapılabilir. Eğer üçüncü parametre NULL geçilmezse bu durumda ikinci ve üçüncü
parametreler 64 bitlik bir offset belirtir. Fonksiyon başarı durumunda konumlandırılmış olan offset'in dosyanın başından itibaren yerine
geri dönmektedir. Eğer üçüncü parametre NULL geçilmediyse konumlandırmanın yapıldığı offset'in yüksek anlamlı 4 byte'lık değeri bu göstericinin
gösteridği yere yerleştirilmektedir. Fonksiyonun son parametresi tıpkı fseek fonksiyonunda olduğu gibi konumlandırmanın nereden itibaren yapılacağını
belirtmektedir. Konumlandırma orijini şunlardan biri olabilmektedir:
FILE_BEGIN (0): Konumlandırma dosyanın başından itibaren yapılır.
FILE_CURRENT (1): Konumlandırma dosya göstericisinin o andaki offset değerine göre yapımaktadır.
FILE_END (2): Konumlandırma EOF durumuna göre yapılmaktadır.
Fonksiyon başarısızlık durumunda INVALID_SET_FILE_POINTER özel değerine geri dönmektedir. Ancak maalesef
bu değer geçerli bir offset değeri olabilmektedir. MSDN dokümanları fonksiyon başrısızsa GetLastError fonksiyonundan elde edilen değerin
NO_ERROR dışında bir değer olduğunu belirtmektedir. O halde fonksiyonun balarısı şöyle kontrol edilmelidir:
if (SetFilePointer(...) == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR)
ExitSys("SetFilePointer");
Aşağıda dosya göstericisi EOF durumuna çekilip dosyanın sonuna yazma yapılmuıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFile;
char buf[] = "this is a test";
DWORD dwWritten;
if ((hFile = CreateFile("test.txt", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
if (SetFilePointer(hFile, 0, NULL, FILE_END) == INVALID_SET_FILE_POINTER && GetLastError() != 0)
ExitSys("SetFilePointer");
if (!WriteFile(hFile, buf, strlen(buf), &dwWritten, NULL))
ExitSys("WriteFile");
CloseHandle(hFile);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi SetFilePointer fonksiyonunda 64 bitlik offset nasıl oluşturulmalıdır? Aslında programcının offset'i oluştururken 64 bitlik bir
değer biçiminde oluşturması sonra onu yüksek anlamlı ve düşük anlamlı 32 bitlik parçaalara ayırması gerekir. Örneğin 64 bit offset ile -2
değerini oluşturmak isteyelim bu -2 değeri aslında şöyle bir değerdir:
FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFFE
Burada yüksek anlamlı 4 byte desimal olarak -1, düşük anlamlı 4 byte desimal olarak -2 biçimindedir.
Aşağıdaki örnekte dosya göstericisi önce EOF durumuna konumlandırılmış sonra 64 bit offset'le -2 değeri kullanılarak 2 geriye konumlandırma
yapılmıştır. Oradan okuma yapıldığında aslında dosyanın son iki byte'ı okunmuş olacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#define BUFFER_SIZE 512
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFile;
char buf[BUFFER_SIZE + 1];
DWORD dwRead, dwWritten;
LONG high = -1;
if ((hFile = CreateFile("test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
if (SetFilePointer(hFile, 0, NULL, FILE_END) == INVALID_SET_FILE_POINTER && GetLastError() != 0)
ExitSys("SetFilePointer");
if (SetFilePointer(hFile, -2, &high, FILE_CURRENT) == INVALID_SET_FILE_POINTER && GetLastError() != 0)
ExitSys("SetFilePointer");
if (!ReadFile(hFile, buf, BUFFER_SIZE, &dwRead, NULL))
ExitSys("ReadFileFile");
buf[dwRead] = '\0';
puts(buf);
CloseHandle(hFile);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde aslında bir kullanıcının yarattığı dosyaya erişim kısıtlanabilmektedir. Örneğin "Kaan" kullancısının bir dosyasına
"Ali" kullanıcısının erişmesi istenmeyebilir. Ancak Windows sistemlerinde maalesef karmaşık bir güvenlik mekanizöası vardır. Bu nedenle bu konu
bu kursumuzda ele alınmayacaktır. Windows'a ilişkin bu tür ayrıntılar "Windows Sistem Programlama" kursunda ele alınmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde de temel dosya işlemleri için beş temel POSIX fonksiyonu bulundurulmuştur. Bu POSIX fonksiyonları aslında pek
çok UNIX türevi sistemde bire bir o sistemdeki sistem fonksiyonlarını çağırmaktadır. Bu beş POSIX fonksiyonu şöyledir:
open
close
read
write
lseek
open fonksiyonu Windows'taki CreateFile fonksiyonunun, close fonksiyonu CloseHandle fonksiyonunun, read fonksiyonu ReadFile fonksiyonunun,
write fonksiyonu WriteFile fonksiyonunun lseek fonksiyonu da SetFilePointer fonksiyonunun UNIX/Linux sistemlerindeki karşılığı olarak düşünülebilir.
Biz POSIX fonksiyonlarınııklarken onların prototiplerinin hangi başlık dosyaları içerisinde olduğunu da belirteceğiz. Genel olarak Windows'ta
buna gerek yoktur. Çünkü temel fonksiyonların hepsinin Windows'ta prototipleri <windows.h> içerisindedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde var olan bir dosyayı açmak ya da yeni bir dosya yaratmak için open isimli POSIX fonksiyonu kullanılmaktadır.
Fonksiyonun prototipi şöyledir:
#include <fcntl.h>
int open(const char *path, int oflag, ...);
open fonksiyonu ya iki argümanla ya da üç argümanla çağrılmaktadır. Eğer open fonksiyonu üç argümanla çağrılacaksa fonksiyon sanki
mode_t türünden bir üçüncü parametreye sahipmiş gibi çağrılmalıdır. Yani open fonksiyonu sanki aşağıdaki iki biçimden biri gibi
kullanılmaktadır:
int open(const char *path, int oflag);
int open(const char *path, int oflag, mode_t mode);
open fonksiyonun birinci parametresi açılacak dosyanın yol ifadesini almaktadır. İkinci parametre açış modunu belirtmektedir. Bu ikinci
parametre O_XXX biçimindeki sembolik sabitlerin bit OR işlemine sokulmasıyla oluşturulur. Bu ikinci parametrenin eb azından aşağıdakilerden
yalnızca birini içermesi gerekmektedir:
O_RDONLY
O_WRONLY
O_RDWR
O_EXEC
O_SEARCH
O_EXEC ve O_SEARCH bayrakları sonnradan eklenmiştir. Biz bunların züerinde durmayacağız. O_RDONLY "yalnızca okuma amaçlı", O_WRONLY
"yalnızca yazma amaçlı", O_RDWR ise "hem okuma hem de yazma amaçlı" dosyayı açma anlamına gelmektedir. Bu bayraklardan biri ile
bazı nayraklar birlikte kullanılabilmektedir. Biz burada tüm bayrak listesini ele almayacağız. Ancak önemli olan birkaç tanesini
ıklayacağız:
O_CREAT: Bu bayrak dosya yoksa dosyanın yaratılarak açılmasını sağlar. Ancak dosya varsa bu bayrağın hiçbir etkisi yoktur. Yani olan
dosya açılr.
O_TRUNC: Bu bayrak dosya varsa dosyanın içinin sıfırlanarak açılacağını belirtir. Dosya yoksa bu bayrağın bir etkisi yoktur. Ancak bu
bayrağın O_WRONLY ya da O_RDWR bayrağı ile kullanılıyor olması gerekmeketdir. Aksi takdirde tanımsız davranış söz konusu olur.
O_EXCL: Bu bayrak O_CREAT bayrağı ile birlikte kullanılmak zorundadır. O_CREAT|O_EXCL "dosya yoksa onu yarat ancak dosya varsa başarısız ol"
anlamına gelmektedir. Böylec programcı var olmayan bir dosyayı kendisinin yarattığına emin olmaktadır.
O_APPEND: Bu modda eğer bayraklaruygunsa dosyanın herhangi bir yerinden okuma yapılabilir ancak tüm yazma işlemleri dosyanın sonuna
yapılacaktır. Başka bir deyişle yazma işleminden önce atomik bir biçimde dosya göstericisi EOF durumuna çekilmektedir.
open fonksiyonunda eğer yeni bir dosyanın yataılma potansiyeli varsa (yani ikinci parametrede O_CREAT bayrağı belirtilmilşse) bu durumda
programcının fonksiyona "dosya erişim haklarını belirten" üçüncü bir argümanı girmesi gerekir. Bu konu izleyen paragraflarda ele alınacaktır.
Ancak yeni bir dosyanın yaratılması gibi bir potansil yoksa (yani ikinci parametrede O_CREAT kullanılmamışsa) bu durumda programcı üçüncü
argümanı girmemelidir. Tabii fonksiyonun ikinci aparametresinde O_CREAT bayrağı kullanılmışsa ancak dosya zaten varsa budurumda programcının
girdiği üçüncü argüman fonksiyon tarafından kullanılmayacaktır.
open fonksiyonu başarı durumunda "dosya betimleyici (file descriptor)" denilen bir handle değerine başarısızlık durumunda ise -1 değerine
geri dönmektedir.
Örneğin:
int fd;
...
if ((fd = open("test.txt", O_RDONLY)) == -1)
exit_sys("open");
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde dosyayı kapatmak için close isimli POSIX fonksiyonu kullanılmaktadır. close fonksiyonuyla kapatılmamış olan dosyalar
işletim sistemi tarafından proses sonlandığında otomatik biçimde kapatılmaktadır. Tabii açık dosyalar belli bir sistem kaynağı harcarlar.
Bir dosya ile işi biten programcının dosyayı kapatması iyi bir tekniktir. close fonksiyonun prototipi şöyledir:
#include <unistd.h>
int close(int fd);
Fonksiyon dosya betimleyicisini parametre olarak alır ve dosyayı kapatır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1
değerine geri dönmektedir. Ancak fonksiyonun başarısının kontrol edilmesine çoğu kez gerek yoktur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Dosyadan okuma yapmak için read isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbyte);
Fonksiyonun birinci parametresi okuma işleminin yapılacağı dosyaya ilişkin dosya betimleyicini belirtmektedir. İkinci parametre okunan
bilgilerin yerleştirileceği bellekteki transfer adresini belirtir. Üçüncü parametre ise okunmak istenen byte sayısını belirtmektedir.
Fonksiyon başarı durumunda okunabilen byte sayısına, başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyonun üçüncü parametresine
dosya göstericinin gösterdiği yerden dosya sonuna kadar var olan byte sayısından daha büyük bir değer girilirse fonksiyon okuyabildiği
kadar byte'ı okur okuyabildiği byte sayısına geri döner. Eğer dosya göstericisi EOF durumundaysa bu durumda dosyadan hiç byte okunamaz.
Fonksiyon 0 ile geri döner. Tabii bu durum fonksiyonun başarısız olduğu anlamına gelmez. read fonksiyonu ile 0 byte okunmak istenebilir.
Bu durumda fonksiyon hata kontrollerini yapar, eğer bir hata söz konusu değilse 0 ile geri döner.
Fonksiyonun geri dönüş değerinin ssize_t türünden olduğuna dikkat ediniz. ssize_t standart C typedef ismi değildir. POSIX sistemlerinde
bulunmaktadır. ssize_t türü <unistd.h> dosyası içerisinde ve <sys/types.h> dosyası içerisinde typedef edilmiştir. ssize_t aslında
size_t türünün işaretli versiyonu olarak bulundurmuştur.Aslında POSIX'teki bütün typedef isimleri toplu halde <sys/types.h> içerisinde
typedef edilmiş durumdadır. Ancak kolaylık sağlamak amacıyla bazı typedef isimleri <sys/types.h> dosyasının yanı sıra başka başlık
dosyalarında da typedef edilmiş durumdadır.
Aşağıda bir dosyadan BUFFER_SIZE kadar bilgiler döngü içerisinde read fonksiyonu ile okunarak ekrana (stdout dosyasına) yazıdırlmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFFER_SIZE 512
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int fd;
char buf[BUFFER_SIZE + 1];
ssize_t result;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((fd = open(argv[1], O_RDONLY)) == -1)
exit_sys("open");
while ((result = read(fd, buf, BUFFER_SIZE)) > 0) {
buf[result] = '\0';
printf("%s", buf);
}
if (result == -1)
exit_sys("read");
close(fd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde dosyaya yazma yapmak için write isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototiği şöyledir:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbyte);
Fonksiyonun parametrik yapısı read fonksiyonundaki gibidir. write fonksiyonu da abaşrı durumunda yazılabilen byte sayısına başarısızlık
durumunda -1 değerine geri dönmektedir. write fonksiyonu ile istediğimiz kadar byte'ın tamamanın yazılaması disk dosyaları için çok seyrek
karşılaşılabilecek bir durumdur. write fonksiyonu ile de 0 byte yazılmak istenebilir. Bu durumda fonksiyon bazı hata kontrollerini yapar.
Eğer bir hata söz konusu olmazsa 0 değeri ile geri döner.
Aşağıdaki örnekte var olan bir dosyanın başına write fonksiyonu ile bir yazı yazılmıştır. Hedef dosyanın yol ifadesi komut satırı argümanı
olarak alınmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int fd;
char buf[] = "this is a test";
ssize_t result;
size_t len;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((fd = open(argv[1], O_WRONLY)) == -1)
exit_sys("open");
len = strlen(buf);
if ((result = write(fd, buf, len)) == -1)
exit_sys("write");
if (result != len) {
fprintf(stderr, "partial write error!...\n");
exit(EXIT_FAILURE);
}
close(fd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde dosya göstericisinin konumlandırılması için lseek isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi
şöyledir:
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
Fonksiyon C'nin fseek fonksiyonuna çok benzemektedir. Fonksiyonun birinci parametresi dosya betimleyicisini, ikinci parametresi konumlandırma
offset'ini ve üçüncü parametresi de konumlandırma orijinini almaktadır. Üçücncü parametre tamamen fseek fonksiyonundaki gibidir ve şu değerlerden
birini alabilir:
#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_SEND 2
Fonksiyon başarı durumunda dosya göstericisinin dosyanın başından itibaren offset değerine başarısızlık durumunda -1 değerine geri dönmektedir.
off_t <unistd.h> ve <sys/types.h> içerisinde "işaretli bir tamsayı belirtmek koşuluyla" typedef edilmi olan bir türdür. lseek fonksiyonun
başarısı da çoğu kez programsılar tarafındna kontrol edilmemektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux dünyasında kullanılan dosya sistemleri ve Microsoft'un NTFS dosya sistemi "dosya deliği (file hole)" denilen bir özelliğe
sahiptir. Bu sistemlerde dosya göstericisi EOF ötesine konumlandırılıp write fonksiyonu ile yazma yapılırsa aradaki bölgeye "delik (hole)"
denilmektedir. Dosya delikleri gerçek anlamda diskte tahsis edilemzler. Dosya deliklerinden okuma yapıldığında 0 byte'ları okunur.
Aşağıda dosya deliği oluşturma bir örnek verilmiştir. Program çalıştırıldıktan sonra dosyanın uzunluğu ve dosyanın diskte kapladığı alan
aşağıdaki gibidir:
$ls -l test.txt
-rw-r--r-- 1 kaan study 5000001 Tem 22 20:07 test.txt
$du test.txt
8 test.txt
Burada dosyanın uzunluğu 5000001 byte gözüktüğü halde diskte kapladığı alan 8 * 1024 = 8192 byte'tır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
int fd;
if ((fd = open("test.txt", O_WRONLY)) == -1)
exit_sys("open");
if (lseek(fd, 5000000, SEEK_SET) == -1)
exit_sys("open");
if (write(fd, "x", 1) == -1)
exit_sys("write");
close(fd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Text mode, binary mode kavramları işletim sistemlerine özgü kavramlar değildir. İşletim sistemlerinin sistem fonksiyonları dosyaları
byte toplulukları olarak görür. Dosyanın text dosya mı binary dosya mı olduğu işletim sisteminin çekirdeğini ilgilendirmemektedir. Bunlar
yüksek seviyeli kavramlardır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde bri kullanıcının sisteme girebilmesi için bir kullanıcı ismi ve bir de parolasının olması gerekir. Bu sistemlerde
proseslere ilişkin "proses kontrol bloğunda turulan" şu önemli erişim bilgileri vardrı:
- Gerçek Kullanıcı Id'si (Real User Id)
- Gerçek Grup Id'si (Real Group Id)
- Etkin Kullanıcı Id'si (Effective User Id)
- Etkin Grup Id'si (Effective Group Id)
Default durumda prosesin gerçek kullanıcı id'si ile etkin kullanıcı id'si, gerçek grup id'si ile etkin grup id'si aynıdır. Test işlemlerine
her zaman etkin kullanıcı id'si ve etkin grup id'si sokulmaktadır.
Pekiyi prosesin kullanıcı ve grup id'leri nasıl belirlenmektedir? İşte bize sisteme sokan yani bize kullanıcı ismini ve parolayı sorun
aslında login isimli programdır. Bu program girdiğimiz kullanıcı ve parola doğruysa /etc/passwd dosyasında yazan kullanıcı id'si ve grup id'si
ile yine burada belirtilen proramı çalıştırmaktadır. Bu program da çoğu kez kabuk programı olmaktadır. Dolayısıyla biz sisteme login olduğumuzda
aslında bizin gerçek ve etkin id'lerimiz /etc/passwd dsoyasında yazan değerlerle set edilmiş olmaktadır. Bu id'ler üst prosesten alt prosese
aktarılırlar. Yani biz kabuktan bir program çalıştırdığımızda kabuğun gerçek ve etkin id'leri bizim programımıza yani prosesimize aktarılmaktadır.
/etc/passwd dosyası satırlardan oluşmaktadır. Her satır bir kullanıcıya ilişkindir. Her satır ':' karakterleriyle 7 alana ayrılmıştır.
Örnek bir satır şöyledir:
student:x:1001:1001:Student,,,:/home/student:/bin/bash
İlk alan kullanıcı ismini belirtir. Burada kullanıcı ismi "student" biçimindedir. İkinci alan kullanıcı şifresine ilişkindir. Burada
'x' varsa bu şifre bilgisinin /etc/shadow dosyası içerisinde olduğunu belirtir. Şifre doğrudan bu alanda da tutulabilmektedir. Ancak
bu alanda şifre "şifrelenmiş bir biçimde" tutulmaktadır. Üçüncü alanda kullanıya ilişkin "gerçek kullanıcı id'si (real user id)"
bulunmaktadır. login programı prosesin etkin kullanıcı id'sini ver gerçek kullanıcı id'sini burada belirtilen biçimde set etmektedir.
Dördüncü alanda kullanıcının ilişkin olduğu gerçek grup id'si (real group id) tutulmaktadır. login programı prosesin gerçek grup id'si
ve etkin grup id'sini bu değer olarak set etmektedir. Beşinci alanda kullanıcıya ilişkin bazı bilgiler bulunmaktadır. Ancak bu bilgiler
sistemin işleyişi ile ilgili değildir. Altıncı alanda kullanıya prosesin "çalışma dizini (current working directory)" tutulmaktadır.
Nihayet yedinci alanda login başarılıyken çalıştırılacak prgram bulunur. Buradaki "bash (Bourne Again Shell)" programı en çok kullanılan kabuk
programıdır.
UNIX/Linux sistemlerinde geleneksel olarak her kullanıcı için /home dizininin altında bir dizin bulundurulmaktadır.
UNIX/Linux sistemlerinde bir grup kullanıcının oluşturduğu topluluğa "grup (group)" denilmektedir. Grup bilgileri /etc/group dosyasında
saklanmaktadır. Bu sistemlerde /etc dizini sistem ile ilgili çeşitli konfigürasyon bilgilerinin tutulduğu ana bir dizindir.
Linux sistemleri kurulurken zaten kurulum programları aynı zamanda bir kullanıcı da oluşturmaktadır. Ancak daha sonra başka kullanıcılar da
oluşturulabilir. Bu işlemler "adduser" ya da "useradd" komutlarıyla yapılabieceği gibi manuel olarak /etc/passwd dosyasına satır ekleyerek de
yapılabilmektedir.
UNIX/Linux sistemlerinde aynı zamanda her dosyanın ve dizin'in de bir "kullanıcı id'si (user id)" ve "grup id'si (group id)" vardır.
Ancak dosyalar söz konusu olduğunda "gerçek" ve "etkin" kavramları söz konusu değildir. Yani dosyaların gerçek ve etkin biçiminde
iki id'si yoktur. Tek bir id'si vardır. Bir dosyanın kullanıcı ve grup id'leri "ls -l" komutuyla görüntülenebilir. Örneğin:
kaan@kaan-virtual-machine:~/Study/SysProg$ ls -l
toplam 76
-rwxr-xr-x 1 kaan study 16136 Haz 17 18:51 a.out
-rw-r--r-- 1 kaan study 3570 Haz 18 18:57 disp.c
-rw-r--r-- 1 kaan study 1622 Haz 11 20:40 mample.c
......
Aslında dosya sisteminde dosyaların kullanıcı id'leri ve grup id'leri sayısal biçimde tutulmaktadır. Ancal "ls" programı "/etc/passwd" ve
"/etc/group" dosyalarına başvurarak onların isimlerini yazdırmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde her dosyanın "erişim hakları" vardır. Bu erişim hakları "ls -l" komutunda 10 tane karakterden oluşan bir sütunda
görüntülenmektedir. Örneğin:
-rw-r--r-- 1 kaan study 3570 Haz 18 18:57 disp.c
Buradaki karakterlerin en solundaki karakter dosyanın türünü belirtmektedir. Dosya türü olarak '-' "sıradan bir disk dosyası (regular file)"
anlamına gelmektedir. Budada 'd' varsa bu dosyanın bir "dizin (directory)" olduğu anlamına gelir. Başka dosya türleri de vardır. Dosya
türündne sonraki karakterler üçlü üç grup oluşturmaktadır:
- rwx rwx rwx
İlk üçlü gruba "owner", sonraki üçlü gruba "group" ve sonraki üçlü gruba "other" denilmektedir. Bu gruplar dosyanın sahibinin, dosya ile aynı
grupta olanların ve herhangi kişilerin bu dosya üzerinde ne yapabileceklerini belirtir. Eğer burada bir hak varsa biz ilgili pozisyonda
"r", "w" ya da "x" harflerini görürüz. Eğer burada bir hak yoksa biz ilgili pozisyonda "-" karakterini görürüz. Örneğin bir dosyanın erişim
hakları şöyle olsun:
-rw-r-----
Bu dosyaya dosyanın sahibi (owner) okuma ve yazma yapabilir. Dosyanın grubuyla aynı gruptan olan kişiler bu dosyadan yalnızca okuma
yapabilirler. Herhangi kişiler bu dosya üzerinde işlem yapamazlar.
Bir dosyanın erişim haklarındaki 'x' kısmında '-' var ise bu durum ya "dosyanın zaten çalıştırılabilir bir dosya olmadığı" ya da
"ilgili kişey çalıştırma hakkı verilmediği" anlamına gelmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Dosya erişim hakları open fonksiyonu tarafından kontrol edilmektedir. Kontrol işlemi şöyle yapılmaktadır (maddeler else if biçimindedir):
1) open fonksiyonu önce prosesin etkin kullanıcı id'sine bakar. Eğer prosesin etkin kullanıcı id'si 0 ise bu durumda bir kontrol yapılmaz.
İstek kabul edilir. Etkin kullanıcı id'si 0 olan proseslere "root process" ay da "super user process" denilmektedir.
2) open fonksiyonu prosesin etkin kullanıcı id'si ile dosyanın kullanıcı id'sine bakar. Eğer bunlar aynıysa dosyanın sahibi ilgili prosese
ilişkin kullanıcıdır. Bu durumda erişim haklarındaki "owner" kısmı dikkate alınarak erişim onayı verilir.
3) open fonksiyonu prosesin etkin grup id'si ile dosyanın grup id'sine bakar. Eğer bunlar aynıysa proses dosyanın grubuyla aynı grupta olan
bir kullanıcıya ilişkindir. Bu durumda erişim haklarının "group" kısmı dikkate alınarak erişim onayı verilir.
4) open fonksiyonu dosyanın erişim haklarının "other" kısmına bakarak erişim onayı verir.
Örneğin bir prosesin etkin kullanıcı id'si kaan (1000) ve etkin grup id'si study (1002) olsun. Aşağıdaki gibi bir dosyanın bulunduğu varsayalım:
-rw-r--r-- 1 ali study 3570 Haz 18 18:57 test.txt
Bu proses dosyayı oen fonksiyonula şöyle açmaya çalışmış olsun:
fd = open("test.txt", O_RDWR);
Burada open fonksiyonu başarısız olacaktır. open fonksiyonunu çağıran prosesin etkin kullanıcı id'si 0 değildir. Prosesin etkin kullanıcı
id'si dosyanın kullanıcı id'si ile de aynı değildir. Ancak prosesin etkin grup id'si dosyanın grup id'si ile aynıdır. Bu durumda erişim haklarının
"group" kısmı dikkate alınır. Erişim haklarının group kısmında "r--" vardır. Halbuki proses dosyaysı O_RDWR modunda açmak istemiştir. O halde
open başarısız olur ve errno EPERM değeri ile set edilir. Tabii aynı proses dosyayı şöyle açabilirdi:
fd = open("test.txt", O_RDONLY);
Şimdi aynı dosyaya etkin kullanıcı id'si "veli (1005)" olan etkin group id'si de "school (1004)" olan bir proses aşağıdaki gibi open
uygulamış olsun:
fd = open("test.txt", O_RDONLY);
Burada prosess dosyaya göre "other" durumundadır. O zaman erişimde "other" kısım dikkate alınacaktır. Dosyanın "other" erişim hakları "r--"
biçimindedir. Bu durumda open başarılı olarak dosyayı açar.
Diğer bir dosyanın erişim hakları şöyle olsun
---------- 1 ali study 3570 Haz 18 18:57 x.txt
Bu dosyayı etkin kullanıcı id'si 0 olan bir proses aşağıdaki gibi open fonksiyonuyla açmak istesin:
fd = open("x.txt", O_RDWR);
Etkin kullanıcı id'si 0 olan proseslere hiçbir kontrol uygulanmadığı için open başarılı olacaktır.
Bir kullanıcının dosyasına aşağıdaki gibi bir erişim hakkı vermesi geçerli olsa da tuhaftır:
-r--rw-rw-
Burada dosyanın sahibi bu dosyaya yazma yapamayacaktır. Ancak dosyayla aynı gruptaki prosesler ve herhangi prosesler dosyaya yazma
yapabilecektir.
Bir programı sudo yaparak çalıştırdığımızda aslıdna program etkin kullanıcı id'si 0 olacak biçimde çalıştırılmaktadır. Dolayısıyla biz
yetkisizlikten bir şeyi yapamıyorsak "sudo" ile bunu yapmaya çalışabiliriz. Tabii modern UNIX/Linux sistemlerinde her kullanıcı "sudo"
yapamamaktadır. Ayrıca "sudo" yapabilmek için kurulum sırasındaki "root parolasının" kullanıcı tarafından biliniyor olması gerekmektedir.
UNIX/Linux sistemlerindeki güvenlik mekanizması "ya hep ya hiç" esasıyla tasarlanmıştır. Eğer proses root proses ise (yani prosesin etkin
kullanıcı id'si 0 ise) proses her şeyi yapabilmektedir. Ancak proses root prosesi değilse (yani prosesin etkin kullanıcı id'si 0 değilse)
proses yalnızca kendisiyle ilgili şeyleri yapabilmektedir. İşte bu "yap hep ya hiç" sistemi bazı UNIX türevi sistemlerde çeşitli biçimlerde
geliştirilmek istenmiştir. Örneğin Linux sistemleri "capability" denilen bir özelliğe sahiptir. Linux sistemlerinde bir proses root olmadığı
halde bazı "capability" özelliklerine sahip olabilir. Bu durumda o konuya ilişkin işlemleri sanki root prosesmiş gibi yapabilir. Linux
sistemlerinde bu biçimde çeşitli konulara ilişkin capablity'ler oluşturulmuştur. Bazı sistemler (Linux'ta isteğe bağlı) "ACL (access Contol List)"
denilen bir mekanizmaya da sahiptir. POSIX standartları "capability" ya da "ACL" konularını kapsamamaktadır. Ancak POSIX standartları UNIX türevi
işletim sistemlerinin bu tür özelliklere sahip olabileceği fikriyle tasarlanmıştır. Bu nedenle POSIX standartlarında "root önceliği" ya da
"etkin kullanıcı id'nin 0 olması" gibi ifadeler yerine "uygun öncelik (appropriate privilege)" terimi kullanılmaktadır. POSIX'in "uygun öncelik"
terimi Linux için "ya prosesin etkin kullanıcı id'sinin 0 olması ya da prosesin o işlemi yapabilecek capability'ye sahip olması" anlamına
gelmektedir. Biz de kursa notlarımızda "root önceliği ya da etkin kullanıcı id'sinin 0 olması" yerine "prosesin uygun önceliğe sahip olması"
terimini kullanacağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi UNIX/Linux sistemlerinde bir dosyanın kullanıcı ve grup id'leri nasıl set edilmektedir? İşte dosyanın kullanıcı ve grup id'leri
bu dosya ilk kez yaratılırken belirlenmektedir. Dosyanın kullanıcı id'si her zaman onu yaratan prosesin etkin kullanıcı id'si olarak
set edilmektedir. Ancak yeni yaratılan dosyanın grup id'sinin set edilmesi konusunda sistemlerde bir anlaşmazlık olmuştur. Bu nedenle POSIX
standartları oluşturulurken o anki mevcut sistemler için ikili semantik benimsenmiştir. Şöyle ki "yeni yaratılan dosyanın grup id'si ya
onu yaratan prosesin etkin grup id'si olarak (System 5 semantiği) ya da o dosyanın içinde bulunduğu dizin'in grup id'si olarak (BSD semantiği)"
set edilmektedir. Linux default durumda yeni yaratılan dosyanın grup id'sini onu yaratan prosesin etkin grup id'si olarak set etmektedir.
Pekiyi dosyanın erişim hakları nasıl belirlenmektedir? İşte dosyalar her zaman open fonksiyonuyla yaratılır. open fonksiyonunda fonksiyonun
üçüncü parametresi erişim haklarını belirtmektedir. Daha önceden de belirttiğimiz gibi eğer open fonksiyonun ikinci parametresinde O_CREAT
bayrağı kullanılmışsa dosyanın yaratılması söz konusu olabilmektedir. Bu durumda programcı üçüncü parametreyi erişim hakları olacak biçimde
oluşturmalıdır. Erişim hakları UNIX/Linux sistemlerinde genel olarak mode_t türü ile temsil edilmektedir. Bu tür <sys/stat.h> ve <sys/types.h>
dosyası içerisinde bir tamsayı türünden olacak biçimde typedef edilmiştir.
open fonksiyonunda erişim haklarını oluşturmak için <sys/stat.h> içerisinde bulunan S_IXXX biçimindeki sembolik sabitler bit OR işlemine
sokulmaktadır. Bu sembolik sabitlerin lsitesi şöyledir:
S_IRUSR
S_IWUSR
S_IXUSR
S_IRGRP
S_IWGRP
S_IXGRP
S_IROTH
S_IWOTH
S_IXOTH
Buradaki sembolik sabitlerin hepsi S_I öneki ile başlatılmıştır. Bunu R, W ya da X harfleri izler. Bu harfleri de USR, GRP ya da OTH
harfleri izlemektedir. Örneğin S_IRUSR|S_IWUSR|S_IRGRP|_SIROTH hakları "rw-r--r--" anlamına gelmektedir.
Yukarıda sembolik sabitlerin sayısal değerleri POSIX 2008'e kadar sşstemdne sisteme değişebilir biçimdeydi. Ancak daha sonra POSIX standartları
bu sembolik sabitlerin değerlerini tamamen bir tamsayının düşük anlamlı 9 biti olarak belirlemiştir. Aşağıda bir tamsayının 9 bitine
karşılık gelen erişim hakları verilmiştir:
rwx rwx rwx
Bir octal digit 3 bit ile açıldığına göre POSIX 2008 ve sonrasında artık bu erişim hakları octal bir sayı biçiminde kolay bir şekilde
girilebilmektedir. Örneğin 0644 ocal sayısı ikilik sistemde 110 100 100 biçimindedir. Bu da rw-r--r-- anlamına gelmektedir. Başka bir
deyişle S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH işleminin eşdeğeri 0644'tür. Ancak eski sistemler bu sembolik sabitleri bu biçimde define
etmemiş olabilirler. Dolayısıyla erişim hakları için doğrudan bir syaı girmek yerine S_IXXX sembolik sabitlerini kullanmak daha
uygun olabilir. Ayrıca <sys/stat.h> dosyası içerisinde aşağıdaki gibi üç sembolik sabit daha vardır:
#define S_IRWXU (S_IRUSR|S_IWUSR|S_IXUSR)
#define S_IRWXG (S_IRGRP|S_IWGRP|S_IXGRP)
#define S_IRWXO (S_IROTH|S_IWOTH|S_IXOTH)
Bu durumda tüm erişim haklarını vermek için S_IRWXU|S_IRWXG|S_IRWXO işlemi yapılabilir. Bu zaten 0777 ile aynı anlamdadır.
Aşağıda open fonksiyonu ile dosya yaratmaya bir örnek verilmiştir:
int fd;
...
if ((fd = open("y.txt", O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1)
exit_sys("open");
Burada bir noktaya yeniden dikkatinizi çekmek istiyoruz: open fonksiyonun ikinci parametresinde O_CREAT bayrağının kullanılm olması
üçüncü parametrenin fonksiyon tarafından kesinlikle kullanılacağı anlamına gelmemektedir. Eğer dosya yoksa dosya yaratılırken bu üçüncü
parametre kullanılmaktadır. open fonksiyonun üçüncü parametresi eğer dosya yaratılacaksa ikinci parametresi üzerinde etkili olmamaktadır.
Örneğin:
fd = open("z.tzt", O_RDWR|O_CREAT, 0);
Burada dosyanın yaratılacağını varsayalım. Dosyayı yaratan kendine "read" ve "write" hakkı da vermemiştir. Ancak open fonksiyonu bu
yaratımda başarılı olacaktır. Tabii bundan sonra artık dosyanın sahibi open fonksiyonu ile dosyayı herhangi bir modda açamayacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Aslında open fonksiyonunda üçüncü parametrede verilen erişim hakları nihai erişim hakları değildir. Bu erişim hakları "prosesin umask değeri"
denilen bir değerle işleme sokulur. Nihai erişim hakları bu işlem sonucunda belirlenir. Prosesin umask değerinde belirtilen haklar open
fonksiyonunda girilse bile dosyaya verilmemektedir. Prosesin umask değeri üst prosesten alt prosese aktarılmaktadır. Kabuk programının (bash)
umask değeri "umask" isimli komutla elde edilebilir. Örneğin:
$umask
0022
Buradaki umask değeri octal değer olarak görüntülenmektedir. Yukarıdaki ocatl değerini binary açılımı şöyledir:
000 010 010
Burada gruba write hakkı ve other'a write hakkı ortadan kaldırılmıştır. Başka bir deyişle open fonksiyonuna girdiğimiz erişim hakları
mode olmak üzere nihai erişim hakları mode & ~umask biçimindedir. Yani umask değerindeki 1 olan bitlere karşı gelen haklar aslında
silinmektedir.
Kabuk programının umask değeri de değiştirilebilir. Bu durumda kabuktan çalıştırdığımız programların da umask değeri değişecektir. Örneğin:
$umask 0
$umask
0000
Bir proses kendi umask değerini istediği zaman umask isimli POSIX fonksiyonu ile değiştirebilir. Bunun için bir koşul gerekmemektedir.
Fonksiyonun prototipi şöyledir:
#include <sys/stat.h>
mode_t umask(mode_t cmask);
Fonksiyon yeni umask değerini parametre olarak alır ve eski umask değerini verir. Parametre için argümanı POSIX 2008 ve sonrasında sayısal
biçimde verebiliriz. Ancak S_IXXX sembolik sabitleriyle vermek iyi bir tekniktir. Fonksiyon başarısız olamamaktadır. O halde biz open
fonksiyonunda verdiğimiz erişim haklarının aynısının dosyaya yansıtılmasını istiyorsak programın başında prosesin umask değerini 0'a
çekebiliriz. Örneğin:
int fd;
...
umask(0);
if ((fd = open("z.txt", O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)) == -1)
exit_sys("open");
Prosesin umask değerini alan arı bir fonksiyon yoktur. O zazan mecburen prosesin umask değerini almak için onu set ediyormuş gibi
yapmak gerekir. Örneğin:
mode_t mode;
...
mode = umask(0);
umask(mode);
Pekiyi prosesler için umask değerine nedne gereksinim duyulmuştur? Bunun birkaç nedeni vardır. Birincisi umask dikkatsizlikle verilen erişimlerin
ortadan otomatik biçimde kaldırılmasını sağlamaktadır. umask değeri aynı zamanda başka programlarda default erişim haklarını ayarlamak için
de kullanılmaktadır. Örneğin biz bir C kütüphanesi yazacak olalım. fopen fonksiyonunda dosya yaratırken erişim haklarını nasıl vermeliyiz?
İşte UNIX/Linux sistemleri için standart C kütüphanesini yazanlar genellikle default erişim haklarını "rw-rw-rw" biçiminde vermektedirler.
Fakat bu işlem prosesin umask değerinden etkileneceği için biz programı çalıştırmadan önce ya da programın içerisinde umask değerini değiştirerek
bunu dışarıdan değiştirmiş gibi oluruz. Başka bir deyişle prosesin umask değeri başkaları tarafından yazılmış olan kodlarda yaratılan dosyalara
ilişkin erişim haklarını değiştirmekte kullanılabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde dosya kopyalamaya bir örnek.
./mycp [-i -n][--interactive --no-clobber] <source path> <destination path>
Bir dosyanın olup olmadığını anlamak için ve ilgili prosesin dosyaya okuma/yazma/çalıştırma işlemini yapıp yapaayacağını
anlayabilmek için access isimli bir POSIX fonksiyonu kullanılmaktadır. Bu fonksiyon open ile dosyayı açmaktan daha etkin bu işlemi
yapabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* mycp.c */
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <getopt.h>
#define BUF_SIZE 8192
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int result;
int i_flag, n_flag;
int err_flag;
char buf[BUF_SIZE];
int fds, fdd;
ssize_t n;
struct option options[] = {
{"interactive", no_argument, NULL, 'i'},
{"no-clobber", required_argument, NULL, 'n'},
{0, 0, 0, 0 },
};
opterr = 0;
i_flag = n_flag = 0;
err_flag = 0;
while ((result = getopt_long(argc, argv, "in", options, NULL)) != -1) {
switch (result) {
case 'i':
i_flag = 1;
break;
case 'n':
n_flag = 1;
break;
case '?':
if (optopt != 0)
fprintf(stderr, "invalid option: -%c\n", optopt);
else
fprintf(stderr, "invalid long option!..\n");
err_flag = 1;
break;
}
}
if (err_flag)
exit(EXIT_FAILURE);
if (argc - optind != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if (n_flag || i_flag) {
if (access(argv[optind + 1], F_OK) == 0) {
if (n_flag) {
fprintf(stderr, "file already exits! (-n specified)\n");
exit(EXIT_FAILURE);
}
if (i_flag) {
printf("file already exists! Overwrite? (Y/N):");
if (tolower(getchar()) != 'y')
exit(EXIT_FAILURE);
}
}
}
if ((fds = open(argv[optind], O_RDONLY)) == -1)
exit_sys("open");
if ((fdd = open(argv[optind + 1], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
exit_sys("open");
while ((n = read(fds, buf, BUF_SIZE)) > 0)
if (write(fdd, buf, n) == -1)
exit_sys("write");
if (n == -1)
exit_sys("read");
close(fds);
close(fdd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
15. Ders 29/07/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Bugün kullandığımız ekranlarda temelde iki çalışma modu vardır: Text mod ve grafik mod. Text modda karakterler bir kalıp biçiminde ekrana
basılır. Bu modda pixel temelinde bir kontrol yoktur. Her karakter için matriste belli bir yer ayrılmıştır. O yere kaarakterler kalıp olarak
basılmaktadır. Text mod ekranlara konsol ekranı da denilmektedir. Eskiden yalnızca text modda çalışma söz konusuydu. Sonra zamanla donanım
teknolojisi gelişince grafik modda çalışma da yaygınlaşmaya başladı. Eğer biz programı text modda çalışacak biçimde oluşturmuşsak bu tür
programlara "konsol tabanlı (console based)" programlar da denilmektedir. Text modda bir imleç (cursor) vardır. stdout dosyasına yazılan
şeyler bu imlecin bulunduğu yerden itibaren yazılırlar. Sonra imleç yazılan miktar kadar ilerletilir. Örneğin printf fonksiyonu stdout
dosyasına yazar. Bu stdout dosyası default durumda pek çok sistemde terminal aygıt sürücüsüne yönlendirilmiştir. Terminal aygıt sürücüsü
de yazılacak şeyleri imlecin bulunduğu yere yazar ve imleci yazılan karakter sayısı kadar ilerletir. Text modda imlecin ilerletilmesi
sütun bittiğinde sonraki satırın başından itibaren yapılmaktadır. Text modda son satırdan sonra ekrana bir şeyler yazılmak istendiğinde
her satır bir yukarı kaydırılır. Bu işleme "scroll" ya da "scroll up" denilmektedir. Text modda çalışmak çok hızlıdır. Text mod genel
olarak basit bir arayüz oluşturmaktadır. Bu nedenle pek çok programalama dili çıktı bağlamında text modda uygun tasarlanmıştır. 25 satırlı
80 sütun eskiden beri en yaygın kullanılan text mod çözünürlüğüydü. Buna standart text mod çözünürlüğü denilmektedir.
Grafik modda ekrandaki en küçük birim bir pixel'dir. Pixel ("picture element" sözcüklerinden kısaltılmıştır) ekranda görüntülenecek en küçük
grafik öğedir. Grafik modda her şey ğixel'lerin uygun biçimde bir araya getirilmesiyle oluşturulmaktadır. Ekran çözünürlükleri pixel matrisinin
genişlik ve yükseklik değeri ile belirtilmektedir. Örneğin 1920X1080 çözünürlük demek ekranda toplam 1980 * 1020 tane pixel var demektir.
Text modda yalnızca karakter görüntülenebilmektedir. Halbuki grafik modda pixel'lerle her şey görüntülenebilmektedir. Biz bir resmi text
modda görüntüleyemeyiz. Ancak grafik modda görüntüleyebiliriz. Bir program çıktısını pixel düzeyinde oluşturuyorsa buı tür programlara
"GUI (Graphical User Interface) tabanlı programlar" denilmektedir. Örneğin Excel, Word gibi programlar GUI tabanlı programlardır.
Text mod için program yazmak oldukça kolaydır. Ancak grafik modda program yazmak için ayrı bir bilgi gerekmektedir. C'nin standart
fonksiyonlarıyla GUI tabanlı programlar yazılamaz. C'de GUI tabanlı programlar yazabilmek için özel kütüphanelerden faydalanmak gerekir.
Bugün GUI tabanlı programlama oldukça yaygın kullanılmaktadır. Ancak GUI çalışma modeli konsole çalışma modeline göre oldukça yavaştır.
Bu nedenle pek çok temel araç GUI çalışma modeli yerine konsole çalışma modeline göre tasarlanmış durumdadır. Örneğin derleyiciler GUI
arayüzü arayüzü kullanmazlar. Klasik konsole tabanlı bir arayüz kullanırlar. Windows ve macOS sistemleri GUI çalışmanın ön planda olduğu
sistemlerdir. Ancak UNIX/Linux dünyasında hala ağırlık konsol tabanlı çalışma modelindedir. Örneğin Linux sistemleri ağırlıklı olarak
server sistemlerinde ve gömülü sistemlerde kullanılmaktadır. Burada grafik tabanlı çalışma yavaş olduğu gerekçesiyle ya da güçlü donanım
gerektirmesinden dolayı tercih edilmemektedir. Tabii bugün kullanıımız Linuz dağıtımları genellşkle bir grafik arayüz bulundurmaktadır.
Ancak bu grafik arayüz kolaylıkla devre dışı bırakılabilmektedir.
Bugün kullandığımız grafik kartlarında ekranın bir bölümü text bir bölümü grafik modda olamamaktadır. Ancak konsole tabanlı uygulamalar için
GUI arayüzleri konsole ekranını bir pencere içerisinde pixel işlemleriyle emüle edebilmektedir. Örneğin Windows sistemlerinde biz aslında
grafik modda çalışmaktayız. Ancak burada "cmd.exe" programını çalıştırdığımızda sanki klasik konsole sisteminde çalışıyormuşuz gibi
bir emülasyon yapılmaktadır. Yani burada text mod görüntüsü sahte bir görüntüdür. Yalnızca klasikj konsol programlarının çalışabilmesi için
pixel işlemleriyle oluşturulmuştur. Yani bugün grafik arayüze sahip işletim sistemlerinde biz bir konsol terminali açtığımızda sanki o terminal
penceresi eski devirdeki text modda çalışan bir konsol penceresini taklit etmektedir.
Renkli görüntünün bilgisayar ekranlarına oluşturulması 80'li yılların ortalarında başlamıştır. Text modda da grafik modda da bir renk
olgusu vardır. Grafik modda her pixel ayrı bir renkle gösterilebilmektedir. Yaygın teknolojide Kırmızı (red), Yeşil (Green) ve Mavi (Blue)
olmak üzere üç temel renk vardır. Diğer bütün renkler bu üç temel rengin tonal birleşimleriyle oluşturulmaktadır. Günmüz teknolojisinde
bu üç ana renk toplam 256 farklı ([0, 255 arasında]) tonal değere sahiptir. Dolayısıyla grafik ekranlarda her pixel 256 * 256 * 256 ~= 16 milyon
renkten biri ile renklendirilebilmektedir. Ayrıca her pixel için modern grafik kartlarında ismine "alpha channel" denilen yine [0, 255] değer alan bir bilşen
daha bulunudurlmaktadır. Bu bileşen pixel'in transparanlığını ayarlamakta kullanılmaktadır.
Pekiyi eskiden text modda renk kavramı var mıydı? Evet grafik modun çok seyrek kullanıldığı yıllarda da yavaş yavaş text moda renk olgusu
sokulmultur. Text moddaki her bir karakter matrisinin zemini ve şekli yarı ayrı boyanabilmektedir. Ayrıca "bold" ve "reverse" özel durumlar
da söz konusu olmaktadır. Tabii bugün grafik arayüzlerdeki terminal ekranları aslında pixel temelinde emüle edildiğinden bu zemin ve şekil
renkleri de aslında pixel temelinde oluşturulmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi konsole ekranında renkli bir yazıyı nasıl oluşturabiliriz? Ya da konsol ekranında ekranın istediğimiz yerine bir yazıyı nasıl
yazdırabiliriz? Bilindiği gibi C'de bunları yapabilecek standart fonksiyonlar yoktur. İşte C'de text ekranda imleçle ilgili işlemler ya da
renkli yazım işlemleri temelde iki yolla yapılmaktadır:
1) ANSI terminal komutları ile
2) Özel kütüphane fonksiyonları ile
stdout için aygıt sürücülerini yazanlar standart bazı özel karakter için bazı özel işlemleri yapacak biçimde aygıt sürücülerini yazmaktadır.
Bu özel karakterler kullanılarak yazılmış olan komutlara ANSI terminal komutları denilmektedir. ANSI terminal komutları 0x1B karakteri ile
başlatılır. ASCII tablosounda 0x1B karakterine "escape karakteri" denilmektedir. Dolayısıyla ANSI terminal komutlarına "ANSI escape komutları" da
denilebilmektedir. Tüm ANSI terminal komutları bir escape karakteriyle başlatılır ve bu escape karakterini bazı başka karakterler izler.
Örneğin imleci (cursor) belli bir yere taşımak için stdout dosyasına (aygıt sürücüye) gönderilecek escape komutu şöyledir:
"\x1B[row;colH"
Buradaki \x1B aslında tek bir karakterdir. Buna ASCII tablosunda "escape karakteri" denilmeketedir. Komuttaki row ve col birer sayı olmalıdır.
row imlecin taşıanacağı satır numarasını col ise imlecin taşınacağı sütun numarasını belirtmektedir. Örneğin:
"\x1B[12;18H"
Bu komut imleci 12'inci satır 18'üncü sütuna taşır. Biz imleci taşıyan bir C fonksiyonunu da aşağıdaki gibi yazabiliriz:
void move_cursor(int row, int col)
{
printf("\x1B[%d;%dH", row, col);
}
İmleci yok etmek şu ANSI terminal komutu kullanılmaktadır:
"\x1B[?25l"
İmleci yeniden görünür yapmak için ise şu komut kullanılmaktadır:
"\x1B[?25h"
Tabii bu işlemleri birer fonksiyon haline de getirebiliriz:
void hide_cursor(void)
{
printf("\x1B[?25l");
}
void show_cursor(void)
{
printf("\x1B[?25h");
}
İmlecin konumunu aygıt sürücünün saklaması için şu escape komutu kullanılmaktadır:
"\x1B\x37"
İmlecin son saklanan pozisyona geri yerleştirilmesi için ise şu komut komut kullanılmaktadır:
"\x1B\x38"
Bu işlemleri fonksiyon olarak da yazabiliriz:
void save_cursor(void)
{
printf("\x1B\x37");
}
void restore_cursor(void)
{
printf("\x1B\x38");
}
ANSI escape komutları için Internet'teki çeşitli kaynaklara başvurabilirsiniz.
Text modda yazının karakterlerinin zemini ve şekli ayrı ayrı renklendirilebilmektedir. Renklendirme işlemi de ANSI escape kodlarıyla
yapılabilmektedir. Örneğin yazıların kırmızı yazılması için (yani yalnızca yazıların şekil renklerini kırmızı yapmak için)
şu komut kullanılır:
"\x1b[31m"
Her renk için buradaki sayı değişmektedir. Şöyle genel bir fonksiyon da yazılabilir:
#define BLACK_COLOR 30
#define RED_COLOR 31
#define WHITE_COLOR 37
#define BLUE_COLOR 34
....
void change_color(int color)
{
printf("\x1b[%dm", color);
}
Renkelndirme için aşağıdaki dokümana başvurabilirsiniz:
https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Text ekranda ayrıntılı işlemler yapmak için özel kütüphaneler de kullanılmaktadır. Örneğin UNIX/Linux dünyasında "curses" ya da bunun daha
yeni versiyonu olan "ncurses" kütüphaneleri bu amaçla çokça kullanılmaktadır. Windows dğnyasında zaten text ekranda işlemler yapmak için
ismine "Console API Fonksiyonları" denilen bir grup API fonksiyonu bulundurulmuştur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda escape karakterle bazı işlemeler yapan bir program örneği verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define BLACK_COLOR 30
#define RED_COLOR 31
#define WHITE_COLOR 37
#define BLUE_COLOR 34
void move_cursor(int row, int col)
{
printf("\x1B[%d;%dH", row, col);
}
void hide_cursor(void)
{
printf("\x1B[?25l");
}
void show_cursor(void)
{
printf("\x1B[?25h");
}
void change_color(int color)
{
printf("\x1b[%dm", color);
}
int main(void)
{
time_t t, prev_t;
struct tm *pt;
int count;
change_color(BLUE_COLOR);
hide_cursor();
prev_t = 0;
count = 0;
for (;;) {
t = time(NULL);
pt = localtime(&t);
move_cursor(1, 80);
printf("%02d:%02d:%02d", pt->tm_hour, pt->tm_min, pt->tm_sec);
if (prev_t != t) {
++count;
if (count == 10)
break;
}
prev_t = t;
}
show_cursor();
change_color(WHITE_COLOR);
return 0;
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Windows'un Console API fonksiyonlarını kullanabilmek için önce terminale ilişkin stdout dosyasının handle değerinin elde edilmesi gerekmektedir.
Bu işlem GetStdHandle isimli API fonksiyonuyla yapılır. Fonksiyonun prototipi şöyledir:
HANDLE WINAPI GetStdHandle(
DWORD nStdHandle
);
Fonksiyon parametre olarak handle değeri alınmak istenen standart dosyayı belirten özel değeri alır. Bu değer şunlardan biri olabilir:
STD_INPUT_HANDLE
STD_OUTPUT_HANDLE
STD_ERROR_HANDLE
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
16. Ders 30/07/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
WrtiteConsole API fonksiyonu bir adresten itibaren belli miktardaki karakteri imlecin bulunduğu yerden itibaren yazmaktadır. Bu bakımdan
puts gibi bir işleve sahiptir. (Tabii imleci aşağı satırın başına geçirmez.) Fonksiyonun prototipi şöyledir:
BOOL WINAPI WriteConsole(
HANDLE hConsoleOutput,
const VOID *lpBuffer,
DWORD nNumberOfCharsToWrite,
LPDWORD lpNumberOfCharsWritten,
LPVOID lpReserved
);
Fonksiyonun birinci parametresi stdout dosyasının handle değerini almaktadır. Fonksiyon ikinci parametresiyle belirtilen adresten itibaren
üçüncü parametresiyle belirtilen miktarda karakteri imlecin bulunduğu yere yazar. Başarılı bir biçimde yazabildiği karakter sayısını dördüncü
parametresiyle girdiğimiz DWORD nesye yerleştirmektedir. Son parametre reserved durumdadır, NULL geçilmelidir. Fonksiyon başarı durumuna geri döner.
Ancak genellikle başarınn kontrol edilmesine gerek yoktur.
İmleci konumlandırmak için SetConsoleCursorPosition isimli API fonksiyonu kulanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD dwCursorPosition
);
Örneğin:
HANDLE hConsole;
char buf[] = "this is a test";
DWORD dwWritten;
...
WriteConsole(hConsole, buf, strlen(buf), &dwWritten, NULL);
Fonksiyonun birinci parametresi stdout dosyasının handle değerini ikinci parametresi ise konumlandırma yapılacak koordinatı belirtmektedir.
COORD yapısı şöyle bildirilmiştir:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
Fonksiyon işlemin başarısı durumuyla geri dönmektedir. Örneğin:
COORD coord;
...
coord.X = 10;
coord.Y = 20;
SetConsoleCursorPosition(hConsole, coord);
C99 ile birlikte dile eklenen "bileşik sabitler (compound literals)" ile aynı kod şöyle de yazılabilmektedir:
SetConsoleCursorPosition(hConsole, (COORD){10, 12});
İmlecin bulunduğu yerden bağımsız olarak bir yazıyı ekranın belli bir koordinatından itibaren yazdırmak için WriteConsoleOutputCharacter
isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL WINAPI WriteConsoleOutputCharacter(
HANDLE hConsoleOutput,
LPCTSTR lpCharacter,
DWORD nLength,
COORD dwWriteCoord,
LPDWORD lpNumberOfCharsWritten
);
fonksiyonun birinci parametres, console ekranının handle değerini, ikinci parametresi yazdırılacak yazının başlangıç adresini, üçüncü
parametresi yazdırılacak karakter sayısını, dördüncü parametresi yazımın başlatılacağı koordinatı ve son parametresi de başarılı biçimde
yazıdılmış olan karakter sayısınının yerleştirileceği DWORD nesnesnin adresini almaktadır. Fonksiyon başarı durumunda sıfır dışı bir değere,
başarısızlık durumunda sıfır değerine geri dönmektedir. WriteConsoleOutputCharacter fonksiyonunu kullanarak saati belli bir koordinata basan
bir fonksiyon şöyle yazılabilir:
void putclock(int row, int col)
{
HANDLE hConsole;
struct tm *pt;
time_t t;
char buf[1024];
DWORD dwWritten;
if ((hConsole = GetStdHandle(STD_OUTPUT_HANDLE)) == INVALID_HANDLE_VALUE)
ExitSys("GetStdHandle");
t = time(NULL);
pt = localtime(&t);
sprintf(buf, "%02d:%02d:%02d", pt->tm_hour, pt->tm_min, pt->tm_sec);
if (!WriteConsoleOutputCharacter(hConsole, buf, strlen(buf), (COORD){ col, row }, &dwWritten))
ExitSys("WriteConsoleOutputCharacter");
}
Yazının şekil ve zemin rengini değiştirmek için SetConsoleTextAttribute API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL WINAPI SetConsoleTextAttribute(
HANDLE hConsoleOutput,
WORD wAttributes
);
Fonsiyonun ikinci parametresi özellik bilgisini belirtmektedir. Özellik bir renk ya da diğer bazı bileşenlerdne oluşmaktadır. Aşağıdaki
sembolik sabitler bit düzeyinde OR işlemiyle özellik oluşturmakta kullanılabilir:
FOREGROUND_BLUE
FOREGROUND_GREEN
FOREGROUND_RED
FOREGROUND_INTENSITY
BACKGROUND_BLUE
BACKGROUND_GREEN
BACKGROUND_RED
BACKGROUND_INTENSITY
COMMON_LVB_REVERSE_VIDEO
COMMON_LVB_UNDERSCORE
Örneğin:
SetConsoleTextAttribute(hConsole, FOREGROUND_BLUE);
WriteConsole(hConsole, buf, strlen(buf), &dwWritten, NULL);
SetConsoleTextAttribute(hConsole, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED);
İmleci yok etmek için SetConsoleCursorInfo isimli fonksiyon kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
Fonksiyonun ikinci parametresi CONSOLE_CURSOR_INFO isimli bir yapı nesnesinin adresini almaktadır. Bu yapı şöyle bildirilmiştir:
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
Yapının dwSize elemanı 1 ile 100 arasında bir değer almaktadır. Bu değer imlecin büyüklüğünü belirtir. Yapının bVisible elemanı TRUE
ya da FALSE girilebilir. Eğer bu elemana FALSE girilirse imleç görünmez olur. Aslında GetConsole.CursorInfo isimli bu bilgileri alan da
bir fonksiyon vardır. Önce get fonksiyonu kullanılıp sonra set fonksiyonu kullanılırsa yapının diğer elemanı değiştirilmeden işlem
yapılabilir. Örneğin:
CONSOLE_CURSOR_INFO cci;
...
GetConsoleCursorInfo(hConsole, &cci);
cci.bVisible = FALSE;
SetConsoleCursorInfo(hConsole, &cci);
Konsole penceresinin karakter genişliğini ve yüksekliğini almak için GetConsoleScreenBufferInfo API fonksiyonu kullakılmaktadır. Fonksiyonun
prototipi şöyledir:
BOOL WINAPI GetConsoleScreenBufferInfo(
HANDLEhConsoleOutput,
PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo
);
Fonksiyonun ikinci parametresi CONSOLE_SCREEN_BUFFER_INFO isimli bir yapı nesnesinin adresini almaktadır. Bu yapı şöyle bildirilmiştir:
typedef struct _CONSOLE_SCREEN_BUFFER_INFO {
COORD dwSize;
COORD dwCursorPosition;
WORD wAttributes;
SMALL_RECT srWindow;
COORD dwMaximumWindowSize;
} CONSOLE_SCREEN_BUFFER_INFO;
Bu fonksiyonla imlecin konumuda alınabilmektedir. Konsole ekranın genişlike ve yüksekliği şöyle edilir:
CONSOLE_SCREEN_BUFFER_INFO csbi;
...
GetConsoleScreenBufferInfo(hConsole, &csbi);
width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
Aşağıda konsol API fonksiyonlarının kullanımına genel bir verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hConsole;
char buf[1024] = "this is a test";
DWORD dwWritten;
CONSOLE_CURSOR_INFO cci;
CONSOLE_SCREEN_BUFFER_INFO csbi;
int width, height;
int len;
if ((hConsole = GetStdHandle(STD_OUTPUT_HANDLE)) == INVALID_HANDLE_VALUE)
ExitSys("GetStdHandle");
GetConsoleCursorInfo(hConsole, &cci);
cci.bVisible = FALSE;
SetConsoleCursorInfo(hConsole, &cci);
SetConsoleTextAttribute(hConsole, FOREGROUND_BLUE);
SetConsoleCursorPosition(hConsole, (COORD){ 10, 12 });
WriteConsole(hConsole, buf, strlen(buf), &dwWritten, NULL);
GetConsoleScreenBufferInfo(hConsole, &csbi);
width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
SetConsoleCursorPosition(hConsole, (COORD){ 10, 13 });
len = snprintf(buf, 1024, "Width: %d, Height: %d\n", width, height);
WriteConsole(hConsole, buf, len, &dwWritten, NULL);
SetConsoleTextAttribute(hConsole, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED);
getchar();
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz gibi eskiden grafik çalışma yoktu. Yalnızca text modda konsol tabanlı bir çalışma modeli vardı. Text modda
karakterlerin kalıp olarak basıldığını pixel temelinde bir kontrolün sağlanamadığını belirtmiştik. Bu nedenle text modda yani konsol
ekranında pixel işlemleri gereken "resim gösterme" gibi işlemler yapılamamaktadır. Ancak yine text modda özel karakterlerle çerçeve
çizimleri yapılabilmektedir. Bunun için çeşitli karakter kümelerinde ve code page'lerde özel "çerçeve karakterleri (bozing character)"
bulundurulmuştur. Bugün bilgisyaralarımızda pek çok karakter kümesi ve code page kullanılabilmektedir. Ancak UNICODE karakter kümesi
ve bunun UTF-8 denilen encoding'i en yaygın kullanıma sahip olanlardan biridir. Çerçeve karakterleri biribirini kapatan özel karakterlerdir.
Örneğin UNICODE tabloda tipik çerçeve karakterlerinin code point'leri şunlardır:
#define BOX_LL "\u2514"
#define BOX_V "\u2502"
#define BOX_H "\u2500"
#define BOX_UL "\u250C"
#define BOX_UR "\u2510"
#define BOX_LR "\u2518"
Bu UNICODE karakterlerin aynılarının çift çizgili biçimleri de vardır. Ayrıca çerçeve ortası için çerçeveye bağlanma için ayrı karakterler
de bulunmaktadır.
Yukarıdaki sembolik sabitlerde \uxxxx biçimindeki karakterler UNICODE UTF-16 code point'lerini bellirtmektedir. Ancak bu code point'ler
derleyicilerin "execution character set" denilen ayarlarına bakılarak derleyiciler tarafından dönüştürülmektedir. Bugün pek çok standart C
derleyicisinin default "execution character set" ayarı UNICODE UTF-8 biçimindedir. Eğer terminal de bu encoding'e ayarlanmışsa sorun
çıkmayacaktır. Burada kullandığımız kavramlar kursumuzun "karakter kodlamalarının (character encoding)" anlatıldığı bölümde ayrıntılı
biçimde ele alınacaktır.
Visual Studio'da derleyicinin "execution character set"i eğer UTF-8 değilse onu proje seçeneklerinden C/C++ komut satırı "Ek Seçenekler"de
"/utf-8" girerek UTF-8 olarak dğeiştirebilirsiniz. Eğer console ekranınızın code page'i UTf-8 değilse aşağıdaki registry ayarından onu
"65001" yaparak UTF-9'e geçirebilirsiniz:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage\OEMCP
Ayrıca default olarak bir konsol penceresinin UTF-8 encoding'i ile açılmasını sağlamak için aşağıdaki anahtara "@chcp 65001>nul"
girmelisiniz:
HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\Autorun
Aşağıda bir çerçeve çizimine örnek verilmiştir
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define BOX_LL "\u2514"
#define BOX_V "\u2502"
#define BOX_H "\u2500"
#define BOX_UL "\u250C"
#define BOX_UR "\u2510"
#define BOX_LR "\u2518"
void save_cursor(void)
{
printf("\x1B\x37");
}
void restore_cursor(void)
{
printf("\x1B\x38");
}
void move_cursor(int row, int col)
{
printf("\x1B[%d;%dH", row, col);
}
void hide_cursor(void)
{
printf("\x1B[?25l");
}
void show_cursor(void)
{
printf("\x1B[?25h");
}
int main(int argc, char *argv[])
{
save_cursor();
hide_cursor();
move_cursor(10, 10);
printf(BOX_LL);
move_cursor(9, 10);
printf(BOX_V);
move_cursor(10, 11);
printf(BOX_H);
move_cursor(10, 12);
printf(BOX_H);
move_cursor(8, 10);
printf(BOX_UL);
move_cursor(8, 11);
printf(BOX_H);
move_cursor(8, 12);
printf(BOX_H);
move_cursor(8, 13);
printf(BOX_UR);
move_cursor(9, 13);
printf(BOX_V);
move_cursor(10, 13);
printf(BOX_LR);
restore_cursor();
show_cursor();
return 0;
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Biz daha önce Windows'ta UNIX/Linux sistemlerinde "temel dosyalarını" görmüştük. Bu fonksiyonlar açık bir dosya üzerinde işlemler
yapmaktaydı. Ancak işletim sistemlerinde dosyalar üzerinde onları açmdan işlem yapan çeşitli yardım dosya fonksiyonları da bulunmaktadır.
Bu yardımcı dosya fonksiyonları aslında birer sistem sistem fonksiyonu olarak çekirdeğin içerisinde bulundurulmaktadır. Windows sistemlerinde
ilgili API fonksiyonları UNIX/Linux sistemlerinde de ilgili POSIX fonksiyonları bu sistem fonksiyonlarını çağırarak işlemlerini yapmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde dosya silmek için DeleteFile isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL DeleteFile(
LPCTSTR lpFileName
);
Fonksiyon silinecek dosyanın yol ifadesini parametre olarak alır. Başarı durumunda sıfır dışı bir değere başarıszlık durumunda 0 değerine
geri döner. Bir dosya çeşitli nedenlereden dolayı silinemeyebilir. Bu nedenle fonksiyonun başarsı kontrol edilmeli ve başarısızlık durumunda
uygun hata mesajı verilmelidir. Örneğin:
if (!DeleteFile("test.txt"))
ExitSys("DeleteFile");
Kursun başında görmüş olduğumuz remove standart C fonksiyonu aslında Windows sistemlerinde DeleteFile API fonksiyonunu çağıran bir sarma
fonksiyondur.
Windows'ta bir dosya açıkken dosyayı silebilir miyiz? Windows'ta bu durum dosyanın nasıl açıldığına göre değişebilmektedir.
Eğer bir dosya CreateFile fonksiyonu ile açılırken fonksiyonun üçüncü parametresinde FILE_SHARE_DELETE bayrağı eklenmezse bu durumda
dosya açıkken başka bir proses dosyayı silemez. Dolayısıyla DeleteFile başarısız olur. Ancak CreateFile fonksiyonunun üçüncü parametresine
FILE_SHARE_DELETE baytrağı eklenirse bu durumda başka bir proses artık dosyayı silebilir. Tabii bu durumda dosya dizin girişinden silinir. Ancak
dosya açmış olan prosesler dosyayı kullanmaya devam ederler. Dosya kullanan son proses de dosyayı kapattığı zaman dosya gerçek anlamda silinecektir.
Microsoft C derleyicisinin fopen fonksiyonu dosyayı CreateFile ile açarken FILE_SHARE_DELETE bayrağını kullanmamaktadır.
Aşağıda dosya silmeye bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
if (!DeleteFile("test.txt"))
ExitSys("DeleteFile");
printf("File successfully deleted...\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
17. Ders 05/08/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Bir dosyanın Windows'ta "taşınması (move edilmesi)" nasıl yapılmaktadır? Örneğin Windows'ta elimizde "c:\Study" dizini içerisinde "test.txt"
isimli bir dosya bulunuyor olsun. Biz de bu dosyayı "c:\temp" dizinine taşımak isteyelim. Normal olarak biz dosyaalara "dizin girişleri (directory enty)"
yoluyla erişiriz. Dosyaların gerçek verileri (yani dosyaların içindeki bilgiler) diskte başka bir yerdedir. Dizin girişleri aslında dosyanın
içindeki bilgilerin bulunduğu disk alanına referans eden bilgileri içermektedir. Dolayısıyla bir dosyanın taşınması sırasında aslında
dosyanın içerisindkei bilgilerde genellikle bir taşıma yapılmamaktadır. Yalnızca eski dizin girişi silinip yeni dizin girişi aynı dosyanın
diskteki bilgilerine referans edecek biçimde oluşturulmaktadır. Yani biz "C:\Study" dizininin içerisindeki "test.txt" dosyasını "C:\temp"
dizinine taşırken aslında dosyanın içerisindeki bilgileri diskte bir yerden bir yere taşımamaktayız. Bu durumda işletim sistemi aslında
"C:\Study" dizinindeki "test.txt" dizin girişini siler, "C:\Sample" dizininde aynı dosyanın bilgilerine referans eden yeni bir "test.txt"
dizin girişi oluşturur. Bu işlem dosyanın içindeki bilgileri taşımaktan çok daha hızlı yapılabilen bir işlemdir. Bu durumda işletim
sistemlerinde "isim değiştirme (rename)" "taşıma (move)" aslında aynı anlama gelmektedir. Yani biz bir dosyanın ismini değiştirdiğimizde aslında
sanki onu aynı dizin içerisine başka bir isimle taşıyor gibi olmaktayız. Tabii bazen işletim sistemi gerçekten dosyanın içerisindeki
bilgileri de taşımak zorunda kalabilmektedir. Örneğin Windows'ta iki hard diskimiz olsun. Biz diskteki dosyayı diğer diske taşırken
mecburen işletim sistemi dosya içeriğini de fiziksel olarak taşıyacaktır.
Windows sistemlerinde dosya taşımak için (ve tabii dosyanın ismini değiştirmek için) MoveFile isimli API fonksiyonu kullanılmaktadır.
Fonksiyonun prototipi şöyledir:
BOOL MoveFile(
LPCTSTR lpExistingFileName,
LPCTSTR lpNewFileName
);
Fonksiyonun birinciparametresi kaynak yol ifadesini ikinci parametresi hedef yol ifadesini almaktadır. Fonksiyonun geri dönüş değeri
işlemin başarısını belirtmektedir. Dosyanın taşınması aslında dosyanın aynı zamanda silinmesi gibi bir etki de yaratmaktadır.
Dolayısıyla bir proses dosyayı FILE_SHARE_DELETE bayrağını kullanmadan açmışsa başka bir proses dosyayı MoveFile ile taşıyamaz.
Aşağıdaki örnekte prosesin çalışma dizinindeki "test.txt" dosyasının ismi "mest.txt" olarak değiştirilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
if (!MoveFile("test.txt", "mest.txt"))
ExitSys("MoveFile");
printf("File successfully moved...\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Windows API fonksiyonlarının büyük bölümü çok eskiden tasarlanmıştır. Zamanla yeni birtakım özelliklerin de bazen etkisiyle bu fonksiyonlarda
genişletme (yani işlevlerini gemişletme) yapma gereksinimi ortaya çıkmıştır. Bu durumda Microsoft eski fonksiyony bulunurmaya devam ederek onun
daha gelişmiş yeni bir versiyonunu da oluşturmuştur. Genel olarak Micrsoft bir fonksiyonun genişletilmiş yeni versyionunu Ex sonekiyle isimlendirmektedir.
Örneğin LockFile fonksiyonunun yeni genişletilmiş ismi LockFileEx biçimindedir. APı fonksiyonlarının Ex'li versiyonları nıormal versiyonlarını da kapsar
niteliktedir. Ancak genel olarak Ex'li versiyonların daha fazla parametreye sahip olma eğilimi vardır. Eğer sizin bu Ex'li versiyonşları kullanmak
için nedeniniz yoksa bunların Ex'siz normal versiyonlarını kullanabilirsiniz.
API fonksiyonlarının Ex'li versyonları oluşturulduğunda genel olarak Ex'siz versiyonları "deprecated" yapılmamaktadır. Bir fonksiyonun
"deprecated" yapılması "şimdilik muhafa edildiği ancak gelecekte kaldırılabileceği dolayıyla da artık bu fonksiyonu kullanmak isteyenlerin
bu fonksiyonu kullanmamaları gerektiği" anlamına gelmektedir. Tabii bir fonksiyon "deprecated" yapılmışsa onun yerini tutan başka bir fonksiyon
da bulunuyor durumdadır. Dokğmanlar "deprecated" olan fonksiyon yerine hangi fonksiyonun tercih edilmesi gerektiğini de belirtmektedir.
"Deprecated" sözcüğü yalnızca Windows API fonksiyonlarında değil aynı zamanda C standartlarında da kullanılan bir sözcüktür
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde GetFileSize API fonksiyonu açılmış bir dosyanın uzunluğunu bize verir. Bu fonksiyonun GetFileSizeEx isminde
genişletilmiş bir biçimi de vardır. GetFileSiz fonksiyonun prototipi şöyledir:
DWORD GetFileSize(
HANDLE hFile,
LPDWORD lpFileSizeHigh
);
Fonksiyonun birinci parametresi açılmış dosyanın handle değerini almaktadır. Fonksiyonun geri dönüş değeri dosya uzunluğunun düşük
anlamlı DWORD kısmıdır. Fonksiyonun ikinci parametresi dosya uzunluğunun yüksek anlamlı DWORD kısmının yerleştirileceği DWORD nesnenin
adresini almaktadır. İkinci pparametre NULL geçilebilir. Bu durumda dosyanın ununluğunun yüksek anlamlı DWORD değeri elde edilemez.
Fonksiyonun ikinci parametresi NULL girilmediğinde fonksiyon başarısız olursa fonksiyon INVALID_FILE_SIZE özel değerine geri dönmektedir.
Tabii bu özel değerin aslında dosyanın gerçek uzunluk değeri olma olasılığı da vardır. Bu nedenle MSDN dokümanları böylesi bir durumda
ayrıca GetLastError fonksiyonun çağrılması gerektiğini belirtmektedir. Fonksiyon başarısız ise GetLastError kesinlikle NO_ERROR dışında bir
değer vermektedir. Ancak fonksiyon da şöyle bir kusur vardır: Eğer fonksiyonun ikinci aparametresi NULL geçilirse bu durumda fonksiyonun geri dönüş
değerinde başarı ya da başarısızlık anlaşılamamaktadır.
dwSizeLow = GetFileSize(hFile, &dwLow);
if (dwSizeLow == INVALID_FILE_SIZE && GetLastError() != NO_ERROR)
ExitSys("GetFileSize");
ık bir dosyanın uzunluğunu elde etmenin diğer bir yolu da dosya göstericisini EOF durumuna yerleştirip dosya göstericisinin konumunu
almak olabilir. Tabii bir dosyanın uzunluğunu almak için dosyanın açılması oldukça zahmetlidir. Aslında dosyayı hiç açmadan dosya uzunluğunun
elde edilmesi de mümkündür. Bu işlemin yapılabileceği izleyen paragraflarda ele alınmaktadır.
Aşağıdaki örnekte dosya uzunluğu GetFileSize fonksiyonu ile elde edilmiştir. Bu örnekte fonksiyonun ikinci aparamtresi NULL girilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFile;
DWORD dwSizeLow, dwSizeHigh;
if ((hFile = CreateFile("Test.c", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
if ((dwSize = GetFileSize(hFile, &dwSizeHigh)) == INVALID_FILE_SIZE && GetLastError() != NO_ERROR)
ExitSys("GetFileSize");
printf("%lu\n", (unsigned long)dwSize); /* prints only low part */
CloseHandle(hFile);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
GetFileSize fonksiyonundaki tasarım hatası GetFileSizeEx fonksiyonuyal gidirilmiştir. GetFileSizeEx fonksiyonunun prototipi şöyledir:
BOOL GetFileSizeEx(
HANDLE hFile,
PLARGE_INTEGER lpFileSize
);
Fonksiyon dosya uzunluğunu LARGE_INTEGER türündne bir yapı nesnesinin içerisine yerleştirmektedir. Fonksiyonun geri dönüş değeri işlemin
başarısını belirtmektedir. LARGE_INTEGER yapısı şöyle bildirilmiştir:
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart;
LONG HighPart;
} DUMMYSTRUCTNAME;
struct {
DWORD LowPart;
LONG HighPart;
} u;
LONGLONG QuadPart;
} LARGE_INTEGER;
Aşağıda GetFileSizeEx API fonksiyonunun kullanımına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFile;
LARGE_INTEGER li;
if ((hFile = CreateFile("Test.c", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
if (!GetFileSizeEx(hFile, &li))
ExitSys("GetFileSizeEx");
printf("%lld\n", li.QuadPart);
CloseHandle(hFile);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Bir dizin (directory) de aslında bir çeşit dosyadır. Normal bir dosyanın içerisinde dosyanın bilgileri bulunur. Ancak bir dizin dosyasının
içerisinde o dizindeki dosyaların neler olduğuna yönelik bilgiler bulunmaktadır. İşletim sistemleri genel olarak normal dosyalarla dizinleri
aynı biçimde organize derler.
Windows'ta bir dizin yaratmak için CreateDirectory isimli API fonksiyonu kullanılır. Fonksiyonun prototipi şöyledir:
BOOL CreateDirectory(
LPCSTR lpPathName,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
Fonksiyonun birinci parametresi yaratılacak dizin'in yol ifadesini belirtmektedir. İkinci parametre yaratılcacak dizin'in güvenlik özelliklerini
almaktadır. Bu parametre NULL eçilebilir. Fonksiyonun geri dönüş değeri işlemin başarsını belirtmektedir.
Bir dizin yaratıldığında içerisine otomatik olarak iki dizin girişi yerleştirilmektedir. Bu dizin girişlerinin isimleri "." ve ".." biçimindedir.
"." ve ".." girişleri de birer dizin belirtir. "." girişi içinde bulunulan dizini, ".." girişi ise içinde bulunulan dizinin üst dizinini
belirtir. Bu dizin girişleri silinememektedir. Kök dizinin dışında tüm alt dizinlerde her zaman "." ve ".." dizin girişleri bulunmaktadır.
Aşağıdaki Windows'ta CreateDirectory fonksiyonunun kullanımına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
if (!CreateDirectory("TestDir", NULL))
ExitSys("CreateDirectory");
printf("Ok\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Windows'ta bir dizin silmek için RemoveDirectory isimli API fonksiyonu kullanılır. Fonksiyonun prototipi şöyledir:
BOOL RemoveDirectory(
LPCSTR lpPathName
);
Fonksiyon parametre olarak silinecek dizin'in yol ifadesini almaktadır. Geri dönüş değeri işlemin başarısını belirtir. RemoevDirectory
fonksiyonu ile biz ancak boş dizinleri silebiliriz. Bir dizin'in boş olması demek onun içerisinde "." ve ".." dışında hiçbir dizin girişinin
olmaması demektir. (Zaten bu "." ve ".." girişlerinin silinemediğine dikkat ediniz.) Fonksiyonun yalnızca boş dizinleri silmesi güvenli bir
kullanım için öngörülmüştür. Aksi takdirde yanlıklıkla büyük bir dizin ağacı silinebilirdi.
Geri Dönüşüm Kutusu (Recycle Bin) işletim sisteminin çekirdeği ile ilgili bir oragizasyon değildir. Kabuk kısmı ile ilgili bir organizasyondur.
Bu nedenle DeleteFile ya da RemoveFile API fonksiyonları silinen öğeleri geri dönüş kutusuna atamaz. Biz "Dosya Gezgini (File Explorer)"
ile bir dosyayı ya da dizini sildiğimizde dosya gezgini default durumda silinen bu öğeleri geri dönüş kutusuna atmaktadır. Bu durum anlaşılabilir.
Çünkü geri dönüşüm kutusu zaten kabuk tarafından organize edilmektedir. Dosya gezgininde bir dizinin üzerine gelip DEL tuşu ile dizini silmenin
de bu bakımdan bir geri dönüş vardır. Halbuki RemoveDirectory ile bu biçimde geri dönüş mümkün değildir.
Aşağıda bir dizinin silinmesi örneği verilmiştir. Bju örneği test ederken dizin'in içerisine dosya yerleştirerek de programı çalıştırınız.
Bu durumda RemoveDirectory fonksiyonunun başarısız olduğunu göreceksiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
if (!RemoveDirectory("TestDir", NULL))
ExitSys("CreateDirectory");
printf("Ok\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
18. Ders 06/08/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde dosya unlink isimli POSIX fonksiyonu ile silinmektedir. Bu sistemlerde aslında remove standart C fonksiyonu
doğrudan bu unlink fonksiyonunu çağırmaktadır. Başka bir deyişle bu sistemlerde remove fonksiyonu ile unlink fonksiyonu arasında bir
fark yoktur. unlink fonksiyonun prototipi şöyledir:
#include <unistd.h>
int unlink(const char *pathname);
Fonksiyon parametre olarak silinecek dosyanın yol ifadesini alır. Başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri döner.
errno değişkeni uygun biçimde set edilir.
Yukarıda da aslında dizinlerin normal dosyalar gibi olduğunu belirtmiştik. Dizinler aslında "dizinler içerisindeki girişlerin tutulduğu"
normal dosyalar gibidir. Bir dosyanın silinmesi aslında o dosyanın içinde bulunduğu dizinde bir yazma işlemini gerektirmektedir. Bu nedenle
UNIX/Linux sistemlerinde bir dosyayı silebilmek için prosesin dosyaya "w" hakkının olması gerekmez. Önemli olan prosesin dosyanın içinde
bunduğu dizine "w" hakkının olmasıdır. O halde bu sistemlerde bir dosyanın silinebilmesi için remove ya da unlink fonksiyonunu çağıran prosesin
dosyanın içinde bulunduğu dizine yazma hakkının olmasıdır.
Aşağıda komut satırı argümanı olarak alınan dosyaların unlink fonksiyonuyla silinmesine yönelik bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
if (argc == 1) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
for (int i = 1; i < argc; ++i)
if (unlink(argv[i]) == -1)
fprintf(stderr, "cannot unlink %s: %s\n", argv[i], strerror(errno));
return 0;
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Anımsanacağı gibi UNIX/Linux sistemlerinde dosyanın erişim hakları dosya open fonksiyonuyla yaratılırken fonksiyonun üçüncü parametresiyle
belirleniyordu. İşte dosyanın erişim hakları daha sonra da chmod isimli POSIX fonkisyonuyla değiştirilebilmektedir. Fonksiyonun prototipi
şöyledir:
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
Fonksiyonun birinci parametresi erişim hakları değiştirilecek dosyanın yl ifadesini ikinci parametresi yeni erşim haklarını belirtmektedir.
Erişim hakları değiştirlirken prosesin umask değeri işlemde etkili olmamaktadır.
ık dosyaların erişim hakları da fchmod fonksiyonuyla değiştirilmektedir. Fonksiyonun prototipi şöyledir:
#include <sys/stat.h>
int fchmod(int fd, mode_t mode);
Fonksiyonun birinci parametresi dosya betimleyicisini ikinci parametresi erişim haklarını belirtmektedir. Fonksiyonlar başarı durumunda
0 değerine, başarısızlık durumunda -1 değerine geri döner ve errno değişkeni uygun biçimde set edilmektedir.
Eğer dosya zaten açıksa dosyanın diskte isimsel olarak aranmasına gerek kalmayacağı için fchmod fonksiyonu chmod fonksiyonuna göre
daha hızlı işlem yapma potansiyelindedir.
chmod fonksiyonuyla bir dosyanın erişim haklarının değiştirilebilmesi için fonksiyonu çağıran prosesin etkin kullanıcı id'si dosyanın kullanıcı
id'si ile aynı olması ya da prosesin "uygun önceliğe (appropriate privilige)" sahip olması gerekmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
void exit_sys(const char *msg);
int main(void)
{
if (chmod("test.txt", S_IRUSR | S_IWUSR) == -1)
exit_sys("chmod");
printf("success...\n");
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda fchmod fonksiyonun kullanılmasına bir ö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;
if ((fd = open("test.txt", O_RDONLY)) == -1)
exit_sys("open");
if (fchmod(fd, S_IRUSR | S_IWUSR) == -1)
exit_sys("fchmod");
close(fd);
printf("success...\n");
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte komut satırından alınan dosyaların erişim hakları komut satırından verilen erişim hakkı değiştirilmektedir.
Programın kullanımı aşağıdaki gibidir:
./mychmod 666 x.dat y.dat ...
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* chmod.c */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
bool check_octal(const char *str);
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int mode;
if (argc < 3) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if (!check_octal(argv[1])) {
fprintf(stderr, "file mode incorrect!..\n");
exit(EXIT_FAILURE);
}
sscanf(argv[1], "%o", &mode);
for (int i = 2; i < argc; ++i)
if (chmod(argv[i], mode) == -1)
fprintf(stderr, "chmode failed for %s: %s\n", argv[i], strerror(errno));
return 0;
}
bool check_octal(const char *str)
{
while (*str != '\0') {
if (*str < '0' || *str > '8')
return false;
++str;
}
return true;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Aslında UNIX/Linux sistemlerinde komut satırından dosyaların erişim haklarını değiştirmek için kullanılan "chmod" isimli bir POSIX
komutu vardır. Tabii bu komut /bin dizinine yerleştirilmiş bir programdır ve bu program da aslında chmod POSIX fonksiyonu kullanılarak
yazılmıştır. Komutun değişik kullanım biçimleri vardır. Örneğin:
chmod 666 x.dat y.dat
chmod a+w x.dat
chmod +w x.dat
chmod o+w x.dat
chmod ugo-w x.dat
Komutun detaylı kullanımı için uygun dokümanlara başvurabilirsiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde bir "dizin (directory)" yaratmak için mkdir isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi
şöyledir:
#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
Fonksiyonun birinci parametresi dizin'in yol ifadesini ikinci parametresi ise dizin'in erişim haklarını almaktadır. Erişim haklarında
prosesin umask değeri etkili olmaktadır. Dizinlerdeki erişim haklarında "x" hakkı özel ve başka bir anlama gelmektedir. Normal olarak
dizinlerde "x" var olduğuna dikkat ediniz. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri dönmektedir.
başarısızlık durumunda errno değişkeni uygun biçimde set edilmektedir.
Bir dizin yaratabilmek için ve bir dosya yaratabilmek için o diznin'in ya da dosyanın yaratılacağı dizine "w" hakkının olması gerekir.
Çünkü yukarıda da belirttiğimiz gibi dizinler aslında "içerisinde dosya bilgilerinin bulunduğu dosyalar" gibidir. Dolayısıyla bir dizinde
dosya ya da dizin yaratmak aslında o dizin dosyası üzerinde bir değişiklik yapma anlamına gelmektedir. Bu nedenle de prosesin ilgili dizine
"w" hakkının bulunması gerekmektedir. Tabii "uygun önceliğe (appropriate privilege)" sahip prosesler her yerde dizin yaratabilirler.
Komut satırında da dizin yaratmakl için "mkdir" isimli bir POSIX komutu bulunmaktadır. mkdir komurunda -m ya da --mode ile erişim hakları
verilmezse komut default olarak tüm erişim haklarını prosesin umask değerine sokarak oluşturmaktadır. Ancak -m ya da --mode seçeneği ile
ıkça erişim hakları verilirse umask değeri dikkate alınmamaktadır.
Yine UNIX/Linux sistemlerinde de bir dizin yaratıldığında dizin içerisinde "." ve ".." isimli iki dizin girişi oluşturulmaktadır.
Aşağıda mkdir programının bir benzeri mymkdir ismiyle yazılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* mymkdir.c */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <getopt.h>
bool check_octal(const char *str);
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int m_flag;
int err_flag;
char *m_arg;
int result;
int mode;
struct option options[] = {
{"mode", required_argument, NULL, 'm'},
{0, 0, 0, 0}
};
m_flag = err_flag = 0;
opterr = 0;
while ((result = getopt_long(argc, argv, "m:", options, NULL)) != -1) {
switch (result) {
case 'm':
m_arg = optarg;
m_flag = 1;
break;
case '?':
if (optopt == 'm')
fprintf(stderr, "option -m or --mode without argument!..\n");
else if (optopt != 0)
fprintf(stderr, "invalid option: -%c\n", optopt);
else
fprintf(stderr, "invalid long option: %s\n", argv[optind - 1]);
err_flag = 1;
break;
}
}
if (err_flag)
exit(EXIT_FAILURE);
if (argc - optind == 0) {
fprintf(stderr, "requires at least one path name!..\n");
exit(EXIT_FAILURE);
}
if (m_flag) {
if (!check_octal(m_arg)) {
fprintf(stderr, "invalid file mode: %s\n", m_arg);
exit(EXIT_FAILURE);
}
sscanf(m_arg, "%o", &mode);
umask(0);
}
else
mode = 0777; /* POSIX 2008 ve sonrasında doğrudna sayı girilebiliyor */
for (int i = optind; i < argc; ++i)
if (mkdir(argv[i], mode) == -1)
perror(argv[i]);
return 0;
}
bool check_octal(const char *str)
{
while (*str != '\0') {
if (*str < '0' || *str > '8')
return false;
++str;
}
return true;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde dizin silmek için rmdir isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
#include <unistd.h>
int rmdir(const char *pathname);
Fonksiyon dizin'in yol ifadesini parametre olarak alır. Başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri döner. Yine
bir dizin'in silinmnesi için prosesin dizin'in içinde bulunduğu dizine "w" hakkına sahip olması ya da "uygun önceliğe (appropriate privilege)"
sahip olması gerekmektedir.
Komut satrtından bir dizini silmek için "rmdir" isimli POSIX fonksiyonu da kullanılmaktadır. Tabii rmdir komutu aslında /bin dizinindeki bir
programdır. Bu program rmdir POSIX fonksiyonu kullanılarak yazılmıştır.
Aşağıda dizin silmeye yönelik bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
for (int i = 1; i < argc; ++i)
if (rmdir(argv[i]) == -1)
perror(argv[i]);
return 0;
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Bir dosyanın çeşitli bilgileri stat, lstat ve fstat fonksiyonlarıyla elde edilmeketdir. Aslında "ls -l" komutu bu fonksiyonlar kullanılarak
yazılmıştır. Biz bu fonksiyonlarla bir dosyanın eriim haklarını, kullanıcı ve grup id'lerini, uzunluğunu vs. elde edebiliriz.
Bu üç fonksiyon aslında aynı amaca hizmet etmektedir. Aralarında küçük farklılılar vardır. Fonksiyonların prototipleri şöyledir:
#include <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 fonksiyonu yol ifadesiyle belirtilen dosyanın bilgilerini elde eder. fstat fonksiyonu ise dosya zaten açılmışsa dosya betimleyicisinden
hareketle dosya bilgilerini elde etmektedir. lstat fonksiyonu da yol ifadesinden hareketle dosya bilgilerini elde eder. Ancak lstat
sembolik bağlantı dosyalarını izlememektedir. Sembolik bağlantı dosyaları hakkında ileride bilgiler verilecektir. Fonksiyonlar dosya bilgilerini
ikinci parametreleriyle belirtilen struct stat isimli bir yapı nesnesinin içerisine yerleştirmektedir. Fonksiyonlar başarı durumunda 0 değrine
başarısızlık durumunda -1 değerine geri dönmektedir. errno değişkeni başarısızlık durumunda uygun biçimde set edilmektedir. struct stat yapısı
<sys/stat.h> dosyası içerisinde şöyle bildirilmiştir:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtine st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
Yapının elemanlarında kullanılan xxx_t biçimindeki tür isimlerinin hepsi <sys/stat.h> ve <sys/types.> dosyaları içerisinde typedef edilmiştir.
Yapının st_dev elemanı dosya içinde bulunduğu aygıtın aygıt numarasını belirtmektedir. dev_t türü bir tamsayı türü biçiminde typedef edilmek
zorundadır. Yapının st_ino elemanı dosyanın i-node numarasını belirtmektedir. inode numarası konusu ileride ele alınacaktır. ino_t türü
işaretsiz bir tamsayı türü biçiminde typedef edilmek zorundadır. Yapının st_mode elemanı dosyanın türünü ver erişim haklarını içermektedir.
mode_t türü bir tamsayı türü biçiminde typedef edilmek zorundadır. Yapının st_nlink elemanı dosyanın "hard link sayısını" belirtmektedir.
Hard link kavramı ileride ele alınacaktır. nlink_t bir tamsayı türü olacak biçimde typedef edilmek zorundadır. Yapının st_uid ve st_gid
elemanları sırasıyla dosyanın kullanıcı ve grup id değerlerini belirtmektedir. uid_t ve gid_t tamsı birer tamsayı türü olarak typedef edilmek
zorundadır. Yapının st_rdev elemanı eğer dosya bir aygıt sürücü dosyası (device file) ise o aygıt sürücü dosyasının aygıt numarasını
belirtmektedir. Yapının st_size elemanı dosyanın uzunluğunu belirtmektedir. off_t işaretli bir tamsayı türü biçiminde typedef edilmek
zorundadır. Yapının st_blksize elemanı dosyanın içinde bulunduğu aygıttaki dosya parçalarının tutulduğu bloğun uzunluğunu belirtmektedir.
Bu uzunluk sektör katlarında (512'nin katlarında) olacaktır. Bugün tipik olarak ext dosya sistemleri formatlanırken bir blok 8 sektör
alınmaktadır. Ancak bu durum çeşitli faktörlere bağlı olarak değişebilmektedir. Dosya bloklarının ne anlam ifade etiiği kursumuzun ilerleyen
bölümlerinde ele alınacaktır. blksize_t işaretli bir tamsayı türü biçiminde typedef edilmek zorundadır. Yapının st_blocks elemanı dosyanın
diskte kaç sektörde (yani kaç tane 512 byte içerisinde) bulunduğunu belirtmektedir. blkcnt_t işaretli bir tamsayı türü biçiminde typedef
edilmek zorundadır. Yapının st_atim, st_mtim ve st_ctim elemanları sırasıyla dosyadan son okuma yazpıldığı zamanı, dosyaya son yazma
yapıldığı zamanı ve dosyanın i-node bilgilerinin (yani stat fonksiyonu şle elde ettiğimiz meta-data bilgilerinin) değiştirildiği zamanı
belirtmektedir. Eskiden bu elemanlar time_t türündendi ve 01/01/1970 tarihinden geçen saniyesi sayısını belirtmekteydi. Sonra POSIX
Standartlarının 2008 versiyonunda bu elemanlar detaylandırılmış ve timespec isimli bir yapı türünden yapılmışitır. timepec yapısı
hem 01/01/1970'ten geçen saniye sayısını ve aynı zamanda o saniyeden sonraki nano saniye sayısını tutmaktadır. Yani timespec yapısı
tarih ve zamanı nano saniye duyarlılığında tutmaktadır. timespec yapısı şöyle bildirilmiştir:
struct timespec {
time_t tv_sec;
long tv_nsec;
};
Eskiden stat yapısının bu tarih zaman bilgisini içeren elemanlarının isimleri ve türleri şöyleydir:
time_t st_atime;
time_t st_mtime;
time_t st_ctime;
POSIX 2008 ile bu konuda değişiklik yapılınca eski programların derlenebilmesi için şu makrolar bulundurulmaktadır:
#define st_atime st_atim.tv_sec
#define st_mtine st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
Böylece programcılar dilerlerse eski isimleri de kullanabilmektedir. Dosya sistemi ile ilgili hangi POSIX fonksiyonlarının bu üç tarih
zaman bilgisinin hangilerini değiştirdiği POSIX standartlarında açıkça belirtilmiştir. Örneğin write POSIX fonksiyonu dosyanın st_mtim
ve st_ctim elemanlarını değiştirmektedir. Örneğin rmdir fonksyonu üst dizinin st_mtim ve st_ctim elemanlarını değiştirmektedir.
stat yapısının st_mode elemanının dosyanın türü ve erişim bilgilerini verdiğini belirtmiştik. Dosyanın türü ve erişim hakları bu st_mode
elemanının çeşitli bitlerine kodlanmıştır. Programcının bu bilgilerin hangi bitlere kodlandığını bilmesine gerek yoktur. POSIX standartları
bu tür bilgilerinin st_mode elemanının hangi bitlerine kodlandığı konusunda da bir açıklama bulunmamaktadır. Ancak <sys/stat.h> dosyası
içerisinde S_ISXXX biçiminde çeşitli tür makroları bulundurulmuştur. Programcı yapının st_mode elemanını bu makrolara argüman olarak verdiğinde
bu makrolar elemanın ilgili bitlerine bakarak dosyanın ilgili türden olup olmadığını belirlerler. Eğer dosya ilgili türdense bu makrolar
sıfır dışı bir değer ilgili türden değilse 0 değerine geri dönmektedir. Bu makrolar şunlardır:
S_ISBLK(m) Dosya bir blok aygıt sürücü dosyası mı? (ls -l komutunda "b" harfi ile temsil ediliyor)
S_ISCHR(m) Dosya bir karakter aygıt sürücü dosyası mı? (ls -l komutunda "c" harfi ile temsil ediliyor)
S_ISDIR(m) Dosya bir dizin dosyası mı? (ls -l komutunda "d" harfi ile temsil ediliyor)
S_ISFIFO(m) Dosya bir boru (pipe) dosyası mı? (ls -l komutunda "p" harfi ile temsil ediliyor)
S_ISREG(m) Dosya sıradan bir disk dosyası mı? (regular file) (ls -l komutunda "-" harfi ile temsil ediliyor)
S_ISLNK(m) Dosya bir sembolik bağlantı dosyası mı? (ls -l komutunda "l" harfi ile temsil ediliyor)
S_ISSOCK(m) Dosya bir UNIX Domain soket dosyası mı? (ls -l komutunda "s" harfi ile temsil ediliyor)
O halde örneğin dosyanın türünü şöyle belirleyebiliriz:
struct stat finfo;
...
if (stat(path, &finfo) == -1)
exit_sys("stat");
...
if (S_ISBLK(finfo.st_mode))
putchar('b');
else if (S_ISCHR(finfo.st_mode))
putchar('c');
else if (S_ISDIR(finfo.st_mode))
putchar('d');
else if (S_ISFIFO(finfo.st_mode))
putchar('p');
else if (S_ISREG(finfo.st_mode))
putchar('-');
else if (S_ISLNK(finfo.st_mode))
putchar('l');
else if (S_ISSOCK(finfo.st_mode))
putchar('s');
else
putchar('?');
Dosyanın türünü stat yapısının st_mode elemanından elde etmenin diğer bir yolu da bu st_mode elemanını önce S_IFMT değeriyle bit AND
çekmek ve bunun sonucunu switch deyimine sokmak olabilir. S_IFMT st_mode elemanı ile bit AND işlemi dosya türüne ilişkin olan bitlerininin
elde edilmesine yol açmaktadır. Bit AND işlemi sonucunda elde edilen değerler şunlardan birine eşit olmak zorundadır:
S_IFBLK
S_IFCHR
S_IFIFO
S_IFREG
S_IFDIR
S_IFLNK
S_IFSOCK
Bu durumda dosya türü şöyle de tespit edebilir:
struct stat finfo;
...
if (stat(path, &finfo) == -1)
exit_sys("stat");
...
switch (finfo.st_mode & S_IFMT) {
case S_IFBLK:
putchar('b');
break;
case S_IFCHR:
putchar('c');
break;
case S_IFIFO:
putchar('p');
break;
case S_IFREG:
putchar('-');
break;
case S_IFDIR:
putchar('d');
break;
case S_IFLNK:
putchar('l');
break;
case S_IFSOCK:
putchar('s');
break;
default:
putchar('?');
break;
}
Dosyanın erişimn haklarını elde etmenin bir yolu doğrudan yapının st_mode elemanını open fonksiyonunda görmüş olduğumuz S_IXXX değerleriyle
bit AND işlemine sokmaktır. Eğer bu işlemin sonucu sıfır dışı bir değerse bu durumda ilgili erişim hakkı dosyada vardır. Eğer 0 ise ilgili
erişim hakkı dosyada yoktur. Bu durumda dosyanın erişim haklarını ls -l formatında şöyle yazdırabiliriz:
mode_t modes[9] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH};
...
for (int i = 0; i < 9; ++i)
putchar(finfo.st_mode & modes[i] ? "rwx"[i % 3] : '-');
Daha önceden de belirttiğimiz gibi POSIX 2008 ile birlikte artık S_IXXX sembolik sabitlerinin değerleri açıkça standartlarda belirtilmiştir.
Bu değerler incelendiğinde aslında bir sayının düşük anlamlı 9 bitine karşılık geldiği örülmektedir. Başka bir deyişle bu sembolik sabitler
aslında bütün bitleri 0 yalnızca düşük anlamlı 9 bitinden bir 1 olan sembolik sabitlerdir. Böylece biz aynı işlemleri aslında POSIX 2008
ve sonrasında değerleri bir diziye yerleştirmeden aşağıdaki gibi de yapabilmekteyiz:
for (int i = 8; i >= 0; --i)
putchar(finfo.st_mode >> i & 1 ? "rwx"[(8 - i) % 3] : '-');
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda stat fonksiyonun kullanılmasına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <locale.h>
#include <sys/stat.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
struct stat finfo;
mode_t modes[9] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH};
char buf[32];
struct tm *ptm;
if (setlocale(LC_ALL, "tr_TR.utf-8") == NULL) {
fprintf(stderr, "cannot set locale!..\n");
exit(EXIT_FAILURE);
}
if (argc == 1) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
for (int i = 1; i < argc; ++i) {
if (stat(argv[i], &finfo) == -1) {
perror(argv[i]);
continue;
}
printf("File Name: %s\n", argv[i]);
printf("File Mode: ");
/*
if (S_ISBLK(finfo.st_mode))
putchar('b');
else if (S_ISCHR(finfo.st_mode))
putchar('c');
else if (S_ISDIR(finfo.st_mode))
putchar('d');
else if (S_ISFIFO(finfo.st_mode))
putchar('p');
else if (S_ISREG(finfo.st_mode))
putchar('-');
else if (S_ISLNK(finfo.st_mode))
putchar('l');
else if (S_ISSOCK(finfo.st_mode))
putchar('s');
else
putchar('?');
*/
switch (finfo.st_mode & S_IFMT) {
case S_IFBLK:
putchar('b');
break;
case S_IFCHR:
putchar('c');
break;
case S_IFIFO:
putchar('p');
break;
case S_IFREG:
putchar('-');
break;
case S_IFDIR:
putchar('d');
break;
case S_IFLNK:
putchar('l');
break;
case S_IFSOCK:
putchar('s');
break;
default:
putchar('?');
break;
}
for (int i = 0; i < 9; ++i)
putchar(finfo.st_mode & modes[i] ? "rwx"[i % 3] : '-');
/*
for (int i = 8; i >= 0; --i)
putchar(finfo.st_mode >> i & 1 ? "rwx"[(8 - i) % 3] : '-');
*/
putchar('\n');
printf("Hard Link Count: %ju\n", (uintmax_t)finfo.st_nlink);
printf("User Id: %ju\n", (uintmax_t)finfo.st_uid);
printf("Group Id: %ju\n", (uintmax_t)finfo.st_gid);
printf("Size: %jd\n", (intmax_t)finfo.st_size);
ptm = localtime(&finfo.st_mtim.tv_sec);
strftime(buf, 32, "%b %2e %Y %H:%M\n", ptm);
printf("Last Modification Time: %s\n", buf);
printf("--------------------------\n");
}
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Aağıdaki örnekte dosya bilgileri "ls -l" formatına benzer biçimde stdout dosyasına yazdırılmıştır. Ancak burada dosyanın kullanıcı ve grup
id'leri isimsel olarak değil sayısal olarak yazdırılmaktadır. İzleyen paragraflarda bu işlemin nasıl yapıldığını göreceğiz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <locale.h>
#include <sys/stat.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
struct stat finfo;
mode_t modes[9] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH};
char buf[32];
struct tm *ptm;
if (setlocale(LC_ALL, "tr_TR.utf-8") == NULL) {
fprintf(stderr, "cannot set locale!..\n");
exit(EXIT_FAILURE);
}
if (argc == 1) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
for (int i = 1; i < argc; ++i) {
if (stat(argv[i], &finfo) == -1) {
perror(argv[i]);
continue;
}
switch (finfo.st_mode & S_IFMT) {
case S_IFBLK:
putchar('b');
break;
case S_IFCHR:
putchar('c');
break;
case S_IFIFO:
putchar('p');
break;
case S_IFREG:
putchar('-');
break;
case S_IFDIR:
putchar('d');
break;
case S_IFLNK:
putchar('l');
break;
case S_IFSOCK:
putchar('s');
break;
default:
putchar('?');
break;
}
for (int i = 0; i < 9; ++i)
putchar(finfo.st_mode & modes[i] ? "rwx"[i % 3] : '-');
printf("%ju ", (uintmax_t)finfo.st_nlink);
printf("%ju ", (uintmax_t)finfo.st_uid);
printf("%ju ", (uintmax_t)finfo.st_gid);
printf("%jd ", (intmax_t)finfo.st_size);
ptm = localtime(&finfo.st_mtim.tv_sec);
strftime(buf, 32, "%b %2e %Y %H:%M ", ptm);
printf("%s ", buf);
printf("%s\n", argv[i]);
}
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde stat fonksiyonunun yanı sıra stat isimli bir kabuk komutu da vardır. Bu komut dosyaların stat bilgilerini elde edip
onları yazdırmaktadır. Komutun örnek bir kullanımı ve çıktısı şöyledir:
kaan@kaan-virtual-machine:~/Study/SysProg$ stat sample.c
Dosya: sample.c
Boyut: 2008 Bloklar: 8 Kimlik bloku: 4096 normal dosya
Device: 803h/2051d Inode: 280141 Links: 1
Erişim: (0644/-rw-r--r--) Uid: ( 1000/ kaan) Gid: ( 1000/ study)
Erişim: 2023-08-12 19:28:23.516502007 +0300
Değiştirme: 2023-08-12 19:28:23.500502009 +0300
Değişiklik: 2023-08-12 19:28:23.500502009 +0300
Doğum: 2023-06-11 19:48:04.987271791 +0300
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Bir kullanıcıya ilişkin ismi ve id'si verildiğinde o kullanıcının bilgilerini elde etmek için getpwnam ve getpwuid isimli iki POSIX
fonksiyonu kullanılmaktadır. Bu iki fonksiyon da Linux sistemlerinde aslında /etc/passwd dosyası içerisinde arama yapıp o satırdaki
bilgileri bize vermektedir. getpwnam fonksiyonu "isimle" arama yaparken getpwuid fonksiyonu "kullanıcı id'si ile" arama yapmaktadır.
Fonksiyonların prototipleri şöyledir:
#include <pwd.h>
struct passwd *getpwnam(const char *name);
struct passwd *getpwuid(uid_t uid);
Fonksiyonlar sırasıyla kullanıcı isimlerini ve kullanıcı id'lerini parametre olarak almakta ve passwd isimli bir yapı nesnesinin adresiyle
geri dönmektedir. passwd yapısı <pwh.h> dosyası içerisinde aşağıdaki gibi 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 */
};
Fonksiyonlar statik ömürlü, nesnelerin adresiyle geri dönmektedir. Dolayısıyla bu adresleri free hale getirmeye çalışmayınız.
Fonksiyonlar başarısızlık durumunda NULL adrese geri dönmekte ve errno değişkeni uygun biçimde set edilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux türevi sistemlerde genellikle grup bilgileri "/etc/group" denilen dosyanın ieçrisinde tutulmaktadır. Bu dosyaya başvuran kişi
belli bir grup id'ye karşı gelen grup ismini, belli bir grup ismine karşı gelen grup id'sini elde edebilir. UNIX/Linux sistemlerinde
grup bilgilerinin elde edilmesi için getgrnam ve getgrgid POSIX fonksiyonları bulundurulmuştur. Fonksiyonların prototipleri şöyledir:
#include <grp.h>
struct group *getgrnam(const char *name);
struct group *getgrgid(gid_t gid);
Fonksiyonların çalışma biçimi getpwnam ve getpwuid fonksiyonlarındaki gibidir. Fonksiyonlar başarı durumunda group isimli yapı nesnesinin
adresine, başarısızlık durumunda NULL adrese geri dönmektedir. Başarısızlık durumunda errno değişkeni uygun biçimde set edilmektedir.
froup yapısı şö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 */
};
Aşağıdaki örnekte dosya bilgileri "ls -l" formatında yazdırılmıştır. Ancak burada küçük bir kusur vardır. Birden fazla dosya alt alta
yazdırılırken hizalanmamıştır. Bunların hizalanması için her sütunun en geniş yer kaplayan elemanına göre hizalama yapmak gerekir. Ayrıca
"ls -l" komutu default olarak dosya isimlerini sıraya dizerek göstermektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <locale.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
struct stat finfo;
mode_t modes[9] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH};
char buf[32];
struct tm *ptm;
struct passwd *pwd;
struct group *grp;
if (setlocale(LC_ALL, "tr_TR.utf-8") == NULL) {
fprintf(stderr, "cannot set locale!..\n");
exit(EXIT_FAILURE);
}
if (argc == 1) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
for (int i = 1; i < argc; ++i) {
if (stat(argv[i], &finfo) == -1) {
perror(argv[i]);
continue;
}
switch (finfo.st_mode & S_IFMT) {
case S_IFBLK:
putchar('b');
break;
case S_IFCHR:
putchar('c');
break;
case S_IFIFO:
putchar('p');
break;
case S_IFREG:
putchar('-');
break;
case S_IFDIR:
putchar('d');
break;
case S_IFLNK:
putchar('l');
break;
case S_IFSOCK:
putchar('s');
break;
default:
putchar('?');
break;
}
for (int i = 0; i < 9; ++i)
putchar(finfo.st_mode & modes[i] ? "rwx"[i % 3] : '-');
printf("%ju ", (uintmax_t)finfo.st_nlink);
if ((pwd = getpwuid(finfo.st_uid)) != NULL)
printf("%s ", pwd->pw_name);
else
printf("%ju ", (uintmax_t)finfo.st_uid);
if ((grp = getgrgid(finfo.st_gid)) != NULL)
printf("%s ", grp->gr_name);
else
printf("%ju ", (uintmax_t)finfo.st_gid);
printf("%jd ", (intmax_t)finfo.st_size);
ptm = localtime(&finfo.st_mtim.tv_sec);
strftime(buf, 32, "%b %2e %Y %H:%M ", ptm);
printf("%s ", buf);
printf("%s\n", argv[i]);
}
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Tüm kullanıcıların bilgilerinin elde edilmesi için üç POSIX fonksiyondan faydalanılmaktadır: setpwent, getpwent, endpwent. setpwent işin
başında bir kez çağrılır ve işlemin /etc/passwd dosyasının başından başlatılmasını sağlar. Programcı getpwent fonksiyonunu döngü içerisinde
çağırır. İşlem bittiğinde de endpwent fonksiyonu çağrılmalıdır. Fonksiyonların prototipleri şöyledir:
#include <pwd.h>
void endpwent(void);
struct passwd *getpwent(void);
void setpwent(void);
getpwent fonksiyonu her çağrıldığında sıradaki kullaıcının bilgilerini vermektedir. Kullanıcı listesinin sonuna gelindiğinde (yani /etc/passwd
dosyasının sonuna gelindiğinde) fonksiyon NULL adrese geri dönmektedir. Ancak fonksiyon IO hatası nedeniyle de başarısız olabilir. Bu durumda
da fonksiyon NULL adrese geri dönmektedir. Fonksiyonun neden NULL adrese geri döndüğünü anlamak için her çağrıdan önce errno değişkeni programcı
tarafından 0'a çekilir. Sonra fonksiyon NULL adrese geri döndüğünde errno değerine bakılır. POSIX dokümanları fonksiyonun kullanıcı listesi
bittiğinde dolayı NULL adrese dönmesi durumunda errno değerini değiştirmeyeceğini söylemektedir.
Aşağıdaki örnekte sistemdeki tüm kullanıcıların listesi 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 *pwd;
setpwent();
while (errno = 0, (pwd = getpwent()) != NULL)
printf("%s\n", pwd->pw_name);
if (errno != 0)
exit_sys("getpwent");
endpwent();
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Grup dosyasındaki tüm grupların elde edilemsi de tamamen kullanıcıların elde edilmesi işlemindeki gibi yapılmaktadır. Bunun için kullanılan
fonksiyonlar şunlardır:
#include <grp.h>
struct group *getgrent(void);
void setgrent(void);
void endgrent(void);
Aşağıdaki örnekte sistemdeki tüm grup isimleri 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;
setgrent();
while (errno = 0, (grp = getgrent()) != NULL)
printf("%s\n", grp->gr_name);
if (errno != 0)
exit_sys("getgrent");
endgrent();
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
20.Ders 13/08/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Dizinlerin içerisinde dosyalar ve dizinler olabilmektedir. İşletim sistemleri dünyasında dizin içerisindeki öğelere genellikle
"dizin girişi (directory entry)" denilmektedir. Bir dizin içerisinde dosyaların kendisi bulunmaz. Yalnızca onların isimleri ve bazı
önemli bilgileri bulundurulur. Daha önceden de belirttiğimiz gibi dizinler aslında "içerisinde dizin girişlerinin bulunduğu" dosyalar
gibi organize edilmiştir. Dizinlerin içerisindeki girişlerin elde edilmesi dizinlerle ilgili önemli işlemlerden biridir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde bir dizin'in içerisindeki dizin girişlerini elde etmek için FindFirstFile, FindNextFile ve FindClose API fonksiyonları
kullanılmaktadır. Bu fonksiyonların biraz daha gelişmiş olan Ext'li biçimleri de vardır. Fonksiyon prototipleri şöyledir:
HANDLE FindFirstFile(
LPCSTR lpFileName,
LPWIN32_FIND_DATA lpFindFileData
);
BOOL FindNextFile(
HANDLE hFindFile,
LPWIN32_FIND_DATA lpFindFileData
);
BOOL FindClose(
HANDLE hFindFile
);
FindFirstFile fonksiyonunun birinci parametresi yol ifadesini almaktadır. Bu fonksiyonla biz tek bir girişin bilgilerini alabileceğimiz
gibi * ve ? gibi joker karakterlerini kullanarak birden fazla girişin bilgilerini de alabiliriz. Tabii eğer FindFirstFile ile birden
fazla girişin bilgileri alınıyorsa bu fonksiyon onların yalnızca ilkinin bilgisini vermektedir. FindFirstFile girişe ilişkin bilgileri
WIN32_FIND_DATA isimli bir yapı nesnesinin içerisine yerleştirmektedir. Fonksiyon başarı durumunda sonraki dizin girişlerini elde etmek
için gereken HANDLE değerine başarısızlık durumunda INVALID_HANDLE_VALUE değerine geri dönmektedir. Başarısızlığın nedeni dizinde
belirtilen kalıba uygun dosyanın bulunamaması ise GetLastError ERROR_FILE_NOT_FOUND değerine geri dönmektedir.
Eğer dizin içerisindeki birden fazla girişin bilgileri elde edilecekse bunların ilki FindFirstFile fonsiyonu tarafından diğerleri ise
FindNextFile fonksiyonu tarafından elde edilmelidir. FindNextFile fonksiyonu bir kez değil döngü içerisinde çağrılmalıdır. Bu fonksiyon
her çağrılışta yeni bir girişin bilgilerini elde etmektedir. FindNextFile fonksiyonunun birinci parametresi FindFirstFile fonksiyonundan
elde edilen HANDLE değeridir. İkinci parametre yine bulunan dizin girişinin bilgilerinin yerleştirileceği WIN32_FIND_DATA türünden nesnenin
adresini almaktadır. Fonksiyonun geri dönüş değeri işlemin başarısını belirtmektedir. FindNextFile fonksiyonu iki nedenden dolayı başarısız
olmaktadır. Birincisi artık tüm dizin girişlerinin elde edilmiş olmasıdır. İkincisi ise bir IO hatasının oluşmuş olmasıdır. Programcı
döngüden çıkınca bir IO hatası olup olmadığını tespit etmek ister. Eğer GetLastError ERROR_NO_MORE_FILES değerine geri dönüyorsa bu durum
gayet normal olarak fonksiyonun tüm girişleri elde ettiğinden dolayı başarısız olduğu anlamına gelmektedir.
İşlemler bitince elde edilen HANDLE alanının serbest bhırakılması için FindClose fonksiyonun çağrılması gerekir. Bu fonksiyon da işlemin
başarısına geri dönmektedir. Tabii bu fonksiyonunun başarısının kontrl edilmesine gerek yoktur.
WIN32_FIND_DATA yapısı şöyledir:
typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
CHAR cFileName[MAX_PATH];
CHAR cAlternateFileName[14];
DWORD dwFileType; // Obsolete. Do not use.
DWORD dwCreatorType; // Obsolete. Do not use
WORD wFinderFlags; // Obsolete. Do not use
} WIN32_FIND_DATA, *PWIN32_FIND_DATA, *LPWIN32_FIND_DATA;
Burada bulunan dizin girişinin ismi yapının cFileName elemanında bulunmaktadır. Bulunan dosyanın uzunluğu iki ayrı DWORD biçiminde
yapının nFileSizeHigh ve nFileSizeLow elemanlarında bulunmaktadır. Dizin girişlerinin uzunlukları 0 biçiminde verilmektedir. Yapının
dwFileAttributes elemanı dizin girişinin özelliklerini vermektedir. Bu eleman bit bit anlamlıdır. Bu elemanın her biri bir özelliğin
olup olmadığını belirtmektedir. Dizin girişi özellikleri FILE_ATTRIBUTE_XXX biçiminde sembolik sabitlerle belirtilmiştir. Örneğin bir
dizin girişinin bir dizine ilişkin olup olmadığını anlamak için FILE_ATTRIBUTE_DIRECTORY değeriyle bu özellik elemanının bit AND işlemine
sokulması gerekir.
Windows dosya sistemine de bağlı olarak dosyalar ve dizinler için üç zaman bilgisi tutmaktadır: Son yazma zamanı, son okuma zamanı
ve ilk yaratma zamanı. WIN32_FIND_DATA yapısı içerisinde bu bilgiler FILETIME isimli bir yapı biçiminde tutulmaktadır. Bu yapı 32 bitlik
iki parçadan oluşmaktadır. Tarih ve zaman bilgisi bu yapı içerisinde 01/01/1601'den geçen 100 nano saniyelerin sayısı biçiminde tutulmaktadır.
Windows her zaman dizin girişlerindeki zamanları UTC (Universial Time Clock) olarak tutmaktadır. Eğer zamanları yerel saata dönüştüreceksek
önce FileTimeToLocalFileTime fonksiyonuna sokmalıyız. FILETIME değerini parçalarına ayırmak için FileTimeToSystemTime fonksiyonu kullanılmaktadır.
Aşağıda bir dizin girişinin komut satırındaki "dir" komutu tarzıyla yazdırılmasına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFileFind;
WIN32_FIND_DATA findData;
unsigned long long ullSize;
SYSTEMTIME sysTime;
if ((hFileFind = FindFirstFile("*.*", &findData)) == INVALID_HANDLE_VALUE)
ExitSys("FindFirstFile");
do {
FileTimeToLocalFileTime(&findData.ftLastWriteTime, &findData.ftLastWriteTime);
FileTimeToSystemTime(&findData.ftLastWriteTime, &sysTime);
printf("%02d.%02d.%04d %02d:%02d ", sysTime.wDay, sysTime.wMonth, sysTime.wYear, sysTime.wHour, sysTime.wMinute);
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
printf("%-14s", "<DIR>");
else {
ullSize = (unsigned long long)findData.nFileSizeHigh << 32 | findData.nFileSizeLow;
printf("%14llu", ullSize);
}
printf(" %s\n", findData.cFileName);
} while (FindNextFile(hFileFind, &findData));
if (GetLastError() != ERROR_NO_MORE_FILES)
ExitSys("FindNextFile");
FindClose(hFileFind);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde dizin içerisindeki dosyaların elde edilmesine örnek
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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 *dent;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((dir = opendir(argv[1])) == NULL)
exit_sys("open");
while (errno = 0, (dent = readdir(dir)) != NULL)
printf("%s\n", dent->d_name);
if (errno != 0)
exit_sys("readdir");
closedir(dir);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde dizinler için "x" hakkı farklı bir anlama gelmektedir. Dizinler için "x" hakkı yol ifadelerinde "içinden geçilebilirlik"
anlamına gelmektedir. Örneğin "/home/kaan/Study/SysProg/test.txt" biçiminde bir yol ifadesi söz konusu olsun. İşletim sistemi bir yol
ifadesinde hedeflenen dizin girişine önceki dizin girişlerinden geçerek erişmektedir. Bu sürece İngilizce "pathname resolution" ya da
"pathname lookup" denilmektedir. Pathname resolution işlemi sırasında prosesin yol ifadesindeki tüm dizinler için "x" hakkına sahip olması
gerekmektedir. Örneğin yukarıdaki yol ifadesinin çözülebilmesi için prosesin kök dizine, "home" dizinine "kaan" dizinine, "study" dizinine,
SysProg dizinine "x" hakkına sahip olması gerekmektedir. Benzer biçimde "test.txt" biçiminde göreli bir yol ifadesi için de yine prosesin
prosesin çalışma dizinine "x" hakkına sahip olması gerekmektedir. Dizinler için "x" hakkını kaldırırsak artık onun ötesine geçilemez.
Bir yol ifadesinde hedef dosyanın içinde bulunduğu dizilere ve önceki dizinlere prosesin "r" hakkının olması gerekmemektedir. Önemli olan
hedefteki dosyaya erişim hakkıdır. Örneğin "/home/kaan/Study/SysProg/test.txt" biçiminde bir yol ifadesi olsun. Bizim bu dosyayı O_RDONLY
modunda açabilmemiz için bu dosyaya "r" hakkına sahip olmamız gerekir. Buradaki dizinler için "r" hakkına sahip olmamız gerekmez. Ancak
buradaki diziler için "x" hakkına sahip olmamız gerekir. Yani "pathname resolution" işlemi bir okuma anlamına gelmemektedir.
Komut satırındaki mkdir komutu zaten default olarak yaratılan dizinde herkese "x" hakkını vermektedir. mkdir POSIX fonksiyonunda bizim
fonksiyonun ikinci parametresinde bu hakları vermemiz gerekir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde bir dizin içerisindeki dizin girişlerini elde etmek için aşağıdaki POSIX fonksiyonları kullanılmaktadır:
opendir
readdir
closedir
Fonksiyonların prototipleri şöyledir:
#include <dirent.h>
DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);
opendir dizini açmak için kullanılmaktadır. Prosesin dizini açabilmesi için dizine "r" hakkının bulunuyor olması gerekir. Fonksiyon başarı
durumunda DIR isimli yapı türünden adrese, başarısızlık durumunda NULL adrese geri dönmektedir. DIR yapısı dokğmante edilmemiştir.
Programcı bu yapının içerğini bilmek zorunda değildir. Bu yapı bir handle olarak kullanılmaktadır. Dizin girişleri bir döngü içerisinde
readdir fonksiyonu çağrılarak elde edilmektedir. Her readdir çağrıldıında bir dizin girişi dirent isimli bir yapı adresi biçiminde
bize verilmektedir. readdir fonksiyonun geri döndürdüğü adresteki yapı nesnesi statik düzeyde tahsis edilmiş durumdadır. readdir fonksiyonu
dizin listesinin sonuna gelindiğinde ya da IO hatası oluştuğunda NULL adrese geri dönmektedir. Ancak eğer dizin listenin sonuna gelinmişse
(bu normal durumdur) errno değişkeninin değeri değiştirilmemektedir. Eğer fonksiyon IO hatasından dolayı başarısız olmuşsa errno değişkeni
uygun biçimde set edilmektedir. Burada programcı fonksiyonu çağırmadan önce errno değişkenine 0 atamalı döngüden çıkıldığında errno
değişkeninin değerine bakmalıdır.
Nihayet programcı işini bitirdikten sonra closedir fonksiyonu ile dizini kapatmalıdır. closedir başarı durumunda 0 değerine başarısızllık
durumunda -1 değerine geri dönmektedir.Fonksiyonun başarısının kontrol edilmesine gerek yoktur.
readdir fonksiyonu her çağrıldığında dizin girişi bilgilerini dirent isimli bir yapı nesnesinin içerisine yerleştirir. Bu yapı nesnesinin
adresiyle geri döner. Yani programcı dizin girişlerine ilişkin bilgileri bu yapıdan almaktadır.
Windows sistemlerinde dizin girişlerinde dosyaya ya da dizine ilişkin pek çok bilgi tutulmaktadır. Ancak UNIX/Linux sistemlerinde dizin
girişlerinde yalnızca (kabaca) "dizin girinin ismi" ve "i-node numarası" tutulmaktadır. Yani UNIX/Linux sistemlerinde dizinler kabaca
aşağidaki gibi bir yapıya sahiptir:
dizin_girişi_ismi i-node_no
dizin_girişi_ismi i-node_no
dizin_girişi_ismi i-node_no
dizin_girişi_ismi i-node_no
...
dirent yapısı şöyle bildirilmiştir:
struct dirent {
ino_t d_ino; /* Inode number */
char d_name[256]; /* Null-terminated filename */
};
Görüldüğü gibi UNIX/Linux sistemlerinde biz dizin girişlerindne yalnızca girişin ismini ve i-node numarasını elde etmekteyiz. Ayrıca
UNIX/Linux sistemlerinde Windows sistemlerinde olduğu gibi "*" ve "?" gibi joker karakterlerini kullanamadığımıza dikkat ediniz. Bu
sistemlerde biz dizindeki belli girişleri değil tüm girişleri elde etmekteyiz.
Aşağıdaki örnekte komut satırı argümanı olarak verilen bir dizindeki tüm girişler alt alta yazdırılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Biz UNIX/Linux sistemlerinde readdir fonksiyonuyla yalnızca dizin girişinin ismini ve i-node numarasınıo elde ettik. Pekiyi dosyanın
uzunluğu, erişim hakları gibi bilgileri nasıl elde edeceğiz? İşte burada yapılması gereken readdir ile dizin girişinin ismi elde edildikten
sonra ayrıca stat fonksiyonuyla dosyanın bilgilerinin elde edilmesidir. Yani tek başına readdir kullanmak yerine aynı zamanda stat
fonksiyonunun da kullanılması gerekmektedir.
Aşağıdkai örnekte belli bir dizindeki dizin girişleri elde edilip stat fonksiyonuna sokulmuş ve ilgili girişe ilişkin dosya bilgileri
elde edilmiştir. Bu örnekte bir noktaya dikkat ediniz. readdir fonksiyonu ile elde ettiğimiz giriş isminde bir yol ifadesi yoktur.
Halbuki bizim stat fonksiyonu için uygun bir yol ifadesine gereksnimimiz vardır. Bu nedenle kodda sprintf fonksiyonu ile uygun yol ifadesi
elde edilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <errno.h>
#include <dirent.h>
#include <sys/stat.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
DIR *dir;
struct dirent *ent;
struct stat finfo;
char path[PATH_MAX];
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((dir = opendir(argv[1])) == NULL)
exit_sys("opendir");
while (errno = 0, (ent = readdir(dir)) != NULL) {
snprintf(path, PATH_MAX, "%s/%s", argv[1], ent->d_name);
if (stat(path, &finfo) == -1)
perror(path);
printf("%-30s%jd\n", ent->d_name, (intmax_t)finfo.st_size);
}
if (errno != 0)
exit_sys("readdir");
closedir(dir);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi readdir fonksiyonu ile okuduğumuz dizin girişlerindeki i-node numarası be anlama gelmektedir? Anımsanacağı gibi dosya bilgileri
stat fonksiyonları ile elde edilirken stat yapısınında st_ino elemanı da ilgili dosyanın i-node numarasını belirtiyordu.
Bir disk i-node tabanlı bir dosya sistemi ile (örneğin ext-2, ext-3 gibi) formatlandığında diskte dört farklı bölüm oluşturulmaktadır:
Boot Block
Super Block
I-Node Block
Data Block
Boot Block 1024 byte uzunluğunda sistemin boot edilmesi için gereken bilgileri içermektedir. Formatlanmış diskin tüm parametrik bilgileri
Super Block'ta bulunmaktadır. Dosyaların parçaları yani dosya bilgileri ise Data Block içerisinde tutulmaktadır. İşte -Node block i-node
elemanlarından oluşmaktadır:
I-Node Block
----------------
i-node elemanı
i-node elemanı
i-node elemanı
...
i-node elemanı
i-node elemanı
...
Bir dosyaya ilişkin tüm bilgiler o dosyaya ilişkin i-node elemanından elde edilmektedir. Başka bir deyişle stat, lstat ve fstat fonksiyonları
aslında dosya bilgilerini dosyaya ilişkin i-node elemanından elde ederler. İşte her i-nde block'taki her i-node elemanının ilk i-node elemanın
numarası 0 olacak biçimde bir sıra numarası vardır:
I-Node Block
----------------
0 i-node elemanı
1 i-node elemanı
2 i-node elemanı
...
1200 i-node elemanı
1201 i-node elemanı
...
İşte bir dosyanın i-node numarası o dosyanın bilgilerinin i-node block'taki kaçıncı i-node elemanında bulunduğunu belirtmektedir.
Ancak bir dosyanın i-node numarasından hareketle bilgilerinin elde edilmesi için kullanılabilecek bir POSIX fonksiyonu yoktur. Dosya
bilgileri yol ifadelerinden ya da dosya betimleyicilerinden hareketle elde edilebilmektedir. Tabii işletim sistemi kendisi bu i-node
numarasından hareketle dosya bilgilerine erişmektedir. Dosyaların i-node numaraları sistem genelinde tektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir dizindeki dosya bilgilerini "ls -l" formatında yazdıran bir program da yazabilirizx. Zaten biz daha önce belli bir dosyanın bilgilerini
"ls -l" formatında yazdırmıştık. Şimdi yapacağımız şey dizin içerisindeki dosyaları bulup bunları tek tek "ls -l" formatında yazdırmak
olacaktır. Ancak "ls -l" komutunda dosyanın öeğeleri hizalı bir biçimde gösterilmektedir. Bizim aşağıdaki örneğimizde ise böyle bir
hizalama uygulanmamaktadır. Aşağıdaki programın örnek çıktısı şöye olacaktır:
-rw-r--r--1 kaan study 99 Haz 11 2023 18:17 y.c
-rwxr-xr-x1 kaan study 16136 Haz 17 2023 18:51 a.out
-rw-r--r--1 kaan study 0 Tem 23 2023 19:56 y.txt
drwxr-xr-x2 kaan study 4096 Ağu 13 2023 19:29 xxx
-rw-r--r--1 kaan study 895 Ağu 26 2023 17:09 sample.c
-rw-r--r--1 kaan study 2645 Ağu 26 2023 17:37 mample.c
-rw-rw-rw-1 kaan study 0 Tem 23 2023 20:57 m.txt
prw-r--r--1 kaan study 0 Ağu 12 2023 17:35 mypipe
-rw-r--r--1 kaan study 0 Tem 23 2023 19:31 x.txt
-rwxr-xr-x1 kaan study 16328 Haz 18 2023 19:17 mycalc
-rwxr-xr-x1 kaan study 16472 Ağu 26 2023 17:09 sample
-rw-r--r--1 kaan study 3570 Haz 18 2023 18:57 disp.c
-rw-r--r--1 kaan study 1161 Haz 18 2023 19:16 mycalc.c
-rw-r--r--1 kaan study 99 Haz 11 2023 18:17 x.c
drwxrwxr-x8 kaan study 4096 Ağu 14 2023 22:15 ..
-rw-r--r--1 kaan study 5000001 Tem 22 2023 20:07 test.txt
-rwxr-xr-x1 kaan study 16816 Ağu 26 2023 17:37 mample
-rw-rw-rw-1 kaan study 0 Tem 23 2023 20:45 z.txt
drwxrwxr-x3 kaan study 4096 Ağu 26 2023 17:37 .
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <errno.h>
#include <locale.h>
#include <dirent.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>
void disp_ls_style(const struct stat *finfo, const char *name);
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
DIR *dir;
struct dirent *ent;
char path[PATH_MAX];
struct stat finfo;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if (setlocale(LC_ALL, "tr_TR.utf-8") == NULL) {
fprintf(stderr, "cannot set locale!..\n");
exit(EXIT_FAILURE);
}
if ((dir = opendir(argv[1])) == NULL)
exit_sys("opendir");
while (errno = 0, (ent = readdir(dir)) != NULL) {
snprintf(path, PATH_MAX, "%s/%s", argv[1], ent->d_name);
if (stat(path, &finfo) == -1)
perror(path);
disp_ls_style(&finfo, ent->d_name);
}
if (errno != 0)
exit_sys("readdir");
closedir(dir);
}
void disp_ls_style(const struct stat *finfo, const char *name)
{
mode_t modes[9] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH};
char buf[32];
struct tm *ptm;
struct passwd *pwd;
struct group *grp;
switch (finfo->st_mode & S_IFMT) {
case S_IFBLK:
putchar('b');
break;
case S_IFCHR:
putchar('c');
break;
case S_IFIFO:
putchar('p');
break;
case S_IFREG:
putchar('-');
break;
case S_IFDIR:
putchar('d');
break;
case S_IFLNK:
putchar('l');
break;
case S_IFSOCK:
putchar('s');
break;
default:
putchar('?');
break;
}
for (int i = 0; i < 9; ++i)
putchar(finfo->st_mode & modes[i] ? "rwx"[i % 3] : '-');
printf("%ju ", (uintmax_t)finfo->st_nlink);
if ((pwd = getpwuid(finfo->st_uid)) != NULL)
printf("%s ", pwd->pw_name);
else
printf("%ju ", (uintmax_t)finfo->st_uid);
if ((grp = getgrgid(finfo->st_gid)) != NULL)
printf("%s ", grp->gr_name);
else
printf("%ju ", (uintmax_t)finfo->st_gid);
printf("%jd ", (intmax_t)finfo->st_size);
ptm = localtime(&finfo->st_mtim.tv_sec);
strftime(buf, 32, "%b %2e %Y %H:%M ", ptm);
printf("%s ", buf);
printf("%s\n", name);
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte dizin listesi "ls -l" formatında hizalanarak yazdırılmıştır. Hizalama işlemi için önce sütunların en geniş yer kaplayan
elemanları bulunmuş hizalama bu en geniş elemanlara göre yapımıştır. Ayrıca aşağıdaki örnekte "ls -l" biçimindeki çıktı get_ls isimli bir
fonksiyona havale edilmiştir. Ancak bu fonksiyon bu bilgileri stdout dosyasına değil char türden statik bir dizinin içerisine yazmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <dirent.h>
void exit_sys(const char *msg);
const char *get_ls(const char *path, int hlink_digit, int uname_digit, int gname_digit, int size_digit);
int main(int argc, char *argv[])
{
DIR *dir;
struct dirent *dent;
struct stat finfo;
char path[PATH_MAX];
struct passwd *pass;
struct group *gr;
int len;
int hlink_digit, uname_digit, gname_digit, size_digit;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((dir = opendir(argv[1])) == NULL)
exit_sys("open");
hlink_digit = uname_digit = gname_digit = size_digit = 0;
while (errno = 0, (dent = readdir(dir)) != NULL) {
snprintf(path, PATH_MAX, "%s/%s", argv[1], dent->d_name);
if (stat(path, &finfo) == -1)
exit_sys("stat");
len = (int)log10(finfo.st_nlink) + 1;
if (len > hlink_digit)
hlink_digit = len;
if ((pass = getpwuid(finfo.st_uid)) == NULL)
exit_sys("getppuid");
len = (int)strlen(pass->pw_name);
if (len > uname_digit)
uname_digit = len;
if ((gr = getgrgid(finfo.st_gid)) == NULL)
exit_sys("getgrgid");
len = (int)strlen(gr->gr_name);
if (len > gname_digit)
gname_digit = len;
len = (int)log10(finfo.st_size) + 1;
if (len > size_digit)
size_digit = len;
}
if (errno != 0)
exit_sys("readdir");
rewinddir(dir);
while (errno = 0, (dent = readdir(dir)) != NULL) {
sprintf(path, "%s/%s", argv[1], dent->d_name);
if (stat(path, &finfo) == -1)
exit_sys("stat");
printf("%s\n", get_ls(path, hlink_digit, uname_digit, gname_digit, size_digit));
}
if (errno != 0)
exit_sys("readdir");
closedir(dir);
return 0;
}
const char *get_ls(const char *path, int hlink_digit, int uname_digit, int gname_digit, int size_digit)
{
struct stat finfo;
static char buf[4096];
static mode_t modes[] = { S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH };
struct passwd *pass;
struct group *gr;
char *str;
int index = 0;
int i;
if (stat(path, &finfo) == -1)
return NULL;
if (S_ISREG(finfo.st_mode))
buf[index] = '-';
else if (S_ISDIR(finfo.st_mode))
buf[index] = 'd';
else if (S_ISCHR(finfo.st_mode))
buf[index] = 'c';
else if (S_ISBLK(finfo.st_mode))
buf[index] = 'b';
else if (S_ISFIFO(finfo.st_mode))
buf[index] = 'p';
else if (S_ISLNK(finfo.st_mode))
buf[index] = 'l';
else if (S_ISSOCK(finfo.st_mode))
buf[index] = 's';
++index;
for (i = 0; i < 9; ++i)
buf[index++] = (finfo.st_mode & modes[i]) ? "rwx"[i % 3] : '-';
buf[index] = '\0';
index += sprintf(buf + index, " %*llu", hlink_digit, (unsigned long long)finfo.st_nlink);
if ((pass = getpwuid(finfo.st_uid)) == NULL)
return NULL;
index += sprintf(buf + index, " %-*s", uname_digit, pass->pw_name);
if ((gr = getgrgid(finfo.st_gid)) == NULL)
return NULL;
index += sprintf(buf + index, " %-*s", gname_digit, gr->gr_name);
index += sprintf(buf + index, " %*lld", size_digit, (long long)finfo.st_size);
index += strftime(buf + index, 100, " %b %e %H:%M", localtime(&finfo.st_mtime));
str = strrchr(path, '/');
sprintf(buf + index, " %s", str ? str + 1 : path);
return buf;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir bildirim işleminin C'deki genel biçimi şöyledir:
<tür belirleyicisi> [yer belirleyicisi] [tür niteleyicileri] <dekleratör> [= <ilkdeğer], <dekleratör> [= <ilkdeğer], ...;
Tür belirten belirleyici ve niteleyicilerinin dışında kalan ve ilkdeğerlerin dışında kalan atom grubuna teknik terminolojide "dekleratör
(declarator)" denilmektedir. Çrneğin:
int a = 10, *pi = &a, *b[10];
Burada a, *pi, ve *b[10] birer dekleratördür. Bildirimdeki türün tüm dekleratörlerin türü olduğunda dikkat ediniz. Örneğin:
int a = 10, *pi = &a, *b[20];
Barada a'nın, *pi'nin *b[20]'nin türleri int biçimdedir. Başka bir deyişle burada a int türden bir değişken, pi int türden bir gösterici
ve b de int türden bir göstericisi dizisidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında fonksiyonlar makine komutlarından oluşmaktadır. Fonksiyonların makine komutları ardışıl bir biçimde bellekte bulunmaktadır.
Bir fonksiyonu çağırmak ise aslında o fonksiyonun başlangıcındaki komutun bulunduğu yere akışın aktarılmasıdır. Makine dillerinde fonksiyon
çağırma işlemi için genellikle CALL biçiminde isimlendirilen makine komutları bulundurulmuştur. Bu makine komutları çalıştırılacak fonksiyonun
başlangıç adresini operand olarak almaktadır. Örneğin:
CALL address
Pekiyi CALL makine komutuyla goto işlemini yapan JMP (bu komut BRANCH olarak da isimlendirilmektedir) komutları arasındaki fark nedir?
İşte CALL makine komutları geri dönüş imkanını verirken JMP komutları geri dönüş imkanını vermemektedir. Pekiyi geri dönüş nasıl olmaktadır?
İşte CALL makine komutu adrese dallanmadan önce sonraki komutun adresini stack denilen alamda saklar sonra adrese dallanır. Fonksiyonun sonunda
bir RET makine kmoutu bulunur. Bu RET makime komutu da stack'te saklanan adrese dallanır. Yani geri dönüşün mümkün olmasının sebebi aslında
geri dönüş adresinin saklanmış olmasındadır. Halbuki JMP komutları geri dönüş adreslerini saklamamaktadır.
Biz fonksiyonları ardışıl makşine komutları olarak düşünebiliriz. Onları çağırmak için onların yalnızca başlangıç adreslerini bilmemiz yeterli
olmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyon adreslerini tutan özel göstericilere "fonksiyon göstericileri (pointer to function)" denilmektedir. Bir fonksiyon göstericisi
tanımlamanın genel biçimi şöyledir:
<geri dönüş değerinin türü> (*<gösterici_ismi>)([parametrik yapı]);
Burada önemli olan noktalardan biri * ve gösterici isminin parantez içerisinde olmasıdır. Örneğin:
int (*pf1)(int);
double (*pf2)(int, int);
int (*pf3)(struct stat *);
Bir fonksiyon göstericisine herhangi bir fonksiyonun adresi atanamaz. Geri dönüş değeri ve parametre türleri belli biçimde olan fonksiyonların
adresleri atanabilmektedir. Yukarıdaki örnekte pf1 göstericisine "geri dönüş değeri int türden olan parametresi int türdne olan" herhangi
bir fonksiyonun adresi atanabilir. pf2 göstericisine "geri dönüş değeri double türden olan ve parametreleri int ve int türden olan" fonksiyonların
adresleri atanabilmektedir. pf3 göstericisine ise "geri dönüş değeri int türden olan ve parametresi struct stat türünden gösterici olan"
fonksiyonların adresleri ataanbilir. Fonksiyon göstericisi bildiriminde parametre parantezinin içerisine yalnızca parametrelerin türleri
yazılabilir ya da istenirse onlara ilişkin isimler de yazılabilir. Tabii bu isimlerin herhangi isimlerle uyuşması gerekemektedir. Bu isimler
okunabilirliği artırmak için bulundurulabilir. Ancak C programcıları genellikle parametre isimlerini belirtmezler. Örneğin:
int (*pf)(int, int);
int (*pf)(int a, int b);
Bu iki prototip arasında hiçbir farklılık yoktur.
Fonksiyon gösterici bildiriminde dekleratördeki parantezler kullanılmazsa bildirimin tamamne başka bir anlam ifade edeceğine dikkat ediniz.
Örneğin:
int (*pf)(int, int);
Burada geri dönüş değeri int olan ve parametreleri int, int olan fonksiyonların adreslerini tutabielcek bir fonksiyon göstericisi tanımlanmıştır.
Fakat örneğin:
int *pf(int, int);
Bu bildirim ise "geri dönüş değeri int * olan parametreleri int, int olan pf isimli bir fonksiyonun" prototip bildirimidir. Örneğin:
int (*pf)(int, int);
Buradaki fonksiyon gösterici bildiriminde dekleratör (*pf)(int, int) biçimindedir.
Fonskiyon göstericisi bildiriminde C'de parametre parantezlerinin içinin boş bırakılmasıyla içine void yazılması arasında önemli bir
farklılık vardır. Eğer bildirimde parametre parantezlerinin içi boş bırakılırsa bu durum "fonksiyon göstericisine geri dönüş değeri uyumu
korunmak üzere herhangi bir parametrik yapıya sahip fonksiyonların adreslerinin atanabileceği" anlamına gelmektedir. Örneğin:
int (*pf1)();
Burada pf1 göstericisine geri dönüş değeri int türden olmak koşuluyla parametrik yapısı nasıl olursa olsun her fonksiyonun adresi atanabilmektedir.
Halbuki örneğin:
int (*pf2)(void);
Burada pf2 göstericisine geri dönüş değeri int olan ve parametresi olmayan (ya da void olan da diyebiliriz) fonksiyonların adresleri
atanabilmektedir. Ancak C++'ta yukarıdaki iki biçim tamamen aynı anlama gelmektedir. Bu iki biçimin her ikisi de C++'ta geri dönüş değeri
int olan parametresi olmayan fonksiyonların adreslerini atayabileceğimiz göstericilere ilişkindir.
C'de tür dönüştürmelerinde kullanabileceğimiz türlerin sembolik isimleri vardır. Çrneğin:
int a;
int *pi;
int b[10];
int *c[20];
Burada a "int" türden, pi "int *" türünden, b "int[10]" türünden ve c de "int *[20]" türündendir. Bir fonksiyon göstericisinin bu biçimdeki
sembolik tür isimleri belirtilirken yine * atomu parantez içerisine alınır. Örneğin:
int (*pf)(void);
Burada pf göstericisi "int (*)(void)" türündendir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C'de bir fonksiyonun yalnızca ismi o fonksiyonun başlangıç adresi anlamına gelmektedir. Örneğin:
int foo(void)
{
...
}
Burada yalnızca foo ismi bu fonksiyonun bellekteki başlangıç adresini belirtir. foo ismi bir nesne belirtmemektedir. Tıpkı dizi isimlerinde
olduğu gibi bir sağ taraf değeri belirtmektedir. Zaten C'de fonksiyonların çağrılmasını sağlayan operatör (...) operatörüdür. Bu durumda
foo ifadesi ile foo() ifadesi tamamen farklıdır. foo ifadesi foo fonksiyonunun bellekteki başlangıç adresi anlamına gelmektedir. Ancak
foo() ifadesinde bu adreste bulunan fonksiyon çağrılmış ve int değeri elde edilmiştir. Bu durumda foo ifadesi "int (*)(void)" türünden,
foo() ifadesi ise "int" türdendir.
Fonksiyon çağırma operatörü aslında "beli bir adresten başlayan fonksiyon kodlarının çalıştırılması" işlemini yapar. Örneğin:
foo();
Burada aslında "foo adresinden başlayan fonksiyon kodları" çalıştırılmaktadır.
Bu durumda biz bir fonksiyon göstericisine bir fonksiyonun adresini atamak istediğimizde fonksiyonun yalnızca ismini atamalıyız. Örneğin:
int foo(void)
{
....
}
...
int (*pf)(void);
pf = foo; /* geçerli */
Atama işlemini şöyle yapamayız:
pf = foo(); /* geçersiz! */
Burada pf değişkenine bir fonksiyon adresi değil düz bir int değer atanmaya çalışılmaktadır. Tabii biz bir fonksiyon göstericisine uyumlu
bir fonksiyon adresi ile ilkdeğer de verebiliriz. Örneğin:
int (*pf)(void) = foo;
Örneğin:
int a, *pi, (*pf)(int);
Bu bildirimde a int türden, pi int * türünden ve pf ise int (*)(int) türündendir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Şimdi aşağıdaki gibi bir fonksiyon göstericisine uyumlu bir fonksiyonun adresini atayalım:
void foo(void)
{
...
}
....
void (*pf)(void);
pf = foo;
Pekiyi bu pf yoluyla bu fonksiyonu nasıl çağırabiliriz? İşte bunun C'de iki yolu vardır:
1) pf(...) sentaksı ile çağrıma
2) (*pf)(...) sentaksı ile çağırma her ikisi de eşdeğerdir.
Her iki sentaksta da pf göstericisinin içerisindeki adrste bulunan fonksiyon çağrılmaktadır. Aslında genel olarak bir fonksiyon da zaten bu
iki sentaksla da çağrılabilmektedir. Örneğin:
void foo(void)
{
...
}
...
foo();
(*foo)();
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void foo(void)
{
printf("foo\n");
}
int main(void)
{
void (*pf)(void) = foo;
pf();
(*pf)();
foo();
(*foo)();
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda fonksiyon göstericileri yoluyla fonksiyon çöağrılmasına bir örnek verilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int mul(int a, int b)
{
return a * b;
}
int main(void)
{
int (*pf)(int, int);
int result;
pf = add;
result = pf(10, 20);
printf("%d\n", result);
pf = mul;
result = pf(10, 20);
printf("%d\n", result);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir fonksiyon göstericisine farklı türdne bir fonksiyonun adresini atayamayız. Eğer atamaya çalışırsak bu durum C'de geçerli olmadığıu için
derleme işlemi başarılı olmaz.i (Tabii bazı derleyiciler bu durumda bir uyarı mesajı verip programcıyı affedebilmektedir.) Örneğin:
void foo(int a)
{
...
}
...
void (*pf)(int);
pf = foo; /* geçersiz' */
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir fonksiyonun parametre değişkeni bir fonksiyon gösterici olabilir. Örneğin:
void foo(void (*pf)(void))
{
....
}
Burada foo fonksiyonunun parametre değişkeni geri dönüş değeri void olan ve parametresi void olan bir fonksiyonun adresini alacaktır.
Örneğin:
void bar(void)
{
...
}
...
foo(bar);
Böyle bir çağırmada bar fonksiyonun adresi foo fonksiyonunun parametre değişkeni olan pf'ye atanacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void foo(void (*pf)(void))
{
pf();
}
void bar(void)
{
printf("bar\n");
}
int main(void)
{
foo(bar);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyon göstericilerine bazı işlemlerin yapılabilmesi için gereksinim duyulmaktadır. Örneğin callback fonksiyon mekanizması fonksiyon
göstericileri yoluyla sağlanmaktadır. B,r fonksiyon belli bir durum oluştuğunda parametresiyle aldığı fonksiyonu çağırıyorsa bu duruma
callback fonksiyon mekanizaması denilmektedir. Örneğin:
void for_each(int *pi, size_t size, void (*pf)(int *))
{
for (size_t i = 0; i < size; ++i)
pf(&pi[i]);
}
Burada for_each fonksiyonu int bir dizinin tüm elemanlarını dolaşmakta ancak dolaşırken bizim verdiğimiz bir fonksiyonu da çağırmaktadır.
Bizim verdiğimiz callback fonksiyona for_each dizinin elemanlarının adreslerini geçirmektedir. Böylece callback fonksiyon dizi elemanlarını
değiştirebilecektir. Örneğin:
void disp(int *pi)
{
printf("%d\n", *pi);
}
void square(int *pi)
{
*pi = *pi * *pi;
}
...
int a[] = {10, 20, 30, 40, 50};
for_each(a, 5, disp);
for_each(a, 5, square);
for_each(a, 5, disp);
Görüldüğü gibi fonksiyon göstericileri bir fonksiyonun davranışının dışarıdan değiştirilerek fonksiyonun genelleştirilmesi amacıyla
kullanılabilmektedir. Örneğin bir GUI uygulamasında bir düğme GUI elemanına (pushbutton) tıklandığında biz belli bir işlemin yapılmasını
isteyebiliriz. Ancak düğmenin kodlarını biz değil ilgili kütüphaneyi yazanlar yazmıştır. İşte düğmenin kodlarını yazan kişiler farfenin
onun üzerinde tıklanıp tıklanmadığını kendiileri kontrol etmekte, eğer fare düğme üzerinde tıklanmışsa bizim verdiğimiz bir fonksiyonu
çağırmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void for_each(int *pi, size_t size, void (*pf)(int *))
{
for (size_t i = 0; i < size; ++i)
pf(&pi[i]);
}
void disp(int *pi)
{
printf("%d\n", *pi);
}
void square(int *pi)
{
*pi = *pi * *pi;
}
int main(void)
{
int a[] = {10, 20, 30, 40, 50};
for_each(a, 5, disp);
for_each(a, 5, square);
for_each(a, 5, disp);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyon parametrelerinde fonksiyon göstericileri (tıpkı normal göstericilerde olduğu gibi) alternatif sentaksla da belirtilebilmektedir.
Örneğin:
void foo(void pf(void))
{
...
}
Bu bildirim tamamne geçerlidir ve aşağıdakiyle tamamen eşdeğerdir:
void foo(void (*pf)(void))
{
...
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyon adresleri typedef edilerek daha kolay bir kullanım sağlanabilmektedir. Örneğin:
typedef void (*PF)(void);
Burada PF geri dönüş değeri void olan ve parametresi void olan bir fonksiyon adresi türüdür. Yani sembolik olarak PF aslında void (*)(void)
türünü temsil etmektedir. Örneğin:
PF pf;
bildirimi ile,
void (*pf)(void);
bildirimi tamamen yanı anlama gelmeketedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Her elemanı bir fonksiyon göstericisi olan fonksiyon gösterici dizileri de bildirilebilir. Bu bildirimde köşeli parantezler ve * atomu
parantez içerisine alınmalıdır. Örneğin:
void (*pfs[5])(void);
Burada pfs 5 eleanlı bir dizidir. Bu dizinin her elemanı "geri dönüş değeri void olan ve parametresi void olan" bir fonksiyon göstericisidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void foo(void)
{
printf("foo\n");
}
void bar(void)
{
printf("bar\n");
}
void tar(void)
{
printf("tar\n");
}
int main(void)
{
void (*pfs[3])(void);
pfs[0] = foo;
pfs[1] = bar;
pfs[2] = foo;
for (int i = 0; i < 3; ++i)
pfs[i]();
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyon gösterici dizilerine de küme parantezleri içerisinde ilkdeğer verebiliriz. Tabii verdiğimiz ilkdeğerlerin aynı türden fonksiyon
adresleri olması gerekir. Örneğin:
void (*pfs[3])(void) = {foo, bar, tar};
Tabii yine ilkdeğer verirken dizi uzunluğu belirtilmeyebilir. Örneğin:
void (*pfs[])(void) = {foo, bar, tar};
Burada pfs dizisine üç elemanla ilkdeğer verildiği için pfs üç elemanlı bir dizidir.
Yukarıda da belirtitğimiz gibi typedef bildirimi ile bu tür tanımlşamaları daha kolay yapabiliriz. Örneğin:
typedef void (*PF)(void);
...
PF pfs[] = {foo, bar, tar};
Tabii istersek fonksiyon gösterici dizileri için de typedef işlemi yapabiliriz. Örneğin:
typedef void (*PFS[3])(void);
...
PFS pfs = {foo, bar, tar};
Burada artık PFS üç elemanlı geri dönüş değeri void parametresi void olan fonksiyon göstericisi dizisini temsil etmektedir. Yani PFS türünün
sembolik gösterimi void (*[3])(void) biçimindedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void foo(void)
{
printf("foo\n");
}
void bar(void)
{
printf("bar\n");
}
void tar(void)
{
printf("tar\n");
}
int main(void)
{
void (*pfs[3])(void) = {foo, bar, tar};
for (int i = 0; i < 3; ++i)
pfs[i]();
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Her ne kadar fonksiyon isimleri zaten fonksiyonların adreslerini belirtiyorsa da istenirse fonksiyon isimleri & operatörüyle de kullanılanilir.
Yani aslında C'de foo bir fonksiyon belirtmek üzere foo ifadesi ile &foo ifadesi eşdeğerdir. Benzer biçimde foo bir fonksiyon adresi belirtmek
üzere bu fonksiyonu çağırmak için foo() ifadesiyle tamamen eşdeğer (*foo)() ifadesi de kullanılabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void foo(void)
{
printf("foo\n");
}
int main(void)
{
void (*pf)(void);
pf = &foo; /* eşdeğeri: pf = foo */
(*pf)(); /* eşdeğer: pf() */
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'de (ve C++'ta) data adreslerinden fonksiyon adreslerine fonksiyon adreslerinden data adreslerine bir dönüştürme yapılamamaktadır. Yani
C standartlarına göre tür dönüştürmesi yapılsa bile bu dönüştürme geçerli değildir. Örneğin:
void (*pf)(void);
int pi;
pi = (int *)pf; /* geçersiz! böyle bir dönüştürme yok! */
Her ne kadar böylesi dönüştürmeler C'de geçerli değilse de Microsoft, gcc ve clang derleyicilerinde bu dönüştürmelere izin verilmektedir.
C'de (C++'ta da böyle) void göstericiler data göstericisi olarak kabul edilmektedir. Yani void göstericilere biz türü ne olursa olsun
nesnelerin adreslerini atayabiliriz ancak fonksiyon adreslerini atayamayız. Örneğin:
int a;
double b;
void foo(void);
void *pv;
...
pv = &a; /* geçerli */
pv = &b; /* geçerli */
pv = foo; /* geçersiz! fonksiyon adreslerinden data adreslerine, data adreslerinden fonksiyon adreslerine dönüştürme yok! */
Ancak Microsoft derleyicileri, gcc ve clang derleyicileri bunu kabul etmektedir.
Fonksiyon adreslerinin data adreslerine dönüştürülmesi ve data adreslerinin fonksiyon adreslerine dönüştürülmesi aslında dolaylı bir
biçimde sağlanabilmektedir. Örneğin elimizde bir fonksiyon adresi olsun biz de bunu void göstericiye atamak isteyelim:
void (*pf)(void);
void *pv;
...
pv = (void *)pf; /* geçersiz! */
Burada pf'nin adresi bir fonksiyon adresi değildir. Fonksiyon göstericisinin adresidir. O halde bu işlem şöyle yapılabilir.
pv = *(void **) &pf;
Buradaki anahtar nokta bir fonksiyon göstericisinin adresinin bir fonksiyon adresi değil data adresi olduğudur. Bunun tersi de şöyle
yapılabilir:
void (*pf)(void);
void *pv;
...
*(void **)&pf = pv;
C'de (ve C++'ta) NULL adres fonksiyon göstericilerine de atanabilmektedir. Örneğin:
void (*pf)(void) = 0;
Burada pf göstericisine int 0 değil, o sistemdeki NULL adres atanmıştır. Tabii C standartlarına göre (ancak C++'ta böyle değil) NULL adres
(void *)0 biçiminde de belirtilebilir. Bu özel bir durum olduğu için data adresi kabul edilmemektedir. Örneğin:
void (*pf)(void) = (void *)0;
Burada her ne kadar sanki void bir adres fonksiyon göstericisine atanıyor gibiyse de (void *)0 ifadesinin NULL adres biçiminde özel bir
anlamı vardır.
Bir fonksiyon adresi başka türden bir fonksiyon adresine dönüştürülebilir. Örneğin:
void foo(int a);
...
void (*pf)(int);
pf = foo; /* geçeriz! */
pf (void (*)(void))foo /* geçerli */
Tabii bu tür dönüştürmeleri typedef işlemleriyle daha kolay yapabiliriz. Örneğin:
typedef void(*PF)(void);
void foo(int a);
...
void (*pf)(int);
pf = foo; /* geçeriz! */
pf (PF)foo /* geçerli */
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir fonksiyonun geri dönüş değeri de bir fonksiyon adresi olabilir. Bu durumda * atomu yine parantez içerisine alınmalı parantezin soluna
geri dönüş değerine ilişkin fonksiyonun geri dönüş değerinin türü parantezin sağına ise geri dönüş değerine ilişkin fonksiyonun parametresi
yazılmalıdır. Örneğin:
void (*foo(int a))(int)
{
...
}
Burada foo fonksiyonunun parametresi int türdendir. Ancak geri dönüş değeri paramtresi int türden olan geri dönüş değeri void türden olan
bir fonksiyon adresidir. Buradaki tanımla adım adım şöyle oluşturulmaktadır:
1) foo(int a)
foo fonksiyonunun parametresi int türdendir.
2) (*foo(int a))
foo fonksiyonunun geri dönüş değeri bir fonksiyon adresidir.
3) void (*foo(int a))
foo fonksiyonunun geri dönüş değerine ilişkin fonksiyonun geri dönüş değeri void türdendir.
4) void (*foo(int a))(int)
foo fonksiyonunun geri dönüş değerine ilişkin fonksiyonun parametresi int türdendir. Örneğin:
#include <stdio.h>
void bar(void)
{
printf("bar\n");
}
void (*foo(void))(void)
{
return bar;
}
int main(void)
{
void (*pf)(void);
pf = foo();
pf();
return 0;
}
Bu tür bildirimlerde typedef işlemleri bildirimleri oldukça kolaylaştırmaktadır. Örneğin:
#include <stdio.h>
typedef void (*PF)(void);
void bar(void)
{
printf("bar\n");
}
PF foo(void)
{
return bar;
}
int main(void)
{
PF pf;
pf = foo();
pf();
return 0;
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void bar(void)
{
printf("bar\n");
}
void (*foo(void))(void)
{
return bar;
}
int main(void)
{
void (*pf)(void);
pf = foo();
pf();
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'de fonksiyon göstericilerine ilişkin daha karmaşık bildirimler söz konusu olabilmektedir. Ancak neyse ki bu tür bildirimlerle oldukça
seyrek karşılaşılmaktadır. Örneğin şçyle bir fonksiyon yazmak isteyelim:
- Fonksiyonumuz ismi foo olsun ve parametresi void türden olsun,
- Fonksiyonumuzun geri dönüş değeri bir fonksiyon adresi olsun ve o fonksiyon adresinin parametresi int türden olsun.
- Fonksiyonumuzun geri dönüş değerine ilişkin fonksiyonun geri dönüş değeri de parametresi void geri dönüş değeri void olan
bir fonksiyon adresi olsun.
Şimdi yuakrıdaki foo fonksiyonunu tek bir cümleyle ifade etmeye çalışalım:
"foo parametresi void olan geri dönüş değeri parametresi int olan geri dönüş değeri paramtresi void olan geri dönüş değeri void olan bir
fonksiyon adresidir."
Şimdi bu fonksiyonu adım adım yazmaya çalışalım:
1) foo(void)
foo fonksiyonun kendi parametresi void
2) (*foo(void))
foo fonksiyonunun geri dönüş değeri bir fonksiyon adresi
3) (*foo(void))(int)
foo fonksiyonunun geri dönüş değerine ilişkin fonksiyonun parametresi int
4) (*(*foo(void))(int))
foo fonksiyonun geri dönüş değerine ilişkin fonksiyon adresinin geri dönüş değeri de fonksiyon adresi
5) (*(*foo(void))(int))(void)
foo fonksiyonun geri dönüş değerine ilişkin fonksiyon adresinin geri dönüş değerine ilişkin fonksiyonun parametresi void
5) void (*(*foo(void))(int))(void)
foo fonksiyonun geri dönüş değerine ilişkin fonksiyon adresinin geri dönüş değerine ilişkin geri dönüş değeri void
Burada foo fonksiyonu şöyle tanımlanabilir:
void(*(*foo(void))(int))(void)
{
...
}
Pekiyi burada foo fonksiyonu nasıl bir fonksiyon adresine geri dönmektedir? Tabii parametresi int olan geri dönüş değeri parametresi void
olan geri dönüş değeri void olan bir fonksiyon adresine. Örneğin:
void (*bar(int a))(void)
{
...
}
void(*(*foo(void))(int))(void)
{
return bar;
}
Pekiyi bar fonksiyonun neye geri dönmesi gerekmektedir? Tabii geri döüş değeri void ola parametresi void olan bir fonksiyonun adresine.
Örneğin:
#include <stdio.h>
void tar(void)
{
printf("tar\n");
}
void (*bar(int a))(void)
{
return tar;
}
void(*(*foo(void))(int))(void)
{
return bar;
}
int main(void)
{
void (*(*pf1)(int))(void);
void (*pf2)(void);
pf1 = foo();
pf2 = pf1(0);
pf2();
foo()(0)();
return 0;
}
Tabii typedef işlemi ile yukarıdaki karmaşık bildirimler daha basit biçimde de ifade edilebilirdi. Örneğin:
#include <stdio.h>
typedef void (*PF1)(void);
typedef PF1(*PF2)(int);
void tar(void)
{
printf("tar\n");
}
PF1 bar(int a)
{
return tar;
}
PF2 foo(void)
{
return bar;
}
int main(void)
{
PF2 pf2;
PF1 pf1;
pf2 = foo();
pf1 = pf2(0);
pf1();
return 0;
}
Bu tür bildirimler daha karmaşık hale de getirilebilir. Ancak uygulamada bu kadar karmaşık bildirimlerle karşılaşılmamaktadır. Uygulamada
en fazla bir fonksiyonun geri dönüş değerinin basit bir fonksiyon adresi olması durumuyla karşılaşılmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void tar(void)
{
printf("tar\n");
}
void (*bar(int a))(void)
{
return tar;
}
void(*(*foo(void))(int))(void)
{
return bar;
}
int main(void)
{
void (*(*pf1)(int))(void);
void (*pf2)(void);
pf1 = foo();
pf2 = pf1(0);
pf2();
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Örneğin signal isimli bir POSIX fonksiyonunun prototipi aşağıdaki gibidir:
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
Burada signal fonksiyonu iki parametreli bir fonksiyondur. Fonksiyonun birinci parametresi int türden ikinci parametresi ise bir fonksiyo
türündendir. Yani fonksiyonun ikinci parametresi geri dönüş değeri void olan parametresi int olan bir fonksiyon adresini almaktadır.
Burada signal fonksiyonunun geri dönüş değeri void, parametresi int olan bir fonksiyon adresidir. <signal.h> başlık dosyası içerisinde
aşağıdaki gibi bir typede file bildirim kolaylaştırılmıştır:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirtildiği gibi fonksiyon göstericileri özellikle birtakım işlemleri genelleştirmek amacıyla kullanılmaktadır. Örneğin
Windows için ListDir isimli bir fonksiyon yazmak isteyelim. Fonksiyon bir dizindeki dizin girişlerini bulsun ancak onları yazdırmak yerine
bizim ona verdiğimiz bir fonksiyonu çağırsın. Bu sayede biz o fonksiyona istediğimiz bir şeyi yaptırabiliriz. Böyle bir fonksiyonun
parametrik yapısı aşağıdaki gibi olabilir:
BOOL ListDir(const char *szPath, BOOL (*Proc)(WIN32_FIND_DATA *));
Fonksiyonun birinci parametresi joker karakterleri de içeren dizine ilişkin bir yol ifadesidir. İkinci parametre ilgili dizinde bir dizin
girişi bulunduğunda onun bilgilerinin bulunduğu WIN32_FIND_DATA yapısının adresi ile çağrılacak olan fonksiyonu belirtmektedir. Yani ListDir
fonksiyonu her dizin girişini buldukça o dizin girişi ile ikinci paramtresinde belirtilen bizim fonksiyonumuzu çağırmaktadır. Tabii bu tür
durumlarda callback fonksiyon yoluyla işlemin durdurulması da gerekebilmektedir. Bu nedenle ikinci parametedeki fonksiyon BOOL bir değere
geri döndürülmüştür. Bu fonksiyon sıfır dışı bir değerleile geri döndürülürse işlemler devam edecek 0 ile geri döndürülürse dolaşım durdurulacaktır.
Pekiyi burada ListDir fonksiyonunun geri dönüş değeri neyi belirtmektedir? İşte fonksiyonun geri dönüş değeri işlemin başarısını belirtebilir.
Aşağıdabu fonksiyonun yazımına ve kullanımına ilişkin bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
BOOL ListDir(const char *szPath, BOOL(*Proc)(WIN32_FIND_DATA *));
void ExitSys(LPCSTR lpszMsg);
BOOL FindFile(WIN32_FIND_DATA *fd)
{
printf("%s\n", fd->cFileName);
if (!_strcmpi(fd->cFileName, "notepad.exe")) {
printf("notepad.exe found\n");
return FALSE;
}
return TRUE;
}
BOOL DispFile(WIN32_FIND_DATA *fd)
{
unsigned long long ullSize;
SYSTEMTIME sysTime;
FileTimeToLocalFileTime(&fd->ftLastWriteTime, &fd->ftLastWriteTime);
FileTimeToSystemTime(&fd->ftLastWriteTime, &sysTime);
printf("%02d.%02d.%04d %02d:%02d ", sysTime.wDay, sysTime.wMonth, sysTime.wYear, sysTime.wHour, sysTime.wMinute);
if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
printf("%-14s", "<DIR>");
else {
ullSize = (unsigned long long)fd->nFileSizeHigh << 32 | fd->nFileSizeLow;
printf("%14llu", ullSize);
}
printf(" %s\n", fd->cFileName);
return TRUE;
}
int main(void)
{
if (!ListDir("c:\\windows\\*.*", FindFile))
ExitSys("ListDir");
printf("----------------------------\n");
if (!ListDir("c:\\windows\\*.*", DispFile))
ExitSys("ListDir");
return 0;
}
BOOL ListDir(const char *szPath, BOOL(*Proc)(WIN32_FIND_DATA *))
{
HANDLE hFileFind;
WIN32_FIND_DATA findData;
BOOL bResult = TRUE;;
if ((hFileFind = FindFirstFile(szPath, &findData)) == INVALID_HANDLE_VALUE)
return FALSE;
do {
if (!Proc(&findData))
goto EXIT;
} while (FindNextFile(hFileFind, &findData));
if (GetLastError() != ERROR_NO_MORE_FILES)
return FALSE;
EXIT:
FindClose(hFileFind);
return TRUE;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıdaki işlemin UNIX/Linux eşdeğeri de aşağıdaki gibi yazılabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <errno.h>
#include <dirent.h>
#include <sys/stat.h>
int list_dir(const char *path, int (*proc)(const struct stat *, const char *name));
void exit_sys(const char *msg);
int disp_file(const struct stat *finfo, const char *name)
{
printf("%-30s%jd\n", name, (intmax_t)finfo->st_size);
return 1;
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if (list_dir(argv[1], disp_file) == -1)
exit_sys("list_dir");
return 0;
}
int list_dir(const char *path, int (*proc)(const struct stat *, const char *name))
{
DIR *dir;
struct dirent *de;
char fpath[PATH_MAX];
struct stat finfo;
int status = 0;
if ((dir = opendir(path)) == NULL)
return -1;
while ((errno = 0, de = readdir(dir)) != NULL) {
snprintf(fpath, PATH_MAX, "%s/%s", path, de->d_name);
if (stat(fpath, &finfo) == -1) {
status = -1;
goto EXIT;
}
if (!proc(&finfo, de->d_name))
goto EXIT;
}
if (errno)
status = -1;
EXIT:
closedir(dir);
return status;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'de her türden diziyi sıraya dizebilen bir fonksiyon yazabilir miyiz? Yazarsak bunu nasıl yazabiliriz? Örneğin aşağıdaki gibi bir fonksiyon
yalnızca int türden dizileri sıraya dizebilir:
void sort(int *pi, size_t size);
Her tür için bu fonksiyonu içi aynı olacak biçimde yeniden yazarsak belki bir ölçüde hedefimize ulaşabiliriz. Örneğin:
void sort_int(int *pi, size_t size);
void sort_long(long *pi, size_t size);
void sort_double(double *pi, size_t size);
...
Ancak burada yine bir problem vardır. Biz kendi yapımızı sıraya dizecek olsak yapımıza uygun yeni bir fonksiyon yazmamız gerekir.
Türden bağımsız dizi işlemleri programlama dilelrinde önemli bir sorundur. C++ bunu sağlamak için ismine "şablon (template)" denilen bir
mekanizma oluşturmuştur. Önce C# sonra da Java C++'ın şablon mekanizmasından esinlenerek benzer bir mekanizmayı "generic" adı altında
oluşturmuştur. Yeni tasarlanan dilelrin çoğunda (Swift gibi, Kotlin gibi Rust gibi) bu generic mekanizma artık bulundurulmaktadır. Ancak
C'de böyle bir mekanizma yoktur. Python gibi Ruby gibi dinamik tür sistemine sahip programlama dillerinde zaten bu generic mekanizma
doğuştan vardır.
Sort işlemi hangi alagoritmaya göre yapılırsa yapılsın dizinin iki elemanın karşılaştırılması ve duruma göre yer değiştirilmesi işlemlerini
uygulamaktadır. Yani biz quick sort algoritmasını uygulasak da boubble sort algoritmasını uygulasak da eninde sonunda dizinin iki elemanını
karşılaştırıp duruma göre onları yer değiştiririz. Türden bağımsız bir sort fonksiyonu yazılacaksa fonksiyon sıraya dizeceği dizinin başlangıç
adresini void gösterici biçiminde almalıdır. Bu fonksiyon bu dizinin türünü bilmediğine göre iki elemanı karşılaştıramaz. Ancak dizinin türünü
onu çağıran programcı bilmektedir. O zaman fonksiyon dizinin iki elemanını karşılaştırma işlemini onu çağıran programcıya bırakabilir.
Tabii bunu bir fonksiyon gösterici kullanarak sağlayacaktır. Fonksiyonun dizinin iki elemanını yer değiştirebilmesi için dizinin elemanlarının
kaç byte uzunlukta olduğunu biliyor olması gerekir. O zaman böyle bir fonksiyon dizinin bir elemanın byte uzunluğunu da parametre olarak
almalıdır. Tabii fonksiyonun dizinin kaç eleman uzunluğunda olduğunu da biliyor olması gerekir. Bu durumda böyle bir fonksiyonun aşağidaki
parametrik yapıya sahip olması uygun olacaktır:
void sort(void *pv, size_t count, size_t width, int (*compare)(const void *, const void *))
Fonksiyonun birinci parametresi dizinin başlangıç adresini belirtir. İkinci parametre dizinin eleman sayısını belirtmektedir. Üçüncü
parametre dizinin bir elemanının byte uzunluğunu belirtir. Son parametre ise karşılaştırma fonksiyonun adresini almaktadır. Fonksiyon
ne zaman bir karşılaştırma yapılacak olsa dizinin karşılaştırılacak iki elemanının adreslerini bu fonksiyona geçirir ve bu fonksiyonu
çağırır. Karşılaştırma fonksiyonunu yazan programcı "birinci parametresiyle belirtilen dizi elemanı ikinci parametresiyle belirtilen
dizi elemanından büyükse fonksiyonu pozitif herhangi bir değerle, ikinci parametresiyle belirtilen dizi elemanı birinci parametresiyle
belirtilen dizi elemanındna büyükse negatif herhangi bir değerle ve bu iki eleman eşitse sıfır değeri ile" geri döndürmelidir. Örneğin
böyle bir fonksiyonla int bir dizi şöyel sıraya dizilebilir:
int cmp(const void *ptr1, const void *ptr2);
int main(void)
{
int a[10] = {3, 6, 2, 12, 1, 87, 45, 23, 54, 78};
sort(a, 10, sizeof(int), cmp);
for (int i = 0; i < 10; ++i)
printf("%d ", a[i]);
printf("\n");
return 0;
}
int cmp(const void *ptr1, const void *ptr2)
{
const int *elem1 = (const int *)ptr1;
const int *elem2 = (const int *)ptr2;
if (*elem1 > *elem2)
return 1;
if (*elem1 < *elem2)
return -1;
return 0;
}
Aşağıda boubble sort algoritması ile türden bağımsız sort işlemi yapan bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
void bsort(void *pv, size_t count, size_t width, int (*compare)(const void *, const void *));
int cmp(const void *ptr1, const void *ptr2);
int main(void)
{
int a[10] = {3, 6, 2, 12, 1, 87, 45, 23, 54, 78};
bsort(a, 10, sizeof(int), cmp);
for (int i = 0; i < 10; ++i)
printf("%d ", a[i]);
printf("\n");
return 0;
}
void bsort(void *pv, size_t count, size_t width, int (*compare)(const void *, const void *))
{
unsigned char *pc = (unsigned char *)pv;
unsigned char *elem1, *elem2;
unsigned char temp;
for (size_t i = 0; i < count - 1; ++i)
for (size_t k = 0; k < count - 1 - i; ++k) {
elem1 = pc + k * width;
elem2 = pc + (k + 1) * width;
if (compare(elem1, elem2) > 0)
for (size_t j = 0; j < width; ++j) {
temp = elem1[j];
elem1[j] = elem2[j];
elem2[j] = temp;
}
}
}
int cmp(const void *ptr1, const void *ptr2)
{
const int *elem1 = (const int *)ptr1;
const int *elem2 = (const int *)ptr2;
if (*elem1 > *elem2)
return 1;
if (*elem1 < *elem2)
return -1;
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında yukarıda yazdığımız bsort fonksiyonu ile aynı parametrik yapıya sahip qsort isimli bir standart C fonksiyonu zaten bulunmaktadır.
void qsort(void *base, size_t nmemb, size_t size, int (*compare)(const void *, const void *));
Fonksiyonun yine birinci parameresi dizinin başlangıç adresini, ikinci parametresi onun eleman sayısını, üçüncü parametresi bir elemanının
byte uzunluğunu, dördüncü parametresi ise karşılaştımra fonksiyonunu belirtmektedir. Bu karşılaştırma fonksiyonu programcı tarafından
eğer soldaki eleman sağdaki elemandan büyükse pozitif herhangi bir değere, eğer sağdaki eleman soldaki elemandan büyükse negatif herhangi
bir değere ve iki eleman biribine eşitse 0 değerine geri dönmelidir. C standartlarında fonksiyonun hangi algoritmayı kullanması gerektiği
belirtilmemiş olsa da tipik olarak fonksiyon mevcut C derleyicilerde "quick sort" algoritmasını kullanarak gerçekleştirilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct PERSON {
char name[64];
int no;
};
int cmp_no(const void *ptr1, const void *ptr2);
int cmp_name(const void *ptr1, const void *ptr2);
int main(void)
{
struct PERSON persons[10] = {
{"Kazim Tanis", 654},
{"Hasan Aslan", 145},
{"Kamil Zorlu", 487},
{"Necati Ergin", 534},
{"Ali Serce", 931},
{"Guray Sonmez", 245},
{"Fehmi Oz", 543},
{"Ayse Er", 321},
{"Sibel Cetinsoy", 423},
{"Mualla Yilmaz", 739}
};
qsort(persons, 10, sizeof(struct PERSON), cmp_no);
for (size_t i = 0; i < 10; ++i) printf("%-20s%d\n", persons[i].name, persons[i].no);
printf("-------------------------------------------\n");
qsort(persons, 10, sizeof(struct PERSON), cmp_name);
for (size_t i = 0; i < 10; ++i)
printf("%-20s%d\n", persons[i].name, persons[i].no);
return 0;
}
int cmp_no(const void *ptr1, const void *ptr2)
{
const struct PERSON *per1 = (const struct PERSON *)ptr1;
const struct PERSON *per2 = (const struct PERSON *)ptr2;
if (per1->no > per2->no)
return 1;
if (per1->no < per2->no)
return -1;
return 0;
}
int cmp_name(const void *ptr1, const void *ptr2)
{
const struct PERSON *per1 = (const struct PERSON *)ptr1;
const struct PERSON *per2 = (const struct PERSON *)ptr2;
return strcmp(per1->name, per2->name);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Biz yukardaki örnekte küçükten büyüğe sıralama yaptık. eğer büyükten küçüğe sıralama yapılmak istenirse bu durumda karşılaştırma fonksiyonu
ters yazılmalıdır. Yani birinci parametre ikinci parametreden büyükse fonksiyon negatif herhangi bir değere, küçükse pozitif herhangi bir
değere ve eşitse sıfır değerine geri döndürülmelidir. Tabii karşılatırma fonksiyonu uzunsa ya da bizim tarafımızdan yazılmadıysa karşılatırma
fonksiyonunu sarmalayan bir fonksiyon yazıp onun negatifi ile geri dönülebilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir gösterici dizisini qsort fonksiyonuyla sıraya dizerken aslında göstericilerin adresleriyle karşılaştırma fonksiyonu çağrılmaktadır.
Aşağıdaki örnekte isimlerden oluşan const char * türünden elemanlara sahip olan bir gösterici dizisi sıraya dizilmiştir. Burada aslında
sıraya dizilen gösterici dizisindeki adreslerdir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct PERSON {
char name[64];
int no;
};
int cmp(const void *ptr1, const void *ptr2);
int main(void)
{
const char *names[10] = {
"Kazim Tanis", "Hasan Aslan", "Kamil Zorlu", "Necati Ergin", "Ali Serce",
"Guray Sonmez", "Fehmi Oz", "Ayse Er", "Sibel Cetinsoy", "Mualla Yilmaz"
};
qsort(names, 10, sizeof(const char *), cmp);
for (size_t i = 0; i < 10; ++i)
printf("%s\n", names[i]);
return 0;
}
int cmp(const void *ptr1, const void *ptr2)
{
const char **elem1 = (const char **)ptr1;
const char **elem2 = (const char **)ptr2;
return strcmp(*elem1, *elem2);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde ls -l işleminden elde edilen dosyaların isme göre sıraya dizilerek yazdırılması
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <dirent.h>
#define MAX_PATH 4096
#define BLOCK_SIZE 32
struct lsinfo {
struct stat finfo;
char *name;
};
void exit_sys(const char *msg);
const char *get_ls(const struct lsinfo *lsinfo, int hlink_digit, int uname_digit, int gname_digit, int size_digit);
int cmp_name(const void *lsinfo1, const void *lsinfo2);
int main(int argc, char *argv[])
{
DIR *dir;
struct dirent *dent;
struct lsinfo *lsinfo;
int count;
char path[MAX_PATH];
struct passwd *pass;
struct group *gr;
int len;
int hlink_digit, uname_digit, gname_digit, size_digit;
int i;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((dir = opendir(argv[1])) == NULL)
exit_sys("open");
lsinfo = NULL;
for (count = 0, errno = 0; (dent = readdir(dir)) != NULL; ++count) {
sprintf(path, "%s/%s", argv[1], dent->d_name);
if (count % BLOCK_SIZE == 0)
if ((lsinfo = realloc(lsinfo, (count + BLOCK_SIZE) * sizeof(struct lsinfo))) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
if ((lsinfo[count].name = (char *)malloc(strlen(dent->d_name) + 1)) == NULL) {
fprintf(stderr, "cannot allocate memory!...\n");
exit(EXIT_FAILURE);
}
strcpy(lsinfo[count].name, dent->d_name);
if (stat(path, &lsinfo[count].finfo) == -1)
exit_sys("stat");
}
if (errno != 0)
exit_sys("readdir");
closedir(dir);
hlink_digit = uname_digit = gname_digit = size_digit = 0;
for (i = 0; i < count; ++i) {
len = (int)log10(lsinfo[i].finfo.st_nlink) + 1;
if (len > hlink_digit)
hlink_digit = len;
if ((pass = getpwuid(lsinfo[i].finfo.st_uid)) == NULL)
exit_sys("getppuid");
len = (int)strlen(pass->pw_name);
if (len > uname_digit)
uname_digit = len;
if ((gr = getgrgid(lsinfo[i].finfo.st_gid)) == NULL)
exit_sys("getgrgid");
len = (int)strlen(gr->gr_name);
if (len > gname_digit)
gname_digit = len;
len = (int)log10(lsinfo[i].finfo.st_size) + 1;
if (len > size_digit)
size_digit = len;
}
qsort(lsinfo, count, sizeof(struct lsinfo), cmp_name);
for (i = 0; i < count; ++i)
printf("%s\n", get_ls(&lsinfo[i], hlink_digit, uname_digit, gname_digit, size_digit));
for (i = 0; i < count; ++i)
free(lsinfo[i].name);
free(lsinfo);
return 0;
}
const char *get_ls(const struct lsinfo *lsinfo, int hlink_digit, int uname_digit, int gname_digit, int size_digit)
{
static char buf[4096];
static mode_t modes[] = { S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH };
struct passwd *pass;
struct group *gr;
int index = 0;
int i;
if (S_ISREG(lsinfo->finfo.st_mode))
buf[index] = '-';
else if (S_ISDIR(lsinfo->finfo.st_mode))
buf[index] = 'd';
else if (S_ISCHR(lsinfo->finfo.st_mode))
buf[index] = 'c';
else if (S_ISBLK(lsinfo->finfo.st_mode))
buf[index] = 'b';
else if (S_ISFIFO(lsinfo->finfo.st_mode))
buf[index] = 'p';
else if (S_ISLNK(lsinfo->finfo.st_mode))
buf[index] = 'l';
else if (S_ISSOCK(lsinfo->finfo.st_mode))
buf[index] = 's';
++index;
for (i = 0; i < 9; ++i)
buf[index++] = (lsinfo->finfo.st_mode & modes[i]) ? "rwx"[i % 3] : '-';
buf[index] = '\0';
index += sprintf(buf + index, " %*llu", hlink_digit, (unsigned long long)lsinfo->finfo.st_nlink);
if ((pass = getpwuid(lsinfo->finfo.st_uid)) == NULL)
return NULL;
index += sprintf(buf + index, " %-*s", uname_digit, pass->pw_name);
if ((gr = getgrgid(lsinfo->finfo.st_gid)) == NULL)
return NULL;
index += sprintf(buf + index, " %-*s", gname_digit, gr->gr_name);
index += sprintf(buf + index, " %*lld", size_digit, (long long)lsinfo->finfo.st_size);
index += strftime(buf + index, 100, " %b %e %H:%M", localtime(&lsinfo->finfo.st_mtime));
sprintf(buf + index, " %s", lsinfo->name);
return buf;
}
int istrcmp(const char *s1, const char *s2)
{
while (tolower(*s1) == tolower(*s2)) {
if (*s1 == '\0')
return 0;
++s1;
++s2;
}
return tolower(*s1) - tolower(*s2);
}
int cmp_name(const void *pv1, const void *pv2)
{
const struct lsinfo *lsinfo1 = (const struct lsinfo *)pv1;
const struct lsinfo *lsinfo2 = (const struct lsinfo *)pv2;
return istrcmp(lsinfo1->name, lsinfo2->name);
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
scandir POSIX fonksiyonun kullanıma bir örnek
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <dirent.h>
void exit_sys(const char *msg);
int mycallback(const struct dirent *de)
{
if (tolower(de->d_name[0]) == 'a')
return 1;
return 0;
}
int main(void)
{
struct dirent **dents;
int i, count;
if ((count = scandir("/usr/include", &dents, mycallback, NULL)) == -1)
exit_sys("scandir");
for (i = 0; i < count; ++i)
printf("%s\n", dents[i]->d_name);
for (i = 0; i < count; ++i)
free(dents[i]);
free(dents);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyon göstericilerine iyi bir alıştırma için scandir fonksiyonu kullanılabilir. scandir bir dizindeki struct dirent bilgilerini elde
eden bir POSIX fonksiyonudur. Fonksiyonun tasarımı biraz karışık biçimdedir. Bu karışıklık fonksiyonun rahat kullanılmasını engellemektedir.
(Kanımızda fonksiyonda bir tasarım problemi vardır.) Fonksiyonun prototipi şöyledir:
#include <dirent.h>
int scandir(const char *dirp, struct dirent ***namelist,
int (*filter)(const struct dirent *),
int (*compar)(const struct dirent **, const struct dirent **));
Fonksiyonun birinci parametresi dizin listesi elde edilecek dizin'in yol ifadesini almaktadır. Fonksiyon o dizindeki girişlerin struct dirent
bilgilerini malloc ile tahsis ettiği dinamik alana kopyalar ve alanların adreslerini de bir gösterici dizisine yerleştirir. Gösterici dizisinin
adresine de bizim ikinci parametreyle verdiğimiz göstericiyi gösteren göstericinin içerisine yerleştirmektedir. Tabii bu gösterici dizisi
de dinamik biçimde tahsis edilmiştir. Dolayısıyla bunların free hale getirilmesi programcının sorumluluğundadır. Fonksiyonun üçüncü parametresi
filtreleme yapmak için kullanılan callback fonksiyonun adresini almaktadır. scandir her bulduğu giriş için bu fonksiyonu çağırır. Eğer bu
fonksiyon sıfır dışı bir değerle geri dönerse o girişi listeye dahil eder. Eğer bu callback fonksiyon sıfır ile geri dönerse o girişi
listeye dahil etmez. scandir fonksiyonunun son parametresi girişlerin sıraya dizilmesi için kullanılan karşılaştırma fonksiyonu almaktadır.
Gerçi dirent yapısı zaten inode ve giriş isminde oluştuğu için anlamlı olan sıraya dizme giriş ismine göre olacaktır. Giriş ismine göre
sıraya dizme işlemi için alpasort isimli hazır bir karşılaştımr fonksiyonu da bulundurulmuştur. Aslında fonksiyonun son iki parametresine
NULL adres de geçilebilmektedir. scandir fonksiyonu çaşarı durumunda diziye yerleştirilen giriş sayısı ile başarısızlık durumunda -1
değeri ile geri dönmektedir.
Aşağıda scandir fonksiyonunun kullanımına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <dirent.h>
#include <sys/stat.h>
void exit_sys(const char *msg);
int mycallback(const struct dirent *de)
{
struct stat finfo;
char path[4096];
sprintf(path, "/usr/include/%s", de->d_name);
if (stat(path, &finfo) == -1)
exit_sys("stat");
return finfo.st_size < 1000;
}
int main(void)
{
struct dirent **dents;
int i, count;
if ((count = scandir("/usr/include", &dents, mycallback, NULL)) == -1)
exit_sys("scandir");
for (i = 0; i < count; ++i)
printf("%s\n", dents[i]->d_name);
for (i = 0; i < count; ++i)
free(dents[i]);
free(dents);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
24. Ders 03/09/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Komut satırı (REPL) oluşturan bir örnek. Bu örnekte komut bulunduğunda belirlenen bir fonksiyon çağrılmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* myshell.c */
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#define PROMPT "CSD>"
#define MAX_CMD_LINE 4096
#define MAX_CMD_PARAMS 128
#define BUFFER_SIZE 8192
void parse_cmdline(void);
void cat_cmd(void);
void cp_cmd(void);
void rename_proc(void);
typedef struct tagCMD {
char *cmd_name;
void (*cmd_proc)(void);
} CMD;
char g_cmdline[MAX_CMD_LINE];
char *g_params[MAX_CMD_PARAMS];
int g_nparams;
CMD g_cmds[] = {
{"cat", cat_cmd},
{"cp", cp_cmd},
{"rename", rename_proc},
{NULL, NULL}
};
int main(void)
{
char *str;
int i;
for (;;) {
printf("%s", PROMPT);
if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL)
continue;
if ((str = strchr(g_cmdline, '\n')) != NULL)
*str = '\0';
parse_cmdline();
if (g_nparams == 0)
continue;
if (!strcmp(g_params[0], "exit"))
break;
for (i = 0; g_cmds[i].cmd_name != NULL; ++i)
if (!strcmp(g_cmds[i].cmd_name, g_params[0])) {
g_cmds[i].cmd_proc();
break;
}
if (g_cmds[i].cmd_name == NULL) {
printf("invalid command: %s\n", g_params[0]);
}
}
return 0;
}
void parse_cmdline(void)
{
char *str;
g_nparams = 0;
for (str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t"))
g_params[g_nparams++] = str;
}
void cat_cmd(void)
{
FILE *f;
int ch;
if (g_nparams != 2) {
printf("cat command missing file!..\n");
return;
}
if ((f = fopen(g_params[1], "r")) == NULL) {
printf("file not found or cannot open file: %s\n", g_params[1]);
return;
}
while ((ch = fgetc(f)) != EOF)
putchar(ch);
if (ferror(f))
printf("cannot read file: %s\n", g_params[1]);
fclose(f);
}
void cp_cmd(void)
{
int fds, fdd;
char buf[BUFFER_SIZE];
ssize_t result;
if (g_nparams != 3) {
printf("source and destination path must be specified!..\n");
return;
}
if ((fds = open(g_params[1], O_RDONLY)) == -1) {
printf("file not found or cannot open: %s\n", g_params[1]);
return;
}
if ((fdd = open(g_params[2], O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) {
printf("file not found or cannot open: %s\n", g_params[2]);
goto EXIT2;
}
while ((result = read(fds, buf, BUFFER_SIZE)) > 0)
if (write(fdd, buf, result) != result) {
printf("cannot write file: %s\n", g_params[2]);
goto EXIT1;
}
if (result == -1) {
printf("cannot read file: %s\n", g_params[1]);
goto EXIT1;
}
printf("1 file copied...\n");
EXIT1:
close(fdd);
EXIT2:
close(fds);
}
void rename_proc(void)
{
printf("rename command...\n");
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir gösterici dizisinin ismi onun ilk elemanının adresi olacağına göre göstericiyi gösteren göstericiye atanmalıdır. Örneğin:
int x = 10, y = 20, z = 30;
int *a[] = {&x, &y, &z};
int **ppi;
ppi = a; /* geçerli */
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const char *names[] = {"ali", "veli", "selami", "ayse", "fatma", NULL};
const char **pps;
pps = names;
for (int i = 0; pps[i] != NULL; ++i)
puts(pps[i]);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'de parametre parantezi içerisindeki dizisel gösterim tamamen "gösterici" anlamına gelmektedir. Köşeli parantezlerin içerisindeki sayıların
da hiçbir önemi yoktur. Dolayısıyla aşağıdaki prototiplerin hepsi aynıdır:
void foo(int *a);
void foo(int a[]);
void foo(int a[100]);
void foo(int a[5]);
Bunların hepsi void foo(int *) anlamına gelmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyonun parametresi göstericiyi gösteren gösterici ise o parametre yine dizi sentaksıyla belirtilebilir. Dolayısıyla aşağıdaki prototipler
de yine eşdeğerdir:
void bar(int **a);
void bar(int *a[10]);
void bar(int *a[]);
Bunların hepsi void bar(int **) anlamına gelmektedir. Bu durumda örneğin aslında main fonksiyonunun ikinci parametresi de şöyle belirtilebilir:
int main(int argc, char **argv)
{
...
}
Ancak geleneksel olarak programcılar bu ikinci parametreyi aşağıdaki gibi belirtmektedir:
int main(int argc, char *argv[])
{
...
}
Ancak bir fonksiyonun parametresi sanki çok boyutlu diziymiş gibi belirtilirse bu tamamen başka bir anlama gelemketdir. Örneğin:
void foo(int a[][3])
{
...
}
Bu bildirmin ne anlamı geldiği izleyen paragraflarda açıklanacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void foo(int *a[], size_t size)
{
for (size_t i = 0; i < size; ++i)
printf("%d\n", *a[i]);
}
int main(void)
{
int x = 10, y = 20, z = 30;
int *a[] = {&x, &y, &z};
foo(a, 3);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyon parametresi fonksiyon göstericisi ise o da alternatif biçimde fonksiyon sentaksıyla belirtilebilmektedir. Aşağıdaki iki prototip
eşdeğerdir. Örneğin:
void foo(int (*a)(double));
void foo(int a(double));
Tabii prototipte değişken ismi belirtilmek zorunda olmadığına göre aşağıdaki prototip de yukarıdakilerle eşdeğerdir:
void foo(int (double));
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Çok boyutlu diziler aslında C'de dizi dizileri olarak düşünülmelidir. Öneğin:
int a[3][2];
Burada a aslında 3 elemanlı bir dizidir. Ancak dizinin her elemanı "int[2] türünden yani 2 elemanlı bir int dizidir. İlk köşeli parantez
her zaman asıl dizinin uzunluğunu belirtmektedir. Diğer köşeli parantezler eleman olan dizinin türü ile ilgilidir. Bir dizinin ismi dizinin
ilk elemanın adresini belirttiğine göre yukarıdaki iki boyutlu dizide a ifadesi aslında iki elemanlı bir int dizinin adresini belirtir.
Biz C'de dizilerin adreslerini alabiliriz. Bu durumda elde edilen adres dizi türünden adres olur. Dizi türünden adresler sembolik olarak
"tür (*)[uzunuk]" biçiminde ifade edilir. Dizi türünden adresleri tutan göstericilere "dizi göstericileri (pointer to array)" denilmektedir.
Örneğin:
int a[2];
int (*pa)[2];
pa = &a;
Mademki bir dizinin ismi o dizinin ilk elemanın adresini belirtmektedir. O halde aşağıdaki iki boyutlu dizinin ismi hangi türden adres belirtir?
int a[3][2];
Burada a 3 elemanlı, her elemanı 2 elemanlı birint dizi olan bir dizidir. Yani a dizisinin türü int[2] biçimindedir. O halde a ifadesi de
aslında int (*)[2] türündendir. Biz a adresini aynı türden bir dizi göstericisine yerleştirebiliriz. Örneğin:
int a[3][2];
int(*pa)[2];
pa = a;
Dizi göstericileri bildirilirken ilk boyut dışındaki boyut uzunlukları belirtilemk zorundadır. Örneğin:
int (*pa)[]; /* geçersiz! */
Bir dizisinin adresini aynı uzunluğa ilişkin bir dizi göstericisine atayabiliriz. Örneğin:
int a[3];
int (*pa)[2];
pa = &a; /* geçersiz! */
Burada pa göstericisinin aşağıdaki gibi tanımlanması gerekirdi:
int (*pa)[3];
Çok boyutlu dizilerin adresleri de benzer biçimde aynı türden bir dizi göstericisine atanabilir. Örneğin:
int a[2][3][4];
int (*pa)[3][4];
pa = a; /* geçerli */
C'de çok yapılan bir hata bir matrisin adresinin gösterici göstericiye atanmaya çalışılmasıdır. Örneğin:
void foo(int **ppi)
{
...
}
...
int a[3][2];
...
foo(a); /* geçersiz! */
Burada foo fonksiyonunun parametresinin aşağıdaki gibi olması gerekirdi:
void foo(int (*pa)[2])
{
...
}
...
int a[3][2];
...
foo(a); /* geçerli ! */
Maalesef C'de her uzunlukta çok boyutlu dizinin adresinin atanabileceği gösterici oluşturmak mümkün değildir. Bu tür durumlarda ne yapılması
gerektiği izleyen paragraflarda açıklanmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir dizi göstericisinin gösterdiği yere erişirsek o dizinin tamamına erişmiş oluruz. Örneğin:
int a[3][2];
int (*pa)[2];
pa = a;
Burada pa aslında matrisin ilk elemanı olan iki elemanlı int dizinin tamamını göstermektedir. Dolayısıyla *pa ifadesi ya da pa[0] ifadesi
aslında 2 elemanlıbir int diziyi temsil eder. Burada pa[1] ifadesi bu matrisin ikinci satırını oluşturan 2 elemanlı int diziyi temsil
etmektedir.
Dizi isimler dizinin tamamını temsil etmektedir. Ancak C'de dizi isimleri ifade içerisinde kullanıldığında aslında dizinin ilk elemanının adresi
anlamına gelmektedir. Dizi isimleri bu nedenle nesne belirtmez. Dizi isimleri C'de adeta bir sembolik sabit gibi düşünülmelidir. Örneğin:
int a[3] = {1, 2, 3};
a = 100; /* a nesne belirtmiyor, bir adres sabiti belirtiyor */
Bu durumda örneğin:
int a[3][2];
burada a[i] ifadesi de *a ifadesi de nesne belirtmemektedir. Bu ifadeler adeta dizi isimleri gibi düşünülmelidir.
Biz C'de çok boyutlu dizilerin elemanlarına birden fazla [...] ile erişiriz. Bunun neden böyle olduğu açıktır. Örneğin:
int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
Burada a[i] aslında a matrisinin i'inci satırındaki diziyi belirtmektedir. O halde a[i][k] ifadesi de aslında a matrisinin i'inci satırındaki
dizinin k'ıncı sütunundaki eleman anlamına gelmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int main(void)
{
int a[3][2] = { {1, 2}, {3, 4}, {5, 6} };
int(*pa)[2];
pa = a;
printf("%d\n", (*pa)[0]);
printf("%d\n", (*pa)[1]);
printf("%d\n", pa[1][0]);
printf("%d\n", pa[1][1]);
printf("%d\n", pa[2][0]);
printf("%d\n", pa[2][1]);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi bir matrisi fonksiyona partametre olarak nasıl geçirebiliriz? İşte fonksiyonun parametre değişkeninin bir dizi göstricisi olması
gerekir. Ayrıca yukarıda da belirtitğimiz gibi matrisin satır uzunluğunun da fonksiyona aktarılması uygun olur. Ne de olsa biz dizilerin
uzunluklarını da fonksiyona aktarmaktayız. Örneğin:
void foo(int (*pa)[2], size_t size);
Bu fonksiyon sütun sayısı 2 olan ancak herhangi miktarda satıra sahip olan iki boyutlu diziler üzerinde işlem yapabilme potansiyelindedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void foo(int(*pa)[2], size_t size)
{
size_t i, k;
for (i = 0; i < size; ++i) {
for (k = 0; k < 2; ++k)
printf("%d ", pa[i][k]);
printf("\n");
}
}
int main(void)
{
int a[3][2] = { {1, 2}, {3, 4}, {5, 6} };
foo(a, 3);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Çok boyutlu dizi kavramı aslında yapay bir kavramdır. Çünkü bellek çok boyutlu değildir tek boyutludur. Dolayısıyla çok boyutlu dizileri
derleyici aslında tek boyutlu dizilermiş gibi bellekte tutmaktadır. C standartları çok boyutlu dizilerin tüm elemanlarının ardışıl olduğunu
ve bu ardışıllığın satır tabanlı biçimde olduğunu belirtmektedir. Örneğin:
int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
Buradaki a dizisi aslında iki elemanlı int dizilerin dizisi olduğuna göre elemanların bellekteki yerleşimi aşağıdaki olacaktır:
1
2
3
4
5
6
Örneğin:
int a[2][2][3] = {{{1, 2, 3}, {4, 5, 6}}, {{7, 8, 9}, {10, 11, 12}}};
Bu dizinin bellekteki yerleşimi ise şöyle olacaktır:
1
2
3
4
5
6
7
8
9
10
11
12
Bu durumda çok boyutlu bir dizinin ismi tek boyutlu bir göstericiye atanıp çok boyutlu dizinin bütün elemanlarına ulaşılabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Tek boyutlu bir dizi de bir dizi göstericisine tür dönüştürme operatörü kullanılarak dönüştürülebilir. Bundan sonra dizi elemanlarına
matris sentaksıyla erişilebilir. Örneğin:
int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
int (*pa)[3];
pa = (int (*)[3]) a;
for (int i = 0; i < 4; ++i) {
for (int k = 0; k < 3; ++k)
printf("%d ", pa[i][k]);
putchar('\n');
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int main(void)
{
int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
int (*pa)[3];
pa = (int (*)[3]) a;
for (int i = 0; i < 4; ++i) {
for (int k = 0; k < 3; ++k)
printf("%d ", pa[i][k]);
putchar('\n');
}
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir matiris için de dinamik tahsisatlar yapabiliriz. Tabii buradaki önemli nokta tahsis edilen alanın başlangıç adresinin atanacağı dizi
göstericisinin nasıl tanımlanacağıdır. Örneğin biz 4x3'lik bir matris için malloc fonksiyonu ile tahsisat yapmak isteyelim. Bu durumda
tahsis edilen alanın adresi aşağıdaki gibi bir dizi göstericisine atanbilir:
int (*pa)[3];
Örneğin:
int(*pa)[3];
if ((pa = (int(*)[3])malloc(4 * 3 * sizeof(int))) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int(*pa)[3];
if ((pa = (int(*)[3])malloc(4 * 3 * sizeof(int))) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 4; ++i)
for (int k = 0; k < 3; ++k)
pa[i][k] = i + k;
for (int i = 0; i < 4; ++i) {
for (int k = 0; k < 3; ++k)
printf("%d ", pa[i][k]);
putchar('\n');
}
free(pa);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz belli bir türden her uzunluktaki matrisi (çok boyutlu diziyi) parametre olarak alabilecek genel bir fonksiyon
yazılamamaktadır. Bu tür genel fonksiyonları yazmanın en pratik yolu onları tek boyutluymuş gibi düşünmektir. Örneğin iki boyutlu int türden
bir matrisi parametre olarak alan bir fonksiyonun parametrik yapısı şöyle olabilir:
void foo(int *pi, size_t rowsize, size_t colsize);
Fonksiyon içerisinde bu matrisin i'inci satır k'ıncı sütun elemanlarına pi[i * colsize + k] ifadesiyle erişebiliriz. Aslında bu erişim
doğal matris erişiminde daha yavaş değildir. Çünkü matrisler zaten doğal türler değildir. Yani bir matrisin elemanına a[i][k] biçiminde
eriştiğimizde de zaten derleyici aynı işlemleri yapmaktadır.
Tabii bu fonksiyonu çağırırken tür dönüştürmesi yapmak gerekir. Örneğin:
int a[3][2] = { {1, 2}, {3, 4}, {5, 6} };
...
foo(a, 3, 2); /* geçersiz! */
foo((int *)a, 3, 2); /* geçerli
Tabii tür dönüştürmesi yapılmak istenmiyorsa fonksiyonun parametresi void gösterici alınıp tür dönüştürmesi fonksiyonun içinde de
yapılabilir. Örneğin:
void foo(void *pv, size_t rowsize, size_t colsize)
{
int *pi = (int *)pv;
...
}
Artık tür dönüştürmesine gerek yoktur:
foo(a, 3, 2);
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void disp_matrix(void *pv, size_t rowsize, size_t colsize)
{
int *pi = (int *)pv;
for (size_t i = 0; i < rowsize; ++i) {
for (size_t k = 0; k < colsize; ++k)
printf("%d ", pi[i * colsize + k]);
putchar('\n');
}
}
int main(void)
{
int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
disp_matrix(a, 3, 2);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Her elemanı bir dizi göstericisi olan bir dizi de oluşturulabilir. Örneğin:
int (*a[10])[2];
Burada a 10 elemanlı bir dizidir. Ancak bu dizinin her elemanı int (*)[2] türünden yani sütun uzunluğu 2 olan matrisleri gösteren dizi
göstericisidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
25. Ders 09/09/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyonun parametre parantezi içerisinde çok boyutlu dizi sentaksı dizi göstericisi anlamına gelmektedir. Örneğin:
void foo(int a[][3])
{
...
}
Bu tanımlama tamamen aşağıdakiyle eşdeğerdir:
void foo(int (*a)[3])
{
...
}
Tabii dizisel gösterimde ikinci koşeli parantezin (çok boyutlu dizilerde ilki haricindeki tüm köşeli parantezlerin) içinde uzunluk belirten
bir sabit ifadesinin bulunuyor olması gerekir. Ancak ilk köşeli parantezin içi boş olabilir. İlk köşeli parantezin içine yazılacak
uzunluğun hiçbir önemi yoktur. Örneğin aşağıdaki prototipler tamamen eşdeğerdir:
void foo(int a[][3]);
void foo(int a[10][3]);
void foo(int (*a)[3]);
-------------------------------------------------------------------------------------------------------------------------------------------*/
void foo(int a[][5]) /* int (*a)[5] */
{
/* ... */
}
int main(void)
{
int a[5];
foo(&a);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyonların geri dönüş değerleri de dizi türünden adresler olabilir. Bu durumda dekleratörde yine * atomu paranteze içerisine alınır.
Parantezin soluna geri dönüş değerine ilişkin dizinin türü, sağında geri dönüş değerine ilişkin didizinin uzunluğu yazılır. Örneğin:
int (*foo(int a))[3]
{
...
}
Burada foo fonksiyonunun parametresi int türdendir. Ancak geri dönüş değeri 3 elemanlı bir dizinin adresidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int(*foo(void))[5]
{
static int a[3] = {1, 2, 3, 4, 5};
return &a;
}
int main(void)
{
int(*pa)[5];
int i;
pa = foo();
for (i = 0; i < 5; ++i)
printf("%d ", (*pa)[i]);
printf("\n");
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
İşletim sistemlerinde bir dosyanın yerini belirten yazısal ifadelere "yol ifadeleri (path names)" denilmektedir. Yol ifadelerinde dizin
geçişleri kullanılabilir. Windows sistemlerinde dizin geçişleri için "\" karakteri UNIX/Linux ve macOS sistemlerinde ise "/" karakteri
kullanılmaktadır. Bir yol ifadesindeki "/" ya da "\" arasındaki her bir dizin girişine "yol ifadesi bileşeni (pathname component)" denilmektedir.
Örneğin:
"/home/kaan/Study/test.txt"
Burada "home", "kaan", "Study", "test.txt" birer yol ifadesi girişidir.
Windows sistemlerinde "sürücü (drive)" kavramı da vardır. Bu sistemlere her sürücünün ayrı bir kökü bulunmaktadır. Dolayısıyla yol ifadelerinde
sürücüler de belirtilebilmektedir. Örneğin:
"F:\Dropbox\Study\test.txt"
Halbuki UNIX/Linux sistemlerinde ve macOS sistemlerinde sürücü kavramı yoktur. Bu sistemlerde tek bir kök vardır. Başka aygıtlar bu kökte
herhangi bir yere monte edilirler. Bu işleme İngilizce "mount" denilmektedir. Mount işleminin yapıldığı dizin artık mount edilen aygıtın
kök dizini gibi işlem görmektedir.
Yol ifadeleri "mutlak (absolute)" ve "göreli (relative)" olmak üzere ikiye ayrılmaktadır. Eğer bir yol ifadesinin ilk karakteri "/" ya da "\"
ise bu tür yol ifadelerine mutlak yol ifadeleri denilmektedir. Örneğin:
"/home/kaan/Study/test.txt"
Bu yol ifadesi UNIX/Linux sistemlerinde mutlak bir yol ifadesidir. Örneğin:
"\Windows\notepad.exe"
Bu yol ifadesi de Windows sistemleri için mutlak bir yol ifadesidir. Mutlak yol ifadeleri her zaman kök dizinden itibaren yer belirtirler.
Eğer yol ifadelerindeki ilk karakter "/" ya da "\" değilse böyle yol ifadelerine de "göreli yol ifadeleri" denilmektedir. Örneğin:
"Study/C/test.txt"
"Doc\Temp\test.txt"
"samle.c"
Bu yol ifadeleri görelidir. İşletim sistemleri her proses için prosesin kontrol bloğu içerisinde ismine "prosesin çalışma dizini (process
current working directory)" denilen bir dizin tutmaktadır. İşte göreli yol ifadeleri prosesin çalışma dizini orijin yapılarak çözülmektedir.
Yani prosesin çalışma dizini göreli yol ifadelerinin nereden itibaren yol ifadesi belirttiğini göstermektedir. Pekiyi prosesin çalışma dizini
proses yaratıldığında hangi dizin olarak set edilmiştir ve program çalışırken değiştirilebilir mi? İşte UNIX/Linux sistemlerinde bir proses
yaratıldığında (yani bir program çalıştırıldığında) onun çalışma dizini üst prosesten (yani onu çalıştıran prosesten) alınmaktadır. Örneğin
"sample" programının çalışma dizini "/home/kaan" olsun. "sample" programı da "mample" programını çalıştırmış olsun. O halde işin başında "mample"
programının çalışma dizini de "home/kaan" olacaktır. Windows sistemlerinde de bir proses yaratıldığında yeni prosesin çalışma dizini ya onu
yaratan prosesin çalışma dizini olarak üst prosesten alınır ya da istenilen bir dizin olarak set edilir. Tabii Windows sistemlerinde prosesin
çalışma dizini sürücü bilgisini de içermektedir.
Windows sistemlerinde bir yol ifadesinde sürücü bilgisi de varsa buna "tam yol ifadesi (full path)" denilmektedir. Örneğin:
"C:\Windows\temp\x.txt"
Pekiyi bu sistemlerde mutlak bir yol ifadesi sürücü içermezse default sürücü ne olacaktır? Örneğin:
"\temp\test.txt"
Bu mutlak ifadesi hangi sürücünün kök dizininden itibaren yer belirtmektedir? İşte Windows sistemlerinde sürücüsü belirtilmemiş olan yol mutlak
yol ifadeleri prosesin çalışma dizini hangi sürücüye ilişkinse o sürücüde yer beelirtmektedir. Örneğin prosesimizin çalışma dizini "F:\Dropbox"
ise yukarıdaki yol ifadesi F sürücüsüün kökünden itibaren yer belirtmektedir. Eğer prosesimizin çalışma dizini örneğin "D:\Ali\Study" olsaydı
yukarıdaki yol ifadesi D dizinin kökünden itibaren yer belirtecekti. Windows sistemlerinde bir yol ifadesinde sürücü varsa ancak yoli fadesi
göreli ise bu durumda proseste bazı özel çevre değişkenlerine bakılmaktadır. Eğer bu çevre değişkenleri yoksa bu duurmda ilgili sürücünün
kök dizini orijin kabul edilmektedir. Örneğin:
"D:Ali\test.txt"
Burada yol ifadesinde sürücü belirtilmiştir. Ancak yol ifadesi görelidir. İşte bu durumda orijin noktası için bazı çevre değişkenlerine bakılır.
Ancak bu çevre değişkenleri yoksa bu yol ifadesi "D:\Ali\test.txt" ile eşdeğer kabul edilir. Tabii programcının böylesi yol ifadelerinden kaçınması
daha uygun olur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde prosesin çalışma dizini (current working directory) GetCurrentDirectory API fonksiyonuyla elde edilmektedir.
Fonksiyonun prototipi şöyledir:
DWORD GetCurrentDirectory(
DWORD nBufferLength,
LPTSTR lpBuffer
);
Fonksiyonun ikinci parametresi prosesin çalışma dizininin yerleştirileceği char türden dizinin başlangıç adresini almaktadır. Birinci parametre
bu dizinin uzunluğunu belirtmektedir. Fonksiyon başarısızlık durumunda 0 değerine geri dönmektedir. Ancak programcının fonksiyona verdiği dizinin
uzunluğu yetersiz kalırsa fonksiyon diziye yerleştirme yapmaz fakat geri dönüş değeri olarak null karakter dahil olmak üzere gereken dizi
uzunluğunu verir. Fonksiyon başarı durumunda diziye yerleştirilen karakter sayısına geri dönmektedir. Ancak başarı durumundaki bu karakter sayısına
null karakter dahil değildir. Eğer fonksiyonunun birinci parametresi 0 ve ikinci parametresi NULL adres geçilirse fonksiyon prosesin çalışma
dizininin yerleştirilmesi için gereken karakter sayısını (null karakter dahil olmak üzere) bize vermektedir.
Windows sistemlerinde bir yol ifadesinin maksimum uzunluğu MAX_PATH değeri ile önceden belirlenmiştir. Bu tür durumlarda dizi uzunluklarını
MAX_PATH kadar açınız. MAX_PATH mevcut Windows sistemlerinde 260 olarak define edilmiştir.
GetCurrentDirectory fonksiyonunun başarısı aşağıdaki gibi kontrol edilebilir:
char cwd[BUFFER_SIZE];
DWORD dwResult;
if (!(dwResult = GetCurrentDirectory(BUFFER_SIZE, cwd)))
ExitSys("GetCurrentDirectory");
if (dwResult > BUFFER_SIZE) {
fprintf(stderr, "Buffer too small!..\n");
exit(EXIT_FAILURE);
}
Aşağıdaki örnekte prosesin çalışma dizini elde edilip yazdırılmıştır. Burada MAX_PATH değeri kullanıldığı için alanın yeterli büyüklükte
olup olmadığı ayrıca kontrol edilmemiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
char cwd[MAX_PATH];
if (!GetCurrentDirectory(MAX_PATH, cwd))
ExitSys("GetCurrentDirectory");
puts(cwd);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
GetCurrentDirectory fonksiyonunda birinci parametre 0, ikinci parametre NULL geçilirse çalışma dizini için gereken karakter uzunluğu (byte değil)
elde edilir. Aşağıdaki örnekte bu yöntem kullanılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
char *cwd;
DWORD dwBufSize;
if (!(dwBufSize = GetCurrentDirectory(0, NULL)))
ExitSys("GetCurrentDirectory");
if ((cwd = (char *)malloc(dwBufSize)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
if (!GetCurrentDirectory(dwBufSize, cwd))
ExitSys("GetCurrentDirectory");
puts(cwd);
free(cwd);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde prosesin çalışma dizinini değiştirmek için SetCurrentDirectory isimli API fonksiyonu kullanılmaktadır. Fonksiyonun
prototipi şöyledir:
BOOL SetCurrentDirectory(
LPCTSTR lpPathName
);
Fonksiyon set edilecek çalışma dizinini parametre olarak alır. Başarı durumunda 0, başarısızlık durumunda sıfır dışı bir değere geri döner.
Aşağıdaki örnekte önce prosesin çalışma dizini SetCurrentDirectory API fonksiyonu ile "C:\windows" olarak değiştirilmiştir. Sonra prosesin
çalışma dizini yine GetCurrentDirectory API fonksiyonu ile alınıp yazdırılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
char cwd[MAX_PATH];
DWORD dwResult;
if (!SetCurrentDirectory("c:\\windows"))
ExitSys("SetCurrentDirectory");
if (!(dwResult = GetCurrentDirectory(MAX_PATH, cwd)))
ExitSys("GetCurrentDirectory");
puts(cwd);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Basit bir shell uygulamasında (cmd.exe benzeri bir shell) GetCurrentDirectory ve SetCurrentDirectory API fonksiyonlarının kullanımı
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include <windows.h>
#define MAX_CMD_LINE 4096
#define MAX_CMD_PARAMS 64
struct CMD {
char *cmd_text;
void (*proc)(void);
};
void ExitSys(LPCSTR lpszMsg);
void ParseCmdLine(void);
void DirProc(void);
void CopyProc(void);
void ClsProc(void);
void RenameProc(void);
void ChangeDirProc(void);
void DispDirectory(LPCTSTR lpszPath);
char g_cmd_line[MAX_CMD_LINE];
struct CMD g_cmds[] = {
{"dir", DirProc},
{"copy", CopyProc},
{"cls", ClsProc},
{"rename", RenameProc},
{"cd", ChangeDirProc},
{NULL, NULL},
};
char *g_params[MAX_CMD_PARAMS];
int g_nparams;
char g_cwd[MAX_PATH];
int main(void)
{
char *str;
int i;
if (!GetCurrentDirectory(MAX_PATH, g_cwd))
ExitSys("GetCurrentDirectory");
for (;;) {
printf("%s>", g_cwd);
fgets(g_cmd_line, MAX_CMD_LINE, stdin);
if ((str = strchr(g_cmd_line, '\n')) != NULL)
*str = '\0';
ParseCmdLine();
if (g_nparams == 0)
continue;
if (!strcmp(g_params[0], "exit"))
break;
for (i = 0; g_cmds[i].cmd_text != NULL; ++i)
if (!strcmp(g_cmds[i].cmd_text, g_params[0])) {
g_cmds[i].proc();
break;
}
if (g_cmds[i].cmd_text == NULL)
printf("command not found: %s\n\n", g_params[0]);
}
return 0;
}
void ParseCmdLine(void)
{
char *str;
g_nparams = 0;
for (str = strtok(g_cmd_line, " \t"); str != NULL; str = strtok(NULL, " \t"))
g_params[g_nparams++] = str;
g_params[g_nparams] = NULL;
}
void DirProc(void)
{
if (g_nparams > 2) {
printf("too many arguments!..\n");
return;
}
DispDirectory(g_nparams == 1 ? g_cwd : g_params[1]);
}
void CopyProc(void)
{
if (g_nparams != 3) {
printf("argument too few or too many!..\n\n");
return;
}
}
void ClsProc(void)
{
if (g_nparams != 1) {
printf("too many arguments!\n\n");
return;
}
printf("cls command\n");
}
void RenameProc(void)
{
printf("rename command\n");
}
void ChangeDirProc(void)
{
if (g_nparams > 2) {
printf("too many arguments!..\n\n");
return;
}
if (g_nparams == 1) {
printf("%s\n\n", g_cwd);
return;
}
if (!SetCurrentDirectory(g_params[1])) {
printf("directory not found or cannot change: %s\n\n", g_params[1]);
return;
}
if (!GetCurrentDirectory(MAX_PATH, g_cwd))
ExitSys("GetCurrentDirectory");
}
void DispDirectory(LPCTSTR lpszPath)
{
WIN32_FIND_DATA wfd;
char lpszDirPath[MAX_PATH];
HANDLE hFF;
sprintf(lpszDirPath, "%s/*.*", lpszPath);
if ((hFF = FindFirstFile(lpszDirPath, &wfd)) == INVALID_HANDLE_VALUE) {
printf("directory not found or cannot display!\n\n");
return;
}
do {
if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
printf("%-10s", "<DIR>");
else
printf("%-10lu", wfd.nFileSizeLow);
printf("%s\n", wfd.cFileName);
} while (FindNextFile(hFF, &wfd));
printf("\n");
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde prosesin çalışma dizini getcwd isimli POSIX fonksiyonuyla elde edilmektedir. Fonksiyonun prototipi şöyledir:
#include <unistd.h>
char *getcwd(char *buf, size_t size);
Fonksiyonun birinci prosesin çalışma dizininin yerleştirileceği dizinin adresini, ikinci parametresi ise onun null karakter dahil olmak üzere
uzunluğunu almaktadır. Eğer yol ifadesi belirtilen uzunluktan null karakter dahil olmak üzere büyükse fonksiyon başarısız olur. Fonksiyon
başarı durumunda birinci parametresiyle belirtilen adresin aynısına, başarısızlık durumunda NULL adrese geri dönmektedir.
UNIX/Linux sistemlerinde yol ifadelerinin maksimum değerleri <limits.h> içerisinde bildirilmiş olan PATH_MAX isimli bir sembolik sabitle
ifade edilmektedir. Ancak bu sembolik sabitin define edilmiş olma zorunluluğu yoktur. Eğer bu sembolik sabit define edilmemişse bu durumda
maksimum yol ifadesinin uzunluğu pathconf isimli POSIX fonksiyonuyla elde edilmektedir. Linux'ta PATH_MAX sembolik sabiti 4096 olarak
define edilmiştir.
Aşağıdaki örnekte prosesin çalışma dizini getcwd fonksiyonuyla alınp stdout dosyasına yazdırılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
char cwd[4096];
if (getcwd(cwd, 4096) == NULL)
exit_sys("getcwd");
puts(cwd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
26. Ders 10/09/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz gibi Windows sistemlerinde yol ifadesinin maksimum uzunluğu MAX_PATH sembolik sabitiyle belirlenmiştir. Ancak
UNIX/Linux sistemlerinde bunun karşılığı olan PATH_MAX sembolik sabiti define edilmemiş olabilir. (Linux sistemlerinde PATH_MAX sembolik
sabitinin 4096 olarak define edildiğini anımsayınız.) İşte UNIX/Linux sistemlerinde yol ifadesinin maksimum uzunluğu taşınabilir bir biçimde
elde edenbilmek için aşağıdaki gibi bir fonksiyonun yazılması gerekmektedir:
long path_max(void)
{
static long result = 0;
#define PATH_MAX_INDETERMINATE_GUESS 4096
#ifdef PATH_MAX
result = PATH_MAX;
#else
if (result == 0) {
errno = 0;
if ((result = pathconf("/", _PC_PATH_MAX)) == -1 && errno == 0)
result = PATH_MAX_INDETERMINATE_GUESS;
}
#endif
return result;
}
Aşağıdaki örnekte önce yokl ifadesi için gerekli olan alan path_max fonksiyonu ile elde edilmiş sonra da yol ifadesinin yerleştirileceği
alan malloc fonksiyonuyla tahsis edilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
void exit_sys(const char *msg);
long path_max(void)
{
static long result = 0;
#define PATH_MAX_INDETERMINATE_GUESS 4096
#ifdef PATH_MAX
result = PATH_MAX;
#else
if (result == 0) {
errno = 0;
if ((result = pathconf("/", _PC_PATH_MAX)) == -1 && errno == 0)
result = PATH_MAX_INDETERMINATE_GUESS;
}
#endif
return result;
}
int main(void)
{
char *cwd;
long size;
size = path_max();
if ((cwd = (char *)malloc(size)) == NULL)
exit_sys("malloc");
if (getcwd(cwd, size) == NULL)
exit_sys("getcwd");
printf("size: %ld, cwd = %s\n", size, cwd);
free(cwd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde proseslerin çalışma dizinini değiştirmek için chdir isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi
şöyledir:
#include <unistd.h>
int chdir(const char *path);
Fonksiyon çalışma dizini yapılacak dizin'in yol ifadesini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda -1
değerine geri döner.
Aşağıdaki örnekte önce prosesin çalışma dizini alınarak ekrana (stdout dosyasına) yazdırılmıştır. Sonra çalışma değiştirilip yeniden
elde edilip yazdırılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
char cwd[4096];
if (getcwd(cwd, 4096) == NULL)
exit_sys("getcwd");
puts(cwd);
if (chdir("/usr/include") == -1)
exit_sys("getcwd");
if (getcwd(cwd, 4096) == NULL)
exit_sys("getcwd");
puts(cwd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte daha önce UNIX/Linux sistemleri için yapmış olduğumuz "myshell" programına pwd, cd, ls ve clear komutları eklenmiştir.
Programda yine myshell programı kendi çalışma dizininin prompt olarak ekran basmaktadır. cd komutu da çalışma dizinini değiştirmektedir.
Normal olarak UNIX/Linux sistemlerinde cd komutu argüman almazsa çalışma dizinini "home dizin" olarak değiştirmektedir. Ancak aşağıdaki
örnekte biz bu özelliği sağlamıyoruz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* myshell.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <math.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <dirent.h>
#include <pwd.h>
#include <grp.h>
#define MAX_CMD_LINE 4096
#define MAX_CMD_PARAMS 128
#define BUFFER_SIZE 8192
#define MAX_PATH_SIZE 4096
void parse_cmdline(void);
void cat_cmd(void);
void cp_cmd(void);
void rename_proc(void);
void pwd_proc(void);
void cd_proc(void);
void ls_proc(void);
void clear_proc(void);
const char *get_ls(const char *path, int hlink_digit, int uname_digit, int gname_digit, int size_digit);
void exit_sys(const char *msg);
typedef struct tagCMD {
char *cmd_name;
void (*cmd_proc)(void);
} CMD;
char g_cmdline[MAX_CMD_LINE];
char *g_params[MAX_CMD_PARAMS];
int g_nparams;
CMD g_cmds[] = {
{"cat", cat_cmd},
{"cp", cp_cmd},
{"rename", rename_proc},
{"pwd", pwd_proc},
{"cd", cd_proc},
{"ls", ls_proc},
{"clear", clear_proc},
{NULL, NULL}
};
char g_cwd[MAX_PATH_SIZE];
int main(void)
{
char *str;
int i;
if (getcwd(g_cwd, MAX_PATH_SIZE) == NULL)
exit_sys("getcwd");
for (;;) {
printf("CSD:%s>", g_cwd);
if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL)
continue;
if ((str = strchr(g_cmdline, '\n')) != NULL)
*str = '\0';
parse_cmdline();
if (g_nparams == 0)
continue;
if (!strcmp(g_params[0], "exit"))
break;
for (i = 0; g_cmds[i].cmd_name != NULL; ++i)
if (!strcmp(g_cmds[i].cmd_name, g_params[0])) {
g_cmds[i].cmd_proc();
break;
}
if (g_cmds[i].cmd_name == NULL) {
printf("invalid command: %s\n", g_params[0]);
}
}
return 0;
}
void parse_cmdline(void)
{
char *str;
g_nparams = 0;
for (str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t"))
g_params[g_nparams++] = str;
g_params[g_nparams] = NULL;
}
void cat_cmd(void)
{
FILE *f;
int ch;
if (g_nparams != 2) {
printf("cat command missing file!..\n");
return;
}
if ((f = fopen(g_params[1], "r")) == NULL) {
printf("file not found or cannot open file: %s\n", g_params[1]);
return;
}
while ((ch = fgetc(f)) != EOF)
putchar(ch);
if (ferror(f))
printf("cannot read file: %s\n", g_params[1]);
fclose(f);
}
void cp_cmd(void)
{
int fds, fdd;
char buf[BUFFER_SIZE];
ssize_t result;
if (g_nparams != 3) {
printf("source and destination path must be specified!..\n");
return;
}
if ((fds = open(g_params[1], O_RDONLY)) == -1) {
printf("file not found or cannot open: %s\n", g_params[1]);
return;
}
if ((fdd = open(g_params[2], O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) {
printf("file not found or cannot open: %s\n", g_params[2]);
goto EXIT2;
}
while ((result = read(fds, buf, BUFFER_SIZE)) > 0)
if (write(fdd, buf, result) != result) {
printf("cannot write file: %s\n", g_params[2]);
goto EXIT1;
}
if (result == -1) {
printf("cannot read file: %s\n", g_params[1]);
goto EXIT1;
}
printf("1 file copied...\n");
EXIT1:
close(fdd);
EXIT2:
close(fds);
}
void rename_proc(void)
{
printf("rename command...\n");
}
void pwd_proc(void)
{
puts(g_cwd);
}
void cd_proc(void)
{
if (g_nparams == 1) {
printf("argument missing!..\n");
return;
}
if (g_nparams > 2) {
printf("too many arguments!..\n");
return;
}
if (chdir(g_params[1]) == -1) {
printf("%s: %s!..\n", g_params[1], strerror(errno));
return;
}
if (getcwd(g_cwd, MAX_PATH_SIZE) == NULL)
exit_sys("getcwd");
}
#include <sys/wait.h>
void ls_proc(void)
{
int result;
int l_flag;
int hlink_digit, uname_digit, gname_digit, size_digit;
DIR *dir;
struct dirent *dent;
char path[PATH_MAX];
struct stat finfo;
int len;
struct passwd *pass;
struct group *gr;
char *target_path;
l_flag = 0;
opterr = 0;
optind = 0;
while ((result = getopt(g_nparams, g_params, "l")) != -1) {
switch (result) {
case 'l':
l_flag = 1;
break;
case '?':
printf("invalid option: -%c\n", optopt);
break;
}
}
if (g_nparams - optind > 1) {
printf("too many arguments!..\n");
return;
}
target_path = optind == g_nparams ? "." : g_params[optind];
if ((dir = opendir(target_path)) == NULL) {
printf("%s: %s\n", target_path, strerror(errno));
return;
}
hlink_digit = uname_digit = gname_digit = size_digit = 0;
while (errno = 0, (dent = readdir(dir)) != NULL) {
snprintf(path, PATH_MAX, "%s/%s", target_path, dent->d_name);
if (stat(path, &finfo) == -1) {
printf("%s: %s\n", path, strerror(errno));
continue;
}
len = (int)log10(finfo.st_nlink) + 1;
if (len > hlink_digit)
hlink_digit = len;
if ((pass = getpwuid(finfo.st_uid)) == NULL)
exit_sys("getppuid");
len = (int)strlen(pass->pw_name);
if (len > uname_digit)
uname_digit = len;
if ((gr = getgrgid(finfo.st_gid)) == NULL)
exit_sys("getgrgid");
len = (int)strlen(gr->gr_name);
if (len > gname_digit)
gname_digit = len;
len = (int)log10(finfo.st_size) + 1;
if (len > size_digit)
size_digit = len;
}
if (errno != 0)
exit_sys("readdir");
rewinddir(dir);
while (errno = 0, (dent = readdir(dir)) != NULL) {
sprintf(path, "%s/%s", target_path, dent->d_name);
if (stat(path, &finfo) == -1) {
printf("%s: %s\n", path, strerror(errno));
continue;
}
if (l_flag)
printf("%s\n", get_ls(path, hlink_digit, uname_digit, gname_digit, size_digit));
else
printf("%s\t", dent->d_name);
}
if (errno != 0)
exit_sys("readdir");
if (!l_flag)
putchar('\n');
closedir(dir);
}
void clear_proc(void)
{
printf("\033[2J");
printf("\033[0;0f");
}
const char *get_ls(const char *path, int hlink_digit, int uname_digit, int gname_digit, int size_digit)
{
struct stat finfo;
static char buf[4096];
static mode_t modes[] = { S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH };
struct passwd *pass;
struct group *gr;
char *str;
int index = 0;
int i;
if (stat(path, &finfo) == -1)
return NULL;
if (S_ISREG(finfo.st_mode))
buf[index] = '-';
else if (S_ISDIR(finfo.st_mode))
buf[index] = 'd';
else if (S_ISCHR(finfo.st_mode))
buf[index] = 'c';
else if (S_ISBLK(finfo.st_mode))
buf[index] = 'b';
else if (S_ISFIFO(finfo.st_mode))
buf[index] = 'p';
else if (S_ISLNK(finfo.st_mode))
buf[index] = 'l';
else if (S_ISSOCK(finfo.st_mode))
buf[index] = 's';
++index;
for (i = 0; i < 9; ++i)
buf[index++] = (finfo.st_mode & modes[i]) ? "rwx"[i % 3] : '-';
buf[index] = '\0';
index += sprintf(buf + index, " %*llu", hlink_digit, (unsigned long long)finfo.st_nlink);
if ((pass = getpwuid(finfo.st_uid)) == NULL)
return NULL;
index += sprintf(buf + index, " %-*s", uname_digit, pass->pw_name);
if ((gr = getgrgid(finfo.st_gid)) == NULL)
return NULL;
index += sprintf(buf + index, " %-*s", gname_digit, gr->gr_name);
index += sprintf(buf + index, " %*lld", size_digit, (long long)finfo.st_size);
index += strftime(buf + index, 100, " %b %e %H:%M", localtime(&finfo.st_mtime));
str = strrchr(path, '/');
sprintf(buf + index, " %s", str ? str + 1 : path);
return buf;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
27. Ders 16/09/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Stack (yığın) sözcüğü hem işlemciler tarafından kullanılan bir mekanizmayı hem de bir veri yapısını anlatmaktadır. Yani işlemciler kendi
çalışmaları sırasında ismine "stack" denilen bir alanı kullanmaktadır. Ancak aynı zamanda algoritmalar ve veri yapıları dünyasında "stack"
isimli bir veri yapısı da vardır. (Tabii stack veri yapısına işlemcilerin kullandığı "stack mekanizmasına" benzemekten dolayı "stack" ismi
verilmiştir.)
Biz C derlerinden fonksiyonların yerel değişkenlerinin stack'te yaratıldığını biliyoruz. Fonksiyonların parametre değişkenleri de bazı
sistemlerde stack'te bazı sistemlerde CPU yazmaçlarında yaratılmaktadır. Stack denilen alan RAM'in bir bölgesidir. Yani RAM'in içerisinde
bir yerdir. Bir programın (genel olarak thread'in) kullanacağı stack alanı ve miktarı işletim sistemi tarafından belirlenmektedir. Yani
program yüklendiğinde işlemcinin kullanacağı stack alanı ve miktarı zaten belirlenmiş durumdadır. Stack aalanı genellikle işlemcilerde
aşağıdan yukarıya doğru kullanılmaktadır. Bir fonksiyon çağrıldığında o fonksiyonun yerel değişkenleri stack'te yaratılır. Stack'in aktif
noktası (top of the stack) işlemcinin bir yazmacı (register) tutulmaktadır. Bu yazamaca genel olarak "stack pointer" denilmektedir.
Her fonksiyon çağrılmasında stack pointer o fonksiyonun yerel değişkenleri kadar yukarı çekilir ve fonksiyonun yerel değişkenleri orada
yaratılır. Örneğin:
void bar(void)
{
int x, y;
}
void foo(void)
{
int a, b;
bar();
}
Buraada başlangıç noktasının aşağıdaki gibi olduğunu düşünelim:
SP ----> Stack'in sonu
foo çağrıldığında şöyle bir durum oluşacaktır:
SP ---->
a
b
Stack'in sonu
Şimdi foo fonksiyonu bar fonksiyonunu çağırmış olsun:
SP ---->
x
y
a
b
Stack'in sonu
Şimdi bar fonksiyonunun sonlandığını düşünelim:
SP ---->
a
b
Stack'in sonu
Fonksiyonun yerel değişkenlerinin stack'teki konumları için standart bir belirleme yapılmamıştır. Genellikle derleyiciler fonksiyonların
yerel değişkenlerini ardışıl bir biçimde stack'te oluştururlar. Bazı derleyiciler ilk bildirilen yerel değişken düşük adreste olacak biçimde
bazıları ise yüksek adreste olacak biçimde oluşturmaktadır.
Ancak stack yalnızca yerel değişkenler için kullanılmaktadır. Bir fonksiyon CALL makine komutu ile çağrıldığında geri dönüşün mümkün
olabilmesi için CALL makine komutunda işlemci sonraki komutun adresini stack'te saklar. ret makine komutu da stack'ten bu adresi alarak geri
dmüşü sağlar. Yani bir fonksiyon çağırdığımızda da stack'te dolaylı bir biçimde yer ayrılmaktadır. Yukarıdaki foo fonksiyonunun bar fonksiyonu
çağırması durumunun daha gerçekçi stack götüntüsü şöyle olacaktır:
SP ---->
x
y
<bar için geri dönüş adresi>
a
b
<foo için geri dönüş adresi>
Stack'in sonu
Stack'te yerel değişkenlerin tahsisatı çok hızlı bir biçimde tek bir makine komutuyla yapılabilmektedir. Örneğin bir fonksiyonun toplam
100 byte uzunluğunda yerel değişkenleri olsun.Tahisat stack pointer'ın 100 byte azaltılması ile yani tek bir makine komutuyla yapılabilmektedir.
Benzer biçimde tahsisatın geri alınması da tek bir makine komutuyla stack pointer'ın 100 byte artırılması ile yapılabilmektedir.
Pekiyi stack için ayrılan alan yetmezse ne olur? Eğer fonksiyon çağrılarıyle stack'ta çok fazla alan tahsis edilirse stack yukarıdan taşabilir.
Buna İngilizce "stack overflow" denilmektedir. Tabii stack'in taşması durumu derleme aşamasında tespit edilemez. Çünkü derleme sırasında
hangi fonksiyonun hangi fonksiyonu çağıracağı kesin olarak bilinememektedir. Stack taşması durumunda koruma mekanizmasının olduğu Windows gibi,
Linux gibi, macOS gibi sistemlerde taşma işlemci tarafından belirlenip işletim sistemine bildirilmektedir. İşletim sistemi de taşmaya yol
açan prosesi sonlandırmaktadır.
32 bit ve 64 bit Windows sistemlerinde default stack 1MB'tır. Ancak 32 bit ve 64 bit Linux sistemlerinde default stack 8MB'dir. 1MB stack
aslında genel olarak büyük bir stack'tir.
Pekiyi fonksiyonların parametre değişkenleri nerede ve nasıl yaratılmaktadır? Yukarıda da belirttiğimiz gibi parametre aktarımı stack yoluyla
ya da yazmaç yoluyla yapılabilmektedir. Örneğin 32 bit Windows, Linux ve macOS sistemlerinde aktarım stack yoluyla yapılırken, 64 bir Windows
Linux ve macOS sistemlerinde yazmaç yoluyla yapılmaktadır. Fonksiyon çağrısı sırasındaki aşağı seviyeli bu ayrıntılara genel olarak "Application
Binary Interface (ABI)" denilmektedir. Fonksiyon çağrısı özelinde uygulanan yönteme "fonksiyon çağırma biçimi (function calling convention)"
da denilmektedir. Eğer parametreler yazmaç yoluyla aktarılıyorsa iç içe çağırmalarda yazmaçların korunması çağrılan fonksiyonun (callee)
sorumluluğundadır. Tabii içteki fonksiyon bunun için stack kullanır. O halde özünde parametre değişkenlerinin de stack yoluyla saklandığını
söyleyebiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Özyineleme (recursion) doğada da karşımıza çıkan bir kavramdır. Bir olgunun kendisine benzer bir olguyu içermesi anlamına gelmektedir.
Örneğin tohumdan ağaç olur, ağaç da yeniden tohum vermektedir. Matruşkalar da özyineleme içermektedir. Programlamada özyineleme bazı
özel algoritmalarda karşımıza çıkmaktadır. Biz algoritmalar dünyasında bir problemi çözmek için yola çıktığımızda belli bir yol kat ettikten
sonra ilk duruma benzer bir durumla karşılaşırsak muhtemelen özyinelemeli bir algoritmik problem söz konusu olmaktadır. Örneğin bir dizin
listesini alt dizinlerle dolaşmak isteyelim. Bunun için kök dizindeki dizin girişlerini elde ederiz. Eğer o dizin girişlerinin biri de
bir dizine ilişkinse o dizine geçtiğimizde yine ilk işe başladığımız duruma benzer bir durum içerisinde oluruz. O zaman dizin ağacının
dolaşılması özyienelemeli bir algoritma içermektedir.
Bir algoritmik problemin özyineleme olmadan çözümüne "iteratif çözüm" de denilmektedir. O halde algoritmalar bu bakımdan üçe ayrılabilir:
1) Yalnızca iteratif olarak çözülen algoritmalar
2) Hem iteratif hem de özyinemelei biçimde çözülebilen algoritmalar
3) Yalnızca özyinelemeli olarak çözülebilen algoritmalar
Bazı algoritmaların özyinelemeyle bir bağı yoktur. Bunlar döngüler yoluyla klasik yöntemlerle çzöülmektedir. Ancak bazı algoritmalar
hem özyineleme ile hem de klasik iteratif yöntemlerle çözülebilmektedir. Ancak bazı algoritmaların iteratif çözümü ya yoktur ya da
makul değildir.
Özyinelemeli karaktere sahip olan algoritmalar tipik olarak kendi kendini çağıran fonksiyonlar yoluyla gerçekleştirilmektedir. Aslında
özyinelemeli algoritmalar yapay stack kullanılarak sanki iteratifmiş gibi de çözülebilmektedir. Ancak bunların en etkin çözümü kendi
kendini çağıran fonksiyonlarla yapılmaktadır. Kendi kendini çağıran fonksiyonlara "özyinelemeli fonksiyonlar (recursive functions)" da
denilmektedir.
Bir algoritma hem iteratif yöntemle hem de özyinelemeli yöntemle çözülebiliyorsa genellikle (ancak her zaman değil) iteratif yöntem daha
etkin bir gerçekleştirim sunmaktadır. Yani bu tür durumlarda genellikle iteratif yöntem tercih edilmelidir. Ancak yukarıda da belirtitğimiz
gibi bazı algoritmaların iteratif çözümleri ya yaoktur ya da makul değildir. Bu durumda biz mecburen özyinelemeli çözümü uygularız.
Genellikle bir fonksiyonun kendini çağırması insanlar tarafından tuhaf ve karmaşık olarak algılanmaktadır. Aslında bir fonksiyonun kendisini
çağırması ile başka bir fonksiyonu çağırması arasında hiçbir farklılık yoktur. Nasıl bir fonksiyon başka bir fonksiyonu çağırdığında çağrılan
fonksiyon bittiğinde akış çağırma noktasından sonraki deyimle devam ediyorsa benzer biçimde bir fonksiyon kendisini çağırdığında o çağrı bitince
yine akış çağrılan noktadan sonraki deyimle devam edecektir. Ancak özyinelemede önemli bir problem "sonsuz döngü" oluşabilmesidir. Yani bir
fonksiyon kendisini kontrolsüz bir biçimde çağırırsa sonsuz döngü oluşur. Örneğin:
void foo(void)
{
printf("foo\n");
foo();
}
Burada foo fonksiyonu çağrıldığında fonksiyon hep kendisini yeniden çağıracağı için sonsuz döngü oluşacaktır. Tabii aslında fonksiyonun
hiç yerel değişkeni olmasa bile CALL işlemi sonucunda geri dönüş adresi stack'te kaydedildiği için bir stack taşması olaşacak ve Windows,
Linux, macOS sistemlerinde program çökecektir.
Özyinelemeli fonksiyonlarda anahtar noktalardan biri fonksiyon her kendini çağırdığında fonksiyonun yerel değişkenlerinin yeniden yeni
kopyasının yaratılmasıdır. Örneğin:
void foo(void)
{
int a, b;
a = 10;
b = 20;
foo();
...
}
Burada foo kendisini çağırdığında stack'te yeni bir a ve b nesneleri yaratılacaktır. Her çağrının a ve b nesneleri farklı olacaktır.
Fonksiyonun parametre değişkenleri de fonksiyonunun her kendini çağırmasında yeniden yaratılmaktadır.
Bir fonksiyon kendini kontrolsüz bir biçimde çağırmamalıdır. Bir noktaya kadar kendi kendini çağırmalı sonra çıkış sürecine girmelidir.
Örneğin:
void foo(int n)
{
if (n == 0)
return;
printf("n before call: %d\n", n);
foo(n - 1);
printf("n after call: %d\n", n);
}
...
foo(3);
Burada her foo çağrısında n isimli o çağrıya özgü yeni bir parametre değişkeni yaratılmaktadır. Bu örnekte fonksiyon üç kere kendisini
çağırmış sonra çıkış sürecine girmiştir. n == 0 durumunda fonksiyon return ile sonlandırılınca bir önceki çağırdan çalışma devam edeceğine
dikkat ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void foo(int n)
{
if (n == 0)
return;
printf("n before call: %d\n", n);
foo(n - 1);
printf("n after call: %d\n", n);
}
int main(void)
{
foo(3);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda faktöriyel hesabı yapan iteratif bir fonksiyon örneği veilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
unsigned long long factorial(int n)
{
unsigned long long total = 1;
for (int i = 2; i <= n; ++i)
total *= i;
return total;
}
int main(void)
{
unsigned long long result;
result = factorial(10);
printf("%llu\n", result);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Şimdi faktörüyel hesabını aşağıdaki gibi özyineleme ile yapalım:
unsigned long long factorial(int n)
{
unsigned long long temp;
if (n == 0)
return 1;
temp = n * factorial(n - 1);
return temp;
}
Aslında burada temp ara değişkeninin kullanılmasına hiç gerek yoktur. Ancak özyinelemenin daha iyi anlaşılması için böyle bir ara değişken
kullandık. Şimdi fonksiyonun aşağıdaki gibi çağrıldığını düşünelim:
factorial(4);
Burada özyinemeli çağırmalarda şöyle bir durum oluşacaktır:
factorial(4)
************
temp = 4 * factorial(3);
factorial(3)
************
temp = 3 * factorial(2);
factorial(2)
************
temp = 2 * factorial(1);
factorial(1)
************
temp = 1 * factorial(0);
factorial(0)
************
return 1;
En sonunda factorial(0) çağrısı 1 ile geri dçnünce çıkış sürecine girilmektedir. Tabii aslında buradaki temp değişkeninin kullanılmasına
gerek yoktur:
unsigned long long factorial(int n)
{
if (n == 0)
return 1;
return n * factorial(n - 1);;
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
unsigned long long factorial(int n)
{
if (n == 0)
return 1;
return n * factorial(n - 1);;
}
int main(void)
{
unsigned long long result;
result = factorial(4);
printf("%llu\n", result);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir yazıyı tersten yazdıran bir fonksiyon iteratif biçimde aşağıdaki şöyle yazılabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void putsrev(const char *str)
{
size_t i;
for (i = 0; str[i] != '\0'; ++i)
;
while (i-- > 0)
putchar(str[i]);
putchar('\n');
}
int main(void)
{
char s[] = "ankara";
putsrev(s);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Genel olarak düzden yapılan bir işlemi tersten yapmak için özyineleme kullanılabilir. Fonksiyon kendini çağırarak ilerler. Sona gelindiğinde
çıkış sürecinde işlemler yaptırılır. Örneğin:
void putsrev(const char *str)
{
if (*str == '\0')
return;
putsrev(str + 1);
putchar(*str);
}
Burada biz putsrev fonksiyonunu putsrev("ali") biçiminde çağırmış olalım. İlk çağrıda str göstericisi "ali" yazısını gösteriyor olur.
İkinci çağrıda "li" yazısını, üçüncü çağrıda "i" yazısını ve son çağrıda null karakteri gösteriyor olur. Bir sonraki çağırmadan çıkıldığında
bir önceki çağırmanın str göstericisini kullanıyor olmaktayız. Burada aslında bir str değişkeninin olmadığına her özyinelemede yeni bir
str değişkeninin yaratıldığına dikkat ediniz. Tabii biz burada yalnızca özyineleme çalışması yapıyoruz. Yoksa bu problemin iteratif çözümü
çok daha etkindir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void putsrev(const char *str)
{
if (*str == '\0')
return;
putsrev(str + 1);
putchar(*str);
}
int main(void)
{
putsrev("ankara");
putchar('\n');
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir yazıyı iteratif yolla in-place biçimde ters çevirmek isteyelim. Klasik yöntem yazının sonuna kadar gidip baştan ve sondan karşılıklı
elemanları yazının uzunluğunun yarısı kadar yer değiştirmektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void revstr(char *str)
{
size_t n;
char temp;
for (n = 0; str[n] != '\0'; ++n)
;
for (size_t k = 0; k < n / 2; ++k) {
temp = str[k];
str[k] = str[n - k - 1];
str[n - k - 1] = temp;
}
}
int main(void)
{
char s[] = "ankara";
revstr(s);
puts(s);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıdaki fonksiyonun iteratif olarak başka bir yazım biçimi de aşağıdaki gibi olabilir. Burada fonksiyon ilk ve son karakterlerin
indeks numaralarıyla çağrılmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
void revstr(char *str, size_t left, size_t right)
{
char temp;
while (left < right) {
temp = str[left];
str[left] = str[right];
str[right] = temp;
++left, --right;
}
}
int main(void)
{
char s[] = "ankara";
revstr(s, 0, strlen(s) - 1);
puts(s);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Şimdi yukarıdaki fonksiyonu özyinelemeli hale dönüştürelim:
void revstr(char *str, size_t left, size_t right)
{
char temp;
if (right <= left)
return;
temp = str[left];
str[left] = str[right];
str[right] = temp;
revstr(str, left + 1, right - 1);
}
Burada hger defasında left bir artırılarak, right ise bir eksiltilerek özyineleme yapılmıştır. Her özyinelemde yazının bir karakteri
yer değiştirilmektedir. Tabii biz burada yalnızca özyineleme çalışması yapıyoruz. Yoksa bu problemin iteratif çözümü çok daha etkindir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
void revstr(char *str, size_t left, size_t right)
{
char temp;
if (right <= left)
return;
temp = str[left];
str[left] = str[right];
str[right] = temp;
revstr(str, left + 1, right - 1);
}
int main(void)
{
char s[] = "ankara";
revstr(s, 0, strlen(s) - 1);
puts(s);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bazen özyinelemeli fonksiyonun parametrik yapısı kolay kullanıma izin vermeyebilir. Bu durumda programcı bir sarma fonksiyon (wrapper
function) yazarak özyinelemeli fonksiyonu çağırır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
void revstr_recur(char *str, size_t left, size_t right)
{
char temp;
if (right <= left)
return;
temp = str[left];
str[left] = str[right];
str[right] = temp;
revstr_recur(str, left + 1, right - 1);
}
void revstr(char *s)
{
size_t n;
for (n = 0; s[n] != '\0'; ++n)
;
revstr_recur(s, 0, n ? n - 1 : 0);
}
int main(void)
{
char s[] = "ankara";
revstr(s);
puts(s);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir int sayıyı binary olarak iteratif biçimde ekrana yazdıran bir fonksiyon şöyle yazılabilir:
void binprint(unsigned val)
{
for (int i = sizeof(val) * 8 - 1; i >= 0; --i)
putchar((val >> i & 1) + '0');
putchar('\n');
}
Burada sayının başındaki 0'lar da yazdırılmaktadır. Eğer sayının başındaki 0'ları yazdırmak istemeseydik kodu şöyle yazabilirdik:
void binprint(unsigned val)
{
int i;
for (i = sizeof(val) * 8 - 1; val >> i == 0 && i >= 0; --i)
;
for (; i >= 0; --i)
putchar((val >> i & 1) + '0');
putchar('\n');
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void binprint(unsigned val)
{
int i;
for (i = sizeof(val) * 8 - 1; val >> i == 0 && i >= 0; --i)
;
for (; i >= 0; --i)
putchar((val >> i & 1) + '0');
putchar('\n');
}
int main(void)
{
binprint(255);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Şimdi sayıyı ikilik sistemde bastıran fonksiyonu özyinelemeli olarak yazmak isteyelim. Burada biz sağdan sola öteleme yaparak fonksiyonu
özyinelemeli biçimde çağırırız. Sonra çıkış sürecinde sayının en düşün anlamlı bitini yazdırırız. Yani aslında burada özyineleme ile
düzden yapılan işlem tersine çevrilmektedir:
void binprint(unsigned val)
{
if (val == 0)
return;
binprint(val >> 1);
putchar((val & 1) + '0');
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void binprint(unsigned val)
{
if (val == 0)
return;
binprint(val >> 1);
putchar((val & 1) + '0');
}
int main(void)
{
binprint(255);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
28. Ders 17/09/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir bilgisayar sisteminde bir ekran varsa genel olarak o ekrana text modda yalnızca karakterler bastırılabilmektedir. Sayılar aslında
karakterle dönüştürülüp yazıdırılırlar. Örneğin printf fonksiyonu "%d" format karakteri ile aslında int değeri karakterlere dönüştürür
bu karakterleri ekrana basar. Başka bir deyişle aslında temel bir bilgisayar sisteminde programcının elinde yalnızca putchar benxeri bir
fonksiyon vardır.
Pekiyi biz int bir değeri yalnızca putchar kullnarak nasıl yazdırabiliriz? İlk akla gelen yöntem sayıyı basamaklarına ayrıştırıp onları
putchar kullanarak yazdırmaktır. Ancak sürekli 10'a bölme yöntemi ile basamak ayrıştırılması yapıldığında basamaklar ters sırada elde
edilmektedir. Örneğin:
while (val) {
digit = val % 10;
val /= 10;
}
O halde bizim basamakları ters sırada elde edip bir diziye yerleştirmemiz sonra da o diziyi ters sırada yazdırmamız gerekir. Sayı negatifse
onun negatif olduğu bilgisini saklayıp sayıyı pozitif hale dönüştürmek uygun olur. Aşağıda muhtemel bir iteratif çözüm örneği verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void printd(int val)
{
int digit;
char buf[64];
int nflag;
int i;
nflag = 0;
i = 0;
if (val < 0) {
nflag = 1;
val = -val;
}
for (i = 0; val != 0; ++i) {
digit = val % 10;
buf[i] = '0' + digit;
val /= 10;
}
if (nflag)
buf[i++] = '-';
for (--i; i >= 0; --i)
putchar(buf[i]);
putchar('\n');
}
int main(void)
{
printd(-123456);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında bir sayıyı 10'luk sistemde yalnızca putchar kullanarak yazdırma işlemi özyinelemeli biçimde çok daha kolay yapılabilmektedir.
Yani bu problemin özyinelemeli çözümü iteratif çözümünden daha etkindir. Özyinelemeli çözüm şöyle olabilir:
void printd(int val)
{
if (val < 0) {
putchar('-');
val = -val;
}
if (val / 10)
printd(val / 10);
putchar(val % 10 + '0');
}
Burada fonksiyonu -12345 değeri ile çağırmış olalım. Fonksiyon ilk çağrıda bir kez '-' karakterini ekrana basacak sonra bir daha bu ekrana
basmayacaktır.Sonra özyinelemenin aşağıdaki gibi yapıldığına dikkat ediniz:
if (val / 10)
printd(val / 10);
Burada 12345 değerinin 10'a bölümü 0 olmadığı için fonksiyon kendini 1234 değeri ile çağıracaktır. Benzer durumlar aşağıdaki gibi
tekrarlanacaktır:
val = 12345
val = 1234
val = 123
val = 12
val = 1
Burada artık fonksiyon sürecine girecektir. Çıkarken de en düşük basamağı yazdıracaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void printd(int val)
{
if (val < 0) {
putchar('-');
val = -val;
}
if (val / 10)
printd(val / 10);
putchar(val % 10 + '0');
}
int main(void)
{
printd(-123456);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında yukarıdaki algoritma diğer tabanlar için de benzer biçimde çalışabilmektedir. Fakat burada dikkat edilmesi gereken nokta 10'luk
sistemden büyük sistemlerde (örneğin 16'lık sistemde) basamakların artık A, B, C, ... biçiminde isimlendirilmesidir. ASCII tablosunda
(ve diğer tablolarda da böyle) 0-9 arasındaki sayılar peşi sıra gitmektedir. Ancak '9' karakterinden sonra 'A' gelmemektedir. Arada 8 tane
farklı karakter vardır. O halde eğer taban 10'dan büyükse bu durumu dikkate almak gerekir. Örneğin:
putchar(n % base > 9 ? n % base - 10 + 'A' : n % base + '0');
Buraada taban 10'dan büyükse düzeltme yapılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void printd(int val, int base)
{
if (val < 0) {
putchar('-');
val = -val;
}
if (val / base)
printd(val / base, base);
putchar(val % base > 9 ? val % base - 10 + 'A' : val % base + '0');
}
int main(void)
{
printd(255, 16);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Kapalı bir şeklin içinin boyanması için birkaç algoritma kullanılmaktadır. Bunlardan birine "floodfill" algoritmsı denilmektedir. Bu
algoritmada iç bir noktadan başlanır. Fonksiyon dört yönde kendini çağırır. Böylece bir su baskını gibi şekle nüfuz eder. Tabii sınıf noktası
gelindiğinde ya da da önce boyabnış olan bir noktaya gelindiğinde fonksiyon hemen return etmelidir.
Aşağıdaki örnekte floodfill algoritması 10x20'lik karakter tabanlı bir bitmap resim üzerine uygulanmıştır. Resim aşağıdaki olabilir:
#######
##### #
####### ######
# #
# #
# ######
#### #
##### #
## #
###
Biz örneğimizde önce resmi bir text dosyaya kaydedip bu text dosyayı char türden bir matrise okumaktayız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
##include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ROWSIZE 10
#define COLSIZE 20
#define FILLCHAR '.'
char g_bitmap[ROWSIZE][COLSIZE];
void read_bitmap(void)
{
FILE *f;
char buf[1024];
if ((f = fopen("bitmap.txt", "r")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < ROWSIZE; ++i) {
fgets(buf, 1024, f);
memcpy(g_bitmap[i], buf, COLSIZE);
}
fclose(f);
}
void disp_bitmap(void)
{
for (int i = 0; i < ROWSIZE; ++i) {
for (int k = 0; k < COLSIZE; ++k)
putchar(g_bitmap[i][k]);
putchar('\n');
}
}
void floodfill(int row, int col)
{
if (g_bitmap[row][col] == '#' || g_bitmap[row][col] == FILLCHAR)
return;
g_bitmap[row][col] = FILLCHAR;
floodfill(row + 1, col);
floodfill(row, col - 1);
floodfill(row - 1, col);
floodfill(row, col + 1);
}
int main(void)
{
read_bitmap();
disp_bitmap();
floodfill(6, 10);
printf("\n\n");
disp_bitmap();
return 0;
}
int main(void)
{
if (!read_bitmap("bitmap.txt")) {
fprintf(stderr, "cannot read bitmap!..\n");
exit(EXIT_FAILURE);
}
floodfill(5, 5, '.');
disp_bitmap();
return 0;
}
/*
#######
##### #
####### ######
# #
# #
# ######
#### #
##### #
## #
###
*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bilindiği gibi ""seçerek sıralama (selection sort)" algoritması bir dizinin en küçük elemanın bulunup dizinin ilk elemanı ile yer değiştirilmesi
temeline daynmaktadır. Bu biçimde dizi her defasında bir eleman daraltılarak aynı işlemler yapılmaktadır. Selection sort algortmasının
iteratif çözümü aşağıda verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void ssort(int *pi, size_t size)
{
size_t i, k;
size_t min_index;
int min;
for (i = 0; i < size - 1; ++i) {
min_index = i;
min = pi[i];
for (k = i + 1; k < size; ++k)
if (pi[k] < min) {
min = pi[k];
min_index = k;
}
pi[min_index] = pi[i];
pi[i] = min;
}
}
void disp(const int *pi, size_t size)
{
size_t i;
for (i = 0; i < size; ++i)
printf("%d ", pi[i]);
printf("\n");
}
#define SIZE 10
int main(void)
{
int a[SIZE] = { 34, 12, 7, 84, 72, 39, 75, 45, 59, 21 };
ssort(a, SIZE);
disp(a, SIZE);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Seçerek sıralama yöntemini özyinelemeli bir biçimde de uygulayabiliriz. Tabii bu yöntemin özyinelemeli uygulaması makul değildir.
Ancak biz burada özyineleme çalışması yapmak istiyoruz. Algoritmanın özyinelemeli versiyonunda dizinin en büyük elemanı bulunur. Bu eleman
son elemanla yer değiştirilir. Sonra fonksiyon bir eksik uzunlukla kendini çağırır. Tabii uzunluk 1'e geldiğinde algoritma artık çıkış sürecine
girmelidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void selection_sort(int *pi, size_t size)
{
int max;
size_t max_index;
if (size <= 1)
return;
max = pi[0];
max_index = 0;
for (size_t i = 1; i < size; ++i)
if (pi[i] > max) {
max = pi[i];
max_index = i;
}
pi[max_index] = pi[size - 1];
pi[size - 1] = max;
selection_sort(pi, size - 1);
}
int main(void)
{
int a[10] = {41, 23, 12, 7, 37, 98, 29, 16, 82, 66};
selection_sort(a, 10);
for (int i = 0; i < 10; ++i)
printf("%d ", a[i]);
printf("\n");
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
8 vezir problemi bir satranç tahtasına birbirini yemeyen 8 vezirin yerleştirilmesi problemidir. Problem özyinelemeli bir karakterdedir.
Tipik bir çözümde satranç tahtası global bir matris ile tamsil edilir. Vezirlerin yemediği yerler ve vezirlerin yediği yerler matriste
işaretlenir. Özyinelemeli fonksiyon global matrise bakarak o anda vezirlerin yemediği ilk kareye verizi yerleştirir. Global matirisi
günceller ve kendisini çağırır. Eğer tahtada vezirlerin yemediği hiç kare kalmazsa fonksiyon kendisini sonlandırır. Eğer 8 vezir
yerleştirilebilmişse tahta print edilir. Bir çağrı sonlandığında global dizinin eski haline getirilmesi gerekmektedir.
Aşağıda 8 vezir probleminin çözümüne ilişkin bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#define SIZE 8
int g_qcount;
int g_count;
char g_board[SIZE][SIZE];
void init_board(void)
{
int r, c;
for (r = 0; r < SIZE; ++r)
for (c = 0; c < SIZE; ++c)
g_board[r][c] = '.';
}
void print_board(void)
{
int r, c;
printf("%d\n", g_count);
for (r = 0; r < SIZE; ++r) {
for (c = 0; c < SIZE; ++c)
printf("%c", g_board[r][c]);
printf("\n");
}
printf("\n");
}
void locate_queen(int row, int col)
{
int r, c;
g_board[row][col] = 'V';
r = row;
for (c = col + 1; c < SIZE; ++c)
g_board[r][c] = 'o';
for (c = col - 1; c >= 0; --c)
g_board[r][c] = 'o';
c = col;
for (r = row - 1; r >= 0; --r)
g_board[r][c] = 'o';
for (r = row + 1; r < SIZE; ++r)
g_board[r][c] = 'o';
for (r = row - 1, c = col - 1; r >= 0 && c >= 0; --r, --c)
g_board[r][c] = 'o';
for (r = row - 1, c = col + 1; r >= 0 && c < SIZE; --r, ++c)
g_board[r][c] = 'o';
for (r = row + 1, c = col - 1; r < SIZE && c >= 0; ++r, --c)
g_board[r][c] = 'o';
for (r = row + 1, c = col + 1; r < SIZE && c < SIZE; ++r, ++c)
g_board[r][c] = 'o';
}
void queen8(int row, int col)
{
char board[SIZE][SIZE];
for (; row < SIZE; ++row) {
for (; col < SIZE; ++col) {
if (g_board[row][col] == '.') {
memcpy(board, g_board, sizeof(board));
++g_qcount;
locate_queen(row, col);
if (g_qcount == SIZE) {
++g_count;
print_board();
}
queen8(row, col);
--g_qcount;
memcpy(g_board, board, sizeof(board));
}
}
col = 0;
}
}
int main(void)
{
init_board();
queen8(0, 0);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
29. Ders 23/09/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Dizin ağacının dolaşılması özyineleme gerektiren tipik algoritmalardandır. Dolaşım bir fonksiyon ile şöyle yapılmaktadır: Önce prosesin
çalışma dizini fonksiyonun başında dolaşılacak dizin olarak set edilir. Sonra o dizin içerisindeki bütün dizin girişleri bulunur. Bir dizin
girişi eğer bir dizin belirtiyorsa fonksiyon o dizin ile kendini çağırır. Ancak bir dizinde dizin listesinin sonuna gelindiğinde yine
prosesin çalışma dizini o dizinin üst dizini olarak set edilir. Ancak gerek Windows sistemlerinde gerekse UNIX/Linux sistemlerinde bir dizinin
içeriği erişim hakkı nedeniyle okunamayabilir. Bu durumda tamamen özyineleme durdurulabilir ya da bu hatalar görmezden gelinebilir. Windows
sistemlerinde bir dizin var olduğu halde SetCurrentDirectory fonksiyonu ile erişim hakları yüzünden o dizin prosesin çalışma dizini yapılamayabilmektedir.
Halbuki UNIX/Linux sistemlerinde chdir fonksiyonu bir dizine erişim hakkı olmasa bile başarılı olmaktadır.
Aşağıda Windows sistemlerinde dizin ağacının dolaşılmasına bir örnek verilmiştir. Bu örnekte SetCurrentDirectory fonksiyonu başarısız olursa
prosesin çalışma dizini değiştirilemediği için üst dizine geri dönüş yapılmamamktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include <windows.h>
void PutSysErr(LPCSTR lpszMsg);
void WalkDir(LPCSTR lpszDirPath)
{
HANDLE hFF;
WIN32_FIND_DATA fd;
if (!SetCurrentDirectory(lpszDirPath)) {
PutSysErr("SetCurrentDirectory");
return;
}
if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) {
PutSysErr("FindFirstFile");
goto EXIT;
}
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0)
continue;
/* printf("%s\n", fd.cFileName); */
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
printf("%s\n", fd.cFileName);
WalkDir(fd.cFileName);
}
} while (FindNextFile(hFF, &fd));
if (GetLastError() != ERROR_NO_MORE_FILES)
PutSysErr("FindNextFile");
FindClose(hFF);
EXIT:
if (!SetCurrentDirectory(".."))
PutSysErr("SetCurrentDirectory");
}
int main(void)
{
WalkDir("C:\\windows");
return 0;
}
void PutSysErr(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Dizin ağacını dolaşırken onları kademeli bir biçimde yazdırdabiliriz. Bunun için WalkDir fonksiyonuna kademeyi belirten bir level parametresi
eklenebilir. Sonra her özyinelemede bu level parametresi bir artırılabilir. Kademeli yazdırmak için printf fonksiyonunda "%*s" format karakterini
kullanabiliriz. Örneğin:
printf("%*s%s\n", level * TABSIZE, "", fd.cFileName);
Burada "%*s" format karakterinde "*" format karakterine level * TABSIZE argümanı, s format karakterine ise "" argümanı karşılık gelmektedir.
Dolayısıyla bu çağrıda soldan level * TABSIZE kadar boşluk bırakılmış olmaktadır.
Aşağıdaki örnekte kademeli yazım uygulanmıştır. Ancak bir dizine geçme ya da dizin listesini almada bir problem ortaya çıkarsa ve stderr
dosyası yönlendirilmemişse kademede bozukluk gözükebilecektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include <windows.h>
#define TABSIZE 4
void PutSysErr(LPCSTR lpszMsg);
void WalkDir(LPCSTR lpszDirPath, int level)
{
HANDLE hFF;
WIN32_FIND_DATA fd;
if (!SetCurrentDirectory(lpszDirPath)) {
PutSysErr("SetCurrentDirectory");
return;
}
if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) {
PutSysErr("FindFirstFile");
goto EXIT;
}
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0)
continue;
/* printf("%s\n", fd.cFileName); */
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
printf("%*s%s\n", level * TABSIZE, "", fd.cFileName);
WalkDir(fd.cFileName, level + 1);
}
} while (FindNextFile(hFF, &fd));
if (GetLastError() != ERROR_NO_MORE_FILES)
PutSysErr("FindNextFile");
FindClose(hFF);
EXIT:
if (!SetCurrentDirectory(".."))
PutSysErr(lpszDirPath);
}
int main(void)
{
WalkDir("C:\Dropbox\Shared\Kurslar\SysProg-1"", 0);
return 0;
}
void PutSysErr(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıdaki WalkDir fonksiyonlarında önemli bir kusur fonksiyonun prosesin çalışma dizinini değiştirerek işini sonlandırmasıdır. Fonksiyonun
çağrılmadan önceki çalışma dizinini yeniden set etmesi için bir sarma fonksiyon kullanılabilir. (access POSIX fonksiyonu Windows sistemlerinde
_access ismiyle de bulunmaktadır.) Bu sayede level parametresi de sarma fonksiyon tarafından asıl özyinelemeli fonksiyon çağrılırken kullanılacaktır.
Dolayısıyla asıl fonksiyonun parametrik yapısı daha sade olacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include <windows.h>
#define TABSIZE 4
void PutSysErr(LPCSTR lpszMsg);
void WalkDirRecur(LPCSTR lpszDirPath, int level)
{
HANDLE hFF;
WIN32_FIND_DATA fd;
if (!SetCurrentDirectory(lpszDirPath)) {
PutSysErr("SetCurrentDirectory");
return;
}
if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) {
PutSysErr("FindFirstFile");
goto EXIT;
}
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0)
continue;
/* printf("%s\n", fd.cFileName); */
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
printf("%*s%s\n", level * TABSIZE, "", fd.cFileName);
WalkDirRecur(fd.cFileName, level + 1);
}
} while (FindNextFile(hFF, &fd));
if (GetLastError() != ERROR_NO_MORE_FILES)
PutSysErr("FindNextFile");
FindClose(hFF);
EXIT:
if (!SetCurrentDirectory(".."))
PutSysErr(lpszDirPath);
}
void WalkDir(LPCTSTR lpszDirPath)
{
char cwd[MAX_PATH];
if (!GetCurrentDirectory(MAX_PATH, cwd))
PutSysErr("GetCurrentDirectory");
WalkDirRecur(lpszDirPath, 0);
if (!SetCurrentDirectory(cwd))
PutSysErr("SetCurrentDirectory");
}
int main(void)
{
WalkDir("C:\\Dropbox");
return 0;
}
void PutSysErr(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Dizin ağacını dolaşırken bulunan dizin girişlerini ekrana yazdırmak yerine başka bir şey yapmak isteyebiliriz. Böyle bir fonksiyonun
genelleştirilmesi için fonksiyon göstericilerinden faydalanılabilir. Aşağıdaki örnekte dizin girişleri bulundukça bir callback fonksiyon
çağrılmaktadır. Böylece WalkDir fonksiyonunu kullanan kişiler dosyaları ekrana yazdırmak yerine başka işlemler de yapabilirler.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include <windows.h>
#define TABSIZE 4
void PutSysErr(LPCSTR lpszMsg);
void WalkDirRecur(LPCSTR lpszDirPath, int level, void (*Proc)(const WIN32_FIND_DATA *, int))
{
HANDLE hFF;
WIN32_FIND_DATA fd;
if (!SetCurrentDirectory(lpszDirPath)) {
PutSysErr("SetCurrentDirectory");
return;
}
if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) {
PutSysErr("FindFirstFile");
goto EXIT;
}
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0)
continue;
Proc(&fd, level);
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
WalkDirRecur(fd.cFileName, level + 1, Proc);
} while (FindNextFile(hFF, &fd));
if (GetLastError() != ERROR_NO_MORE_FILES)
PutSysErr("FindNextFile");
FindClose(hFF);
EXIT:
if (!SetCurrentDirectory(".."))
PutSysErr(lpszDirPath);
}
void WalkDir(LPCTSTR lpszDirPath, void (*Proc)(const WIN32_FIND_DATA *, int))
{
char cwd[MAX_PATH];
if (!GetCurrentDirectory(MAX_PATH, cwd))
PutSysErr("GetCurrentDirectory");
WalkDirRecur(lpszDirPath, 0, Proc);
if (!SetCurrentDirectory(cwd))
PutSysErr("SetCurrentDirectory");
}
void DispFile(const WIN32_FIND_DATA *fd, int level)
{
printf("%*s%s\n", level * TABSIZE, "", fd->cFileName);
}
int main(void)
{
WalkDir("C:\\Dropbox\\Shared\\Kurslar\\SysProg-1", DispFile);
return 0;
}
void PutSysErr(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Özyinelemeli fonksiyonlarda belli bir koşul sağlandığında özyinelemenin sonlandırılması da istenebilmektedir. Aşağıdaki örnekte callback
fonksiyonun prototipi şöyledir:
BOOL Proc(const WIN32_FIND_DATA *fd, int level);
Fonksiyon her dizin girişi bulundukça çağrılmaktadır. Callback fonksiyon eğer sıfır dıı bir değerle geri dönerse bu durum özyinelemenin
devam edeceği anlamına gelmektedir. Eğer fonksiyon 0 ile geri dönerse bu durum özyinelemenin sonlandırılacağı anlamına gelmektedir.
Tabii özyineleme sonlandırılırken eğer kaynak tahsisatı yapılmışsa (örneğimiz FindFirstFile fonksiyonunun geri döndürdüğü handle gibi)
bunların serbest bırakılmasına dikkat edilmelidir.
Aşağıdaki örnekte "sample.c" dosyası bulunduğunda özyineleme sonlandırılmaktadır. Bu örnekte Microsoft'a özgü _stricmp fonksiyonu
kullanılmıştır. Bu fonksiyon büyük hard küçük harf duyarlılığı olmadan karşılaştırma yapmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include <windows.h>
#define TABSIZE 4
void PutSysErr(LPCSTR lpszMsg);
BOOL WalkDirRecur(LPCSTR lpszDirPath, int level, BOOL (*Proc)(const WIN32_FIND_DATA *, int))
{
HANDLE hFF;
WIN32_FIND_DATA fd;
BOOL bResult;
bResult = TRUE;
if (!SetCurrentDirectory(lpszDirPath)) {
PutSysErr("SetCurrentDirectory");
return TRUE;
}
if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) {
PutSysErr("FindFirstFile");
goto EXIT1;
}
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0)
continue;
if (!Proc(&fd, level)) {
bResult = FALSE;
goto EXIT2;
}
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
if (!WalkDirRecur(fd.cFileName, level + 1, Proc)) {
bResult = FALSE;
goto EXIT2;
}
} while (FindNextFile(hFF, &fd));
if (GetLastError() != ERROR_NO_MORE_FILES)
PutSysErr("FindNextFile");
EXIT2:
FindClose(hFF);
EXIT1:
if (!SetCurrentDirectory(".."))
PutSysErr(lpszDirPath);
return bResult;
}
BOOL WalkDir(LPCTSTR lpszDirPath, BOOL (*Proc)(const WIN32_FIND_DATA *, int))
{
char cwd[MAX_PATH];
BOOL bResult;
if (!GetCurrentDirectory(MAX_PATH, cwd))
PutSysErr("GetCurrentDirectory");
bResult = WalkDirRecur(lpszDirPath, 0, Proc);
if (!SetCurrentDirectory(cwd))
PutSysErr("SetCurrentDirectory");
return bResult;
}
BOOL DispFile(const WIN32_FIND_DATA *fd, int level)
{
printf("%*s%s\n", level * TABSIZE, "", fd->cFileName);
if (!_stricmp(fd->cFileName, "sample.c"))
return FALSE;
return TRUE;
}
int main(void)
{
WalkDir("C:\\Dropbox\\Shared\\Kurslar\\SysProg-1", DispFile);
return 0;
}
void PutSysErr(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi callback fonksiyon içerisinde dosyaya ilişkin tam yol ifadesi (full path) nasıl elde edilebilir? Yukarıdaki örneğimizde zaten
callback fonksiyon çağrıldığında prosesin çalışma dizini dizin girişinin içinde bulunduğu dizin biçimindedir. Dolayısıyla callback
fonksiyon içerisinde GetCurrentDirectory fonksiyonu uygulanıp buradan elde edilen yol ifadesi ile callback fonskyiona geçirilen
WIN32_FIND_DATA içerisindeki dosya birleştirilirse tam yol ifadesi eld edilebilir.
Aşağıdaki örnekte "sample.c" dosyalarının bulunduğu tam yol ifadeleri yazdırılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include <windows.h>
#define TABSIZE 4
void PutSysErr(LPCSTR lpszMsg);
BOOL WalkDirRecur(LPCSTR lpszDirPath, int level, BOOL(*Proc)(const WIN32_FIND_DATA *, int))
{
HANDLE hFF;
WIN32_FIND_DATA fd;
BOOL bResult;
bResult = TRUE;
if (!SetCurrentDirectory(lpszDirPath)) {
PutSysErr("SetCurrentDirectory");
return TRUE;
}
if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) {
PutSysErr("FindFirstFile");
goto EXIT1;
}
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0)
continue;
if (!Proc(&fd, level)) {
bResult = FALSE;
goto EXIT2;
}
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
if (!WalkDirRecur(fd.cFileName, level + 1, Proc)) {
bResult = FALSE;
goto EXIT2;
}
} while (FindNextFile(hFF, &fd));
if (GetLastError() != ERROR_NO_MORE_FILES)
PutSysErr("FindNextFile");
EXIT2:
FindClose(hFF);
EXIT1:
if (!SetCurrentDirectory(".."))
PutSysErr(lpszDirPath);
return bResult;
}
BOOL WalkDir(LPCTSTR lpszDirPath, BOOL(*Proc)(const WIN32_FIND_DATA *, int))
{
char cwd[MAX_PATH];
BOOL bResult;
if (!GetCurrentDirectory(MAX_PATH, cwd))
PutSysErr("GetCurrentDirectory");
bResult = WalkDirRecur(lpszDirPath, 0, Proc);
if (!SetCurrentDirectory(cwd))
PutSysErr("SetCurrentDirectory");
return bResult;
}
BOOL DispFile(const WIN32_FIND_DATA *fd, int level)
{
char buf[1024];
if (!_stricmp(fd->cFileName, "sample.c")) {
GetCurrentDirectory(MAX_PATH, buf);
strcat(buf, "\\");
strcat(buf, fd->cFileName);
printf("%s\n", buf);
}
return TRUE;
}
int main(void)
{
WalkDir("C:\\Dropbox\\Shared\\Kurslar\\SysProg-1", DispFile);
return 0;
}
void PutSysErr(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde de dizin ağacı benzer biçimde dolaşılır. Yine özyinelemeli fonksiyon girişte prosesin çalışma dizinini set eder,
çıkışta da çalışma dizinini yeniden üst dizin olacak biçimde ayarlar. Aşağıda tipik bir örnek verilmiştir. UNIX/Linux sistemlerinde
readdir fonksiyonu ile yalnızca dizin girişinin isminin ve i-node numarasının elde edildiğini anımsayınız. Dosya bilgilerinin elde edilmesi
için stat fonksiyonları kullanılmalıdır. Ancak bu tür uygulamalarda stat yerine lstat fonksiyonu tercih edilmelidir. stat fonksiyonunun sembolik
bağlantıları izlemesi sonsuz döngü oluşumuna yol açabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>
void walkdir(const char *path)
{
DIR *dir;
struct dirent *de;
struct stat finfo;
if (chdir(path) == -1) {
perror("chdir");
return;
}
if ((dir = opendir(".")) == NULL) {
perror("opendir");
goto EXIT;
}
while (errno = 0, (de = readdir(dir)) != NULL) {
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
continue;
if (lstat(de->d_name, &finfo) == -1) {
perror("lstat");
continue;
}
if (S_ISDIR(finfo.st_mode)) {
printf("%s\n", de->d_name);
walkdir(de->d_name);
}
}
if (errno != 0)
perror("readdir");
closedir(dir);
EXIT:
if (chdir("..") == -1)
perror("chdir");
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
walkdir(argv[1]);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
30. Ders 24/09/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıdaki programı yine Windows sistemlerinde yaptığımız gibi kademeli görüntüleyecek aşağıdaki gibi biçimde değiştebiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>
#define TABSIZE 4
void walkdir(const char *path, int level)
{
DIR *dir;
struct dirent *de;
struct stat finfo;
if (chdir(path) == -1) {
perror("chdir");
return;
}
if ((dir = opendir(".")) == NULL) {
perror("opendir");
goto EXIT;
}
while (errno = 0, (de = readdir(dir)) != NULL) {
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
continue;
if (lstat(de->d_name, &finfo) == -1) {
perror("lstat");
continue;
}
if (S_ISDIR(finfo.st_mode)) {
printf("%*s%s\n", level * TABSIZE, "", de->d_name);
walkdir(de->d_name, level + 1);
}
}
if (errno != 0)
perror("readdir");
closedir(dir);
EXIT:
if (chdir("..") == -1)
perror("chdir");
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
walkdir(argv[1], 0);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Fonksiyonun prosesin çalışma dizinini geri set eden sarmalı biçimi de aşağıdaki gibi olabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>
#define TABSIZE 4
void walkdir_recur(const char *path, int level)
{
DIR *dir;
struct dirent *de;
struct stat finfo;
if (chdir(path) == -1) {
perror("chdir");
return;
}
if ((dir = opendir(".")) == NULL) {
perror("opendir");
goto EXIT;
}
while (errno = 0, (de = readdir(dir)) != NULL) {
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
continue;
if (lstat(de->d_name, &finfo) == -1) {
perror("lstat");
continue;
}
if (S_ISDIR(finfo.st_mode)) {
printf("%*s%s\n", level * TABSIZE, "", de->d_name);
walkdir_recur(de->d_name, level + 1);
}
}
if (errno != 0)
perror("readddir");
closedir(dir);
EXIT:
if (chdir("..") == -1)
perror("chdir");
}
void walkdir(const char *path)
{
char cwd[4096];
if (getcwd(cwd, 4096) == NULL) {
perror("getcwd");
return;
}
walkdir_recur(path, 0);
if (chdir(cwd) == -1) {
perror("chdir");
return;
}
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
walkdir(argv[1]);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda dizin ağacını dolaşırken callback fonksiyonu çağıran bir rönek verilmiştir. Burada callback fonksiyon Windows sistemlerinde
yaptığımız örneğe benzerdir:
bool walkproc(const char *name, const struct stat *finfo, int level);
Yine callback fonksiyon çağrıldığında prosesin çalışma dizini dizin girişinin bulunduğu dizindedir. Fonksiyon sıfır dışı bir değerle
geri döndürülürse dolaşma devam ettirlir, 0 değeri ile geri döndürülürse dolaşma sonlandırılır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>
#define TABSIZE 4
bool walkdir_recur(const char *path, bool (*proc)(const char *, const struct stat *, int), int level)
{
DIR *dir;
struct dirent *de;
struct stat finfo;
bool retval;
retval = true;
if (chdir(path) == -1) {
perror("chdir");
return true;
}
if ((dir = opendir(".")) == NULL) {
perror("opendir");
goto EXIT1;
}
while (errno = 0, (de = readdir(dir)) != NULL) {
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
continue;
if (lstat(de->d_name, &finfo) == -1) {
perror("lstat");
continue;
}
if (!proc(de->d_name, &finfo, level)) {
retval = false;
goto EXIT2;
}
if (S_ISDIR(finfo.st_mode))
if (!walkdir_recur(de->d_name, proc, level + 1)) {
retval = false;
goto EXIT2;
}
}
if (errno != 0)
perror("readddir");
EXIT2:
closedir(dir);
EXIT1:
if (chdir("..") == -1)
perror("chdir");
return retval;
}
void walkdir(const char *path, bool (*proc)(const char *, const struct stat *, int))
{
char cwd[4096];
if (getcwd(cwd, 4096) == NULL) {
perror("getcwd");
return;
}
walkdir_recur(path, proc, 0);
if (chdir(cwd) == -1) {
perror("chdir");
return;
}
}
bool disp(const char *name, const struct stat *finfo, int level)
{
printf("%*s%s\n", level * TABSIZE, "", name);
return true;
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
walkdir(argv[1], disp);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte belli bir kök dizinden başlanarak "sample.c" dosyaları bulunmuş, onların mutlak yol ifadeleri ve uzunlukları yazdırılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>
#define TABSIZE 4
bool walkdir_recur(const char *path, bool (*proc)(const char *, const struct stat *, int), int level)
{
DIR *dir;
struct dirent *de;
struct stat finfo;
bool retval;
retval = true;
if (chdir(path) == -1) {
perror("chdir");
return true;
}
if ((dir = opendir(".")) == NULL) {
perror("opendir");
goto EXIT1;
}
while (errno = 0, (de = readdir(dir)) != NULL) {
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
continue;
if (lstat(de->d_name, &finfo) == -1) {
perror("lstat");
continue;
}
if (!proc(de->d_name, &finfo, level)) {
retval = false;
goto EXIT2;
}
if (S_ISDIR(finfo.st_mode))
if (!walkdir_recur(de->d_name, proc, level + 1)) {
retval = false;
goto EXIT2;
}
}
if (errno != 0)
perror("readddir");
EXIT2:
closedir(dir);
EXIT1:
if (chdir("..") == -1)
perror("chdir");
return retval;
}
void walkdir(const char *path, bool (*proc)(const char *, const struct stat *, int))
{
char cwd[4096];
if (getcwd(cwd, 4096) == NULL) {
perror("getcwd");
return;
}
walkdir_recur(path, proc, 0);
if (chdir(cwd) == -1) {
perror("chdir");
return;
}
}
bool disp(const char *name, const struct stat *finfo, int level)
{
char buf[4096];
if (!strcmp(name, "sample.c")) {
getcwd(buf, 4096);
strcat(buf, "/");
strcat(buf, name);
printf("%s, %lld\n", buf, (long long)finfo->st_size);
}
return true;
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
walkdir(argv[1], disp);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'de derleyiciler yapı elemanlarına erişimi hızlandırmak için elemanların arasında belli bir kurala göre boşluklar bırakabilmektedir.
Bu duruma "yapı elemanlarının hizalanması (struct member alignment)" ya da kısaca "hizalama (alignment)" denilmektedir. Örneğin:
struct SAMPLE {
short a;
int b;
short c;
int d;
};
struct SAMPLE s;
Burada short türünün 2 byte int türünün 4 byte olduğu bir sistemde ilk bakışta s nesnesinin sizeof değeri 12 olacakmış gibi gözükmektedir.
Ancak muhtemelen bu sizeof değeri yazdırıldığında 16 olduğu görülecektir. Çünkü derleyici a ile b arasında ve s ile d arasında 2 byte'lık
boşluklar bırakacaktır. Yani s nesnesinin bellekteki organizasyonu şöyle olacaktır:
a (2)
Boşluk (2)
b (4)
c (2)
Boşluk (2)
d (4)
Derleyicinin bıraktığı bu boşluklara İnglizce "padding" denilmektedir. Yapı elemanlarına erişim sırasında derleyici hangi elemanlar arasında
ne kadar boşluk bıraktığını bildiği için bir sorun oluşmamaktadır. Örneğin struct SAMPLE türünden ps isimli bir gösterici olsun. Biz de
ps->c ifadesiyle ps adresinden başlayan yapının c elemanına erişmek ieteyelim. Derleyci boşlukları nerelerde bıraktığını bildiği için c
elemanının ps adresinden 8 byte ileride olduğunu hesaplayabilmektedir. C standartlarına göre yapı elemanları ilk eleman düşük adreste
olacak biçimde sırasıyla dizilmektedir. Yapı elemanları arasında boşlukların bırakılabileceği standartlarda belirtilmiştir. Başka bir deyişle
elemanlar arasında bırakılan boşluklar (paddings) elemen ardışıllığını aslında bozmamaktadır.
Pekiyi derleyici yapı elemanları arasında neden boşluklar bırakabilmektedir? Bunun en önemli gerekçesi "hız kazancı" sağlamaktır. Hız
kazancının nasıl sağlandığı işlemci ile RAM arasındaki bağlantı biçi ile açıklanabilir. Örneğin 32 bit işlemcilerde işlemci RAM ile
bilgileri dörder byte'lık bloklar biçiminde transfer etmektedir. Yani işlemci bellekten 2 byte'lık bir bilgiyi 2 byte olarak okumaz.
Onun içinde bulunduğu 4 byte'ın tamamını okuyup onun içerisindeki 2 byte'ı ayrıştırır. Bu sistemlerde RAM'in işlemciye göre görüntüsü
şöyledir:
xxxx
xxxx
xxxx
xxxx
....
xxxx
xxxx
xxxx
xxxx
Burada her satır dördün katlarındadır. Şimdi işlemcinin aşağıdaki gibi 4 byte'lık int bir nesneye erişmek istediğini düşünelim:
xxxx
xxxx
xxxx
xxxx
....
xxxx
xxii
iixx
xxxx
Burada erişelecek int nesne 4'ün katında değildir. İşte işlemci iki bus ahareketi yaparak int nesnenin parçalarını ayrı ayrı 4 byte'ı
okuyup kendi içinde birleştirmektedir. Tabii bu erişim makine komutuna yansımamaktadır. Makine komutunu uygulayan programcı yine tek bir
komut olarak onu yazmıştır. Ancak bu komut kendi içerisinde iki kere RAM erişimiş yaptığı için nano düzeyde daha yavaş çalışacaktır.
Pekiyi aynı int nesne aşağıdakiş gibi 4'ün katlarında bulunsaydı ne olurdu?
xxxx
xxxx
xxxx
xxxx
....
xxxx
xxxx
iiii
xxxx
Burada işlemci tek bir RAM erişimi ile int nesneyi tek hamlede elde edebilecekti. Buradan çıkan basit sonuç şudur: 32 bit işlemcilerde
4 byte'lık nesnelere (örneğin int bir nesne) hızlı erişilebilmesi için bu nesnelerin 4'ün katlarında bulunması gerekir. Pekiyi 2 byte'lık
(örneğin short türden) bir nesne için bu böyle bir hizalamaya gerek var mıdır? İşte 2 byte'lık nesnenin 4 byte'ın neresinden başladığına
göre bu durum değişebilir. Örneğin:
xxxx
xxxx
xxxx
xxxx
....
xxxx
xxxx
xxss
xxxx
Burada iki byte'lık short nesneye erişimde bir yavaşlık söz konusu olmayacaktır. Ancak örneğin:
xxxx
xxxx
xxxx
xxxx
....
xxxx
xxxx
xxxs
sxxx
Burada 2 byte'lık short nesneye erişim diğer duruma göre nano düzeyde yavaş olacaktır. Pekiyi 1 byte'lık char gibi bir nesne için hizalama
önemli midir? Bunun yanıtı 1 byte'lık nesneler için hizalamanın önemli olmadığıdır. Bu 1 byte'lık nesneler 4 byte'tın neresinde olursa olsun
işlemci tarafından tek hamlede alınabilmektedir. Örneğin:
cxxx
xxxx
xcxx
xxxx
....
xxxc
xxxx
xxcx
xxxx
Burada tüm 1 byte'lık char nesnelere aynı hızda erişilecektir. Pekiyi bu durumda derleyicinin nasıl bir strateji izlemesi anlamlı olur?
Aslında 32 bit bir işlemci için izlenecek strateji basittir: Tüm nesnelerin kendi uzunluklarının katlarına yerleştirilmesi hızlı erişim
için yeterli olmaktadır. Yani örneğin int bir nesne 4'ün katlarına, short bir nesne 2'nin katlarına char bir nesne 1'in katlarına
yerleştirilmelidir. Pekiyi 8 byte'lık double türü kaçın katlarına yerleştirilmelidir? İşte matematik işlemci bağlantısı da dikkate alındığında
bu nesnelrin de 8'in katlarında olması en iyi durumdur. Şimdi yapı elemanları arasında derleyicinin nedne ne nasıl boşluk bıraktığı artık
anlaşılabilir.
/*------------------------------------------------------------------------------------------------------------------------------------------
31. Ders - 30/09/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi hizalama yalnızca yapı elemanları için mi önemlidir. Yerel değişkenler de benzer biçimde hizalanmakta mıdır? Derleyici aslında hizalamayı
tüm nesneler için yapmaktadır. Ancak zaten C ve C++ standartları yerel değişkenlerin yerleşimi hakkında bir şey söylememektedir. Örneğin:
void foo(void)
{
char a;
int b;
...
}
Burada bu iki yerel değişkenin ardışıl olmasının bir garantisi yoktur. İlk bildirilen değişkenin stack'te düşük adreste olmasının da bir
garantisi yoktur. Dolayısıyla genellikle bu durum programcıyı ilgilendirmemektedir. Ancak dizi elemanları her zaman ardışıldır. Yani bir
elemanın bittiği yerde boşluk olmaksızın diğeri başlamıdır. Örneğin:
short a[10];
Derleyici bu diziyi 2'nin katlarına yerleştirirse zaten dizinin tüm elemanları 2'nin katlarında olacaktır. Örneğin:
int b[10];
Burada da derleyici b dizisini 4'ün katına yerleştirirse zaten dizinin tüm elemanları 4'ün katlarında olur. Pekiyi dinamik bellek fonksiyonlarında
durum nasıldır? Çünkü biz malloc gibi bir fonksiyonun geri döndürdüğü değeri herhangi biçimde kullanabiliriz. Örneğin:
struct SAMPLE {
char a;
int b;
char c;
int d;
};
...
struct SAMPLE *ps;
ps = malloc(sizeof(struct SAMPLE));
Burada malloc alan tahsis ederken zaten oranın herhangi bir türden olabileceği fikriyle uygun değerin katlarında olan bir adres verecektir.
Örneğin burada malloc fonksiyonun 4'ün katlarında bir adres vermesi gerekir. Tabii malloc fonksiyonu bizim bu adresi nasıl kullanıcığımızı
bilmemektedir. Bu nedenle en kötü olasılığa göre bir hizalama uygulayacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Derleyiciler genel olarak beş farklı hizalama stratejisi izleyebilmektedir:
1 Byte Hizalama (Byte Alignment)
2 Byte Hizalama (Word Alignment)
4 Byte Hizalama (Double Word Alignment)
8 Byte Hizalama (Quad Word Alignemnet)
16 Byte Hizalama (Double Quad Word Alignment)
N byte hizalama şu anlama gelmektedir: "Nesnenin uzunluğu ve N değerinin hangisi küçükse nesne o değerin katlarına hizalanır". Nesnenin
tamamı da N'in katlarına hizalanmaktadır. Örneğin 4 byte hizalama söz konusu olsun. Bu durumda 1 byte'lık bir nesne (örneğin char bir
nesne) 1'in katlarına, 2 byte'lık bir nesne (örneğin short bir nesne) 2'nin katlarına, 4 byte'lık bir nesne (örneğin int bir nesne) 4'ün
katlarına ve 8 byte'lık bir nesne 4'ün katlarına yerleştirilir. Örneğin 8 byte (Quad Word alignment) söz konusu olsun. Bu durumda 1 byte'lık
nesne 1'in katlarına, 2 byte'lık bir nesne 2'nin katlarına, dört byte'lık bir nesne 4'ün katlarına, 8 byte'lık bir nesne 8'in katlarına hizalanır.
"1 byte hizalamanın aslında hizalama yapmamakla" aynı anlama geldiğine dikkat ediniz. Dolayısıyla programcı hizalamayı kaldıracaksa derleyiciyi
1 byte hizalama ayaralayabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Derleyicinin yaptığı hizalamalar bazen programcının işine gelmeyebilir. Yani programcı çeşitli gerekçelerle derleyicinin hizalama yapmasını
istemeyebilir. Hizalamanın programcı tarafından kontrolü genellikle derleyicilerin sunduğu ek özelliklerle sağlanmaktadır. Microsoft
derleyicilerinde hizalama komut satırından derleme yapılırken /ZpN (buradaki N 1, 2, 4, 8, 16 olabilir) seçeneği ile ayarlanmaktadır.
Hizalama Visual Studio IDE'sinde proje seçeneklerinden "C-C++/Code Generation/Struct Member Alignment" combo box seçneğinden ayarlanabilmektedir.
Eğer bu ayarlamalar yapılmazsa 32 bit derleme için default durum /Zp8 (yani quad word alignment), 64 bit derleme için /Zp16 biçimindedir.
Microsoft derleyicileri bir yapının en büyük elemanı neyse yapının tamamını da o en büyük elemanı referans alarak hizalamaktadır. Yani
bu durumda yapı nesnenin sonunda da yapının en büyük elemanının hizalama bilgisi kadar boşluk bulundurulacaktır.
gcc ve clang derleyicilerinde de default durumda Microsoft'taki gibi 32 bit derleyiciler için 8 byte hizalama, 64 bit derleyiciler için 16
byte hizalama kullanılmaktadır. Hizalamayı değiştirmek için -fpack-struct=N komut satırı seçeneği kullanılmalıdır. Örneğin
gcc -fpack-struct=1 -o sample sample.c
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Hizalama konusunda default belirlemeye neden müdahale etmek isteriz? İşte bunun en tipik örneği bir dosyanın içerisindeki bilgilerin fread
ya da readgibi bir fonksiyonla bir yapı nesnesinin içerisine okunmasının istendiği durumlardır. Örneğin bir BMP dosyasının başındaki 12
byte'a BMP başlığı denilmektedir. BMP başlığı şu biçimde içeriğe sahiptir:
Uzunluk İçerik
2 byte Magic Number
4 byte Dosya uzunluğu
2 byte Reserved
2 byte Reserved
4 byte Image bilgilerinin bulunduğu offset
Şimdi biz bu dosyanın başından 12 byte okuyarak okuduklarımızı bir yapı ile çakıştırmak isteyelim:
struct BITMAP_HEADER {
char magic[2]; /* 2 byte */
uint32_t size; /* 4 byte */
uint16_t reserved1; /* 2 byte */
uint16_t reserved2; /* 2 byte */
uint32_t dataloc; /* 4 byte */
};
İşte okudğumuz 12 byte bu elemanlarla default hizalama yüzünden çakışmayacaktır. Bu çakışmayı sağlamak için bizim hizalamayı byte hizalaması
olarak ayarlamamız gerekir. Aşağıdaki örneği "test.bmp" isimli bir BMP dosyası oluşturup hep default hizalama ile hem de 1 byte hizalama
ile deneyiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
struct BITMAP_HEADER {
char magic[2]; /* 2 byte */
uint32_t size; /* 4 byte */
uint16_t reserved1; /* 2 byte */
uint16_t reserved2; /* 2 byte */
uint32_t dataloc; /* 4 byte */
};
#pragma pack(4)
int main(void)
{
FILE *f;
struct BITMAP_HEADER bh;
if ((f = fopen("test.bmp", "rb")) == NULL) {
fprintf(stderr, "Cannot open file!..\n");
exit(EXIT_FAILURE);
}
fread(&bh, sizeof(struct BITMAP_HEADER), 1, f);
printf("Magic: %c%c\n", bh.magic[0], bh.magic[1]);
printf("Size: %u\n", bh.size);
printf("Bitmap Data Locatiion: %u\n", bh.dataloc);
fclose(f);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi hizalama program kodunun içerisinde ayaralanabilir mi? İşte hizalamayı değiştirmek için hem Microsoft hem gcc hem de clang
derleyicilerinde #pragma pack(N) direktifi kullanılabilmektedir. Bu direktif komut satırında belirtilen hizalama seçeneğine göre daha
yüksek önceliklidir. (Yani hem komut satırında belirleme yapıp hem de £pragma pack ile belirleme yaparsak #pragma pack belirlemesi dikkate
alınır. Örneğin:
#include <stdio.h>
#pragma pack(1)
struct SAMPLE {
char a;
int b;
char c;
int d;
};
int main(void)
{
struct SAMPLE s;
printf("%zd\n", sizeof s); /* 10 */
return 0;
}
#pragma pack direktifi sonraki #pragma pack direktifine kadar etkili olmaktadır. Böylece programcı isterse programın farklı yerlerinde farklı
hizalamalar kullanabilir. Örneğin:
#include <stdio.h>
#pragma pack(1)
struct SAMPLE {
char a;
int b;
char c;
int d;
};
#pragma pack(8)
struct MAMPLE {
char a;
int b;
char c;
int d;
};
int main(void)
{
struct SAMPLE s;
struct MAMPLE m;
printf("%zd\n", sizeof s); /* 10 */
printf("%zd\n", sizeof m); /* 16*/
return 0;
}
C11 ile birlikte bir nesnenin (bir yapının elemanı için de söz konusu olabilir) hizalaması _Alignas(N) belirleyicisi ile değiştirilebilmektedir.
Örneğin:
#include <stdio.h>
struct SAMPLE {
int a;
_Alignas(8) int b;
};
int main(void)
{
struct SAMPLE s;
printf("%zd\n", sizeof s); /* 16 */
return 0;
}
Burada yapının b elemanının önüne _Alignas(8) belirleyicisi getirilmiştir. Bu belirleyici aslında dördün katlarına yerleştirilecek int nesnesnin
8'in katlarına yerleştirilmesini sağlamaktadır. Ancak C11 standartlarına göre _Alignas(N) belirleyicisi ile yüksek bir hizalama gereksinimi düşük
bir hizalamaya çevrielemez. Örneğin:
struct SAMPLE {
int a;
_Alignas(1) int b; /* geçersiz! */
};
_Alignas ile belli nesnelerin ya da belli yapı elemanlarının hizalama gereksinimlerinin değiştirilebildiğine dikkat ediniz.
Ayrıca C11 ile birlikte _Alignof(tür_ismi) isminde bir operatör de dile eklenmiştir. Bu operatör o anda o tür için derleyicinin uyguladığı hizalamayı
bize vermektedir. Örneğin:
#include <stdio.h>
int main(void)
{
printf("%zd\n", _Alignof(int)); /* 4 */
return 0;
}
_Alignas belirleyicisi bir tür ismiyle de kullanılabilmektedir. Örneğin:
_Alignas(int) char c;
_Alignas(tür_ismi) aslında _Alignas(_Alignof(tür_ismi)) anlamına gelmektedir. Yani bir _Alignas(int) dediğimizde int türünün hizalama gereksinimi neyse
o sayıyı parantez içerisine yazmış gibi oluruz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Anımsanacağı derleyicilerin yapı elemanlarının arasında hizalama amacıyla bıraktığı boşluklara "padding" deniyordu. Aynı türden iki yapı
nesnesi birbirine atandığında "padding" kısımlarının birbirine atanması konusunda bir garanti verilmemektedir. Örneğin:
struct SAMPLE {
char a;
int b;
};
...
struct SAMPLE x = {'a', 3}, y;
x = y;
Burada C standartları a elemanı ile b elemanı arasındaki muhtemelen 3 byte'lık padding alanının atama sırasında hedefe kopyalanaacağı konusunda
bir garanti vermemektedir. Programcının bu padding alanlarını kullanmaya çalışması iyi bir teknik değildir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Sistem programalama karşımıza çıkan önemli olgulardan biri de "cache sistemleridir". Bir sistem programcısının cache sistemlerine ilişkin
temel bilgileri edinmiş olması gerekmektedir. Bu bölümde cache sistemlerinde üzerinde durulacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir bilgiye erişilmek istenen durumlarda çoğu kez karşımıza iki bellek türü çıkmaktadır: Yavaş bellek ve hızlı bellek. Yavaş bellek genellikle
bol ve ucuzdur. Hızlı bellek ise kıt ve pahalıdır. Dolayısıyla tüm belleğin hızlı bellek olarak tasarlanması uygun olamayabilmektedir.
Ayrıca bazı sistemlerde sistemin doğası gereği yavaş bellekler hızlı belleklerle yer değiştirememektedir. Genellikle asıl bilgiler yavaş
belleklerde saklanır. Hızlı bellekler yavaş belleklere erişimi azaltarak hız kazancı sağlamak için kullanılırlar.
Bir cache sisteminde yavaş belleğin belli bölümleri hızla bellekte tutulur. Yavaş bellekteki bilgiye başvurmak isteyen kişi önce bilgiyi
hızlı bellekte arar. Eğer yavaş belleğin o kısmı hızlı bellek içerisinde bulunuyorsa bilgi hızlı bir biçimde elde edilir. Eğer yavaş belleğin
içerisindeki bilgi o anda hızlı bellekte bulunmuyorsa bu durumda gerçekten yavaş belleğe başvurularak bilgi oradan elde edilir. Bu sistemde
yavaş belleğin belli bölümlerini tutan hızlı belleğe "cache bellek" denilmektedir. Bu sistemler de "cache sistemleri" olarak adlandırılmaktadır.
(Cache sözcüğünün genellikle Türkçe karşılığı "ön bellek" biçiminde ifade edilmektedir. Biz bazen "cache" sözcüğünü bazen de "ön bellek"
sözcüğünü kullanacağız.)
Cache sisteminde bilgiye erişilmek istendiğinde eğer bilgi cache bellekte bulunuyorsa bu duruma İngilizce "cache hit (ön belleğin isabet
ettirilmesi") denilmektedir. Eğer bilgi cache bellekte yoksa bu duruma da İngilizce "cache miss (cache belleğin ıskalanması)" denilmektedir.
İyi bir cache sisteminde "cache hit" oranının yükseltimesi istenir. Cache hit oranı N tane erişimin yüzde kaçının cache'ten karşılandığına
denilmektedir. Bunu şöyle gösterebiliriz:
Cache hit oranı = cache hit sayısı / N
Pekiyi bir cache sisteminde cache hit oranı nasıl artırılabilir? Şüphesiz cache bellek büyütüldükçe bunun cache hit oranı üzerinde olumlu
bir etkisi olacaktır. Ancak bu durumda maliyet de artacaktır. Cache hit oranı üzerinde en önemli etkenlerden biri yavaş belleğin nerelerinin
hangi sürelerde hızlı bellekte tutulacağına ilişkin stratejilerdir. Bunlara İngilizce "cache replacement" algoritmaları denilmektedir.
Bazen "cache" sözcüğü ile "buffer (tampon)" sözcükleri biribirine karıştırılmaktadır. Cache sistemleri hızlandırma amacıyla kullnılan sistemlerdir.
Halbuki buffer mekanizması "bilgileri sırası bozulmadan geçici süre tutmak için oluşturulmuş" sistemlerdir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir cache sistemi donanımsal ya da yazılımsal biçimde oluşturulabilmektedir. Donanımsaşl cache sistemlerinde gerçekleştirim elektrik
devreleriyle yapılmıştır dolayısıyla gebellikle yazılımsal bir müdahale söz konusu olamamaktadır. Yazılımsal cache sistemleri ise
tamamen programalama yoluyla oluşturulmuş cache sistemleridir.
Sistem programlama faaliyetlerinde cache sistemleri değişik biçimlerde karşımıza çıkabilmektedir. Bunlara birkaç örnek vermek istiyoruz:
- Bugün bilgisayar sistemlerinde genellikle ana bellek olarak DRAM (Dynamic RAM) denilen bellekler kullanılmaktadır. DRAM belleklerde
belleğin bir biti tipik olarak bir kapasitif elemanla bir transistörden oluşturulmaktadır. Bu yapı DRAM'ların az yer kaplamasını ve ucuz
olmasını sağlamaktadır. Bugün kullandığımız DRAM bellekler 10 nanosaniyeye kadar hızlandırılmış durumdadır. Eskiden (80'li yılların
oralarına kadar) CPU'lar DRAM belleklerden daha yavaştı. Bu yıllarda dolayısıyla CPU'lar DRAM bellekleri beklemiyordu. Ancak 80'li yılların
sonlarına doğru CPU'lar DRAM bellekleri hız bakımından geçti. Bu durumda CPU DRAM'dan bir bilgi talep ettiğinde o bilgi DRAM'dan gelene
kadar bekliyordu (buna CPU terminolojisinde "wait state" denilmektedir) İşte 80'li yılların sonlarına doğu yavaş yavaş kişisel bilgisauarlarda
DRAM beklemeleri azaltmak için donanımsal biçimde çalışan cache bellekler oluşturulmaya başlandı. Bu tasarımda SRAM (static RAM) teknolojisi
ile üretilen hızlı RAM'ler cache olarak kullanılıyordu. (SRAM'lerde bir bir tipik olarak SR Latch denilen flip flop devresi ile gerçekleştirilmektedir.
Bu tasarım bir bir için fazla sayıda transistörün kullanılmasına yol açmaktadır. Böylece SRAM'ler hem daha fazla yer kaplamakta hem de daha pahalı
olmaktadır.) İşte o yıllarda artık CPU'lar önce cache balleğe vuruyor eğer cache "miss olursa" DRAM bellekten bilgiyi alıyordu. Daha sonraları
CPU'lar daha da hızlanınca artık CPU'ların içerisine de SRAM olarak üretilmiş cache bellekler yerleştirilmeye başlandı. Böylece çok kademeli
cache sistemleri uygulandı. CPU bilgiyi önce en hızlı biçimde kendi içerisindeki L1 cache denilen bellekte arıyor orada bulamazsa CPU
dışındaki L2 cache cache denilen belleğe başvuruyordu. Eğer bilgi L2 cache'te de bulunamazsa DRAM erişimi yapılıyordu. Bugünkü bilgisayar
sistemlerinde genellikle 3 kademe cache kullanılmaktadır. L1 ve L2 cache'ler CPU içerisinde L3 cache CPU dışında konuşlandırılmaktadır.
Çok çekirdekli sistemlerde her çekirdeğin ayrı bir L1 ve L2 cache'i bulunmaktadır.
- Çok karşılaşılan bir cache sistemi de işletim sistemleri tarafından oluşturulan ve ismine "disk cache" sistemi, "buffer cache" sistemi
ya da "page cache" denilen cache sistemleridir. İşletim sistemleri yazılımsal olarak diskte okunan blokları RAM'de bir cache alanında
tutmaktadır. Böylece bir disk okuması yapılmak istendiğinde önce RAM'deki bu cache'e başvurulmakta blok zaten orada varsa hiç okuması
yapılmadan oradan alınmaktadır. Bu disk cache (buffer cache) sistemi işletim sistemlerinin performanslarında önemli bir etkiye sahiptir.
Çünkü bilgisayar sistemlerinin en yavaş bileşenlerinden biri disk sistemidir. Bu cache sisteminde yavaş bellek diski hızlı bellek DRAM
belleği temsil etmektedir.
- İşletim sistemlerinin çekirdekleri disk cache (buffer cache) sisteminin yanı sıra son işleme sokulan dosya bilgilerinin saklandığı
"i-node cache" ve son işleme sokulan dizin girişlerinin saklandığı "directory entry cache (dentry cache)" denilen cache sistemlerini de
kullanmaktadır. Böylece bir dosya işlemi yapılırken dosya bilgilerine hızlı bir biçimde erişilebilmektedir.
- Web tarayıcıları da kendi içerisinde bir cache sistemi kullanabilmektedir. Bir web sayfasına eriştiğimizde o sayfanın içeriği tarayıcının
cache sisteminde bulunuyor olabilir. Bu durumda doğrudan sayfa kullanıcıya gösterilebilir. Burada yavaş bellek web sayfasının barındırıldığı
sunucuyu, hızlı bellek ise yerel bilgisayarı temsil etmektedir.
- Dağıtık uygulamalarda da cache sistemleriyle çokça karşılaşılmaktadır. Örneğin bir sosyal medya uygulamasında resimler o coğrafi bölgeye
yakın olan daha hızlı erişilebilen CDN denilem server'larda tutulabilmektedir.
- Standart C kütüphaenesindeki dosya fonksiyonları da ileride detaylı biçimde ele alacağımız gibi bir cache sistemi kullanmaktadır.
- Manyetik temelli Hard Disklerde de bir cache sistemi bulundurulmaktadır. Sistem programcısı HDD'den bir blok okumak istediğinde eğer
o blok HDD'nin kendi cache'i içerisinde varsa doğrudan oradan alınmaktadır. Eğer yoksa disk kafaları hareklet ettirilip bilgi elektro
mekanik bir biçimde diskten elde edilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Cache bellek genel olarak bloklardan (yani ardışıl byte topluluklarından) oluşmaktadır.Her bloğu yavaş belleğin farklı bir yerini tutabilmektedir.
Eğer cache sisteminde cache bellek yalnızca okuma amaçlı kullanılıyorsa yazma işlemi doğruan yavaş belleğe yapılıyorsa bu tür cache sistemlerine
"read only cache sistemleri" denilmektedir. Eğer yazma işlemi de cache belleğe yapılabiliyorsa bu tür cache sistemlerine de "read-write
sistemleri" ya da "write throuh cache sistemleri" denilmektedir. Şüphesiz read-write cache sistemleri daha yüksek bir performans sunmaktadır.
Ancak bu sistemlerde birtakım anomaliler oluştuğunda cache belleğe yazılan ancak yavaş belleğe henüz aktarılmamış bilgilerin kaybedilme
olasılığı vardır. Read-write cache sistemlerinde bir cache bloğu cache'ten atılacağı zaman onun daha önce değiştirilip değiştirilmediğine
bakılması gerekir. Eğer o cache bloğuna yazma yapılmışsa o cache bloğu cache'ten atılmadan önce yavaş belleğe yazılmalıdır. Genel olarak
cache terminolojisinde bir cache bloğunun içeriğinin güncellenmiş olmasına o cache bloğunun ""kirlenmiş (dirty)" olması denilmektedir.
Bir cache bloğunun yavaş belleğe içeriği değiştiğinden dolayı geri yazılmasına "flush" işlemi de denilmektedir. Pekiyi cache blokları
ne zaman flush edilmelidir? Bir cache bloğu cache'ten atılacağı zaman eğer kirlenmişse flush edilmelidir. Ancak kirlenmemişse onun flush
edilmesine gerek yoktur. Ancak kirlenmiş cache bloklarının uzun süre flush edilmeden bekletilmesi de anomali durumlarında bilgi kayıplarına
yol açabilmektedir. Örneğin işletim sistemlerinin disk cache (buffer cache) sistemleri read-write cache sistemleridir. Ancak kirlenmiş cache
blokları çok da fazla bekletilmeden belli periyotlarla çeşitli kernel thread'ler tarafından diske flush edilmektedir. Bu tür yazımlara
"delayed write" denilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
32. Ders - 01/10/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz gibi cache bellek tipik olarak blok ya da "line" denilen ardışıl byte topluluklarındanm oluşmaktadır. Genel olarak
cache'teki her bir bloğun bir numarası vardır. Örneğin elimizde 100K'lık bir cache olsun. Bir cache bloğu da 1K'dan oluşsun. Bu cache içeisinde
toplam 100 tane cache bloğu vardır. İşte bu bloklara sırasıyla numaraler verilmektedir. Örneğin:
Cache
0. Blok (1K)
1. Blok (1K)
2. Blok (1K)
...
97. Blok (1K)
98. Blok (1K)
99. Blok (1K)
Yavaş belleğin de 1MB olduğunu varsayalım. İşte yavaş bellek de yine cache bloklarında olduğu gibi bloklara ayrılmaktadır. Örneğin:
Yavaş Bellek
0. Blok (1K)
1. Blok (1K)
2. Blok (1K)
...
997. Blok (1K)
998. Blok (1K)
999. Blok (1K)
Bu örnekte toplam 1000 bloktan oluşan yavaş belleğin ardışıl olmayan 100 bloğu cache bellekte tutulmaktadır. Bu sistem yazılımsal olarak
oluşturulacaksa bizim cache bloklerının ahngisinin yavaş belleğin hangi bloğundaki bilgiyi tuttuğunu bir biçimde izlememiz gerekir.
Bu sistemde örneğin biz yavaş belleğin 718'inci bloğuna erişmek isteyelim. Sistemin önce bu 718'inci bloğun cache içerisinde olup olmadığına
bakması gerekir. Örneğin bu 718'inci bloğun içeriği cache belleğin 54'üncü bloğunda olabilir. O zaman erişim hızlı bir biçimde cache'in
54'üncü bloğundan sağlanacaktır. Ancak eğer bu 718'inci blok cache'te yoksa bu durumda bu blok gerçekten yavaş belleğin 718'inci bloğundan
elde edilecektir. Mademki her blok işleminde önce o blok cache'te mi diye bakılmaktadır o halde bu arama işleminin hızlı yapılması gerekir.
Bu tür sistemlerde sıralı arama (sequential search) iyi bir fikir değildir. Böylesi aramalarda tipik olarak "hash tabloları (hash table)"
denilen veri yapıları tercih edilmektedir. Kursumuzun algortimaalar ve veri yapıları kısmında hash tabloları incelenmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda açıkladığımız gibi bir cache sistemi olsun. Yavaş belleğin 1000 bloktan cache belleğin ise 100 bloktan oluştuğunu varsayalım.
Cache sistemlerindeki en önemli algoritmik sorun şudur: Biz yavaş belleğin hangi bloklarını cache bellekte tutmalıyız? Yani yukarıdaki
örnekte 1000 blokluk yavaş belleğin hangi 100 bloğunu cache bellekte tutmalıyız? Bizim amacımız "cache hit" oranını yükseltmektir. Eğer biz
yavaş belleğin nerelerinin çok kullanıldığını önceden biliyor olsak problem çok basit olurdu. Bu duurmda en çok kullanılacak blokları cache'te
tutardık. Ancak böyle bir bilgiye genellikle sistem programcıları sahip olmazlar. Cache sistemlerinde genellikle cache bellekteki bloklar
dinamik bir biçimde kullanılmaktadır. Yani duruma göre bir cache bloğunun içeriği atılıp yavaş belleğin daha uygun bir bloğu cache'e
alınabilmektedir. Yavaş belleğin hangi bloklarının cache'te tutulacağına ilişkin algoritmalara İngilizce "cache replacement" algortimaları
denilmektedir. Biz buna "cache algoritmaları" diyeceğiz.
Literatürde değişik durumlar için düşünülmüş çeşitli cache algoritmaları vardır. Biz burada en fazla kullanılan algoritmaları tanıtacağız.
1) En Az Kullanılanın Cache'ten Atılması Algoritması (Least Frequently Used (LFU)): Bu yöntemde yavaş bellekten okunan bloklar cache'e alınır.
Cache'teki her blok için bir sayaç tutulur. Her "cache hit" oluştuğunda o bloğun sayacı bir artırlır. Cache dolduğunda ve bir "cache miss"
oluştuğunda o zamana kadar en az kullanılan blok cache'ten çıkarılır. Cache miss olan yavaş bellek bloğu cache'e alınır. Böylece cache'te
o zamana kadar fazla kullanılan bloklar tutulmuş olur. Buradaki varsayım şudur: Şimdiye kadar az kullanılmış olan bloklar bundan sonra da
az kullanılacaktır. Bu algoritmada "thrashing" denilen bir problemin giderilmesi gerekmektedir. Cache'e yeni çekilen bloğun başlangıç sayacı
0'da tutulursa ilk cache miss olduğunda bu blok yeniden cache'ten atılır. O halde cache'e alınan bloğun başlangıç sayacının 0 yapılması
uygun değildir.
2) Son Zamanlarda En Az Kullanılanın Cache'ten Atılması (Least Recently Used (LRU)): Açık ara en fazla tercih edilen cache algoritması budur.
Örneğin Linux çekirdeğindeki çeşitli cache sistemlerinde hep bu algortima kullanılmaktadır. Bu algoritmada cache'ten blok çıkatılacağı
zaman onların toplam hit sayaçlarına bakılmaz. Onların son zamanlarda çok kullanılıp kullanılmadığına bakılır. Bilgisayar sistemlerinde
tipik çalışmada birtakım bloklar bir zaman dilimi içerisinde çok kullanılıp sonra bir daha seyrek kullanılmakta ya da hiç kullanılmamaktadır.
Örneğin işletim sisteminin disk cache sistemini düşünelim. Bir program çalıştığında o program belli disk bloklarını sürekli kullanıyor olabilir.
Ancak programın çalışması bittiğinde artık o bloklar çok seyrek kullanılıyor olacaktır. Bu algoritma tipik olarak bri bağlı liste ile
gerçekleştirilmektedir. Cache hit olan bloklar bağlı listede öne çekilir. Böylece son zamanlara en az kullanılan bloklar bağlı listenin sonunda
kalır. Cache'ten blok çıkartılacağı zaman bağlı listenin sonundaki bloklar çıkartılır.
3) En Fazla Kullanılanın Cache'ten Atılması (Most Frequenly Used (MRU)): Bu algroritma LFU algoritmasının tersini yapmaktadır. Yani toplamda
o zamana kadar en fazla kullanılan bloğu cache'ten atmaktadır. Bu yöntem ilk başta saçma gelebilir. Ancak bazı seyrek sistemlerde her bloğun
yaklaşık eşit sayıda kullanılacağı baştan biliniyor olabilir. Bu durumda o zamana kadar çok hit almış cache blokları gelecekte daha az kullanılma
potansiyilene sahiptir. Tabii bu algoritma çok seyrek kullanılmaktadır.
4) Rastgele Blokların Cache'ten Atılması (Random Cache Replacement): Bazı sistemlerde hiçbir öngürü yapılamamaktadır. Bu durumda rastgele
blokların cache'ten atılması uygun olabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda read-onlu LRU cache sistemine bir örnek verilmiştir. Örneğimizde diski temsil eden "test.dat" isimli her bloğu 32 byte olan
100 bloktan oluşaşn (toplam 3200 byte) bir dosya kullanılmaktadır. İşin başında bu dosyaya rastgele text içerik atanmıştır. Burada
oluşturulan cache sistemindeki fonksiyonlar şunlardır:
HCACHE open_disk(const char *path, int flags);
int read_disk(HCACHE hc, int blockno, void *buf);
int close_disk(HCACHE hc);
open_disk fonksiyonu "test.dat" dosyasını açar, cache veri yapısına ilkdeğerlerini verir. read_disk fonksiyonu diskten cache sistemini kullanarak
bir blok okumaktadır. close_disk fonksiyonu ise cache sistemini kapatmaktadır. Kullanılan cache veri yapısı şöyledir:
typedef struct tagCACHELINE {
char buf[LINE_SIZE];
int blockno;
size_t count;
} CACHE_LINE;
typedef struct tagCACHE {
int fd;
CACHE_LINE clines[NCACHE_LINES];
size_t tcount;
} CACHE, *HCACHE;
Burada cache sistemi CACHE isimli bir yapıyla temsil edilmiştir. Her cache bloğu CACHE_LINE isimli bir yapıyla temsil edilmiştir.
Tüm cache blokları CACHE_LINE türünden bir dizide toplanmıştır. Her cache bloğunda cache içeriği, diskteki blok numarası ve hit sayacı
tutulmaktadır.
Buradaki simülasyon kodundaki algoritmik kusurlar şunlardır:
1) Okunmak istenen bloğun tek tek cache bloklarında sıralı bir biçimde aranması. Tabii buradaki simülasyonda 10 tane cache bloğu vardır.
Dolayısıyla sıralı arama çok hızlı bir biçimde gerçekleştilmektedir. Az sayıda (örneğin 10 civarında) elemanın bulunduğu durumda sıralı
arama iyi bir yöntemdir. Yukarıda da belirttiğimiz gibi arama işlemi için genellikle "hash tabloları" kullanılmaktadır.
2) En düşük sayaca sahip olan cache bloğu da sıralı arama ile bulunmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* diskcache.h */
#ifndef DISKCACHE_H_
#define DISKCACHE_H_
/* Symbolic Constants */
#define LINE_SIZE 32
#define NCACHE_LINES 10
#define INITIAL_COUNT 2
#ifdef DEBUG
#define DEBUG_PRINT(fmt, ...) fprintf(stderr, fmt, ## __VA_ARGS__)
#else
#define DEBUG_PRINT(fmt, ...)
#endif
/* Type Definitions */
typedef struct tagCACHELINE {
char buf[LINE_SIZE];
int blockno;
size_t count;
} CACHE_LINE;
typedef struct tagCACHE {
int fd;
CACHE_LINE clines[NCACHE_LINES];
size_t tcount;
} CACHE, *HCACHE;
/* Function Prototypes */
HCACHE open_disk(const char *path, int flags);
int read_disk(HCACHE hc, int blockno, void *buf);
void close_disk(HCACHE hc);
#endif
/* diskcache.c */
#define DEBUG
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include "diskcache.h"
#ifdef DEBUG
static void print_cline_counts(HCACHE hc);
#endif
static size_t select_line(HCACHE hc);
HCACHE open_disk(const char *path, int flags)
{
HCACHE hc;
int i;
if ((hc = (HCACHE)malloc(sizeof(CACHE))) == NULL)
return NULL;
if ((hc->fd = open(path, flags)) == -1) {
free(hc);
return NULL;
}
for (i = 0; i < NCACHE_LINES; ++i) {
hc->clines[i].blockno = -1;
hc->clines[i].count = 0;
}
hc->tcount = 0;
return hc;
}
int read_disk(HCACHE hc, int blockno, void *buf)
{
int i;
int rline;
for (i = 0; i < NCACHE_LINES; ++i)
if (hc->clines[i].blockno == blockno) {
DEBUG_PRINT("Cache hit block %d, used cache line %d\n", blockno, i);
memcpy(buf, hc->clines[i].buf, LINE_SIZE);
++hc->clines[i].count;
++hc->tcount;
#ifdef DEBUG
print_cline_counts(hc);
#endif
return 0;
}
rline = select_line(hc);
if (lseek(hc->fd, (off_t)blockno * LINE_SIZE, SEEK_SET) == -1)
return -1;
if (read(hc->fd, hc->clines[rline].buf, LINE_SIZE) == -1)
return -1;
hc->clines[rline].blockno = blockno;
hc->clines[rline].count = hc->tcount / NCACHE_LINES + 1;
hc->tcount += hc->clines[rline].count;
memcpy(buf, hc->clines[rline].buf, LINE_SIZE);
DEBUG_PRINT("Cache miss block %d, used cache line %d\n", blockno, rline);
#ifdef DEBUG
print_cline_counts(hc);
#endif
return 0;
}
void close_disk(HCACHE hc)
{
close(hc->fd);
DEBUG_PRINT("Cache closed!\n");
free(hc);
}
static size_t select_line(HCACHE hc)
{
size_t min_count;
size_t min_index;
int i;
min_count = hc->clines[0].count;
min_index = 0;
for (i = 1; i < NCACHE_LINES; ++i) {
if (hc->clines[i].count < min_count) {
min_count = hc->clines[i].count;
min_index = i;
}
}
return min_index;
}
#ifdef DEBUG
static void print_cline_counts(HCACHE hc)
{
int i;
putchar('\n');
printf("Total count: %llu\n", (unsigned long long)hc->tcount);
for (i = 0; i < NCACHE_LINES; ++i)
printf("Cachle Line %d --> Block: %d, Count: %llu\n", i, hc->clines[i].blockno, (unsigned long long)hc->clines[i].count);
putchar('\n');
}
#endif
/* diskcache-test.c */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include "diskcache.h"
int create_test_file(const char *path, int nblocks);
int main(void)
{
HCACHE hc;
char buf[32 + 1];
int blockno;
if (create_test_file("test.dat", 100) == -1) {
perror("create_test_file");
exit(EXIT_FAILURE);
}
if ((hc = open_disk("test.dat", O_RDONLY)) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
for (;;) {
printf("Block No:");
scanf("%d", &blockno);
putchar('\n');
if (blockno == -1)
break;
if (read_disk(hc, blockno, buf) == -1) {
perror("read_file");
exit(EXIT_FAILURE);
}
buf[32] = '\0';
printf("Block content: %s\n\n", buf);
}
close_disk(hc);
return 0;
}
int create_test_file(const char *path, int nblocks)
{
FILE *f;
int i, k;
char buf[LINE_SIZE];
srand(time(NULL));
if ((f = fopen(path, "wb")) == NULL)
return -1;
for (i = 0; i < nblocks; ++i) {
sprintf(buf, "%04d ", i);
for (k = 5; k < 32; ++k)
buf[k] = rand() % 26 + 'A';
if (fwrite(buf, LINE_SIZE, 1, f) != 1)
return -1;
}
fclose(f);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Read-write cache sistemlerinde cache belleğin yazma sırasında da kullanıldığını belirtmiştik. Yani bir blok yazılmak istendiğinde eğer
o blok cache'te varsa doğrudan cache bloğu üzerine yazma yapılır. Tabii bu durumda bu blok atılacağı zaman güncellenmiş (kirlenmiş) olduğu
için yavaş belleğe geri yazılmalıdır. Pekiyi yazma sırasında "cache miss" olursa ne yapılmalıdır? Burada iki strateji izlenebilir:
1) Blok doğrudan yavaş belleğe yazılabilir.
2) Blok önce cache'e çekilip yazma cache'e yapılabilir. Tabii burada aslında yavaş bellekteki bloğun cache'e çekilmesine gerek yoktur.
Doğurdan yazma cache üzerine yapılabilir.
Bazen yazma sırasında "cache hit" olduğu durumda yazma işlemi hem cache'e ham de yavaş belleğe yapılabilmektedir. Böylece anomalilerde
yavaş bellekte bir kayıp oluşmayacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte daha önce yapmış olduğumuz disk cache simülasyonundaki cache sistemi read/write cache haline gtirilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* filecache.h */
#ifndef FILECACHE_H_
#define FILECACHE_H_
/* Symbolic Constants */
#define LINE_SIZE 32
#define NCACHE_LINES 10
#define INITIAL_COUNT 2
#ifdef DEBUG
#define DEBUG_PRINT(fmt, ...) fprintf(stderr, fmt, ## __VA_ARGS__)
#else
#define DEBUG_PRINT(fmt, ...)
#endif
/* Type Definitions */
typedef struct tagCACHELINE {
char buf[LINE_SIZE];
int blockno;
size_t count;
int dirty;
} CACHE_LINE;
typedef struct tagCACHE {
int fd;
CACHE_LINE clines[NCACHE_LINES];
size_t tcount;
} CACHE, *HCACHE;
/* Function Prototypes */
HCACHE open_disk(const char *path, int flags);
int read_disk(HCACHE hc, int blockno, void *buf);
int write_disk(HCACHE hc, int blockno, const void *buf);
int fluch_disk(HCACHE hc);
int close_disk(HCACHE hc);
#endif
/* filecahe.c */
#define DEBUG
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include "diskcache.h"
#ifdef DEBUG
static void print_cline_counts(HCACHE hc);
#endif
static size_t select_line(HCACHE hc);
HCACHE open_disk(const char *path, int flags)
{
HCACHE hc;
int i;
if ((hc = (HCACHE)malloc(sizeof(CACHE))) == NULL)
return NULL;
if ((hc->fd = open(path, flags)) == -1) {
free(hc);
return NULL;
}
for (i = 0; i < NCACHE_LINES; ++i) {
hc->clines[i].blockno = -1;
hc->clines[i].count = 0;
hc->clines[i].dirty = 0;
}
hc->tcount = 0;
return hc;
}
int read_disk(HCACHE hc, int blockno, void *buf)
{
int i;
int rline;
for (i = 0; i < NCACHE_LINES; ++i)
if (hc->clines[i].blockno == blockno) {
DEBUG_PRINT("Cache hit block %d, used cache line %d\n", blockno, i);
memcpy(buf, hc->clines[i].buf, LINE_SIZE);
++hc->clines[i].count;
++hc->tcount;
#ifdef DEBUG
print_cline_counts(hc);
#endif
return 0;
}
rline = select_line(hc);
if (hc->clines[rline].dirty) {
if (lseek(hc->fd, hc->clines[rline].blockno * LINE_SIZE, SEEK_SET) == -1)
return -1;
if (write(hc->fd, hc->clines[rline].buf, LINE_SIZE) == -1)
return -1;
hc->clines[rline].dirty = 0;
}
if (lseek(hc->fd, (off_t)blockno * LINE_SIZE, SEEK_SET) == -1)
return -1;
if (read(hc->fd, hc->clines[rline].buf, LINE_SIZE) == -1)
return -1;
hc->clines[rline].blockno = blockno;
hc->clines[rline].count = hc->tcount / NCACHE_LINES + 1;
hc->tcount += hc->clines[rline].count;
memcpy(buf, hc->clines[rline].buf, LINE_SIZE);
DEBUG_PRINT("Cache miss block %d, used cache line %d\n", blockno, rline);
#ifdef DEBUG
print_cline_counts(hc);
#endif
return 0;
}
int write_disk(HCACHE hc, int blockno, const void *buf)
{
int rline;
for (int i = 0; i < NCACHE_LINES; ++i)
if (hc->clines[i].blockno == blockno) {
DEBUG_PRINT("Cache hit block %d, used cache line %d\n", blockno, i);
memcpy(hc->clines[i].buf, buf, LINE_SIZE);
++hc->clines[i].count;
++hc->tcount;
hc->clines[i].dirty = 1;
#ifdef DEBUG
print_cline_counts(hc);
#endif
return 0;
}
rline = select_line(hc);
if (hc->clines[rline].dirty) {
if (lseek(hc->fd, hc->clines[rline].blockno * LINE_SIZE, SEEK_SET) == -1)
return -1;
if (write(hc->fd, hc->clines[rline].buf, LINE_SIZE) == -1)
return -1;
}
memcpy(hc->clines[rline].buf, buf, LINE_SIZE);
hc->clines[rline].blockno = blockno;
hc->clines[rline].count = hc->tcount / NCACHE_LINES + 1;
hc->clines[rline].dirty = 1;
DEBUG_PRINT("Cache miss block %d, used cache line %d\n", blockno, rline);
#ifdef DEBUG
print_cline_counts(hc);
#endif
return 0;
}
int flush_disk(HCACHE hc)
{
for (int i = 0; i < NCACHE_LINES; ++i) {
if (hc->clines[i].dirty) {
if (lseek(hc->fd, hc->clines[i].blockno * LINE_SIZE, SEEK_SET) == -1)
return -1;
if (write(hc->fd, hc->clines[i].buf, LINE_SIZE) == -1)
return -1;
}
}
return 0;
}
int close_disk(HCACHE hc)
{
if (flush_disk(hc) == -1)
return -1;
close(hc->fd);
DEBUG_PRINT("Cache closed!\n");
free(hc);
return 0;
}
static size_t select_line(HCACHE hc)
{
size_t min_count;
size_t min_index;
int i;
min_count = hc->clines[0].count;
min_index = 0;
for (i = 1; i < NCACHE_LINES; ++i) {
if (hc->clines[i].count < min_count) {
min_count = hc->clines[i].count;
min_index = i;
}
}
return min_index;
}
#ifdef DEBUG
static void print_cline_counts(HCACHE hc)
{
int i;
putchar('\n');
printf("Total count: %llu\n", (unsigned long long)hc->tcount);
for (i = 0; i < NCACHE_LINES; ++i)
printf("Cachle Line %d --> Block: %d, Count: %llu, Dirty: %d\n", i, hc->clines[i].blockno, (unsigned long long)hc->clines[i].count, hc->clines[i].dirty);
putchar('\n');
}
#endif
/* filecache-test.c */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include "diskcache.h"
int create_test_file(const char *path, int nblocks);
int main(void)
{
HCACHE hc;
char buf[32 + 1];
int rw;
int blockno;
if (create_test_file("test.dat", 100) == -1) {
perror("create_test_file");
exit(EXIT_FAILURE);
}
if ((hc = open_disk("test.dat", O_RDWR)) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
for (;;) {
printf("Block No:");
scanf("%d", &blockno);
if (blockno == -1)
break;
printf("(r)ead/(w)rite?");
while (getchar() != '\n')
;
rw = getchar();
if (rw == 'r') {
if (read_disk(hc, blockno, buf) == -1) {
perror("read_file");
exit(EXIT_FAILURE);
}
}
else if (rw == 'w') {
for (int k = 5; k < 32; ++k)
buf[k] = rand() % 26 + 'A';
if (write_disk(hc, blockno, buf) == -1) {
perror("read_file");
exit(EXIT_FAILURE);
}
}
else {
printf("invalid operation!...\n");
continue;
}
buf[32] = '\0';
printf("Block content: %s\n\n", buf);
}
close_disk(hc);
return 0;
}
int create_test_file(const char *path, int nblocks)
{
FILE *f;
int i, k;
char buf[LINE_SIZE];
srand(time(NULL));
if ((f = fopen(path, "wb")) == NULL)
return -1;
for (i = 0; i < nblocks; ++i) {
sprintf(buf, "%04d ", i);
for (k = 5; k < 32; ++k)
buf[k] = rand() % 26 + 'A';
if (fwrite(buf, LINE_SIZE, 1, f) != 1)
return -1;
}
fclose(f);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
33. Ders 07/10/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bugün kullandığımız güçlü mikroişlemciler "koruma mekanizması (protection mechanism)" denilen önemli bir özelliğe sahiptir. Örneğin A serisi
ARM işlemcileri, Intel işlemcileri, Power PC işlemcileri, Alpha işlemcileri, Itanium işlemcileri koruma mekanizmalarına sahiptir. Ancak
mikrodenetleyiciler ve küçük kapasiteli işlemcilerde bu mekanizma bulunmamaktadır. Intel işlemcileri 80286 modelleriyle birlikte segment tabanlı,
80386 modelleriyle birlikte sayfa tabanlı koruma mekanizmasına sahip olmuşlardır. A (Applivation) serisi cortex'lere sahip ARM işlemcilerinde
koruma mekanizması vardır. Ancak M (Microcontroller) cortext'lerine sahip ARM işlemcilerinde genel olarak bu mekanizma bulunmamaktadır.
Bugün Windows, Linux, macOS gibi işletim sistemleri ancak koruma mekanizmasına sahip işlemcilerde çalışabilmektedir.
Koruma mekanizmasının temelde iki işlevi vardır:
1) Bellek Koruması
2) Komut Koruması
Çok prosesli işletim sistemlerinde farklı programlar aynı anda RAM'de bulunmaktadır. Bir program kendi bellek alanının dışına çıkıp başka
programların (ve hatta işletim sisteminin) bellek alanına erişip orada değişiklikler yaparsa bundan tüm sistem olumsuz etkilenebilir, diğer
prgramın çalışması bozulabilir, o programlar yaznlı çalışabilirler. Bir programın değişiklik yapmadan başka bir programın bellek alanına
erişmesi de tehlikeldir. Bu sayede kötü amaçlı programlar başka programın çalışması hakkında casusluk yapabilirler. Halbuki C'de bir göstericiye
RAM'deki herhangi bir adresi yerleştirip RAM'in o bölgesine erişebiliriz. Pekiyi bir programın kendi bellek alanı dışına erişmesi nasıl
engellenebilir? Bu engelleme yalnızca işletim sistemi tarafından yapılamaz. Engellemenin en alt düzeyde işlemci tarafından yapılması gerekir.
Bir program kendi bellek alanının dışına eriştiği zaman bu erişim birinci elden işlemci tarafından tespit edilir. İşlemci bu durumu işletim
sistemine bildirir. İşletim sistemi de programı cezalandırarak sonlandırır.
Bazı makine komutları uygunsuz kullanıldığında tüm sistemin çökmesine yol açabilmektedir. İşte işlemcilerin koruma mekanizmaları bu makine
komutlarını da denetlemektedir. Buna "komut koruması" denilmektedir. Bu tür komutları kullanan programlar işlemci tarafından tespit edilir.
İşlemci durumu işletim sistemine bildirir. İşletim sistemi de programı cezalandırarak sonlandırır.
Yukarıda açıkladığımız koruma mekanizmasının işlemci tarafından nasıl sağlandığı ve ihlallerin işletim sistemine nasıl iletildiği kişiler
tarafından merak edilmektedir. Bu konu kursumuzun konusu dışındadır. Mekanizmanın ayrıntıları "Sembolik Makine Dili" kurslarında ele
alınmaktadır.
İşletim sistemleri bellekte her yere erişebilmeli her makine komutunu kullanabilmeldir. Aksi takdirde faaliyetlerini yürütemezler. O halde
koruma mekanizması işletim sistemi için uygulanmamalı yalnızca sıradan uygulama programları için uygulanmalıdır. İşte işlemcileri tasarlayanlar
genellikle çalışmnakta olan akış için iki çalışma modu kullanırlar: Kernel mod ve user mod. Bir kdo kernel modda çalışıyorsa ona koruma
mekanizması uygulanmaz. Böylece kernel modda çalışan programlar bellekte her yere erişebilirler her makine komutunu kullanabilirler. Ancak
user modda çalışan kodlara koruma mekanizması uygulanmaktadır. Programların hemen hepsi user modda çalımaktadır. örneğin Windows'taki Excel,
Word, Visual Studio, Internet Explorer vs hep user modda çalışırlar. Benzer biçimde UNIX/Linux sistemlerindeki programlar kabuğun kendisi
(örneğin bash), dışsal komutlar user modda çalışırlar. UNIX/linux sistemlerindeki root proseslerin bu konuyla hiçbir ilgisi yoktur. Yani bu
sistemlerde biz bir programı sudo yaparak çalıştırdığımızda yine o program user modda çalışmaktadır.
İşletim sisteminin kodları, aygıt sürücü kodları kernel modda çalışmaktadır. Programcı bir kod parçasının koruma engeline takılmadan kernel
modda çalışmasını istiyorsa onu "aygıt sürücüsü (device driver)" olarak yazmalıdır. Intel işlemcilerinde dört çalışma modu vardır. Bunlara
"ring" de denilmektedir. Bu çalışma modlarına 0, 1, 2, 3 biçiminde numara verilmiştir. Korumanın hiç uygulanmadığı mod 0, korumanın tam uygulandığı
mod 3'tür. Windows, Linux ve macOS sistemleri Intel işlemcilerinin iki modunu kullanmaktadır: 0 ve 3. Yani her ne kadar Intel işlemcilerinde
4 çalışam modu olsa da aktif olarak zaten iki mod kullanılmaktadır.
Pekiyi user mod bir program işletim sisteminin istem fonksiyonunu çağırdığında ne olur? İşletim sisteminin sistem fonksiyonları bellekte
her yere erişebilmekte ve özel makine komutlarını kullanabilmektedir. Eğer user mod program işletim sisteminin sistem fonksyonarını user modda
çalıştırsa hemen koruma mekanizmasına takılırdı. İşte user mod bir program işletim sisteminin sistem fonksiyonlarını çağırdığında geçici süre
otomatik olarak user mod'tan kernel mod'a geçirilmektedir. Böylece sistem fonksiyonları kernel modda çalıştırılmaktadır. Sistem fonksiyonundan
çıkılırken yeniden user moda geri dönülmektedir. Bu duruma "prosesin user mod'tan kernel moda geçmesi" ve "kernel moddan user moda geri dönmesi"
denilmektedir. Tabii kernel moda geçişin ve geri dönüşün bir zaman maliyeti vardır. Yani işletim sisteminin sistem fonksiyonlarını çağırmanın
diğer fonksiyonları çağırmaktan zamansal bakımdan daha maliyetli olduğu söylenebilir.
Biz kendi programımızın belleğin her yerine erişmesini istiyorsak, onun özel makine komutlarını kullanmasını sağlamak isiyorsak programamızı
"aygıt sürücü (device driver)" biçiminde organize etmeliyiz. Tabii aygıt sürücüleri sisteme sıradan kullanıcılar yükleyememektedir. Aygıt
sürücüler sistemin yöneticileri tarafından (örneğin Linux'ta root kullanıcısı tarafından) yüklenebilmektedir. Aygıt sürücüler sistemin boot
edilmesi sırasında işletim sisteminin bir parçası olarak yüklenebilir ya da sistem çalışırken çeşitli komutlarla da yklenebilmektedir.
İşlemcileri tasarlayanlar genellikle koruma mekanizamasını "açılıp kapatılacak biçimde" tasarlamaktadır. Pek çok işlemci reset edildiğinde
koruma mekanizması aktif değildir. Koruma mekanizasması genellikle işletim sistemi yüklenirken işletim sisteminn kodları tarafından aktif
hale getirilmektedir. Koruma mekanizmasını kapatabilmek için yine kernel modda olnması gerekmektedir. Ancak işletim sistemleri bir kere
koruma mekanizmasını aktif hale getirince artık bir daha pasif hale getirmemektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir dosyanın bütün byte'ları üzerinde bir işlem yapacak olalım. Dosyanın her byte'ını tek tek read fonksiyonu ile aşağıdaki gibi okumaya
çalışmak kötü bir tekniktir:
while ((result = read(fd, &ch, 1)) > 0) {
/* ... */
}
Çünkü her byte için read fonksiyonun çağrılması (read fonksiyonu da sistem fonksiyonunu doğrudan çağırmaktadır) user mod, kernel mod
geçişleri nedneiyle zaman kaybının oluşmasına yol açar. Bu durumda ilk akla gelen alternatif yöntem okuma işlmelerini blok blok yapmaktır.
Örneğin:
while ((result = read(fd, buf, 8192)) > 0)
for (ssize_t i = 0; i < result; ++i) {
/* ... */
}
Burada sistem fonksiyonu çok daha az çağrılacak ve önemli bir hız kanacı oluşacaktır.
Aşağıda bu durumun simülasyonuna ilişkin bir örnek verilmiştir. Örneğimizde içi sıfırlarla dolu 5120000 uzunluğunda bir dosya kullanılmıştır.
Bu dosya aşağıdaki gibi yaratılabilir:
dd if=/dev/zero of=test.dat bs=512 count=10000
Aşağıdaki örnekte test1 programı her byte için read fonksiyonunu çağırmıştır. test2 programı ise 8192 byte'lık bloklarlarla okumayaı yapmıştır.
test1 programının çalışma süresi 3.040 saniye, test2 programının çalışma süresi ise 0.014 saniye sürmüştür. İki yöntem arasında yaklaşık 300
kat bir hız farkı vardır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* test1.c */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
void proc(int ch)
{
/* EMPTY */
}
int main(int argc, char *argv[])
{
int fd;
char ch;
ssize_t result;
clock_t start, stop;
double telapsed;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
start = clock();
if ((fd = open(argv[1], O_RDONLY)) == -1)
exit_sys("open");
while ((result = read(fd, &ch, 1)) > 0)
proc(ch);
if (result == -1)
exit_sys("read");
close(fd);
stop = clock();
telapsed = (double) (stop - start) / CLOCKS_PER_SEC;
printf("%.3f\n", telapsed);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* test2.c */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
void proc(int ch)
{
/* empty */
}
int main(int argc, char *argv[])
{
int fd;
ssize_t result;
clock_t start, stop;
double telapsed;
char buf[8192];
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
start = clock();
if ((fd = open(argv[1], O_RDONLY)) == -1)
exit_sys("open");
while ((result = read(fd, buf, 8192)) > 0)
for (ssize_t i = 0; i < result; ++i)
proc(buf[i]);
if (result == -1)
exit_sys("read");
close(fd);
stop = clock();
telapsed = (double) (stop - start) / CLOCKS_PER_SEC;
printf("%.3f\n", telapsed);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'nin prototipleri <stdio.h> içerisinde bulunan dosya fonksiyonları sistem fonksiyonlarının daha az çağrılmasına sağlamk içim tıpkı yukarıdaki
örnekte yaptığımız gibi bir cache sistemi oluşturmaktadır. Standart C fonksiyonlarının oluşturduğu bu cache sistemine genellikle "cache"
yerine "buffer" denilmektedir. Bu nedenle SC'nin standart dosya fonksiyonlarına "tamponlu IO fonksiyonları (buffered IO functions)" da
denilmektedir. Biz örneğin fgetc fonksiyonu ile dosyadan bir byte bile okumak istesek aslında fgetc bir grup byte'ı tek bir sistem fonksiyonu
ile okuyup bize okunan tampondan byte'ı verir. Böylece diğer fgetc çağırmaları için yeniden sistem fonksiyonu çağrılmamakta doğrudan bilgiler
bu tampondan alınmaktadır. Bu nedenle bir dosyadan bu biçimde byte byte okumalar için standart C fonksiyonları tercih edilmelidir.
Şimdi yukarıda örneği standart C fonksiyonlarıyla deneyelim. Okumayı şöyle yapacağız:
while ((ch = fgetc()) != EOF) {
/* ... */
}
Burada programın bu versiyonu aynı koşullar altında 0.025 saniye zaman almıştır. Üç deneyin aldığı zamanları yeniden anımsatmak istiyoruz:
Her defasında read çağrılması (test1.c): 3.040
read fonksiyonu ile blok blok okuma yapılması (test2.c): 0.014
C'nin tamponlu fgetc fonksiyonu il byte byte okuma yapıması (test3.c): 0.025
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* test3.c */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void proc(int ch)
{
/* EMPTY */
}
int main(int argc, char *argv[])
{
FILE *f;
int ch;
double telapsed;
clock_t start, stop;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
start = clock();
if ((f = fopen(argv[1], "rb")) == NULL) {
fprintf(stderr, "cannot open file!...\n");
exit(EXIT_FAILURE);
}
while ((ch = fgetc(f)) != EOF)
proc(ch);
if (ferror(f)) {
fprintf(stderr, "IO error!..\n");
exit(EXIT_FAILURE);
}
fclose(f);
stop = clock();
telapsed = (double) (stop - start) / CLOCKS_PER_SEC;
printf("%.3f\n", telapsed);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bu noktada terminolojiye ilişkin bir noktayı belirtmek istiyoruz. C'nin standart dosya fonksiyonlarının kullandığı FILE yapısına ilişkin
göstericiye İngilizce "stream" denilmektedir. Biz buna Derneğimizde "dosya bilgi göstericisi" diyoruz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C'nin dosya fonksiyonlarının kullandığı tamponun büyüklüğü <stdio.h> içerisindeki BUFSIZ sembolik sabitiyle dış dünyaya ifade edilmiştir.
gcc derleyicilerinde (yani glibc kütüphanesinde) BUFIZ değeri 8192, Microsoft derleyicilerinde 512 biçimindedir. Tabii biz BUFSIZ değerini
değiştirmekle bu tamponun büyüklüğünü değiştirmiş olmayız. Çünkü kütüphane kodları çoktan derlenmiştir. BUFSIZ sembolik sabiti bizim kullanılan
tampon büyüklüğünü öğrenebilmemiz için <stdio.h> içerisinde bulundurulmuştur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir dosyayı fgetc fonksiyonuyla byte byte okumak isteyelim. Bu fonksiyon tamponlu (yani cache sistemi ile) çalıştığına göre etkin bir
kullanıma yol açacaktır. Pekiyi yukarıdaki örnekteki zamanların aşağıdaki gibi olmasının anlamı nedir?
Her defasında read çağrılması (test1.c): 3.040
read fonksiyonu ile blok blok okuma yapılması (test2.c): 0.014
C'nin tamponlu fgetc fonksiyonu il byte byte okuma yapıması (test3.c): 0.025
Neden fgetc fonksiyonun kullanıldığı versiyon bloklo read okumalarının yapıldığı versiyondan daha yavaş çalışmıtır? İki yöntemde de tampon
8192 byte uzunluğundadır. Programın fgetc versiyonunda her byte için fgetc fonksiyonu çaağrılmıştır. Bir fonksiyon çağırmanın da mikro
düzeyde bir zmana kaybı vardır. Ayrıca fgetc fonksiyonu tampondan bilgiyi almak için de biraz zaman kaybı oluşturmaktadır. Bu nedenle standart
C kütüphanesinde fgetc fonksiyonunun yanı sıra getc isimli bener bir fonksiyon da bulundurulmuştur. Bu iki fonksiyon arasındaki tek fark getc
fonksiyonunun bir makro olarak yazılmasına izin verilmesidir. Eğer getc fonksiyonu bir makro olarak yazılmışsa fonksiyon çağırmanın oluşturduğu
göreli zaman kaybı (function call overhead) elimine edilmiş olacaktır. Ancak getc fonksiyonun bir makro biçiminde yazılması zorunlu tutulmamıştır.
Yukarıdaki örnekte fgetc yerine getc fonksiyonun çağrılması gcc derleyicilerinde bir zaman kazancı sağlamamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C'nin <stdio.h> fonksiyonları kütüphanelerde genellikle dosyanın ardışıl tek bir kısmını tamponda tutmaktadır. Bu tampon read/write bir
cache siştemi gibidir. Dolayısıyla bizim dosyaya yazdığımız şeyler de aslında önce tampona yazılmaktadır. Pekiyi tampona yazılan bilgiler
ne zaman dosyaya aktarılacaktır? İşte tampondaki bilgilerin dosyaya yazılması tipik olarak şu durumlarda sağlanmaktadır:
- Tampona dosyanın yeni bir kısmı çekilirken eğer tampon güncellenmişse tampondakiler önce dosyaya yazılırlar. Bu durum tamponlama stratejisi
ile de ilgilidir.
- Dosya fclose fonksiyonu ile kapatılırken eğer tampona bir yazma yapılmışsa tampondakiler dosyaya yazılırlar.
- exit standart C fonksiyonu zaten tüm dosyaları kapatııktan sonra prosesi sonlandırmaktadır. Dolayısıyla en kötü olasılıkla program sonlanırken
tampona yazılmış olan bilgiler dosyaya aktarılmaktadır.
- fflsuh fonksiyonu her çağrıldığında tampondakiler açıkça dosya yazılmaktadır.
Aşağıdaki programı çalıştırınız akış getchar fonksiyonunda beklerken Ctrl+C tuşlarına basarak programı sinyal yoluyla sonlandırınız.
Bu durumda tampona yazılan bilgiler flush edilmeden program sonlandırılacak ve dosyada 0 byte görülecektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *f;
if ((f = fopen("test.txt", "w")) == NULL) {
fprintf(stderr, "Cannot open file!..\n");
exit(EXIT_FAILURE);
}
fputc('a', f);
fputc('b', f);
getchar();
fclose(f);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'nin dosya fonksiyonlarında tamponlu çalışmadan kaynaklanan bir nedenle okumadan yazmaya yazmadan okumaya geçişte ya fflush fonksiynu
çağrılmalı ya da fseek fonksiyonu çağrılmalıdır. Örneğin aşağıdaki işlem hatalıdır ve tanımsız davranışa yol açmaktadır:
ch = fgetc(f);
fputc('x', f);
Burada fgetc ile 1 byte okunduğunda dosya göstericisi 1 byte ilerletilmektedir. Böylece fputc fonksiyonu sonraki offset'e yazılmak istenmiştir.
Ancak işlem hatalıdır. Çünkü okumadan yazmaya yazmadan okumaya geçiş doğrudan yapılmaz. Bu kod şöyle düzeltilebilir:
ch = fgetc(f);
fseek(f, 0, SEEK_CUR);
fputc('x', f);
Burada aslında fseek fonksiyonu dosya göstericisini hareket ettirmemktedir. Ancak fseek okumadan yazmaya geçişte gerekli olan tampon
hazırlığını da yapmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C'de her fopen fonksiyonu ile dosya açıldığında o dosya için o açıma ilişkin ayrı bir tampon oluşturulmaktadır. Yani tampon toplamda
bir tane değildir, dosya başına da bir tane değildir. Her fopen çağrısı ayrı bir tampon oluşturmaktadır. Aslında tampon bilgileri fopen
donksiyonunun geri döndürdüğü FILE türünden yapı nesnesinin içerisinde saklanmaktadır. Biz aynı dosyayı birden fazla kez fopen fonksiyonu
ile açabiliriz. Bu durumda bu açımlardan elde edilen FILE nesnelerinin ayrı tamponları ve ayrı dosya göstericileri olacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C'nin standart dosya fonksiyonları üç farklı tamponlama stratejisi (yani yöntemi) kullanabilmektedir. Buna dosyanın (stream'in) tamponlama
modu da denilmektedir. Bu modlar şunlardır:
1) Tam Tamponlamalı Mod (Full Buffered Mode).
2) Satır Tamponlamalı Mod (Line Buffered Mode)
3) Sıfır Tamponlamalı Mod (No Buffered Mode)
Tam tamponlamalı modda tampon (yani cache) tam kapasiteyle kullanmaya çalışılmaktadır. Dosyanın ardışıl bölümü tapona çekilir. Okunmak istenen
bilgiler mümkün olduğunca tampondan verilir. Tamponun dışna çıkılmışsa dosyanın yeni bir bölümü tampona aktarılır. Tabii bu sırada tampon
kirlenmişse flush edilir. Bu en doğal çalışma modudur.
Satır tamponlamalı modda tampona tek bir satır çekilir. Satırın sonında da '\n' karakteri vardır. Dosyadan okuma yapıldıkça buradan verilir.
Satır bittiğinde yeni bir satır tampona çekilir. Yani tamponda tipik olarak tek bir satır bulunmaktadır. Tabii yine tampon kirlenmişse
yeni satır tampona çekilirken flush işlemi yapılmaktadır. Bu modda tampona yazılan '\n' karakteri kesinlikle flush işlemine yol açmaktadır.
Halbuki tam tamponlamada flush oluşturan özel bir byte yoktur.
Sıfır tamponlamalı modda tampon hiç kullanılmaz. C'nin standart dosya fonksiyonları tamponlamayı kullanmadan doğrudan işletim sisteminin
sistem fonksiyonlarıyla (UNIX/Linux sistemlerinde POSIX fonksiyonlarıyla, Windows sistemlerinde Windows API fonksiyonlarıyla) transferi
gerçekleştirirler.
Biz yukarıda tamponlama modlarının tipik davranışlarını belirttik. Aslında C standartları bu konudaki belirlemeleri biraz muğlak ve kesin
olmayan biçimde yani bir "niyet biçiminde" belirtmiştir. Detayları derleyiciyi yazanlara bırakmıştır. Bu durumda örneğin staır tabanlı
tamponlama seçilse bile işlemler tam tamponlamalı gibi yapılabilir. Standart C kütüphaneleri mümkün olduğunca tamponlama davranışını yukarıda
ıkladıpımız gibi gerçekleştirmektedir. Ancak örneğin normal disk dosyasının '\n' görene kadar satırsal bir biçimde okunması uygun olmayabilmektdir.
Bu nedenle örneğin Microsoft derleyicileri, gcc ve clang derleyicileri (glibc kütüpahnesi) normal disk dosyaları için satır tamponalamlı modda
'\n' karakteri tampona yazıldığında flush işlemi yapmakta ancak okuma sırasında tamponu tamamen doldurmaktadırlar.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi bir dosyayı fopen fonksiyonu ile açtığımızda dosyanın default tamponlama modu nedir? İşte C standartları bu konuda bir şey söylememektedir.
Fakat en normal durum disk dosyaları için default tamponalama modunun tam tamponlama olmasıdır. Yaygın derleyiciler default tamponlama modunu
tam tamponlamalı mod olarak belirlemektedir. Ancak standratlarda stdin, stdout ve stderr dosyalarının default tamponlama modu için bazı
şeyler söylenmiştir. Bunu izleyen graflarda ele alacağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Dosyanın tamponlama modu ve tamponun yeri setbuf ve setvbuf standart C fonksiyonlarıyla değiştirilebilmektedir. sevbuf fonksiyonu setbuf
fonksiyonunu işlevsel olarak kapsamaktadır. Zaten bu fonksiyon setbuf fonksiyonunun yetersizlikleri nedeniyle tasarlanmıştır. setbuf ve
setvbuf fonksiyonları dosya açıldıktan sonra ancak dosya üzerinde henüz hiçbir işlem yapılmadan kullanılmalıdır. Aksi tadirde "tanımsız
davranış (undefined behavior)" oluşmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
setbuf fonksiyonu temel olarak dosya için ayrılan tamponun yerini değiştirmek için kullanılmaktadır. Bu fonksiyon tamponun boyutunu değiştirmez,
Tampon her zaman BUFSIZ uzunluğundadır. Fonksiyon yalnızca tamponun yerini değiştirmektedir. Fonksiyonun prototipi şöyledir:
void setbuf(FILE *stream, char *buf);
Fonksiyonun birinci parametresi tamponu değiştirilecek dosyaya ilişkin dosya bilgi göstericisini belirtmektedir. İkinci parametre yeni
tamponun yerini belirtmektedir. Bu parametreye en azından BUFSIZ uzunluğunda tahsis edilmiş bir alanın adresi geçirilmelidir. Fonksiyonun
ikinci parametresine NULL adres geçirilirse dosya sıfır tamponlamalı moda sokulmaktadır.
Aşağıda tam tamponlamanın nasıl etki gösterdiğine yönelik basit bir örnek verilmiştir. Örnekte önce tamponun yeri setbuf fonksiyonuyla
değiştirilmiş sonra dosyadan okuma yapılıp tampon görüntülenmiştir. Sonra da dosyaya yazma yapılmış yeniden tampon görüntülenmiştir.
Okumadan yazmaya geçerken bir fseek çağırmasının yapıldığında dikkat ediniz. Genellikle fseek çağrıları tamamen tamponu yeniden doldurmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
void disp_hex(const void *buf, size_t size, size_t lbytes)
{
size_t i, k, remainder;
unsigned char *cbuf = (unsigned char *)buf;
for (i = 0; i < size; ++i) {
if (i % lbytes == 0)
printf("%08x ", (unsigned int)i);
printf("%02X ", cbuf[i]);
if (i % lbytes == lbytes - 1) {
for (k = 0; k < lbytes; ++k)
printf("%c", iscntrl(cbuf[i - lbytes + k]) ? '.' : cbuf[i - lbytes + k]);
putchar('\n');
}
}
remainder = size % lbytes;
for (k = 0; k < 3 * (lbytes - remainder); ++k)
putchar(' ');
for (k = 0; k < remainder; ++k)
printf("%c", iscntrl(cbuf[i - remainder + k]) ? '.' : cbuf[i - remainder + k]);
putchar('\n');
}
int main(void)
{
FILE *f;
char buf[BUFSIZ];
if ((f = fopen("test.txt", "r+")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
setbuf(f, buf);
fgetc(f);
disp_hex(buf, BUFSIZ, 16);
printf("---------------------------------------------\n\n");
fseek(f, 0, SEEK_CUR);
fputc('x', f);
disp_hex(buf, BUFSIZ, 16);
fclose(f);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz gibi setvbuf fonksiyonu işlevsel olarak setbuf fonksiyonunu kapsamaktadır. setvbuf fonksiyonu ile biz dosyanın
tamponlama modunu hem de tamponun yerini ve büyüklüğünü değiştirebiliriz. Fonksiyonun prototipi şöyledir:
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
Fonksiyonun birinci parametresi ilgili dosyaya ilişkin dosya bilgi göstericisini, ikinci parametresi yeni tamponun yerini, üçüncü parametresi
yeni tamponlama modunu ve dördüncü parametresi de tamponun yeni uzunluğunu belirtmektedir. Tamponlama modu için şu değerlerden biri
girilmelidir:
_IOFBF (Tam tamponlama)
_IOLBF (Sator tamponlaması)
_IONBF (Sıfır tamponlama)
Tamponlama modunu belirten üçünü parametreye _IONBF değeri girilirse artık ikinci dördüncü parametreler fonksiyon tarafından kullanılmamaktadır.
Fonksiyonun ikinci parametresine NULL adres geçilebilir. Bu durumda tampon fonksiyon tarafından dördüncü parametre belirtilen uzunlukta tahsis
edilmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda sıfır dışı herhangi bir değere geri dönmektedir.
Aşağıdaki örnekte biz açılmış olan dosyanın hem tampon büyüklüğü hem de tamponun yeri değiştirilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
void disp_hex(const void *buf, size_t size, size_t lbytes)
{
size_t i, k, remainder;
unsigned char *cbuf = (unsigned char *)buf;
for (i = 0; i < size; ++i) {
if (i % lbytes == 0)
printf("%08x ", (unsigned int)i);
printf("%02X ", cbuf[i]);
if (i % lbytes == lbytes - 1) {
for (k = 0; k < lbytes; ++k)
printf("%c", iscntrl(cbuf[i - lbytes + k]) ? '.' : cbuf[i - lbytes + k]);
putchar('\n');
}
}
remainder = size % lbytes;
for (k = 0; k < 3 * (lbytes - remainder); ++k)
putchar(' ');
for (k = 0; k < remainder; ++k)
printf("%c", iscntrl(cbuf[i - remainder + k]) ? '.' : cbuf[i - remainder + k]);
putchar('\n');
}
int main(void)
{
FILE *f;
char buf[BUFSIZ * 2];
if ((f = fopen("test.txt", "r+")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
setvbuf(f, buf, _IOFBF, BUFSIZ * 2);
fgetc(f);
disp_hex(buf, BUFSIZ * 2, 16);
printf("---------------------------------------------\n\n");
fseek(f, 0, SEEK_CUR);
fputc('x', f);
disp_hex(buf, BUFSIZ * 2, 16);
fclose(f);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bilgisayar sistemlerinde tüm işlemler aslında elektriksel düzeyde gerçekleştirilmektedir. Örneğin bilgiyarımızın genişleme yuvasına
bir kart taktıldığını düşünelim. Bu kartın üzerinde birtakım entegre devreler potansiyel olarak birtakım işleri yapabilecek biçimde
yerleştirilmiştir. Ancak bu tür donanım birimlerinin de hedef doğruktusunda programlanması gerekmektedir. Bu donanım birimlerine iş
yaptırmak için komutlar elektriksel işaretler biçiminde gönderilmektedir. Ancak bu gönderim de makine komutlarıyla sağlanmaktadır.
Yani genişleme yuvasına takılan kart aslında elektrisksel olarak programlanmakta ancak o elektiriksel işaretlerin karta gönderilmesi için de
programlar yazılmaktadır. Bu tür donanım birimlerinin programlanması genellikle sembolik makine dili düzeyinde yapılmaktadır. Ancak programlama
sırasında gerekli pek çok makine komutu özel komutlardır ve işlemcinin koruma mekanizamasına takılma potansiyelindedir. Bu nedenle bu tür
kodların kernel modda çalştırılması gerekmektedir. İşte bir donanım birimini programlayan ve kernel modda çalışan kodlara "aygıt sürücüler
(device drivers)" denilmektedir. O halde nerede bir donanım aygıtı varsa onu programlayan aşağı seviyeli kodların bulunuyor olması gerekir.
Bu kodlar da aygıt sürücü biçimde yazılımalıdır. Örneğin bir ses kartını genişleme yuvasına taktiğimızda onun aygıt sürücüsü yüklenmedikten
sonra kart bir işe yaramaktadır.
Aygıt sürücüler işletim sisteminin kernel yapısına uygun bir mimari ile yazılmak zorundadır. Her işletim sisteminin belli bir aygıt sürücü
mimarisi vardır. Aygıt sürücü yazımı işletim işletim sistemine hatta aynı işletim sisteminin versiyonlarına göre değişebilmektedir. Nasıl
masaüstü bilgisayarların genişleme yuvalarına kart takmak için o kartın belli özelliklere sahip olması gerekiyorsa bir aygıt sürücünün de
işletim sisteminin belirlediği bazı özelliklere sahip olması gerekmektedir.
Pekiyi aygıt sürücüler ne zaman ve nasıl yüklenmektedir? Aygıt sürücülerin bir bölümü kernel içerisine entegre edilmiş durumdadır. Bir
bölümü sistem boot edilirken yüklenmektedir. Diğer bir bölümü ise gerektiğinde donanın aygıtı sisteme bağlandığında otomatik yüklenmektedir.
Örneğin modern sistemlerde genişleme yuvalarına bir kart takıldığında ya da USB soketine bir donanım birimi takıldığında bu kartlar ve
donanım birimleri kendini sisteme tanıtmaktadır. İşletim sistemelri de kendi aygıt sürücü istesinde bu kartlara ya da donanım birimlerine
ilişkin aygıt sürücüler varsa onları otomatik olarak yüklemektedir. Eskiden bu tür işlemler tamamen manuel yapılıyordu. Her durumda modern
sistemlerde bir aygıt sürücünün yüklenebilmesi ancak sistem yöneticisinin onayıyla yapılabilmektedir. Örneğin UNIX/Linux sistemlerinde
aygıt sürücüleri ancak root kullancısı sisteme yükleyebilir. Windows sistemlerinde default kullanıcı genellike admin durumundadır. Aygıt
sürücüler yüklenirken bir pop pencereyle sistem yöneticisi bilgilendirilir ve onun onayı alınır.
Bazı aygıt sürücleri bir donanım arayüzü biimindedir. Yani sistem programcısından komutlar alıp onu donanıma iletmektedir. Örneğin biz
programcı olarak ses kartına ilişkin aygıt sürücüye komut gönderebiliriz. Bu aygıt sürücü de ses kartını programlayarak işlemleri
gerçekleştirebilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aygıt sürücüler genel olarak birer dosya gibi kullanılmaktadır. Yani aygıt sürücünün bir ismi vardır. Bir dosya nasıl açılıyorsa aygıt sürücü
öyle açılır. Aygıt sürücü dosya gibi açıldıktan sonra ona yazma yapıldığında yazılanlar aygıt sürücüyel gönderilir. Aygıt sürücüden yine
dosya fonksiyonlarıyla okuma yapılır. Ayrıca aygıt sürücülerin içerisindeki önceden belirlenmiş fonksiyonlar user modtan çağrılabilmektedir.
Örneğin UNIX/Linux sistemlerinde bir aygıt sürücü open fonksiyonuyla açılır. Ona write fonksiyonuyla bilgi gönderilir. Ondan read fonksiyonuyla
okuma yapılır. ioctl isimli bir fonksiyonla da aygır sürücüdeki belli fonksiyonlar çalıştırılır. En sonunda aygıt sürücü close fonksiyonuyla
kapatılır.
Örneğin Windows sistemlerinde yine aygıt ssürücü CreateFile API fonksiyonuyla bir dosya biçiminde açılır. WriteFile API fonksiyonuyla
aygıt sürücüye bilgi gönderilir. ReadFile API fonksiyonuyla aygıt sürücüden okuma yapılır. DeviceIOControl API fonksiyonuyla aygıt sürücü
içerisindeki fonksiyonlar çağrılır.
/*------------------------------------------------------------------------------------------------------------------------------------------
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi aygıt sürücüler kabaca nasıl yazılmaktadır? İşte aygıt sürücüleri yazanlar aygıt sürücü açıldığında belli bir fonksiyonun çağrılmasını,
aygıt sürücüye yazma yapıldığında belli bir fonksiyonun çağrılmasını, aygıt sürücüden okuma yapıldığında belli bir fonksiyonun çağrılamsını,
aygıt sürücü kapatıldığında belli bir fonksiyonun çağrılmasını sağlamaktadır. Ayrıca aygıt sürücü içerisindeki bazı fonksiyonlara numaralar
verip user moddan bu fonksiyonların çağrılması sağlarlar.
Örneğin bir termometre devresini kontrol eden bir aygıt sürücü yazacak olalım. Aygıt sürücümüzün bir ismi vardır. Aygıt sürücü bir dosya gibi
ıldığında aygıt sürücümüzün bir fonskyionu çağrılır. Biz orada gerekiyorsa birtakım ilk işlemleri yaparız. Aygıt sürücümüzden okuma yapıldığında
yine bizim bir fonksiyonumuz çağrılır. Örneğin bu fonksiyonda biz termometre devresinden ısıyı alarak okuma yapan programa veririz. Aygıt sürücü
kapatılınca yine bizim bir fonksiyonumuz çağrılır biz de gereken bazı son işlemleri yaparız.
Aygıt sürücüsü içerisindeki fonksiyonlar user moddan çağrılırken yine sistem fonksiyonlarında olduğu gibi proses kernel moddan user moda
otomatik olarak geçirilmektedir. Yani aygıt sürücü içerisindeki fonksiyonlar kernel modda çalıştırılmaktadır. Fonksiyonun çalışması bittiğinde
yeniden user moddan kernel moda geri dönülmektedir.
/*------------------------------------------------------------------------------------------------------------------------------------------
/*------------------------------------------------------------------------------------------------------------------------------------------
Klavye ve ekran aslında donanımsal birimlerdir. Dolayısıyla bunlar aygıt sürücüler tarafından yönetilirler. Yani aslında biz ekrana bir
şeyler yazdırabilmek için yazılacak şeyleri aygıt sürücüye göndeririz. Aygıt sürücü onları ekrana çıkartır. Benzer biçimde biz aslında
klavyeden okuma yaparken aygıt sürücüden okuma yaparız. Aygıt sürücü de kalvyeden alınanları bize verir. Ekran ve klavye birimelrine
"terminal" bunlara aygıt sürücülere de "terminal aygıt sürücüleri" denilmektedir.
C standartlarında ekran ve klavye sözcükleri kullanılmamıştır. Çünkü bir bilgisayar sisteminde ekran ve klavyenin bulunması zorunlu değildir.
Ekran ve klavye yerine C standartlarında "stdout (standart output)" ve "stdin (standart input)" terimleri kullanılmıştır. C standartlarında
stdout ve stdin birer dosya olarak geçmektedir. Böylece örneğin printf fonksiyonu ekrana yazmamaktadır. stdout isimli bir dosyaya yazamktadır.
scanf fonksiyonu klavyeden okumamaktadır. stdin isimli bir dosyadan okuma yapmaktadır. stdout ve stdin dosyalarının gerçekte ne olduğu
standartlarda belirtilmemiştir. Tabii modern masaüstü sistemlerinde stdout dosyası aslında ekranı kontrol eden terminal aygıt sürücüsünü,
stdin dosyası ise klavyeyi kontrol eden terminal aygıt sürücüsünü temsil etmektedir. Aygıt sürücüler birer dosya gibi kullanıldığı için
bunların bir dosya olması da tasarımla uyumludur. Böylece biz bu sistemlerde aslında stdout dosyasına bir şeyler yazdığımızda yazdığımız
şeyler aygıt sürücüsüne gider. Aygıt sürücüsü de onları ekrana çıkartır. Benzer biçimde biz stdin dosyasından bir şeyler okumak istediğimizde
aslında terminal klavyeyi kontrol eden aygıt sürücüden okuma yaparız. O da klavyeden girilenleri bize verir.
Modrn işletim sistemlerinde açık dosyanın hedefi değiştirebilmektedir. Örneğin stdout dosyasına yazılanlar aslında terminal aygıt sürücüsüne
gönderilmektedir. Ancak biz istersek bu dosyaya yazılanların başka bir yere örneğin diskte bir dosyaya gönderilmesini sağlayabiliriz.
Bu işleme işletim sistemleri dünyasında "IO yönlendirmesi (IO redirection)" denilmektedir. O halde biz örneğin IO yöönlendirmesi yoluyla
stdout ve stdin dosyalarını başka hedeflere de yönlendirebiliriz.
/*------------------------------------------------------------------------------------------------------------------------------------------
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux dünyasında aygıt sürücüleri açmakta kullanılan dizin girişleri genel olarak /dev dizininde bulundurulmuştur. Bulunduğunuz
terminalde tty komutunu vererek aygıt sürücünüzün ismini öğrenebilrisiniz. Örneğin:
$tty
/dev/pts/1
Aygıt sürücüler dosya gibi açılıp kullanıldığına göre biz bu aygıt sürücüyü open ile açıp, ona write ile yazma yaparsak yazılanlar o
terminale çıkacaktır. Örneğin:
int fd;
if ((fd = open("/dev/pts/1", O_WRONLY)) == -1)
exit_sys("open");
write(fd, "ankara\n", 7);
close(fd);
İşte aslında C'nin stdout dosyasına yazma yapan fonksiyonları da neticede write fonksiyonunu kullanarak ekranda çıkacak şeyleri bu aygıt
sürücüye yollamaktadır. Örneğin puts fonksiyonunu çağırdığımızda kabaca şu işlemler gerçekleşmektedir:
puts ---> write(stdout, ....) ----> Aygıt sürücü ----> Ekrana basma
Aşaıdaki örnekte önce terminal aygıt sürücüsünden (kalvyeden) okuma yapılıp sonra okunanlar ona (ekrana) yazdırılmıştır. Genellikle
ekran ve klavye işlemleri tek bir aygıt sürücü ile yapılmaktadır. Yani bu aygıt sürücü hem ekranı hem de klavyeyi konrol etmektedir.
Zaten terminal kavramı "ekran ve klavyeyi" içeren bir kavramdır. Dolayısıyla terminal aygıt sürücüsü de her iki aygıtı kontrol eden aygıt
sürücüsüdür.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
int fd;
char buf[4096];
ssize_t result;
if ((fd = open("/dev/pts/1", O_RDWR)) == -1)
exit_sys("open");
if ((result = read(fd, buf, 4096)) == -1)
exit_sys("read");
buf[result] = '\0';
if (write(fd, buf, strlen(buf)) == -1)
exit_sys("write");
close(fd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'de stdin, stdout ve stderr değişken isimleri aslında FILE * türünden dosya bilgi göstericisi belirtmektedir. Bunlar masaüstü sistemlerde
ekran ve klavyeyi kontrol eden aygıt sürücü dosyaları olarak açılmışlardır. Yani biz stdout dosyasına bir şey yazdığımızda aslında yazdığımız
şeyler terminal aygıt sürücüsüne gönderilmektedir. Ancak bu değişkenlerin FILE * türünden yani tamponlu bir dosya belittiğine dikkat ediniz.
Yani diğer dosyalar için nasıl bir tampon eşliğinde aktarım yapılıyorsa bu aygıt sürücü dosyalarına da yine bir tampon eşliğinde aktarım
yapılmaktadır. <stdio.h> içerisinde başı f ile başlamayan fonskiyonlar da aslında birer dosya fonksiyonudur. Ancak onlar default olarak
stdout ve stdin dosyalarını kullanmaktadır. Yani örneğin aşağıdaki iki fonksiyon çağrısı tamamen eşdeğerdir:
fprintf(stdout, ...);
printf(....);
Aşağıdaki gibi çağrı da eşdeğerdir:
fscanf(stdin, ...);
scanf(....);
Zaten C standartları örneğin fprintf fonksiyonunu açıklayıp printf fonksiyonu için fprintf fosnksiyonunun stdout dosyasına yazan biçimi
diye kısa açıklama bulundurmuştur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
35. Ders 14/10/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
UNIX/Linux sistemlerinde bir proses çalışmaya başladığında genel olarak 0, 1 ve 2 numaralı dosya betimleyicileri zaten açık durumdadır.
0 numaralı betimleyici klavyeyi temsil eden terminal aygıt sürücüsüne ilişkindir. Yani bu betimleyiciden read fonksiyonu ile okuma
yapılmak istenirse aslında klavyeden okuma yapılacaktır. stdin betimleyicisinin O_RDONLY modda açıldığı varsayılmaktadır. 1 numaralı betimleyici
de yine terminal aygıt sürücüsüne ilişkindir. Ancak bu betimleyici ile yazma yapıldığında terminal aygıt sürücüsü yazılanları ekrana
bastırmaktadır. 1 numaralı betimleyicinin O_WRONLY açıldığı kabul edilmektedir. 2 numaralı betimleyici default durumda tamamen 1 numaralı
betimleyicde olduğu gibi terminal aygıt sürücüsüne ilişkindir. 2 numaralı betimleyici ile yazma yapılırsa yazılanlar yine ekrana basılacaktır.
UNIX/Linux dünyasında 0 numaralı betimleyiciye "stdin betimleyicisi", 1 numaralı betimleyiciye "stdout betimleycisi" ve 2 numaralı
betimelyiciye de "stderr betimleyicisi" denilmektedir. Bizim açmadığımız 0, 1 ve 2 numaralaı betimleyicileri özel bir durum yoksa biz
kapatmamalıyız. C'deki stdin, stdout ve stderr dosya bilgi göstericileri arka planda UNIX/Linux sistemlerinde sırasıyla 0, 1 ve 2 numaralı
betimleyicileri kullanmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 4096
void exit_sys(const char *msg);
int main(void)
{
char buf[BUFFER_SIZE];
ssize_t result;
if ((result = read(0, buf, BUFFER_SIZE)) == -1)
exit_sys("read");
if (write(1, buf, result) == -1)
exit_sys("write");
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Benzer biçimde Windows sistemlerinde de bir proses çalışmaya başladığında genellikle (ama her zaman değil) klavye, ekran ve hata dosyalarını
temsil eden dosyalar açık durumdadır. Bu dosyalar yine terminal aygıt sürücüsüne ilişkindir. Ancak bunların Windows'taki HANDLE değerleri
önceden belli değildir. Dolayısıyla program çalışırken programcı tarafından GetStdHandle isimli bir API fonksiyonuyla elde edilirler.
Fonksiyonun prototipi şöyledir:
HANDLE WINAPI GetStdHandle(
DWORD nStdHandle
);
Fonksiyona parametre olarak aşağıdakiklerden biri girilebilir:
STD_INPUT_HANDLE
STD_OUTPUT_HANDLE
STD_ERROR_HADNLER
Bu değerler biçizm hangi standart handle'ı elde edeceğimizi belirtmektedir. Fonksiyon başarısızlık durumunda INVALID_HANDLE_VALUE değerine
geri döner. Prosesin standart handle'ları olmayabilir. Bu durumda fonksiyon NULL değerine geri döner. Ancak GetLastError değeri bu durumda
set edilmemektedir.
Windows sistemlerinde de standart C kütühanesindeki stdin, stdout ve stderr dosya bilgi göstericileri GetStdHandle ile elde eidlen aygıt sürücülere
ilişkin dosyaları kullanmaktadır.
Aşağıdaki örnekte Windows sistemlerinde önce stdin ve stdout dosyalarının handle değerleri elde edilmiş sonra stdin dfosyasından okuma yapılıp
okunanlar stdout dosyasına yazdırılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#define BUFFER_SIZE 4096
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hStdIn, hStdOut;
char buf[BUFFER_SIZE];
DWORD dwRead, dwWritten;
if ((hStdIn = GetStdHandle(STD_INPUT_HANDLE)) == INVALID_HANDLE_VALUE)
ExitSys("GetStdHandle");
if ((hStdOut = GetStdHandle(STD_OUTPUT_HANDLE)) == INVALID_HANDLE_VALUE)
ExitSys("GetStdHandle");
if (hStdIn == NULL || hStdOut == NULL) {
fprintf(stderr, "Standard input or standard output handle doesn't exist!..\n");
exit(EXIT_FAILURE);
}
if (!ReadFile(hStdIn, buf, BUFFER_SIZE, &dwRead, NULL))
ExitSys("ReadFile");
if (!WriteFile(hStdOut, buf, dwRead, &dwWritten, NULL))
ExitSys("WriteLile");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Biz default olarak stderr dosyasının (hem C'de hem de POSIX ve API düzeyinde) stdout dosyasıyla aynı terminal aygıt sürücüsüne yönlendirildiğini
belirtmiştik. Pekiyi stdout ile stderr arasında ne farklılık vardır. Biz stdout dosyasına da stderr dosyasına bir şeyler yazdırdığımızda
ikisi de ekranda görüntülenmektedir.
Bir dosya dosyanın bir hedefi vardır. Ancakbu hedef değiştirilebilmektedir. Dosyanın hedefenin hedefinin değiştirilemsine işletim sistemi
dünyasında "IO Yönlendirmesi (IO Redirection)" denilmektedir. Örneğin stdout dosyasının hedefi ekranı kontrol eden eaygıt sürücüsü iken
biz onu bir disk dosyasına yönlendirebiliriz. Bu durumda printf fonksiyonu gibi stdout dosyasına yazan fonksiyonlar aslında dosya yazmış
olurlar. Benzer biçimde biz stdin ve stderr dosyalarını da yönlendirebiliriz.
IO yönlendirmelerinin kernel tarafından nasıl yapıldığı ileride ayrı bir başlıka ele alınacaktır. Biz şimdilik kullanıcı düzeyinde IO
yönlenidrmesi ile ilgileneceğiz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Biz bir programı kabuk üzerindne çalıştırırken program isminden sonra ">" karakterini kullanırsak çalıştırdığımız programın stdout dosyasını
yönlendirmiş oluruz. Örneğin:
./sample > test.txt
Burada stdou dosyasına yazılanlar artık ekrana çıkmatacak "test.txt" dosyasına yazılacaktır. Tabii stderr dosyasına yazılanlar yine
ekrana çıkmaya devam edecektir. Kabuk üzerinde program isminden sonra "2>" karakterlerini kullandığımızda ise stderr dosyasını yönlendirmiş
oluruz. Örneğin:
./sample 2> test.txt
Burada sample programının stderr dosyasına yazdığı şeyler artık ekranda görülmeyecek "test.txt" dosyasına yazılacaktır. Tabii iki yönlendirmeyi
birlikte de yapabliriz. Örneğin:
./sample > test.txt 2> mest.txt
Kabukta yönlendirme Windows'un komut satırında da aynı biçimde yapılmaktadır.
Biz programımızdaki hata mesajlarını stderr dosyasına yazdırmalıyız. Default durumda bu hata mesajları ekrana çıkacaktır. Ancak programı
çalıştıran kişiler IO yönlendirmesi ile hedefi değiştirebilecekleridir. Eğer bi hata mesajlarını doğrudan stdout dosyasına yazdırırsak
bu durumda programın hata mesajlarıyla normal mesajları ayırmanın bir yolu kalmaz.
Örneğin find isimli programla bir dosyayı dizin ağacında aşağıdaki gibi arayacak olalım:
$>find / -name "sample.c"
find bu ii yaparken erişim haklarından dolayı bazı dizinler okuyamayacaktır. Okuyamadığı dizinler için hata mesajlarını program stderr
dosyasına yazdırmaktadır. Default durumda stderr dosyasına yazılanlar ekrana çıkacaktır. Böylece ekranda hem hata mesajları hem de normal
mesajlar görünecektir. Ancak biz stderr dosyasını bir dosyaya yönlendirirsek iki tür mesajın da ekranda gözükmesini engellmiş oluruz. Örneğin:
$>find / -name "sample.c" 2> err.txt
Burada kullanıcı hata mesajlarının dosyada gereksiz yer kaplamasını da istemeyebilir. UNIX/Linux sistemlerinde /dev dizinin altında
bazı özel aygıt sürücüler vardır. Örneğin /dev/null aygıt sürücüsü kendisne yazılan bilgileri doğrudan atmaktadır. O halde find programını
şöyle de öalıştırabiliriz:
$>find / -name "sample.c" 2> /dev/null
Bu kullanımın bir benzeri Windows'ta NUL biçiminde bulunmaktadır.
stdout ve stderr dosyalarını aynı hedefe yönlendirme aaşağıdaki gibi yapılmamalıdır:
./sample > test.txt 2> test.txt
Çünkü genel olarak kabuk programları her yönlendirme sembolünde ilgili dosyayı yeniden "truncate" modunda açmaktadır. Eğer böyle bir şey
isteniyorsa aşağıdkai gibi yapılmalıdır:
./sample > test.txt 2>&1
Burada "2>&1" karakterleri "2 numaralı betimleyiciyi (stderr betimleyicisini) bir numaralı betimleyici ile (stdout betimleyicisi) aynı dosyaya
yönlendir" anlamına gelmektedir.
Normal olarak klavyeden okunacak bilgiler sanki klavyeden girilmiş gibi dosyadan da okunabilir. Bunun için kabuk üzerinde "<" sembolü ile
stdin dosyasının yönlendirilmesi gerekir. Örneğin:
$>sample < test.txt
Burada sample programının stdin dosyasından okudukları aslında "test.txt" dosyasından okunacaktır. Yani biz bu işlemle default olarak
terminal aygıt sürücüsüne yönlendirilmiş olan stdin dosyasınııkça "test.txt" dosyasına yönlendirmiş olduk.
Biz bir dosyadan okuma yapan dosya sonuna geldiğinde sonlanan bir program yazmış olalım. Bu programı stdin dosyasından çalışır hale
getirdiğimizde EOF etkisi nasıl oluşturulacaktır. Ne de olsa terminal gerçek bir dosya değildir. Yani terminalin (kalvyenin) sonuna
gelmek biçiminde bir kavram yoktur. İşte terminal aygıt sürücüleri EOF etkisi yaratan özel tuş kombinasyonları kullanmaktadır. UNIX/Linux
dünyasında "Ctrl+d" tuşları Windows dünyasında "Ctrl+Z" tuşları "dosya sonu" etkisi oluşturmaktadır. Tabii bu tuşlara bastığımızda terminali
kapatmış olmayız. Yaşnızca anlık bir EOF etkisi oluşturulmaktadır. Yine stdin dosyasından okuma yapmaya devam edebiliriz. EOF etkisi
yaratmak için Ctrl+z ve Ctrl+d tuşlarına satır başlarında basınız. Pek çok terminal aygıt sürücüsü ancak satır başlarında bu özel karakterlere
basılmışsa EOF etkisi yaratmaktadır.
Aşağıda bir dosyayı okuyarak ekrana yazdıran bir C programı verilmiştir. Eğer program komut satırı argümanı verilmeden çalıştırılırsa
stdin dosyasından (yani klavyeden) okuma yapmaktadır. İşte bu durumda bu programdan çıkabilmek için UNIX/Linux sistemlerinde "Ctrl+d"
tuşlarına, Windows sistemlerinde ise "Ctrl+Z" tuşlarına basmak gerekir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
FILE *f;
int ch;
if (argc > 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if (argc == 1)
f = stdin;
else
if ((f = fopen(argv[1], "r")) == NULL) {
fprintf(stderr, "cannot open file: %s\n", argv[1]);
exit(EXIT_FAILURE);
}
while ((ch = fgetc(f)) != EOF)
putchar(ch);
if (ferror(f)) {
fprintf(stderr, "cannot read file!..\n");
exit(EXIT_FAILURE);
}
fclose(f);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'deki (işletim sisteminin değil) stdin, stdout ve stderr dosyaları da diğer dosyalarda olduğu gibi tamponlu çalışmaktadır. Yani örneğin
biz stdout dosyasına bir şeyler yazdığımız zaman önce o tampona yazılmakta sonra işletim sisteminin sistem fonksiyonlarıyla (POSIX
fonksiyonları ya da Windows API fonksiyonlarıyla) aktarım yapılmaktadır. Pekiyi C'nin stdin, stdout ve stderr dosyaları iiçin default
tamponlama modu nedir? Standartlar şunları söylemektedir
1) stdout ve stdin dosyaları işin başında eğer interaktif bir aygıta yönlendirilmişse hiçbir zaman tamponlamalı modda olamaz. Ancak satır
tamponlamalı ya da sıfır tamponlamalı modda olabilir. Ancak bu dosyalar interaktif olmayan bir aygıta yönlendirilmişse işin başında tam
tamponlamalı modda olmak zorundadır. Terminal (klavye ve ekran) interaktif aygıt kabul edilmektedir. Normal disk dosyaları interaktif
olmayan aygıtı temsil etmektedir.
2) stderr dosyası ister interaktif bir aygıta isterse interaktif olmayan bir aygıta yönlendirilmiş olsun hiçbir zaman işin başında tam
tamponlamalı modda olamaz.
Standartlardaki bu anlatımdan çıkan sonuçlar şunlardır:
- Biz stdin ve stdout dosyalarını diskte bir dosyaya yönlendirirsek kesinlikle tam tamponlamalı modda olurlar. Ancak bir yönlendirme yapmazsak
C derleyicilerini yazanlara bağlı olarak satır tamponlamalı ya da sıfır tamponlamalı modda olabilirler. Gerçekten de Windows sistemlerinde
işin başında stdout "sıfır tamponalamalı" modda ilen Linux sistemlerinde "satır tamponlamalı" moddadır.
- stderr dosyası bir disk dosyasına yönlendirilmiş olsa bile tam işin başında tamponalamalı modda olamamaktadır. Ancak satır tamponlamalı
ya da sıfır tamponlamalı modda olabilmektedir.
Bu durumda örneğin aşağıdaki gibi bir printf çağrısında yazılanların ekranda görünmesi garanti değildir:
printf("this is a test");
Eğer ilgili derleyici satır tamponlamalı mod kullanıyorsa (Linux derleyicilerinde olduğu gibi) bu yazılanlar henüz ekranda görünmeyecektir.
Eğer ilgil derleyici sıfır tamponlamalı mod kullanıyorsa (Windows derleyicilerinde olduğu) gibi bu yazılanlar ekranda görünecektir.
Pekiyi yukarıdaki printf çağrısında çağrı bittikten sonra yazdırılanların ekranda çıkmasını nasıl garanti ederiz? Bunu garanti etmek
için şunlar yapılabilir:
- Yazdırılacak şeylerin sonuna "\n" karakteri eklenebilir. En kötü olasılık satır tamponalaması olacğına göre yzılanlar kesinlikle
printf bitince ekranda görünecektir. Örneğin:
printf("this is a test\n");
Tabii burada imleç aşağı satırın başına da geçecektir.
- printf çağrısından sonra stdout tamponu flush edilebilir. Örneğin:
printf("this is a test");
fflush(stdout);
- stdout dosyası setbuf ya da setvbuf fonksiyonlarıyla sıfır tamponlamalı moda sokulabilir. Ancak bunun hemen programın başında yapılması
gerekir. Örneğin:
setbuf(stdout, NULL);
printf("this is a test");
C derleyicilerinin çoğunda stdin dosyasından okuma yapılmak istendiğinde bu fonksiyonlar kendi içlerinde önce stdout dosyasını flush
etmektedir. Bu nedenle aşağıdaki bir kodda stdout default durumda satır tamponlamalı olsa bile yazı ekranda görünebilecektir:
printf("input a number:");
scanf("%d", &val);
Ancak C standartlarında böyle bir garanti verilmemiştir. Taşınabilirlik için fflush işleminin açıkça uygulanması gerekir:
printf("input a number:");
fflush(stdout);
scanf("%d", &val);
Her ne kadar C'nin stdin dosyası standartlarda işin başında "satır tamponlamalı ya da sıfır tamponlamalı" modda olabilir denmişse de
pratikte hep bu dosya staır tamponlamalı biçimde oluşturulmaktadır. Bunun endeni işletim sistemlerinin terminal aygıt sürücülerinin klavyeden
satır satır okuma yapmasıdır.stdin dosyası istense de bu sistemlerde sıfır tamponlamalı moda sokulamamaktadır.
Aşağıdaki programı Windows ve Linux sistemlerinde ayrı ayrı derleyerek çalıştrınız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int main(void)
{
printf("ankara");
for (;;)
;
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
stdin dosyasından (klavyeden) biz bir karakter okumak istesek bile bu dosya "satır tamponlamalı" modda olduğu için bir satırlık bilgi
klavyeden okunup tampona yerleştirilecek ve tampondaki, ilk karakter okunacaktır. Her zaman satır tamponlamada satırın sonunda '\n'
karakteri tampona yerleştirilmektedir. Örneğin:
ch = getchar();
ch = getchar();
Burada birinci getchar fonksiyonu eğer stdin tamponununda karakter varsa hemen karakteri tampondan alır. Ancak stdin tamponu boşsa bir
satırlık bilgiyi tampona yerleştirir ve onun ilk karakterini verir. Biz kalvyeden "a" tuşuna basıp ENTER tuşuna basmış olalım. stdin tamponu
şöyle olacaktır:
a\n
İlk getchar fonksiyonu a karakterini tampondna alıp geri döncektir. İkinci getchar fonksiyonu da tampon dolu olduğu için tampondaki \n
karakterini alıp hemen geri dönecektir. C'ye yeni başlayanlar ikinci getchar fonksiyonunun neden klavyeden okumaya yol açmadığına
şaşırmaktadır. Muhtemelen burada satır tamponlamalı çalışma mekanizması bilinmemektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
36. Ders 15/10/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki programda birinci getchar stdin tamponu boş olduğu için bir satırlık bilgiyi stdin tamponuna yerleştirecek ve tampondaki ilk
karakter ile geri dönecektir. İkinci getchar tampon boş olmadığı tamponda \n karakteri olduğu için onu alıp onunla geri dönecektir.
Dolayısıyla ikinci getchar klavyeden bir giriş beklemeyecektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <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;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
stdin işin başında açılmış olan bir C dosyası olduğuna göre onun toplamda tek bir tamponu vardır. Yani stdin dosyasından okuma yapan
standart C fonksiyonları aynı tamponu kullanmaktadır. Aşağıdaki örnekte birinci getchar stdin tamponunu bir satır ile dolduracaktır.
Tamponun sonunda da \n karakteri bulunacakır. gets fonksiyonu (her ne kadar C11 ile C'den kaldırıldıysa da) \n karakterine kadar stdin
dosyasından okuma yapıp okudukarını parametresiyle belirtilen adrsten itibaren yerleştirmektedir. gets fonksiyonu stdin dosyasında \n
karakterini gördüğünde onu okur ancak parametresiyle aldığı adrese yerleştirmez. Onun yerine bu adrese null karakter yerleştirmektedir.
Dolayısıyla aşağıdaki örnekte gets fonksiyonu klavyeden bir giriş beklemeyecektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int main(void)
{
int ch;
char buf[1024];
ch = getchar();
printf("%c\n", ch);
gets(buf);
printf("%s\n", buf);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki döngü nasıl bir etki oluşturur?
int ch;
...
while ((ch = getchar()) != EOF)
putchar(ch);
Burada ilk getchar stdin tamponu boş olduğu için bir satır bilgi isteyip onu tampona yerleştirecektir. Biz burada "ankara" yazısını girip
ENTER tuşuna basmış olalım. Tamponun durumu şöyle olacaktır:
ankara\n
İlk getchar tampondan 'a' karakterini alıp bunu ekrana (stdout dosyasına) yazdıracaktır. Diğer getchar çağrıları tampon dolu olduğu için
diğer karakterleri alıp yazdıracktır. Tamponun sonundaki \n karakteri de alınıp ekrana yazdırılacaktır. Ancak bu karakter ekrana yazdırıldığında
imleç aşağı satırın başına geçecektir. Artık tampon boştur. O halde yazdığımız satırın aynısı ekrana yazılmış olacaktır. Tampon boş olduğu
için aynı olaylar yinelenecektir. Burada Windows'ta Ctrl+z tuşları ile UNIX/Linux sistemlerinde Ctrld+d tuşları ile EOF etkisi yaratarak
döngünden çıkabiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int main(void)
{
int ch;
while ((ch = getchar()) != EOF)
putchar(ch);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Biz gerçekten klavyedem yeni bir giriş yapılması istiyorsak bu durumda stdin tamponunu manuel bir biçimde boşaltmamız gerekir. stdin
tamponunu başaltabilen standart bir C fonksiyonu yoktur. '\n' karakteri görene kadar stdin tamponundan karakterleri atmak için aşağıdaki
gibi bir döngü yeterli olmaktadır:
while (getchar() != '\n')
;
Bu döngüde en son '\n' karakteri okunacak ve döngüden çıkılacaktır. Döngüden çıkıldığında stdin tamponu artık boş olduğuna göre sonraki
fonksiyonlar klavyeden yeni bir giriş isteyecektir. Bu tür durumlarda stdin dosyasının bir disk dosyasına yönlendirilmesi ve dosyanın
sonunda '\n' karakterinin olmaması bir sonsuz döngü oluşumuna yol açabilir. Bu durumu ihmal edebilirsiniz. Eğer bu durumu da ele almak
istiyorsanız döngüyü aşağıdaki gibi düzenleyebilirsiniz:
while ((ch = getchar()) != '\n' && ch != EOF)
;
stdin taponun boşaltılması için bir fonksiyon yazılabilir:
void clear_stdin(void)
{
int ch;
while ((ch = getchar()) != '\n' && ch != EOF)
;
}
Tabii bu fonksiyonu biz zaten boşken çağırırsak fonksiyon bizden giriş ister. Bu fonksiyonu tampon doluyken çağırmalıyız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void clear_stdin(void)
{
int ch;
while ((ch = getchar()) != '\n' && ch != EOF)
;
}
int main(void)
{
int ch;
char buf[1024];
ch = getchar();
printf("%c\n", ch);
clear_stdin();
ch = getchar();
printf("%c\n", ch);
clear_stdin();
gets(buf);
puts(buf);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Anımsanacağı gibi blok içeren makroların do-while biçiminde yazılması gerekmektedir. Yukarıdaki fonksiyon makro biçiminde aşağıdaki gibi
yazılabilir:
#define clear_stdin() \
do \
{ \
int ch; \
\
while ((ch = getchar()) != '\n' && ch != EOF) \
; \
} \
while (0)
-------------------------------------------------------------------------------------------------------------------------------------------*/
#define clear_stdin() \
do \
{ \
int ch; \
\
while ((ch = getchar()) != '\n' && ch != EOF) \
; \
} \
while (0)
int main(void)
{
int ch;
char buf[1024];
ch = getchar();
printf("%c\n", ch);
clear_stdin();
ch = getchar();
printf("%c\n", ch);
clear_stdin();
gets(buf);
puts(buf);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
İstersek işin başında stdout dosyasının da default tapmonlama modunu setbuf ya da setvbuf fonksiyonlarıyla değiştirebiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("ali");
for (;;)
;
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
getchar fonksiyonu stdin dosyasından tek bir karakter okur ve okudğu karakterin sıra numarasına geri döner. getchar dosyanın sonuna gelindiğinde
ya da IO hatası oluştuğunda EOF değerine geri dönmektedir. Fonksiyonun prototipi şöyledir:
int getchar(void);
getchar fonksyonu aşağıdakiyle eşdeğerdir:
fgetc(stdin)
Hatta bazı eski C derleyicilerinde getchar bir makro biçiminde aşağıdkai gibi yazılmıştır:
#define getchar() fgetc(stdin)
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Şimdi de stdin dosyasından okuma yapan standart C fonksiyonları üzerinde biraz duralım.
gets fonksiyonu yukarıda belirttiğimiz gibi C99'da deprecated yapılmış C11'de C'den çıkartılmıştır. Ancak bu fonksiyon yine de yaygın
derleyicilerde geçmişe uyum için bulundurulmaktadır. gets fonksiyonun prototipi şöyledir:
#include <stdio.h>
char *gets(char *s);
gets fonksiyonu stdin dosyasından okuma yapar okuduğu karakterleri programcının belirlediği diziye yerleştirir. En son '\n' karakterini
okuduğunda ya da dosya sonuna gelindiğinde bu karakter yerine hedef diziye '\0' yerleştirip işlemini sonlandırmaktadır. Yani gets fonksiyonu
'\n' karakteri görene kadar ya da dosya sonunagelinene kadar stdin dosyasından okuma yapmaktadır. Ancak '\n' karakterini hedef diziye
yerleştirmemektedir. gets fonksiyonu başarı durumunda parametresiyle belirtilen adresin aynısına geri dönmektedir. Eğer gets hiçbir karakter
okuyamadan EOF ile karşılaşırsa diziye bir yerleştirme yapmaz ve NULL adresle geri döner. gets aynı zamanda IO hatası oluştuğunda da
NULL adresle geri dönmektedir. Bu durumda dizinin içeriği yarı doldurulmuş bir biçimde olabilir.
gets fonksiyonu genellikle getchar fonksiyonu kullanılarak yazılmaktadır. Tipik bir gerçekleştirimi aşağıdaki gibi olabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
char *mygets(char *s);
int main(void)
{
char buf[1024];
printf("Bir yazi giriniz:");
fflush(stdout);
mygets(buf);
puts(buf);
return 0;
}
char *mygets(char *s)
{
int ch;
size_t i;
for (i = 0; (ch = getchar()) != '\n' && ch != EOF; ++i)
s[i] = ch;
if (ch == EOF)
if (i == 0 || ferror(stdin))
return NULL;
s[i] = '\0';
return s;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirtitğimiz gibi C11'de gets fonksiyonu kaldırılmış ve yerine isteğe bağlı biçimde gets_s fonksiyonu eklenmiştir.
gets_s Microsoft derleyicilerinde zaten uzun süredir bulunmaktadır. Ancak gcc ve clang'ın kullandığı glibc kütüphanesinde bu fonksiyon
bulunmamaktadır. gets_s fonksiyonunun prototipi şöyledir:
char *gets_s(char *s, rsize_t n);
Fonksiyon stdin dosyasındna en fazla iinci parametresiyle beliritlen sayıdan bir eksik karakter okur. Yine'\n' karakterini gördüğünde
yada dosya sonuna geldiğinde ya da IO hatası oluştuğunda işlemini sonlandırır. Fonksiyonun geri dönüş değeri gets fonksiyonundaa olduğu
gibidir.
Aşağıda bu fonksiyonun gerçekleştirimi yapılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
char *mygets_s(char *str, size_t size);
int main(void)
{
char buf[5];
mygets_s(buf, 5);
puts(buf);
return 0;
}
char *mygets_s(char *str, size_t n)
{
size_t i;
int ch;
if (str == NULL || n == 0)
return NULL;
for (i = 0; i < n - 1; ++i) {
if ((ch = getchar()) == '\n')
break;
if (ch == EOF) {
if (i == 0 || ferror(stdin))
return NULL;
break;
}
str[i] = ch;
}
str[i] = '\0';
return str;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
gets fonksiyonu gğvensiz olduğundan ve C'den kaldırıldığı için ve gets_s fonksiyonu da isteğe bağlı bulundurulduğu için programcılar
gets yerine fgets fonksiyonunu tercih etmektedir. fgets fonksiyonunun prototipini anımsayınız:
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
fgets fonksiyonu gets_s fonksiyonuna benzemekle birlikte önemli bir farklılığa sahiptir. fgtes eğer '\n' karakterini erken görürse
bu '\n' karakterini de diziye yerleştirmektedir. Oysa gets ve gets_s hiçbir zaman '\n' karakterini diziye yerleştirmemektedir.
fgets fonksiyonunun diğer davranışları gets_s fonksiyonundaki gibidir. Örneğin:
fgets(buf, 10, stdin);
Burada fgtes en fazla 9 karakter okuyacaktır. Çünkü dizinin sonuna null karakteri de yerleştirecektir. Girilen karakterler şunlar olsun:
ankara\n
Burada fgtes '\n' dahil olmak üzere bu karakterlerin hepsini diziye yerleştirir ayrıca yazının sonuna null karakteri de ekler. Şimdi
girilen karakterler şunlar olsun:
kastamonu\n
Burada fgets 9 karakter okurken '\n' karakterini görmediği için '\n' karakterini diziye yerleştirmez. Ancak null karakteri diziye
yerleştirir. Yine fgets henüz hiçbir karakter okunmadan dosya sonuna gelinmişse NULL adresle geri dönmektedir. Normal durumda fgets
parametresi ile belirtilen adresin aynısına geri dönmektedir.
Görüldüğü fgets maalesef bazı durumlarda '\n' karakterini diziye yerleştirmekte bazı durumlarda yerleştirmemektedir. İşte programcılar
genellikle fgets çağrısında sonra "eğer diziye '\n' karakteri yerleştirilmişse onun yerine null karakteri yerleştirmektedir. fgets
fonksiyonu ile stdin dosyasından yazı okuma işlemi tipik olarak şöyle yapılmaktadır:
char buf[10];
char *str;
...
fgets(buf, 10, stdin);
if ((str = strchr(buf, '\n')) != NULL)
*str = '\0';
Burada dizinin sonunda '\n' karakterinin olup olmadığına bakılmış eğer '\n' karakteri dizisinin sonunda varsa onun yerine null
karakter diziye yerleştirilmiştir. Burada fgtes hiçbir okuma yapılmadan dosya sonu ile karşılaşırsa (yani örneğin kullanıcı hemen
Ctrl+z ya da Ctrl+d tuşlarına basarsa) bir anomali oluşacaktır. Programcı bu durumu da eğer gerekiyorsa kontrol etmelidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
scanf fonksiyonunun prototipi şöyledir:
#include <stdio.h>
int scanf(const char *format, ...);
Fonksiyon aslında fscanf fonksiyonunun stdin dosyasından çalışan biçimidir:
#include <stdio.h>
int fscanf(FILE *f, const char *format, ...);
scanf fonksiyonunun bazı ayrıntıları vardır. Biz burada fonksiyonun tamponlamayı ilgilendiren bazı özelliklerini açıklayacağız. scanf
fonksiyonu stdin dosyasından karakter karakter okuma yapar. Eğer format karakterine uygun olmayan bir karakter ile karşılaşırsa onu dosyaya
(tampona) geri yerleştirir ve işlemi sonlandırır. scanf normal durumda başarılı olarak yerşletirdiği parçasayısına geri dönmektedir. Örneğin:
result = scanf("%d%d", &a, &b);
Şimdi dosyada (tamponda) şu karakterler bulunyor olsun:
123istanbul\n
scanf 123 sayısını a nesnesine yerleştirir. Ancak 'i' karakteri "%d" format karakterine uymadığı için işlemini keser. Yani b nesnesine
bir yerleştirme yapmaz ve 1 değerine geri döner. Dosyadaki (tampondaki) karakterler şöyle olsun:
istanbul\n
Burada scanf hiç yerleştirme yapamayacağı için 0 ile geri dönecektir. scanf fonksiyonu baştaki boşluk karakterlerini (leading space)
atmaktadır. Ancak baştaki boşluk karakterlerini atıp henüz format karakterine uygun olmayan bir karakterle de karşılaşmadan dosya sonunu
görürse bu durumda EOF özl değerine geri dönmektedir. Örneğin dosyanın içeriği şöyle olsun (_ karakteri SPACE karakterlerini belirtmektedir):
___EOF
Bu durumda scanf EOF değerine geri dönecektir. Ancak örneğin:
___istanbulEOF
Bu durumda scanf 0 değerine geri döncektir.
scanf işle birden fazla okuma yapılırken scanf baştaki ve girişler arasındaki boşluk karakterlerini atmaktadır. Örneğin:
result = scanf("%d%d", &a, &b);
stdin dosyasında (tamponunda) şu karakterler olsun (_ karakteri SPACE karakterlerini belirtmektedir):
__10___20__\n
scanf burada iki yerleştirmeyi de başarılı bir biçimde yapacak ve 2 değerine geri dönecektir. scanf fonksiyonun baştaki boşluk karakterlerini
(leading space)" attığına ancak sondakileri (trailing space) atmadığına dikkat ediniz. Yukarıdaki okumadna sonra stdin dosyasında (tamponda)
şu karakterler kalacaktır:
__\n
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
scanf fonksiyonu ile menü oluştururken geçirsiz girişlerin ele alınması örneği
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
void clear_stdin(void)
{
int ch;
while ((ch = getchar()) != '\n' && ch != EOF)
;
}
int disp_menu(void)
{
int choice;
printf("1) Kayit ekle\n");
printf("2) Kayit sil\n");
printf("3) Arama\n");
printf("4) Cikis\n\n");
printf("Seciminiz:");
fflush(stdout);
if (scanf("%d", &choice) == 0) {
clear_stdin();
return 0;
}
return choice;
}
int main(void)
{
int choice;
for (;;) {
choice = disp_menu();
switch (choice) {
case 1:
printf("kayit ekleniyor\n\n");
break;
case 2:
printf("kayit siliniyor\n\n");
break;
case 3:
printf("arama yapiliyor\n\n");
break;
case 4:
goto EXIT;
default:
printf("gecersiz giris!..\n\n");
break;
}
}
EXIT:
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
37. Ders 22/10/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
scanf fonksiyonunda format karakterleri arasındaki karakterlerin girişlerde mutlaka bulundurulması gerekir. Örneğin:
scanf("%d/%d/%d", &day, &month, &year);
Burada her ilk iki int değerden hemen sonra bir tane '/' karakterinin kullanılması gerekir. Uygun bir giriş şöyle olabilir:
12/10/2009
'/' karakterelerinin solunda ve sağında boşluk karakterleri bulundurulamaz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int main(void)
{
int day, month, year;
printf("Tarihi dd/mm/yyyy biçiminde giriniz:");
scanf("%d/%d/%d", &day, &month, &year);
printf("%d-%d-%d\n", day, month, year);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
scanf fonksiyonunda format karakterlerindeki herhangi bir boşluk karakteri (SPACE ve \n olabilir) "boşluk karakteri görmeyene kadar stdin'den
okuma yap ve onları at" anlamına gelmektedir. C'yi yeni öğrenenler bu durumu bilmedikleri için scanf fonksiyonunu yanlışlıkla printf gibi
kullanabilmektedir. Örneğin:
scanf("%d%d\n", &a, &b);
Burada programcı yanlışlıkla format karakterlerinin sonuna '\n' karakterini yerleştirmiştir. (Burada '\n' yerine SPACE karakteri de
yerleştirilseydi davranışta bir değişiklik olmayacaktı.) scanf fonksiyonu formatlarinde bir boşluk karakteri gördüğünde "boşluk görmeyene
kadar stdin dosyasından okuma" yapmaya çalışmaktadır. FDolayısıyla burada iki değer girildikten sonra csanf fonksiyonu hemen işlemini
bitirmeyecektir. Örneğin:
scanf("%d / %d / %d", &day, &month, &year);
Burada artık '/' karakterlerinin solunda ve sağında boşluk karakterleri olabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
scanf fonksiyonunun baştaki boşluk karakterlerini (leading space) attığına, ancak sondakileri (trailing space) atmadığına dikkat edniz.
Bu durumda scanf fonksiyonundan sonra gets, getchar gibi fonksiyonları kullanmadan önce stdin tamponun boşaltılması gerekebilir.
scanf fonksiyonunun başka ayrıntıları da vardır. Ancak burada bunların üzerinde d
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
void clear_stdin(void)
{
int ch;
while ((ch = getchar()) != '\n' && ch != EOF)
;
}
int main(void)
{
int a;
char ch;
printf("Bir sayi giriniz:");
fflush(stdout);
scanf("%d", &a);
clear_stdin();
printf("Bir karakter giriniz:");
fflush(stdout);
ch = getchar();
printf("%d %c\n", a, ch);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Daha önceden de belirttiğimiz gibi scanf eğer baştaki bpşluk karakterlerini attıktan sonra EOF ile karşılaşırsa EOF özel değerine geri
dönmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int main(void)
{
int val;
while (scanf("%d", &val) != EOF)
printf("%d\n", val);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
scanf fonksiyonu ile ilgili başka yarıntılar da vardır. Bunları standartlardan elde edebilirsiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde open fonksiyonu ile bir dosya betimleyicisini elde etmiş olalım. Bu betimleyiciden hareketle tamponlu işlemler
yapabilmek ve C'nin standart dosya fonksiyonlarını kullanabilmek için bu dosya betimleyicisinin FILE * türünden bir dosya bilgi göstericisine
geri dönüştürülmesi gerekmektedir. Bu işlem fdopen isimli POSIX fonksiyonuyla yapılmaktadır. Fonksiyonun prototipi şöyledir:
#include <stdio.h>
FILE *fdopen(int fd, const char *mode);
Fonksiyonun birinci parametresi open fonksiyonundan eldee dilmş olan dosya betimleyicisini, ikinci parametresi ise dosya açış modunu
belirtmektedir. Fonksiyon başarı durumunda dosya bilgi göstericisine (stream), başarısızlık durumunda NULL adrese geri dönmektedir.
Şüphesiz burada belirtilen açış modu ile open fonksiyonunda belirtilen açış modunun uyumlu olması gerekmektedir. Tabii bu biçimde bir
dönüştürmeden sonra dosya fclose ile kapatıldığında fclose zaten söz konusu betimleyici ile close fonksiyonunu çağıracaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int fd;
FILE *f;
int ch;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((fd = open(argv[1], O_RDONLY)) == -1)
exit_sys("open");
if ((f = fdopen(fd, "r")) == NULL)
exit_sys("fdopen");
while ((ch = fgetc(f)) != EOF)
putchar(ch);
if (ferror(f)) {
fprintf(stderr, "cannot read!..\n");
exit(EXIT_FAILURE);
}
fclose(f);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde fdopen fonksiyonunun tersini yapan fileno isimli bir fonksiyon da bulunmaktadır. Bu fonksiyon FILE * türünden
dosya bilgi göstericisini bizden alır bize aşağı seviyeli POSIX dosya fonksiyonlarında kullanılabilecek dosya betimleyicisini verir.
Fonksiyonun prototipi şöyledir:
#include <stdio.h>
int fileno(FILE *stream);
Fonksiyon başarı durumunda dosya betimleyicisine, balarısızlık durumunda -1 değerine geri dönmektedir. errno değişkeni uygun biçimde
set edilmektedir.
Aşağıdaki örnekte önce bir dosya fopen fonksiyonuyla açılmış sonra da fileno fonksiyonuya dosya betimleyicisi elde edilip read fonksiyonu
ile dosyadna okuma yapılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int fd;
FILE *f;
char buf[30 + 1];
ssize_t result;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((f = fopen(argv[1], "r")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
if ((fd = fileno(f)) == -1)
exit_sys("fileno");
if ((result = read(fd, buf, 30)) == -1)
exit_sys("read");
buf[result] = '\0';
puts(buf);
fclose(f);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi fdopen ve fileno fonksiyonlarının mantıksal olarak eşdeğerleri Windows'ta var mıdır? Anımsanacağı gibi Windows'ta dosya betimleyicisi
yerine HANDLE türü ile temsil edilen handle değerlei kullanılıyordu. Bu nedenle fdopen fonksiyonun ve fileopen fonksiyonunun Windows eşdeğerleri
yoktur. Ancak Microsoft DOS zamanlarından beri POSIX fonksiyonlarına benzer dosya fonksiyonlarını da bulundurmaktadır. Tabii bı fonksiyonlar
işletim sistemi çekirdeği tarafından desteklenen fonksiyonlar değildir. Bu fonksiyonlar user modda çalışan bir çeşit "sarma (wrapper)"
fonksiyonlardır. Yani söz konsu bu fonksiyonlar aslında arka planda Windows'un API fonksiyonları çağrılarak gerçekleştirilmiştir.
Microsoft'un UNIX/Linux sistemlerindeki POSIX fonksiyonlarına benzer fonksiyonlarının prototipleri <io.h> dosyası içerisinde bulunmaktadır.
Bu fonksiyonlar klasik UNIX tarzı harflendirme ile isimlendirilmiştir ve bunların başlarında bir '_' karakteri bulunmaktadır. Örneğin:
_open ve _sopen_s
_read
_write
_close
_lseek
...
İşte bu <io.h> içerisinde _fdopen ve _fileno fonksiyonları da bulunmaktadır. Ancak bu fonksiyonlar UNIX/Linux sistemlerindeki gibi parametrik
yapıya sahiptirler.
Aşağıda Microsoft'a özgü "sarma <io.h> fonksiyonları" kullanılarak UNIX/Linux strili küçük bir program yazılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <io.h>
void exit_sys(const char *msg);
int main(void)
{
int fd;
FILE *f;
int ch;
if ((fd = _open("sample.c", _O_RDONLY)) == -1)
exit_sys("_open");
if ((f = _fdopen(fd, "r")) == NULL)
exit_sys("_fdopen");
while ((ch = fgetc(f)) != EOF)
putchar(ch);
if (ferror(f)) {
fprintf(stderr, "cannot read file!..\n");
exit(EXIT_FAILURE);
}
fclose(f);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi biz sıfırdan C'nin stdio kütüphanesini yazmak istersek bunu nasıl yapabiliriz? Böyle bir standart stdio kütüphanesi yazabilmek için
kabaca aşağıdaki işlemlerin yapılması gerekmektedir.
- Öncelikle bir FILE yapısının oluşturulması gerekir. C standartları FILE yapısının elemanları hakkında hiçbir açıklama yapmamıştır.
Dolayısıyla FILE yapısını istediğiniz gibi oluşturabilirsiniz. Ana nokta bu FILE yapısının oluşturulmaıdır. Örneğin:
typedef struct {
....
} FILE;
Pekiyi bu FILE yapısının içerisindeki elemanlar neler olmalıdır? Bu FILE yapısının elemanları tamponu yönetebilmek için gerekli bilgileri
barındırmalıdır. Kabaca bu yapıda tutulması gereken elemanlar şunlardır:
- Aşağı seviyeli dosya işlemlerinde kullanılacak işletim sistemine özgü handle değeri (yani UNIX/Linux sistemleri için dosya betimleyicisi,
Windows sistemleri için HANDLE değeri).
- Dosyanın açış modu
- Tamponun yeri ve uzunluğu
- Tamponun aktif noktası (yani dosya işlemi yapıldığında tamponun neresinden işlem yapılacaktır)
- Tamponun kirli (dirty) olup olmadığı bilgisi
- Asıl dosyanın neresinin tamponda olduğu bilgisi
- Son işlem EOF yüzünden başarısızsa bu bilgi FILE yapısının içerisindeki bir bayrakta tutulmalıdır. Benzer biçimde son yapılan IO
işlemi başarısızsa bu da bir bayrakla tutulmalıdır. feof ve ferror fonksiyonları bu bayraklarla geri dönebilir.
- Diğer bilgiler
- fopen fonksiyonu yazılırken önce FILE yapısı ve tampon tahsis edilmeli sonra dosya işletim sisteminni aşağı seviyeli fonksiyonlarıyla
ılmalıdır. Örneğin:
FILE *fopen(const char *path, const char *mode)
{
1) mode parametresi parse edilmeli
2) FILE yapısı tahsis edilmeli
3) Tampon tahsis edilip bilgileri FILE yapısının içerisinde saklanmalı
4) Diğer işlemler
5) FILE yapısının adresi ile geri dönülmeli.
}
C'nin Stdio ktüphenesinin yazımı için bir ipucu (tam bitilmiş bir kod değil)
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* csd_stdio.h */
#ifndef CSD_STDIO_H_
#define CSD_STDIO_H_
#define CSD_EOF -1
#define CSD_BUFSIZ 5
#include <sys/types.h>
typedef struct {
int fd;
unsigned char *beg; /* starting buffer address */
size_t size; /* buffer size */
size_t count; /* number of bytes in the buffer */
unsigned char *pos; /* current buffer address */
off_t offset; /* file offset */
int dirty;
int error;
int eof;
/* .... */
} CSD_FILE;
CSD_FILE *csd_fopen(const char *path, const char *mode);
int csd_fgetc(CSD_FILE *f);
#endif
/* csd_stdio.c */
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include "csd_stdio.h"
static ssize_t refresh_buffer(CSD_FILE *f);
CSD_FILE *csd_fopen(const char *path, const char *mode)
{
CSD_FILE *f;
int i;
static char *modes[] = { "r", "r+", "w", "w+", "a", "a+", "rb", "r+b", "wb", "w+b", "ab", "a+b", NULL };
static int flags[] = { O_RDONLY, O_RDWR, O_WRONLY | O_CREAT | O_TRUNC, O_RDWR | O_CREAT | O_TRUNC, O_WRONLY | O_CREAT | O_APPEND,
O_WRONLY | O_CREAT | O_APPEND, O_WRONLY | O_CREAT | O_APPEND };
if ((f = (CSD_FILE *)malloc(sizeof(CSD_FILE))) == NULL)
return NULL;
for (i = 0; modes[i] != NULL; ++i)
if (!strcmp(mode, modes[i]))
break;
if (modes[i] == NULL) {
free(f);
return NULL;
}
if ((f->fd = open(path, flags[i % 6], S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) {
free(f);
return NULL;
}
if ((f->beg = (unsigned char *)malloc(CSD_BUFSIZ)) == NULL) {
free(f);
return NULL;
}
f->size = CSD_BUFSIZ;
f->count = 0;
f->pos = NULL;
f->offset = 0;
f->dirty = 0;
f->error = 0;
return f;
}
static ssize_t refresh_buffer(CSD_FILE *f)
{
ssize_t result;
if (f->dirty) {
if (lseek(f->fd, f->offset, SEEK_SET) == -1)
return -1;
if (write(f->fd, f->beg, f->pos - f->beg) == -1)
return -1;
}
if (lseek(f->fd, f->offset + f->count, SEEK_SET) == -1)
return -1;
if ((result = read(f->fd, f->beg, f->size)) == -1)
return -1;
f->pos = f->beg;
f->offset = f->offset + f->count;
f->count = result;
return result;
}
int csd_fgetc(CSD_FILE *f)
{
ssize_t result;
if (f->pos == NULL || f->pos == f->beg + f->count) {
if ((result = refresh_buffer(f)) == -1) {
f->error = 1;
return CSD_EOF;
}
if (result == 0) {
f->eof = 1;
return CSD_EOF;
}
}
return *f->pos++;
}
int csd_ferror(CSD_FILE *f)
{
return f->error;
}
/* sample.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "csd_stdio.h"
void exit_sys(const char *msg);
int main(void)
{
CSD_FILE *f;
int ch;
if ((f = csd_fopen("test.txt", "r")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
ch = csd_fgetc(f);
putchar(ch);
while ((ch = csd_fgetc(f)) != CSD_EOF)
putchar(ch);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
freopen isimli standart C fonksiyonu yüksek seviyeli biçimde ""dosya yönlendirmeesi (IO redirection)" yapmaktadır. Fonksiyonun prototipi
şöyledir:
#include <stdio.h>
FILE *freopen(const char *path, const char *mode, FILE *stream);
Fonksiyonun birinci parametresi yönlendirmenin yapılacağı disk dosyasını belirtmektedir. İkinci parametre bu dosyanın nasıl açılacağını
belirtir. Eğer bu ikinci parametre "w" içeren bir parametre ise yönlendirme bu dosyaya yazılacak biçimde yapılmaktadır. Eğer bu parametre
"r" içeren biçimdeyse yönlendirme bu dosyadan okunacak biçimde yapılmaktadır. Fonksiyonun son parametresi yönlendirilecek dosyaya ilişkin
dosya bilgi göstericisini (stream) almaktadır. Fonksiyon başarı durumunda dosyaya ilişkin dosya bilgi göstericisine, başarısızlık durumunda
NULL adrese geri dönmektedir. Fonksiyonun bize verdiği dosya bilgi göstericisi birinci parametreyle belirttiğimiz dosyaya ilişkin dosya
bilgi göstericisidir. Bu işlem sonrasında son parametreyle belirtilmiş olan gösterici ile fonksiyonun geri döndürdüğü gösterici aynı
FILE nesnesini gösteriyor durumda olur. Örneğin:
FILE *f;
if ((f = freopen("test.txt", "w", stdout)) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
Burada stdout dosyası "test.txt" dosyasına yönlendirilmiş durumdadır. Yani artık stdout dosyasına yazılacak şeyler "test.txt" dosyasına
yazılacaktır. Aslında fonksiyon stdout göstericisinin gösterdiği yerdeki FILE nesnesi üzerinde değişiklik yapmaktadır. Dolayısıyla stdout
gösteicisinin gösterdiği yer aslında değişmemekte ve freopen fonksiyonu yine aynı adresi geri döndürmektedir. Dolayısıyla freopen fonksiyonunun
geri döndürdüğü dosya bilgi göstericisine aslında gerçek anlamda gereksinim duyulmamaktadır.
Aşağıda freopen fonksiyonunun kullanımına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *f;
if ((f = freopen("test.txt", "w", stdout)) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 10; ++i)
printf("%d\n", i);
fprintf(f, "Ok\n");
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi freopen ile yaılan yönlendirme geriye alınabilir mi? Yani örneğin biz stdout ya da stderr dosyalarını bir disk dosyasına yönlendirdikten
sonra onları yeniden ilgili aygıt sürücülere yönlendirebilit miyiz? Maalesef bunu yapmanın standart bir yolu yoktur.
IO yönlendirmesi çokça karşılaşılan bir uygulama olduğu halde freopen fonksiyonu çok seyrek kullanılmaktadır. Çünkü freopen fonksiyonunda
yönlendirilecek dosyasının C'ce açılmış bir dosya olması gerekmektedir. Halbuki IO yönlendirmelerinin çoğu daha aşağı seviyede gerçekleştirilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
freopen fonksiyonu ile stdin dosyasını yönlendirebiliriz. Aşağıda bu işleme bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *f;
int ch;
if ((f = freopen("test.txt", "r", stdin)) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
while ((ch = getchar()) != EOF)
putchar(ch);
if (ferror(stdin)) {
fprintf(stderr, "cannot read file!..\n");
exit(EXIT_FAILURE);
}
fclose(f);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
getenv standart C fonksiyonu ile bir çevre değişkeninin değerinin elde eidlmesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *val;
if ((val = getenv("PATH")) == NULL) {
fprintf(stderr, "Environment variable not found!..\n");
exit(EXIT_FAILURE);
}
printf("%s\n", val);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde çevre değişkenini elde eden asıl fonksiyon GetEnvironmentVariable isimli API fonksiyonudur. Aslında
getenv bu sistemlerde bu API fonksiyonunu çağırmaktadır. UNIX/Linux sistemlerinde ise gerçekten asıl fonksiyon getenv fonksiyonudur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#define SIZE 4096
int main(void)
{
char val[SIZE];
DWORD dwResult;
if ((dwResult = GetEnvironmentVariable("PATH", val, SIZE)) == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND) {
fprintf(stderr, "Environment variable not found!..\n");
exit(EXIT_FAILURE);
}
if (dwResult > SIZE) {
fprintf(stderr, "Insufficient buffer!..\n");
exit(EXIT_FAILURE);
}
printf("%s\n", val);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
38. Ders 28/10/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
İşletim sistemleri dünyasında sıkça karşımıza çıkan önemli bir kavram da "çevre değişkenleri (environment variables)" denilen kavramdır.
Sistem programcısının bu kavramı bilmesi ve gerektiğinde bundan faydalanması önemlidir. Biz de bu bölümde "proseslerin çevre değişkenleri"
üzerinde duracağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Proseslerin çevre değişkenleri "anahtar-değer (key-value)" çiftlerini tutan, anahtar verildiğinde ona karşı gelen değerin elde edildiği
sözlük tarzı bir listedir. Anahtarlar ve değerler birer yazı (string) biçimindedir. Çevre değişkenleri konusunda söz konusu anahtar
yazıya "çevre değişkeni (environment variable)" ona karşılık gelen yazıya da "çevre değişkeninin değeri" denilmektedir. Örneğin bir çevre
değişkeni "CITY" isminde olabilir. Ona karşı gelen değer de "Ankara" olabilir.
Çevre değişkenleri ilk bakışta "basit bir sözlük veri yapısı" gibi düşünülmektedir. Konuya yeni başlayan kişiler böyle bir veri
yapısının neden işletim sistemi düzeyinde oluşturulduğunu neden kütüphane fonksiyonlarıyla oluşturulmadığını merak etmektedir.
Gerçekten de pek çok programlama dilinin standart kütüphanesinde ya da bizzat sentaks ve semantik yapısı içerisinde sözlük (dictiobary)
tarzı veri yapıları hazır bir biçimde bulunmaktadır.
Genellikle işletim sistemleri (örneğin Windows, Linux, macOS) prosesin çevre değişkenlerini "anahtar-değer" çiftleri biçiminde liste
tarzı bir veri yapısında saklamaktadır. Çevre değişkenleri için bu sistemlerde prosese özgü bellek bölgelerei bulundurulmaktadır. Yani
çevre değişkenleri pek çok işletim sisteminde proses kontrol blokta değil bizzat prosesin bellek alanında tutulmaktadır. Prosesin çevre
değişken listesinin tutulduğu alan İngilizce genellikle "environment block" biçiminde isimlendirilmektedir.
İşletim sisteminde prosesin çevre değişkenleri default durumda genellikle proses yaratılırken üst prosesten alt prosese aktarılmaktadır.
Yani bir programı hangi program çalıştırıyorsa çalıştırılan program (alt process) çevre değişkenlerini çalıştıran programdan (üst proses)
almaktadır. Ancak program çalışmaya başladıktan sonra programcı bu çevre değişkenleri üzerinde değişiklikler yapabilmektedir. Örneğin biz
Linux ya da macOS sistemlerinde komut satırından bir program çalıştırdığımızda çalıştırdığımız programın çevre değişkenleri onu çalıştıran
komut satırı programında (tipik olarak bash) aktarılmaktadır. Aynı dırım Windows sistemlerinde de böyledir. Windows sistemlerinde bir simgeye
çift tıklayarak bir programı çalıştırdığımızda çalıştırılan programın çevre değişkenleri masaüstü prossi olan "explorer.exe" isimli üst
prosesten alınmaktadır.
Bir sistem programcısının çevre değişikenleri üzerinde bazı işlemleri yapabiliyor olması gerekmektedir. İzleyen paragraflarda çevre değişkenlerine
ilişkin bazı önemli ilemlerin nasıl yapıldığı üzerinde duracağız. Tabii çevre değişkenleri programlana dilinden bağımzıs aşağı seviyeli
bir konu olduğu için ilgili sistemdeki düşük seviyeli fonksiyonlarla gerçekleştirilmektedir. Çevre değişkenleri üzerinde işlemler Windows
sistemlerinde API fonksiyonlarıyla, UNIX/Linux ve macOS sistemlerinde ise POSIX fonksiyonlarıyla yapılmaktadır.
Çevre değişkenlerinin (yani anahtarların) Windows sistemlerinde büyük harf küçük harf duyarlılığı yoktur. Ancak UNIX/Linux sistemlerinde
ve macOS sistemlerinde büyük harf küçük harf duyarlılığı vardır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir çevre değişkeninin anahtarı verildiğinde onun değerini elde etmek için getenv isimli bir standart C fonksiyonu bulundurulmuştur. Bu
fonksiyon genel olarak ilgili sistemdeki daha aşağı seviyeli, fonksiyonları çağırarak işlemi yapmaktadır. getenv fonksiyonunun prototipi
şöyledir:
#include <stdlib.h>
char *getenv(const char *name);
Fonksiyon parametre olarak çevre değişkenin ismini alır. Çevre değişkeninin değerinin bulunduğu char türden dizinin adresine geri döner.
Fonksiyonun geri döndürdüğü adresteki dizi statik biçimde tahsis edilmiştir. Dolayısıyla güvenli bir biçimde kullanılabilir. Ancak bu
adresteki dizi üzerinde değişiklik yapılmamalıdır. Eğer proseste ilgili çevre değişkeni yoksa fonksiyon NULL adresle geri dönmektedir.
Aşağıdaki örnekte komut satırı argümanı ile verilen çevre değişkeninin değeri ekrana (stdout dosyasına) yazdırılmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde ve macOS sistemlerinde getenv aynı zamanda bir POSIX fonksiyonudur. (Aslında tüm standart C fonksiyonları aynı
zamanda bir POSIX fonksiyonu kabul edilmektedir.) Dolayısıyla bu sistemlerde getenv zaten düşük seviyeli bir fonksiyondur. Ancak Windows
sistemlerinde prosesin çevre değişkenini elde edebilmek için GetEnvironmentVariable isimli bi API fonksiyonu kullanılmaktadır. getenv
standart C fonksiyonu Windows sistemlerinde aslında bu API fonksiyonu kullanılarak yazılmıştır.
GetEnvironmentVariable fonksiyonunun prototipi şöyledir:
DWORD GetEnvironmentVariable(
LPCTSTR lpName,
LPTSTR lpBuffer,
DWORD nSize
);
Fonksiyonunun birinci parametresi aranacak çevre değişkenini belirtmektedir. İkinci parametre çevre değişkeninin değerinin yerleştirileceği
char türden dizinin adresini almaktadır. Üçüncü parametre ise bu dizinin uzunluğunu alır. Fonksiyon başarı durumunda diziye yerleştirilen
karakter sayısına geri dönmektedir. Bu sayıya null karakter dahil değildir. Eğer verilen dizinin uzunluğu yetersiz olursa fonksiyon
dizi için gereken uzunluğu (null karakter dahil olacak biçimde) bize verir. Windows sistemlerinde bir çevre değişkeninin maksimum değeri
32768 karakter olabilmektedir.
Fonksiyonda hata kontrolü yapılırken bir noktaya dikkat etmek gerekir. Bir eçvre değişkeni var olduğu halde değeri boş olabilir. Bu durumda
GetEnvironmentVariable fonksiyonu yine 0 değerine geri dönecektir. O halde fonksiyonun 0 değerine dönmesinin iki gerekçesi olabilir. Birincisi
çevre değişkenini bulamaması, ikincisi çevre değişkenini bulması ancak onun değerinin boş olması. Bu nedenle fonksiyon 0 ile geri döndüğü
zaman GetLastError fonksiyonu ile durum tespit edilmelidir.
Aşağıda GetEnvironmentVariable fonksiyonunun kullanımına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define BUFFER_SIZE 4096
int main(void)
{
char value[BUFFER_SIZE];
DWORD dwResult;
dwResult = GetEnvironmentVariable("PATH", value, BUFFER_SIZE);
if ((dwResult == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND)) {
fprintf(stderr, "envirionment variable not found!..\n");
exit(EXIT_FAILURE);
}
if (dwResult > BUFFER_SIZE) {
fprintf(stderr, "buffer too small, requşred size: %u\n", dwResult);
exit(EXIT_FAILURE);
}
puts(value);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde ve macOS sistemlerinde program çalışırken prosesin çevre değişkenine bir ekleme yapmak için setenv ve putenv
isimli POSIX fonksiyonları kullanılmaktadır. Tabii bu fonksiyonlarla eklenen çevre değişkenleri kalıcı değildir. Proses sonlandığında
prosesin bütün çevre değişkenleri zaten yok edilmektedir.
setenv fonksiyonunun protoripi şöyledir:
#include <stdlib.h>
int setenv(const char *envname, const char *envval, int overwrite);
Fonksiyonun birinci parametresi çevre değişkeninin ismini (anahtar değeri), ikinc parametresi ise değerini almaktadır. Son parametre
sıfır ya da sıfır dışı bir değer biçiminde girilir. Bu parametre sıfır dışı bir değer rolarak geçilirse söz konusu çevre değişkeninin
olduğu durumda artık yeni değer set edilir. Bu parametre 0 geçilirse bu durumda çevre değişkeni varsa fonksiyon yine başarılı olur ancak
eçre değişkeninin değeri değişmez.
Aşağıdaki örnekte program çevre değişkeninin ismini vedeğerini komut satırı argümanlarıyla almıştır. Önce eçre değişkenini set etmiş
sonra da get ederek yazdırmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
char *value;
if (argc != 3) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if (setenv(argv[1], argv[2], 1) == -1)
exit_sys("setenv");
if ((value = getenv(argv[1])) == NULL) {
fprintf(stderr, "environment variable not found: %s\n", argv[1]);
exit(EXIT_FAILURE);
}
puts(value);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
putenv fonksiyonunu ele almadan önce tipik bir UNIX türevi işletim sisteminin çevre değişkenlerini bellekte nasıl tuttuğunu açıklamak
istiyoruz. Çevre değişkenleri için "environ" isminde global bir göstericiyi gösteren gösterici bulundurulmaktadır. Bu environ değişkeni
bir gösterici dizisini göstermektedir. Gösterici dizisinin sonunda NULL adres vardır.
environ (char **) -----> adres (char *) -----> "anahtar=değer"
adres (char *) -----> "anahtar=değer"
adres (char *) -----> "anahtar=değer"
...
adres (char *) -----> "anahtar=değer"
adres (char *) -----> "anahtar=değer"
NULL
Örneğin getenv fonksiyonu aslında bu gösterici dizisinde arama yapmaktadır. getenv foksiyonu doğrudan eçvre değişkenini bulursa '='
karakterinin sağındaki adresle geri dönmektedir. setenv fonksiyonu buradaki gösterici dizisine eleman eklemektedir. Örneğin yukarıdaki
çevre değişken bloğuna anahtarı "CITY" olan değeri "ankara" olan bir çevre değişkeni ekleyelim. environ dizisi şu hale gelecektir:
environ (char **) -----> adres (char *) -----> "anahtar=değer" (malloc ile tahsis edilmiş)
adres (char *) -----> "anahtar=değer" (malloc ile tahsis edilmiş)
adres (char *) -----> "anahtar=değer" (malloc ile tahsis edilmiş)
...
adres (char *) -----> "anahtar=değer" (malloc ile tahsis edilmiş)
adres (char *) -----> "anahtar=değer" (malloc ile tahsis edilmiş)
YENİ EKLENEN ADRES -----> "CITY=ankara" (burası malloc ile tahsis ediliyor)
NULL
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
putenv isimli POSIX fonksiyonu da prosesin çevre değişkenlerine ekleme yapmak için kullanılabilir. Fonksiyonun prototipi şöyledir:
#include <stdlib.h>
int putenv(char *string);
Fonksiyon parametre anahtar=değer" biçiminde yazının adresini almaktadır. Alında putenv fonksiyonu yukarıda açıkladığımız çevre değişkenlerine
ek yaparken environ göstericisini gösterdiği yeri malloc ile tahsis etmemktedir. Doprudna bizim verdiğimiz adresi o gösterisi dizisine
yazmaktadır. Dolayısıyla setenv fonksiyonundan farklı bir çalışması vardır. Örneğin:
char buf[] = "COUNTRY=turkey";
putenv(buf);
Bu işlemi yaptıktan sonra çevre değişken bloğu aşağıdaki gibi bir hale gelecektir:
environ (char **) -----> adres (char *) -----> "anahtar=değer"
adres (char *) -----> "anahtar=değer"
adres (char *) -----> "anahtar=değer"
...
adres (char *) -----> "anahtar=değer"
adres (char *) -----> "anahtar=değer"
YENI EKLENEN ADRES (buf) -----> "COUNTRY=turkey"
NULL
Dolayısıyla putenv fonksiyonu ile verilen adresin program sonlanana kadar yaşıyor olması gerekmektedir. putenv fonksiyonu başarı durumunda
0 değerine, başarısızlık durumunda sıfır dışı bir değere geri dönmektedir.
putenv fonksiyonu ile olan bir çevre değişkeninin değerini de değiştirebiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
void exit_sys(const char *msg);
int main(void)
{
char *value;
if (putenv("CITY=istanbul") != 0) /* string'ler statik ömürlüdür */
exit_sys("putenv");
if ((value = getenv("CITY")) == NULL) {
fprintf(stderr, "cannot find environment variable!..\n");
exit(EXIT_FAILURE);
}
puts(value);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde çevre değişkenini değiştirmek ve çevre değişken bloğuna yeni bir çevre değişkeni eklemek için SetEnvironmentVariable
isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL SetEnvironmentVariable(
LPCTSTR lpName,
LPCTSTR lpValue
);
Fonksiyonun birinci parametresi çevre değişkeninin ismini (yani anahtarını), ikinci parametresi ise değerini almaktadır. Fonksiyon başarı
durumunda sıfır dışı bir değere, balarısızlık durumunda 0 değerine geri dönmektedir. Eğer ilgili çevre değişkeni zaten varsa fonksiyon
onun değerini değiştirmektedir. Fonksiyonun ikinci parametresi NULL adres geçilebilir. Bu durumda ilgili eçvre değişkeni silinmektedir.
SetEnvironmentVariable fonksiyonunda ikinci parametre NULL adres geçilirse ilgili çevre değişkeni silinmektedir. Ancak çevre değişkenlerinin
silinmesi seyrek olarak karşımıza çıkabilecek bir durumdur.
Aşağıdaki SetEnvironmentVariable API fonksiyonunun kullanımına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#define SIZE 4096
int main(void)
{
char val[SIZE];
DWORD dwResult;
if (!SetEnvironmentVariable("CITY", "Ankara")) {
fprintf(stderr, "Cannot set environment variable!..\n");
exit(EXIT_FAILURE);
}
if ((dwResult = GetEnvironmentVariable("CITY", val, SIZE)) == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND) {
fprintf(stderr, "Environment variable not found!..\n");
exit(EXIT_FAILURE);
}
if (dwResult > SIZE) {
fprintf(stderr, "Insufficient buffer!..\n");
exit(EXIT_FAILURE);
}
printf("%s\n", val);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
39. Ders 04/11/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Nasıl prosesin çevre değişken bloğuna çevre değişkenleri ekleniyorsa aynı zamanda çevre değişken bloğundan bir çevre değişkeni de silinebilir.
Ancak oluşturulmuş çevre değişkenlerinin değerlerinin değiştirilmesi çok karşılaşılan bir durum iken çevre değişkenlerinin silinmesi
çok seyrek karşılaşılabilecek bir durumdur. Windows sistemlerinde SetenvironmentVariable fonksiyonunun ikinci parametresi NULL adres
girilierek ilgili çevre dğeişkeni silinebilir. Örneğin:
if (!SetEnvironmentVariable("CITY", NULL)) {
fprintf(stderr, "Cannot set environment variable!..\n");
exit(EXIT_FAILURE);
}
UNIX/Linux sistemlerinde çevre değişkenlerinin silinmesi için unsetenv POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
#include <stdlib.h>
int unsetenv(const char *name);
Fonksiyon silinecek çevre değişkeninin ismini (anahtarını) parametre olarak alır ve siler. unsetenv fonksiyonu başarı durumunda 0 değerine,
başarısızlık durumunda -1 değerine geri dönmektedir.
Aşağıdaki Windows örneğinde önce PATH çevre değişkeni elde edilip değeri yazdırılmıştır. Sonra bu çevre değişkeni silinmiştir. Sonra yeniden
elde edilmeye çalışıldığında sorun oluşacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define SIZE 4096
int main(void)
{
char val[SIZE];
DWORD dwResult;
if ((dwResult = GetEnvironmentVariable("PATH", val, SIZE)) == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND) {
fprintf(stderr, "Environment variable not found!..\n");
exit(EXIT_FAILURE);
}
if (dwResult > SIZE) {
fprintf(stderr, "Insufficient buffer!..\n");
exit(EXIT_FAILURE);
}
printf("%s\n", val);
if (!SetEnvironmentVariable("PATH", NULL)) {
fprintf(stderr, "Cannot set environment variable!..\n");
exit(EXIT_FAILURE);
}
if ((dwResult = GetEnvironmentVariable("PATH", val, SIZE)) == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND) {
fprintf(stderr, "Environment variable not found!..\n");
exit(EXIT_FAILURE);
}
if (dwResult > SIZE) {
fprintf(stderr, "Insufficient buffer!..\n");
exit(EXIT_FAILURE);
}
printf("%s\n", val);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde prosesin tüm çevre değişkenlerini elde edebilmek için environ isimli global göstericiyi gösteren göstericiden
faydalanılmaktadır. Ancak göstericinin extern bildirimi herhangi bir başlık dosyasında bulundurulmamıştır. Bu nedenle programcının
environ değişkeninin extern bildirimini kendisinin yapması gerekmektedir:
extern char **environ;
environ global değişkeninin bir gösterici dizisini gösterdiğini ve bu dizinin sonunda NULL adres bulunduğunu anımsayınız:
environ (char **) -----> adres (char *) -----> "anahtar=değer"
adres (char *) -----> "anahtar=değer"
adres (char *) -----> "anahtar=değer"
...
adres (char *) -----> "anahtar=değer"
adres (char *) -----> "anahtar=değer"
NULL
Tabii biz bu biçimde eçre değişkenlerini elde edeken onları tek bir yazı halinde "anahtar=değer" biçiminde elde edeceğiz. Anahtarla değerleri
'=' karaketerini dikkate alarak ayrıştırabilirsiniz. Ayrıştırma işlemini yaparken geçici biçimde '=' karakterini '\0' ile değiştirebilirsiniz.
Ancak çevre değişken bloğunu mümkün olduğunca değiştirmeye çalışmayınız.
Aşağıdaki UNIX/Linux sistemlerinde prosesin tüm çevre değişkenleri elde edilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern char **environ;
int main(void)
{
char *str;
for (int i = 0; environ[i] != NULL; ++i)
puts(environ[i]);
puts("--------------------------");
for (int i = 0; environ[i] != NULL; ++i) {
if ((str = strchr(environ[i], '=')) != NULL) {
*str = '\0';
printf("%s --> %s\n", environ[i], str + 1);
*str = '=';
}
}
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde "env" isimli kabuk komutu kabuğun çevre değişken listesini tıpkı yukarıda yazdığımız programd aolduğu gibi
görüntülemektedir. Programcılar kendi programlarına aktarılacak çevre değişkenlerini bu komutla görüntüleyebilirler.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde GetEnvironmentStrings isimli API fonksiyonu çevre değişken listesini tek bir adresle aşağıdaki gibi vermektedir:
Anahtar=Değer\0Anahtar=Değer\0Anahtar=Değer\0\0
Fonksiyonun prototipi şöyledir:
LPCH GetEnvironmentStrings(VOID);
Burada LPCH aslında char türden bir adres türünü belirtmektedir. (LPSTR türü yanlış izlenmim verebileceği gerekçesiyle kullanılmamıştır.)
Windows'ta komut satırında kabuğun tüm çevre değişkenlerini görüntülemek için "set" komutu kullanılmaktadır.
Aşağıdaki programda prosesin çevre değişken listesi yazıdırlmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main(void)
{
LPCH envStr;
if ((envStr = GetEnvironmentStrings()) == NULL) {
fprintf(stderr, "Cannot get environment strings!..\n");
exit(EXIT_FAILURE);
}
while (*envStr != '\0') {
puts(envStr);
envStr += strlen(envStr) + 1;
}
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Prosesin tüm çevre değişken bloğunu yok etmek için Windows sistemlerinde FreeEnvironmentStrings API fonksiyonu kullanılmaktadır:
BOOL FreeEnvironmentStringsA(
LPCH penv
);
Fonksiyon parametre olarak GetEnvirionmentStrings fonksiyonundan elde edilen adresi almaktadır. Başarı durumunda 0 dışı herhangi bir
değere başarısızlık durumunda 0 değerine geri dönmektedir. Aynı işlem UNIX/Linux sistemlerinde clearenv POSIX fonksiyonu ile yapılmaktadır.
Fonksiyonun prototipi şöyledir:
#include <stdlib.h>
int clearenv(void);
Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda sıfır dışı bir değere (-1 değerine değil) geri dönmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Peikiyi prosesin çevre değişkenlerinden kim ve nasıl faydalanmaktadır? İşte çevre dğeişkenleri genel olarak kullanıcılar tarafından
(ya da bazen programlar tarafından) set edilir. Birtakım fonksiyonlar ve programlar da onlara bakarak eylemleri üzerinde bazı belirlemeler
yaparlar. Örneğin biz Microsoft ya da gcc derleyicilerinde derleme işlemini yaparken açısal parantezler içerisinde ya da iki tırnak içerisinde
berlirtilmiş olan include dosyaları nerede bu derleyiciler tarafından nerede aranmaktadır? İşte derleyiciler bunları belli bir dizinde ararlar.
Ancak komut satırı argümanlarıyla verilen dizinlere ve bazı çevre değişkenlerinin belirttiği dizinlere de bakarlar. Bu sayede biz kendi
include dosyalarımızı bir dizine yerleştirebiliriz ve derleyicimizin o dizine bakmasını çevre değişkenini set ederek sağlayabiliriz.
Aşağı seviyeli programı kullanırken o programların başvurduğu çevre değişkenlerinin neler olduğunu onların dokümanlarından öğrenebilirsiniz.
Örneğin biz bir program yazacak olalım. Programımız bir "data dosyası" kullanacak olsun. Bu data dosyası default durumda programın çalıştığı
dizinde "data.dat" isminde bulunabilir. Ancak programımız kullanıcının bu adat dosasının ismini ve yerini değiştirmesine izin verebilir.
Bunun için biz DATA_LOCATION isminde bir çevre değişkeni uydurabiliriz. Programı kullanan kişinin bu data dosyasını yerleştirdikten sonra
yol ifadesini bu çevre değişkenine yazmasını isteyebiliriz. Böylece kullanıcılar için daha esnek kullanım sunabiliriz. Örneğin:
#define DEFAULT_DATA_PATH "data.dat"
...
char *data_path;
FILE *f;
...
if ((data_path = getenv("DATA_LOCATION")) == NULL)
data_path = DEFAULT_DATA_PATH;
if ((f = fopen(data_path, "r+b")) == NULL) {
fprintf(stderr, "cannot open data file: %s\n", data_path);
exit(EXIT_FAILURE);
}
Buarada eğer kullanıcı DATA_LOCATION isimli eçvre değişkenini oluşturmamışsa data dosyası bulunulan dizinde "data.dat" ismiyle aranacaktır.
Eğer kullanıcı bu çevre değişkenini set etmişse bu durumda bu çevre değişkeninin belirttiği yol ifadesi ile data dosyasının yeri tespit
edilecektir.
Çevre değişkenlerini işletim sistemi düzeyindeki global değişkenler gibi düşünebilirsiniz. Bunları birileri set edip birileri kullanmaktadır.
Kursumuz ilerledikçe çeşitli eçvre değişkenlerinin kim tarafından ve nasıl kullanıldığına yönelik gerçek örneklerle karşılaşacağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Çevre değişkenleri üst prosesten alt prosese aktarılmaktadır. Program kabuk üzerinde çalıştırılıyorsa kabuğun çevre değişkenleri kabuktan
çalıştırılan programlara aktaralacaktır. Bu durumda bir programa çevre değişkenini aktarmak istiyorsak onu kabuğun çevre değişken listesine
ekleyebiliriz.
UNIX/Linux sistemlerinde komut satırında kabuk programının çevre değişken listesine ekleme yapmak için "export" komutu kullanılmaktadır.
Komutun genel biçimi şöyledir:
export değişken=değer
Örneğin:
export CITY=Ankara
Eğer değer boşluk içeriyorsa tırnaklanması gerekir. Örneğin:
export CITY="NewYork City"
Değişken bir kere export edildikten sonra artık değeri export yazılmadan da değiştirilebilir. Örneğin:
CITY=Trabzon
Aynı işlem Windows sistemlerinde set komutuyla u yapılmaktadır. Örneğin:
set CITY=Ankara
Bu sistemlerde değeri değiştirmek için yeniden set kullanmak zorunludur.
UNIX/Linux sistemlerinde komut satırında $isim biçimindeki sentaks "bunu çevre değişkeninin değeri ile değiştir" anlamına gelmektedir.
Örneğin:
$ export CITY=ankara
$ $CITY
ankara: komut bulunamadı
Burada $CITY yazılıp ENTER tuşuna basıldığnda sanki "ankara" yazılıp ENTER tuşuna basılmış gibi bir etki oluşmaktadır. Kabuktaki echo
komutu yazıları ekrana yazdırmak için kullanılmaktadır. Örneğin:
$ echo bugün hava çok güzel
bugün hava çok güzel
O halde bir eçvre değişkeninin değerini ekrana "echo $isim" komutunu uygulayarak yazdırabiliriz. Örneğin:
$ echo $CITY
ankara
Eğer bir çevre değişkeni yoksa kabul $isim yerine boşluk karakteri yerleştirmektedir. Yani bu durum bir hataya yol açmamaktadır.
Windows'ta çevre değişkenini değeriyle değişirmek için %isim% sentaksı kullanılmaktadır. Örneğin:
C:\>set CITY=ankara
C:\>echo %CITY%
ankara
Şimdi de bir çevre değişkeninin değerinin sonuna ekeleme yaapalım:
$ export CITY=Ankara
$ echo $CITY
Ankara
$ CITY=$CITY"-Istanbul"
$ echo $CITY
Ankara-Istanbul
Şimdi aynı şeyi Windows'ta yapalım:
C:\>set CITY=Ankara
C:\>echo %CITY%
Ankara
C:\>set CITY=%CITY%-Istanbul
C:\>echo %CITY%
Ankara-Istanbul
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Biz kabuk üzerinde kabuk komutlarıyla kabuğa bir çevre değişkeni eklediğimizde o çevre değişkeni yalnızca o kabuk prosesinin çevre değişken
listesine eklenmektedir. Başka bir terminal penceresi açtığımızda o eçvre değişkeni orada görünmeyecektir. Çünkü her terminalde aynı
kabuk çalışıyor olsa bile bunlar farklı proseslerdir. (Örneğin biz sample programını birden fazla kez çalıştırabiliriz. Bunların her
biri ayrı prosesler olur.) Pekiyi biz çevre değişkenlerinin her kabuk tarafından görünmesini nasıl sağlayabiliriz. İşte bu işlemler
genel Windows ve UNIX/Linux (macOS'te UNIX türevi bir sistem gibidir) sistemlerinde farklı biçimlerde sağlanmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde bir kabuk programı çalıştırılırken önce bazı "script dosyalarına" bakıp oradaki komutları çalıştırmaktadır.
Kabuk programlarının çalışmaya başladığında otomatik olarak baktığı bu dosyalara İngilizce "shell start-up files" denilmektedir. Bu dosyaların
neler olduğu kabuktan kabuğa ve kabuğun çalıştırma biçimlerine göre değişebilmektedir. Bix burada bash kabuğunun start-up dosyalarına
değineceğiz. bash kabuğunun baktığı start-up dosyalar onun nasıl çalıştırıldığına göre değişebilmektedir. bash programı üç biçimde
çalıştırılabilmektdir:
1) Interactive login shell
2) Interactive nopn-login shell
3) Non-interactive shell
Burada "interactive" sözcüğü komut satırlı çalışmayı belirtmektedir. Yani kullanıcı bir komut uygular komut çalıştırılır yeniden prompt'a
düşülür. Buradaki "login shell" kabuk çalıştırıldığında "user name/password" sorulup sorulmayacağını belirtmektedir. Nihayet kabuk programları
tek bir komut çalıştırılarak sonlandırılabilmektedir. Bu çalışma biçimine de "non-interactive" çalıştırma biçimi denilmektedir. Örneğin
biz bash programını "Ctrl+F+N" tuşlarına basarak çalıştrırsak "interactive login shell" biçiminde çalıştırmış oluruz. (Ctrl+F+N tuşlarıya
ılan terminallere "sanal terminal (virtual terminal)" da denilmektedir. Eğer biz grafik arayüz içerisinde terminal açarsak buradaki bash
programını "interactive non-login shell" biçiminde çalıştırmış oluruz. Bu tür terminal açmaya "sahte terminal (pseudo termimal)" de denilmektedir.
Nihayet biz bash programını "-c" seçenei ile çalıştırırsak bir tek komutu çalıştırıp bash programını sonlandırırız. Bu biçimdeki çalışma
da "non-interactive" çalışmadır.
1) Eğer bash "interactive login shell" biçiminde çalıştırılmışsa önce "/etc/profile" dosyasına başvurur. Eğer bu dosya varsa buradaki
komutları çalıştırır. Sonra "~/.bash_profile", "~/.bash_login", "~/.profile" dosyalarından ilk olarak hangisini bulursa yalnızca onu
çalıştırır.
2) Eğer bash "interactive non-login shell" biçiminde çalıştırılırsa bu durumda "~/.bashrc" dosyasını okuyup çalıştıurmaktadır. "~/.bashrc"
dosyası bazı programlar tarafından oluşturulmuş olabilir. Bu durumda komutları dosyanın sonuna ekleyebilirsiniz.
bash programının dokğmanlarında bu konuyla ilgili bilgileri bulabilirsiniz:
https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
40. Ders 05/11/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde aslında masaüstü "explorer.exe" denilen bir prosestir. Dolayısıyla aslında biz masaüstünden bir program çalıştırdığımızda
(terminal programı da dahil olmak üzere) bu programı bu "explorer.exe" programı çalıştırmaktadır. Çevre değişkenleri de üst prosesten
alt prosese aktarıldığına göre bizim yapmamız gereken şey çevre değişkenlerini kalıcı bir biçimde "explorer.exe" prosesine yerleştirmektir.
Bunu sağlamak için Windows 11'de "Ayarlar/Sistem Bilgisi/Gelişmiş Sistem Ayarları/Ortam Değişkenleri" penceresi açılır. Buradaki pencerede
alt alta iki bölüm bulunmaktadır. Üstteki o anki aktif kullanıya ilişkin çevre değişkenlerini, aşağıdaki ise sistem genelindeli çevre
değişkenlerini belirtmektedir. Eğer biz bir çevre dğeişkeninin belli bir kullanıcı için eklersek o çevre değişkeni yalnızca o kullanıcı
ile giriş yapıldığında etkili olmaktadır. Eğer biz bir çevre değişkeninin sistem genelinde eklersek bu durumda bundan tüm prosesler yani
başka kullanılar da etkilenecektir. Tabii buradaki pencerelerde çevre değişkenleri üzerinde güncellemeler yapıldığı zaman bu güncellemeler
o anda çalışmakta olan prosesleri etkilemeyecektir. Çünkü çevre değişkenleri proses yaratılırken üst prosesten aktarılmaktadır. Bu tür
durumlarda programlardan çıkıp onları yeniden çalıştırmalısınız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi proses içerisinde pek çok çevre dğeişkeni görmekteyiz. Bunlar nereden gelmektedir? Aslında işletim sistemlerinde sistem boot
edilirken peşi sıra bir grup proses yaratılmaktadır. Yani bir program başkasını o da başkasını çalıştırmaktadır. İşte bu sırada çeşitli
prosesler yeni çevre değişkenlerini eklerler. Böylece çevre değişkenleri arta arta oluşmaktadır. Tabii en sonunda kabuk programı da
yukarıda belirttiğimiz start-up dosyalardaki komutları çalıştırdığında oradan da çevre değişkenleri gelmektedir. Örneğin Linux'ta
"user name/password" login isimli program tarafından sorulmaktadır. login programı girişi başarılı bir biçimde yaparsa "/etc/passwd"
dosyası içerisinden elde ettiği bilgilerden hareketler HOME, USER, SHELL, PATH, LOGNAME, MAIL çevre değişkenlerini oluşturmaktadır.
Kabul programı login programı tarafından çalıştırılmaktadır. İşte bash isimli kabuk programının kendisi de birtakım çevre değişkenlerini
prosese eklemektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Daha önceden de belirttiğimiz gibi çalışmakta olan programalara "proses (process)" denilmektedir. İşletim sistemleri bir proses yaaratıldığında
prosesi izlemek için ismine "Proses Kontrol Block (Process Control Block)" bir veri yapısı oluşturmaktadır. Windows, Linux, macOS gibi
koruma mekanizmasına sahip işlemlerin kullanıldığı işletim sistemlerinde prosesler genel olarak birbirinden izole edilmiştir. Biz bir
programı birden fazla kez çalıştırdığımızda oluşturulan prosesler birbirinden bağımsızdır.
İşletim sistemlerinde pek çok özellik prosese özgdür. Örneğin her prosesin ayrı bir "çalışma dizini (current working directory)"si vardır.
Her prosesin açtığı dosyalar diğerinden farklıdır. Yetkiler yine prose özgüdür. Her prosesin bellek alanı biribirinden ayrılmıştır. Örneğin
sistemlerin çoğunda "heap" alanı da prosese özgüdür.
Eskiden proseslerin tek bir akışı oluyordu. Ancak 90'lı yıllarla birlikte "thread" kavramı işletim sistemlerine sokulmuştur. Bugünkü
modern işlem sistemleri "multithread" çalışmaya izin vermektedir. Thread'ler proseslerin bağımsız çizelgelenen akışlarıdır. Örneğin
programın bir akışı foo fonksiyonunda ilerlerken diğer bir akışı bar fonksiyonunda ilerleyebilir. İşte böyle prosesin bağımsız akışlarına
"thread" denilmektedir. Bir proses tek bir akışka (yani thread ile) çalışmaya başlar. Prosesin diğer thread'lerini programcı kendisi
yaratmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bugün kullandığımız kapasiteli modern işletim sistemleri thread temelinde "zaman paylaşımlı (time sharing)" bir çalışma mekanizması
oluşturmaktadır. Bu mekanizmada proseslerin thread'leri bir kuyruk sisteminde toplanır. Buna "çalışma kuyruğu (run queue)" denilmektedir.
Çalışma kuyruğundan sıradaki thread alınır, CPU'ya atanır. Belli bir süre onun CPU'da çalışmasına izin verilir. O süre dolduğunda thread
CPU'dan alınarak sıradaki diğer thread CPU'ya atanır. O da belli bir süre çalıştırılır. Bir thread'in parçalı çalışma süresine "quanta süresi
(time quantum)" denilmektedir. Quanta süreleri işlemcinin hızı da göz önünde bulundurularak işletim sistemleri tarafından belirlenmektedir.
Quanta süresi uzun tutulursa interaktivite azalır, kıs turulursa birim zamanda yapılan iş miktarı (througput) düşer. Kullanıcı programının
sürekli çalıştığı gibi bir illüzyon yaşamaktadır. Aslında kullanıcının programı kesikli kesikli çalıştırılmaktadır. Bir thread'in CPU'ya
atanıp belli bir süre çalıştırılması ve çalışma bittikten sonra diğer bir thread'in CPU'ya atanması sürecine "bağlamsal geçiş (context switch)"
denilmektedir. Tabii context switch işlemi de belli bir zman almaktadır. İşletim sistemelrinin bu işlemlerle uğraşan alt sistemlerine
"çizelgeleyici (scheduler)" denilmektedir.
Bugün bilgisayarlarımızda birden fazla işlemci ya da çekşrdek vardır. Ancak zaman paylaşımlı çalışma modelinde bir farklılık oluşmamaktadır.
Nasıl bir işletmeden tek bir yemel servis noktası yerine birden fazla servis noktası olduğunda temel kuyruk mekanizmasında bir farklılık
oluşmuyorsa aynı durum çok işlemcili ya da çekirdekli sistemlerde de benzer biçimdedir. Tipik olarak işletim sistemleri her CPU ya da
çekirdek için ayrı çalışma kuyrukları oluşturmaktadır. Tabii işletim sistemi bu CPU ya da çekirdeklerdeki iş yükünü de dengelemeye çalışır.
(Yani örneğin bir CPU ya da çekirdeğin açlışma kuyruğunda az sayıda thread kalmışsa başka kuyruktaki thread'leri buraya taşıyabilmektedir.)
CPU'ya atanan bir thread'in quanta süresini bitirdiğinde CPU'dan kopartılması donanım kesmeleri yoluyla zorla yapılmaktadır. Bu tür sistemlere
"preemtive" sistemler denilmektedir. Bazı sistemlerde thread CPU atandığında zorla koparma işlemi yapılmaz. Thread CPU'yu kendisi kendi
isteğiyle bırakır. Aslında bu tür sistemler eskiden genellikle thread'siz sistemler olarak karşımıza çıkıyordu. Thread'siz sistem tek thread'li
sistem gibi düşünülebilir. Bu tür işletim sistem sistemlerine "non-preemptive" sistemler ya da "cooperative multitask" sistemler denilmektedir.
Örneğin Windows 3.X işletim sistemleri, PalmOS işletim sistemleri böyle sistemlerdi. Ancak günümüzde sistemler hep preemtive biçimdedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Prosesler konusunda en temel işlem bir prosesin yaratılmasıdır. Bir prosesin yaratılması demekle aslında dolaylı olarak bir programın
çalıştırılması kastedilmektedir. Biz programları işletim sisteminin sunduğu kabuk ortamından faydalanarak çalıştırmaktayız (örneğin
Windows'ta bir dosyaya çift tıklayarak, Linux'ta kabukta programın ismini yazarak.) Aslında proseslerin yaratılması işletim sisteminin
sistem fonksiyonlarıyla yapılmaktadır. Dolayısıyla kabuk programları da aslında bu sistem fonksiyonlarını kullanmaktadır. Proses yaratmak
için Windows'ta sistem fonksiyonlarını çağıran API fonksiyonları UNIX/Linux macOS sistemlerinde de POSIX fonksiyonları bulunmaktadır.
Biz bu bölümde öcne Windows sistemlerinde sonra UNIX/Linux ve macOS sistemlerinde proses yaratımı üzerinde duracağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde proses yaratmak için (yani bir programı çalıştırabilmek için) CreateProcess isimli API fonksiyonu kullanılmaktadır.
Fonksiyonun prototipi şöyledir:
BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
Fonskiyonun birinci parametresi çalıştırılacak olan programın yol ifadesini almaktadır. Bu yol ifadesi mutlak ya da göreli olabilir.
Fonksiyonun ikinci parametresi programın komt satırı argümanlarını belirtmektedir. Bu komut satırı argümanları tek bir yazı biçiminde
oluşturulur. Ancak bu parametrenin const olamayan bir gösterici olduğuna dikkat ediniz. Fonksiyon verilen adreste değişiklik yapıp sonra
onu eski haline getirmektedir. Bu nedenle ikinci parametrenin bir string olarak girilmemesi gerekir. (C'de stringlerin karakterlerinin
değiştirilmesi tanımsız davranışa yol açmaktadır.) Komut satırı argümanları fonksiyon tarafından boşluk karakterlerinden ayrıştırılıp
gösterici dizisine yerleştirilmekte ve main fonksiyonun argv parametresi bu biçimde oluşturulmaktadır. Tabii ilk komut satırı argümanının
programın ismi olması zorunlu olmasa da genel bir kabuldür. Örneğin:
char szCmdLine[] = "C:\\windows\\notepad.exe test.txt";
...
CreateProcess("C:\\windows\\notepad.exe", szCmdLine, ...);
Burada "C:\windows\notepad.exe" programı çalıştırılmak istenmiştir. Komut satırı argümanları boşluk parse edildiğinde iki tane olacaktır:
C:\\windows\\notepad.exe
test.txt
Program için uzantı verilmezse programın default ".exe" uzantılı olduğu kabul edilmektedir.
Fonksiyonun birinci parametresi NULL adres geçilebilmektedir. Bu durumda çalıştırılacak program ikicni parametredeki boşluksuz ilk string
olarak belirlenmektedir. Örneğin:
char szCmdLine[] = "C:\\windows\\notepad.exe test.txt";
...
CreateProcess(NULL, szCmdLine, ...);
Burada birinci parametre NULL adres geçilmiştir. Bu durumda fonksiyon ikinci parametrededki ilk string'i çalıştırılacak program olarak
ele alır. İkinci parametrenin tamamı yine aynı zamanda komut satırı argümanı olmaktadır. Genellikle programcılar bu birinciyi parametreyi
NULL geçip çalıştırılacak programı ve komut satırı argümanlarını ikinci parametreye girerler. Ancak bu durumda çalıştırılacak program
boşluklardan parse edildiği için dikkat etmek gerekir. Windows eğer iki tırnaklanmamışsa boşluklardan sırasıyla keserek aramayı yapar.
Örneğin ikinci parametre şöyle girilmiş olsun:
"c:\\program files\\sub dir\\program name"
Burada sistem sırasıyla şu aramaları yapar ve ilk bulduğu dosyayı çalıştırmaya çalışır:
c:\program.exe
c:\program files\sub.exe
c:\program files\sub dir\program.exe
c:\program files\sub dir\program name.exe
Bu tür durumlarda boşluk içeren kısım iki tırnak içerisine alımabilir. Örneğin:
"\"c:\\my folder\\prog.exe\" ali veli selami"
CreateProcess fonksiyonunun ikinci parametresinde önemli bir özellik vardır. (Ancak bu özellik birinci parametresinde yoktur.) Eğer
CreateProcess fonksiyonun birinci NULL geçilip ikinci parametresindeki ilk boşluksuz yazı ile belirtilen program isminde hiçbir ters
bölü karakteri kullanılmamışsa bu durum özel bir anlama gelmektedir. Bu durumda söz konusu "çalıtırılabilir (executable)" dosyanın
aranması sırasıyla şu biçimde yapılmaktadır:
1) CreateProcess uygulayan prosesin ".exe" dosyasının bulunduğu dizin
2) CreateProcess uygulayan prosesin çalışma dizini (current working directory)
3) 32 Bit Windows Sistem Dizini ("Windows" dizininin içerisindeki "System32" dizini)
4) 16 bit Windows Sistem Dizini ("Windows" dizininin içerisindeki "System" dizini)
5) Windows dizininin kendisi
6) CreateProcess uygulayan prosesin PATH çevre değişkininde belirtlen dizinler sırasıyla
Tabii bu sırada arama yapılırken eğer söz konusu dosya bulunursa aramaya deavm edilmemektedir. Burada dikkat edilmesi gereken önemli
nokta şudur. Yukarıdaki arama davranışı yalnızca "CreateProcess fonksiyonun birinci paraöetresi NULL geçilip, ikinci paramtresindeki
ilk boşluksuz yazının hiçbir ters bölü karakteri içermemesi durumunda" gösterilmektedir. Yukarıdaki arama listesinin sonundaki PATH
çevre değişkenine dikkat ediniz. Eğer CreateProcess söz konusu dosyayı diğer dizinlerde bulamazsa PATH çevre değişkeninin değerinde
belirtilen dizinlerde de aramaktadır. PATH çevre değişkeninin değeri Windows sistemlerinde ";" karakteriyle ayrılan alanlardan oluşnmaktadır.
Her ";" arası ayrı bir dizini belirtmektedir. Örneğin:
C:\Program Files (x86)\VMware\VMware Player\bin\;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.4\bin;
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.4\libnvvp;C:\Program Files\Java\jdk-11.0.2\bin;
C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;
...
O halde CreateProcess son maddedeki aramaya yapmak için PATH çevre değişkeninin değerini elde etmekte ve buradaki yazıyı ";" karakterlerinden
ayrıştırmaktadır. Tabii eğer söz konusu çalıştırılabilir dosya bu PATH çevre değişkeni ile belirtilen birden fazla dizinde varsa ilk
bulunan dizindeki program çalıştırılmaktadır.
Windows'un komut satırı uygulaması olan "cmd.exe" programında biz komut satırında bir şeyler yazıp ENTER tuşuna bastığımızda CreateProcess
bu "cmd.exe" tarafından uygulanmaktadır. Dolayısıyla eğer komutun ilk boşluksuz kısmında hiçbir "\" karakteri yoksa dosya nihayetinde
PATH çevre değişkeni ile belirtilen dizinlerde de aranacaktır. Tersten gidersek "eğer biz komut satırında bir programın ismini yazdığımızda
onun çalışmasını istiyorsak onun bulunduğu dizini PATH çevre değişkenin değerinde belirtmelitiz."
CreateProcess fonksiyonunun üçüncü ve dördüncü parametreleri (lpProcessAttributes ve lpThreadAttributes) kernel tarafından yaratılacak olan
proses ve thread nesnelerinin güvenlik bilgilerini (yetki derecelerini) belirtmektedir. Ancak Windows sistemlerinde proseslerin yetki dereceleri
UNIX/Linux sistemlerindeki kadar basit değildir. Biz bu kursta bu konu üzerinde durmayacağız. Bu konu "Windows Sistem Programlama" kurslarında
ele alınmaktadır. Buradaki SECURITY_ATTRIBUTES bir yapı belirtmektedir. Bu iki parametreye default olarak NULL geçilebilmektedir.
Windows işletim sisteminde "kernel nesnesi (kernel object)" denildiğinde kendi HANDLE değeri olan kernel tarafından izlenen bir grup nesne
anlaşılmaktadır. Bu terim UNIX/Linux sistemlerinde kullanılmamaktadır. Örneğin CreateFile fonksiyonu ile açmış olduğumuz dosyalar da
"kernel nesnesi" grubundadır. Tüm kernel nesneleri Windows sistemlerinde ortak bazı özelliklere sahiptir. Örneğin hepsi CloseHandle API
fonksiyonuyla boşaltılmaktadır. Örneğin tüm kernel nesnelerinin bir güvenlik (yetki) bilgisi vardır. Bu güvenlik bilgisi o kernel nesnesinin
yaratılması sırasında CreateXXX fonksiyonlarının LPSECURITY_ATTRIBUTES parametresiyle temsil edilmektedir.
CreateProcess fonksiyonunun beşinci parametresi (bInheritHandles) kernel nesnelerinin yaratılmakta olan alt prosese aktarılıp aktarılmayacağnı
belirtmektedir. Bu bir ana şalter görevindedir. Bu parametreye sıfır dışı bir değer (örneğin TRUE değeri) geçirilirse aktarım yapılmaktadır.
sıfır değeri (FALSE) geçirilirse aktarım yapılmamaktadır. Aslında her kernel nesnesi yaratılırken LPSECURITY_ATTRIBUTES parametresiyle
o kernel nesnenin bireysel olarak alt proseslere aktarılabilirliği belirtilmektedir. Ancak buradaki parametre ana şalter görevindedir. Yani
ilgili kernek nesnesi yaratılırken "alt prosese aktarılsın" demiş olsak bile bu parametre FALSE geçildikten sonra aktarım yapılmamaktadır.
Programcı "özel bir gerekçesi yoksa" bu parametreye TRUE geçebilir.
CreateProcess fonksiyonunun altıncı parametresi (dwCreationFlags) yaratılacak prosesin bazı özelliklerini belirlemek için kullanılmaktadır.
Bu parametreye bazı sembolik sabitler bit düzeyinde OR işlemine sokularak girilebilir. Kursumuzda buradaki bayraklar üzerinde durmayacağız.
Ancak thread'ler konusunda buraya atıfta bulunacağız. Bu nedenle bu parametreyi 0 olarak geçebiliriz.
Fonksiyonun yedinci parametresi (lpEnvironment) yaratılacak alt prosin çevre değişken listesini belirtmektedir. Buraya aşağıdaki gibi bir
bloğun adresi geçirilmelidir:
değişken=değer\0değişken=değer\0....değişken=değer\0\0
Eğer bu parametreye NULL adres geçilirse alt prosesin çevre değişken listesi üzet prosesten alınmaktadır. Genellikle bu parametreye NULL
adres geçilmektedir.
Fonksiyonun sekizinci parametresi (lpCurrentDirectory) oluşturulacak olan alt prosesin çalışma dizinini belirtmektedir. Yani üst proses
isterse alt proses yaratıldığında onun çalışma dizininin ne olması gerektiğini belirleyebilmektedir. Bu parametre de NULL adres geçilebilir.
Bu durumda alt prosesin çalışma dizini üst prosesle aynı olur. Genellikle bu parametre NULL adres biçiminde geçilmektedir.
CreateProcess fonksiyonunun dokuzuncu parametresi STARTUPINFO isimli bir yapı nesnesinin adresini almaktadır. Programcı bu yapı nesnesinin
içini doldurmalı ve adresini fonksiyona geçirmelidir. Bu oldukça fazla elemana sahiptir. Yaratılacak prosesin bazı ikincil özelliklerinin
belirlenmesi amacıyla kullanılmaktadır. Bu yapıdaki 0 elemanları default değer anlamına gelir. Dolayısıyla programcı yapı elemanlarını
sıfırlayarak default değerler geçebilir. Bu parametreye NULL adres girilememektedir. Burada küçük ayrıntı vardır. STARTUPINFO yapısının
ilk elemanı olan cb elemanına yapının sizeof değeri geçirilmelidir. Bunun nedeni ileriye doğru uyumun korunmak istenmesidir. Bu tür yapılara
ileride elemanlar eklenebilmektedir. Bu durumda yapının hangi versiyonunun kullanıldığının anlaşılabilmesi için bu elemandan faydalanılmaktadır.
Bu yapı nesnesinin elemanlarının sıfırlanması şöyle yapılabilir:
STARTUPINFO si = {sizeof(STARTUPINFO)};
Fonksiyonun onuncu ve son parametresi (lpProcessInformation) PROCESS_INFORMATION isimli bir yapı nesnesinin adresini almaktadır. Bu
yapı nesnesini programcı doldurmaz. Bu yapı nesnesi fonksiyon tarafından doldurulmalıdır. Bu yapı aşağıdaki gibi bildirilmiştir.
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
Bir proses yaratıldığında iki kernel nesnesi de yaratılmaktadır. Bunlardan biri proses için diğeri ise ana thread içindir. Bu kernel
nesnelerinin handle değerleri ve id değerleri vardır. İşte bu fonksiyon bu değerleri yerleştirmektedir. Bu parametreye de NULL adres
geçilememektedir.
Fonksiyon başarı durumunda sıfır dışı bir değere başarısızlık durumunda sıfır değerine geri dönmektedir.
Fonksiyonun örnek bir kullanımı şöyle olabilir:
char szPath[] = "notepad.exe";
STARTUPINFO si = {sizeof(STARTUPINFO)};
PROCESS_INFORMATION pi;
...
if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
ExitSys("CreateProcess");
Aşağıdaki örnekte "prog1" ve "prog2" biçiminde iki program bulunmaktadır. prog1 programı CreateProcess uygulayarak prog2 programını
çalıştırmaktadır. Bu çalıştırmada birinci NULL geçildiği için ve ikinci parametrede programın ismi "\" karakteri olmadan belirtildiği
için yukarıda açıklamış olduğumuz arama işlemleri belirttiğimiz dizinlerde en sonunda da PATH çevre değişkeniin belirtildiği dizinde
uygulanacaktır. Bu örnekte "prog2" programının aranan dizinlerin birinde olması gerekmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* Prog1.c */
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
char szPath[] = "prog2.exe prog1.c";
char env[] = "ALI=10\0Veli=20\0";
STARTUPINFO si = {sizeof(STARTUPINFO)};
PROCESS_INFORMATION pi;
if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, env, "c:\\windows", &si, &pi))
ExitSys("CreateProcess");
Sleep(1000);
printf("Ok\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(int argc, char *argv[])
{
LPCH envStr;
char cwd[MAX_PATH];
printf("Prog2 command line arguments:\n");
printf("--------------------------\n");
for (int i = 0; i < argc; ++i)
puts(argv[i]);
printf("--------------------------\n");
printf("Prog2 environment variables:\n");
if ((envStr = GetEnvironmentStrings()) == NULL) {
fprintf(stderr, "Cannot get environment strings!..\n");
exit(EXIT_FAILURE);
}
while (*envStr != '\0') {
puts(envStr);
envStr += strlen(envStr) + 1;
}
printf("--------------------------\n");
printf("Prog2 current workşng directory:\n");
if (!GetCurrentDirectory(MAX_PATH, cwd))
ExitSys("GetCurrentDirectory");
printf("%s\n", cwd);
printf("--------------------------\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte chrome programı komut satırı argümanları verilerek çalıştırılmıştır. Ancak "chrome" programı sizin bilgisayarınızda
farklı bir dizine yüklenmiş olabilir. Sizin chrome programının kendi bilgisayarınızdaki yol ifadesini vermeniz gerekir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
char szPath[] = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", csystem.org cumhuriyet.com\"";
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pa;
if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pa))
ExitSys("CreateProcess");
printf("Ok\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz gibi Windows sistemlerinde HANDLE türüyle belirtilen tüm nesnelere (dosyalara, thread'lere, proseslere vs.)
"kernel nesneleri" denilmektedir. Tüm kernel nesneleri prosese özgü "proses handle tablosu" denilen bir tabloda giriş belirtmektedir.
Tüm kernel nesnelerinin yok edilmesi (ya da kapatılması) CloseHandle isimli API fonksiyonu ile yapılmaktadır. Bir prosesi yarattıktan sonra
PROCESS_INFORMATION yapısı ile elde ettiğiniz proses ve thread handle alanlarını CloseHandle fonksiyonuyla kapatabilirsiniz. Bunları kapatıyor
olmanız bu prosesin sonlandırılacağı anlamına geşmemektedir. Zaten program sonlandığında bütün kernel nesneleri kapatılmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
char szPath[] = "notepad.exe";
STARTUPINFO si = {sizeof(STARTUPINFO)};
PROCESS_INFORMATION pi;
if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
ExitSys("CreateProcess");
printf("Ok\n");
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
CreateProcess API fonksiyonu bir kernel fonksiyonudur. Yani en alt seviyedki proses yaratan fonksiyondur. Windows'ta kernel API fonksiyonlarının
yanı sıra ismine "Shell API fonksiyonları" denilen bir grup API fonksiyonu da bulunmaktadır. Shell API fonksiyonarı daha yüksek seviyeli
fonksiyonlardır. Bunlar işlemlerini yaparken kernel API fonksiyonlarını çağırmaktadır. Örneğin bir programı çalıştırmak için en aşağı
seviyeli API fonksiyonu CreateProcess fonksiyonudur. Ancak ShellExecute isimli bir shell API fonksiyonu da bulunmaktadır. ShallExecute
fonksiyonu yüksek seviyeli bir fonksiyondur ve nihai olarak CreateProcess fonksiyonunu çağırmaktadır. Burada biz ShellExecute fonksiyonu
üzerinde de duracağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
ShellExecute kabuk fonksiyonunun en önemli özelliği "dosya ilişkilendirmesini" dikkate almasıdır. Yani biz bu fonksiyon ile örneğin bir
".docx" dosyasını çalıştırmak istersek ShellExecute bu uzantının ilişkin olduğu programı tespit eder ve CreateProcess fonksiyonu ile o
programı ("word.exe" programını) çalıştırır. Bu ".docx" dosyasını da bu programa ("word.exe" programına) komut satıı argümanı yapar.
Hangi uzantılı dosyaların hangi programla ilişkilendirildiği "registry" denilen dosyalarda tutulmaktadır. Bu registry dosyalarında
manuel işlem yapabilmek için "regedit" isimli bir program da bulundurulmaktadır. Biz masaüstünde bir dosyaya çift tıkladığımız zaman
ya da komut satırında bir dosyanın ismini yazıp ENTER tuşuna bastığımız zaman aslında ShellExecute fonksiyonu ile çalıştırma işlemi yapılmaktadır.
Dosya ilişkilendirmelerine bakan ShellExecute fonksiyondur. Ancak proses aslında CreateProcess fonksiyonu tarafından yaratılmaktadır.
ShellExecute ---> Dosya ilişkilendirmesine bak ----> CreateProcess
ShellExecute API fonksiyonunun parametrik yapısı şöyledir:
HINSTANCE ShellExecuteA(
HWND hwnd,
LPCSTR lpOperation,
LPCSTR lpFile,
LPCSTR lpParameters,
LPCSTR lpDirectory,
INT nShowCmd
);
Fonksiyonun birinci parametresi (hwnd) UI mesajlarının yazdırılacağı pencerenin üst penceresini belirtmektedir. Biz bu konu üzerinde
durmayacağız. Ancak bu parametreye NULL adres geçilebilir. Fonksiyonun ikinci parametresi yapılacak işleme ilişkin bir komut yazısını
almaktadır. omut belirten bu yazılar şunlar olabilir:
"edit"
"explore"
"find"
"open"
"print"
"runas"
Program çalıştırmak için bu komut yazsının "open" biçiminde girilmesi gerekmektedir.
Fonksiyonun üçüncü parametresi (lpFile) işlem uygulanacak dosyanın yol ifadesini belirtmektedir. Bu yol ifadesi mutlak ya da göreli olabilir.
Burada belirtilen dosya çalıştırılabilir bir dosya olabileceği gibi bir doküman dosyası da (doküman dosyası demekle çalıştırılabilir olmayan
dosyalar kastedişmektedir) olabilir. Yukarıda belirttiğimiz gibi bu durumda ShellExecute aslında bu doküman dosyasının ilişkin olduğu
program dosyasını çalıştırıp bu doküman dosyasını komut satırı argümanı yapmaktadır. Buradaki dosya ismi çalıştırılabilir bir dosya ise
ve bu dosya ismi hiç "\" içermiyorsa yine CreateProcess fonksiyonunda açıkladığımız dizinlere ve PATH çevre dğeişkenine bakılmaktadır.
Fonksiyonun dördüncü parametresi (lpParameters) üçüncü
parametre çalıştırılabilir bir dosya ise ona aktarılacak komut satırı argümanlarını belirtmektedir. Doküman dosyalarında bu parametre NULL
geçilebilir. Beşinci parametre (lpDirectory) çalıştırılacak programın çalışma dizinini belirtmektedir. Bu parametre NULL geçilebilir. Bu
durumda ShellExecute fonksiyonunu uygulayan prosesin çalışma dizini kullanılır. Fonksiyonun son parametresi (nCmdShow) GUI penceresinin
hangi boyutta açılacağını belirtmektedir. Bu parametre SW_NORMAL olarak geçilebilir. Bu durumda programa ilişkin GUI penceresi normal boyutla
(restore boyutunda) açılmaktadır.
ShellExecute fonksiyonu başarı durumunda >= 32 olan bir değere geri dönmektedir. Ancak fonksiyonun geri dönüş değeri 16 bit Windows uyumunu
korumak için HINSTANCE olarak alınmıştır. HINSTACE void bir adres biiminde typedef edilmiştir. Dolayısıyla karşılaştırma öncesinde bu değerin
adres ile aynı uzunlukta bulunan bir tamsayı türüne dönüştürülmesi gerekir. Bu tür <windows.h> içerisinde INT_PTR olarak typedef edilmiştir.
Aşağıda ShellExecute fonksiyonunun kullanımına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <shellapi.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HINSTANCE hResult;
hResult = ShellExecute(NULL, "open", "test.txt", NULL, NULL, SW_NORMAL);
if ((INT_PTR)hResult < 32)
ExitSys("ShellExecute");
printf("Ok\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
ShellExecute fonksiyonunun ikinci parametresinde "explore" komutu kullanılırsa "windows explorer" açılmaktadır. Bu durumda üçüncü parametrenin
bir dizin belirtmesi gerekmektedir. Aşağıdaki örnekte "windows dizini" windows explorer ile programlama yoluyla açılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <shellapi.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HINSTANCE hResult;
hResult = ShellExecute(NULL, "explore", "c:\\windows", NULL, NULL, SW_NORMAL);
if ((INT_PTR)hResult < 32)
ExitSys("ShellExecute");
printf("Ok\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
42. Ders 12/11/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi Windows'ta kendi uzantılı dosyalarımızı kendi programlarımızla nasıl ilişkilendirebiliriz? Bunun için öncelikle bizim programımızı
komut satırı argümanı alacak biçimde organize etmemiz gerekir. Yani rpogramımızda en azsından argc > 1 koşulu sağlanmaıdır. Çünkü ShellExecute
ilgili dokğman dosyasını bizim programımıza komut satırı argümanı olarak (argv[1]) geçirecektir. Tabii bizim dosya ilişkilendirmesini de
yapmamız gerekir. Dosya ilişkilendirmesi yukarıda da belirttiğimiz Windows'un "registry" kayıt dosyalarına yazılmalıdır. Ancak bu işlem manuel
olarak da yapılabilmektedir. Manuel ilişkilendirme ve silme Windows versiyonları arasında farklılıklar gösterebilmektedir.
Programlama yoluyla dosya ilişkilendirmesi için hazır shell API fonksiyonu yoktur. Windows registry dosyalarına erişim için ismine
"Resgistry API'leri" denilen API fonksiyonları bulundurmaktadır. Registry API fonksiyonlarının hepsi RegXXX biçiminde isimlendirilmiştir.
Registry işlemlerini yapabilmek için hangi ayarın registry içerisinde nerede tutulduğunun bilinmesi gerekmektedir. Bu bilgiye Microsoft
dokümanlarından erişilebilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde her prosesin sistem genelinde tek (unique) olan bir "proses id" değeri vardır. Proses id değerleri DWORD türü ile
temsil edilen tamsayı değerlerdir. Ancak bir prosesle ilgili işlem yapabilmek için onun handle değerinin elde edilmesi gerekmektedir.
Anımsanacağı gibi CreateProcess API fonksiyonunda biz bir proses yarattığımız zaman onun id ve handle değerlerini fonksiyonun PROCESS_INFORMATION
parametresi yoluyla elde edebiliyorduk. Windows sistemlerinde proses id değerleri sistem genelinde tek olduğu halde handle değerleri tek
değildir. Biz bir proses nesnesini her açtığımızda değişik bir handle değeri elde edebiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Proseslerle ilgili sık gereksinim duyulan bir işlem de proses listesinin elde edilmesidir. Bu işlem Windows sistemlerinde bir grup API
fonksiyonuyla yapılmaktadır. EnumProcesses isimli API fonksiyonu sistemdeki tüm proseslerin (bazı ayrtıntılar vardır) ID değerlerini elde
etmek için kullanılmaktadır. EnumProcesses fonksiyonunun prototipi şöyledir:
#include <psapi.h>
BOOL EnumProcesses(
DWORD *lpidProcess,
DWORD cb,
LPDWORD lpcbNeeded
);
Fonksiyonun birinci parametresi prosslerin id değerlerinin yerleştirileceği DWORD dizinin adresini almaktadır. Fonksiyonun ikinci parametresi
birinci parametresindeki dizinin byte uzunluğunu (eleman uzunluğunu değil) belirtmektedir. Üçüncü parametre ise diziye yerleştirilen
byte sayısının (eleman sayısının değil) yerleştirileceği DOWORD nesnenin adresini almaktadır. Fonksiyon başarı durumunda 0 dışı değerine,
başarısızlık durumunda 0 değerine geri dönmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <Psapi.h>
#define NPROC_IDS 4096
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
DWORD dwProcessIds[NPROC_IDS];
DWORD dwNeeded;
DWORD i;
if (!EnumProcesses(dwProcessIds, sizeof(dwProcessIds), &dwNeeded))
ExitSys("EnumProcesses");
for (i = 0; i < dwNeeded / sizeof(DWORD); ++i)
printf("%lu\n", (unsigned long)dwProcessIds[i]);
printf("%lu process Ids listed...\n", dwNeeded / sizeof(DWORD));
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Sistemdeki prosesler hakkında bilgi elde edebilmek için id değeri yetmemektedir. Bunun için proseslere ilişkin handle değerlerine
gereksinim vardır. İşte prosesin id değerinden handle değerinin elde edilmesi için OpenProcess isimli API fonksiyonu kullanılmaktadır.
Fonksiyonun prototipi şöyledir:
HANDLE OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId
);
Fonksiyonun birinci parametresi açış işleminin ne amaçla yapılacağnı belirten bayrak değerlerinin bit OR işlemine sokulmasıyla oluşturulmaktadır.
Buradaki bayrak değerleri PROCESS_XXXX biçiminde isimlendirilmiş sembolik sabitlerden oluşmaktadır. Proses bilgisini almak için bu
parametreye en azından PROCESS_QUERY_INFORMATION|PROCESS_VM_READ bayraklarının girilmesi gerekmektedir. Fonksiyonun ikinci parametresi
bu handle değerinin alt prosese aktarılabilirliğini belirtmektedir. Bu parametre FALSE olarak geçilebilir. Üçünü parametre ilgili prosesin
id değerini belirtmektedir. Fonksiyon başarı durumunda proses nesnesinin handle değerine, başarısızlık durumunda NULL adrese geri dönmektedir.
Windows sistemlerinde bir proses nesnenin açılabilmesi için ilgili prosesin bazı yetkilere sahip olması gerekmektedir. Dolayısıyla biz
bu fonksiyonla id değerini bildiğimiz her prosesi açamayız.
Windows sistemlerinde bağımsız olarak yüklenme bilgilerine sahip olan "exe" ve "dll"" dosyalarına "modül (module)" denilmektedir. Bir
uygulama bir "exe dosya" ve birtakım "dll dosyalarından" oluşmaktadır. Uygulamayı oluşturan modüllerin elde edilmesi EnumProcessModules
fonksiyonuyla yapılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL EnumProcessModules(
HANDLE hProcess,
HMODULE *lphModule,
DWORD cb,
LPDWORD lpcbNeeded
);
Fonksiyonun birinci parametresi prosesin handle değerini belirtmektedir. İkinci parametre proses modüllerinin handle değerlerinin yerleştirileceği
HMODULE türünden dizinin adresini almaktadır. HMODULE proses modüllerini temsil eden handle değeridir. Fonksiyonun üçüncü parametresi
bu dizinin byte uzunluğunu (eleman uzunluğunu değil) belirtmektedir. Fonksiyonun son parametresi diziye yerleştirilen byte sayısınının
(eleman sayısının değil) yerleştirileceği DOWORD nesnenin adresini almaktadır. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık
durumunda sıfır değerine geri dönmektedir. Her zaman prosesin ilk modülü "exe dosyaya ilişkin" modüldür.
Nihayet modülün handle değerindne hareketle GetModuleBaseName API fonksiyonu ile modülün ismi elde edilebilmektedir. Fonksiyonun prototipi
şöyledir:
DWORD GetModuleBaseName(
HANDLE hProcess,
HMODULE hModule,
LPSTR lpBaseName,
DWORD nSize
);
Fonksiyonun ilk iki parametresi sırasıyla ilgili prosesin ve modülün handle değerlerini belirtmektedir. Çüncü parametre modül isminin
yerleştirileceği dizinin adresini alır. Fonksiyonun son parametresi ismin yerleştirileceği dizinin uzunluğunu belirtmektedir. Fonksiyon
başarı durumunda diziye yerleştirilen karakter sayısına başarısızlık durumunda 0 değerine geri dönmektedir.
Aşağıdaki örnekte prosesimizin yetki bakımdan elde edebileceği proses bilgileri yazdırılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <psapi.h>
#define NPROCESSES 4096
#define NMODULES 4096
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
DWORD dwProcessIds[NPROCESSES];
DWORD cbNeeded;
HANDLE hProcess;
HMODULE hModules[NMODULES];
DWORD dwProcessCount;
DWORD dwModuleCount;
char szModuleName[MAX_PATH];
if (!EnumProcesses(dwProcessIds, sizeof(dwProcessIds), &cbNeeded))
ExitSys("EnumProcesses");
dwProcessCount = cbNeeded / sizeof(DWORD);
for (DWORD i = 0; i < dwProcessCount; ++i) {
if ((hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessIds[i])) == NULL)
continue;
if (!EnumProcessModules(hProcess, hModules, sizeof(hModules), &cbNeeded)) {
CloseHandle(hProcess);
continue;
}
dwModuleCount = cbNeeded / sizeof(DWORD);
for (DWORD k = 0; k < dwModuleCount; ++k) {
if (!GetModuleBaseName(hProcess, hModules[k], szModuleName, MAX_PATH))
continue;
if (k == 0) {
printf("%s: ", szModuleName);
continue;
}
if (k != 1)
printf(", ");
printf("%s", szModuleName);
}
printf("\n\n");
CloseHandle(hProcess);
}
printf("%lu process(es) listed...\n", dwProcessCount);
getchar();
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
C derleyicilerinin çok büyük kısmında komut satırı argümanlarıyla "önceedn tanımlanmış (predefined) sembolik sabitler" oluşturulabilmektedir.
Bunun için Microsoft derleyicilerinde /D seçeneği, gcc ve clang derleyicilerinde -D seçeneği kullanılmaktadır. Bu seçeneklerin argümanı
sembolik sabitin ismini almaktadır. Örneğin:
cl -D TestXXX sample.c
Burada sanki "sample.c" dosyasının tepesinde aşağıdaki gibi bir satır varmış gibi işlem uygulanmaktadır:
#define TestXXX
Tabii sembolik sabite değer de verilebilir. Örneğin:
cl /D TestXXX=123 sample.c
Burada da sanki kaynak kodun tepesinde aşağıdaki gibi bir #define komutu varmış gibi işlem uygulanmaktadır:
#define TestXXX 123
Aynı durum gcc ve clang derleyicilerinde -D seçeneği ile yapılmaktadır. Örneğin:
gcc /D TestXXX=123 -o sample sample.c
Tabii IDE'lerle bu seçenkler görsel biçimde de oluşturulabilmektedir. Microsoft Visual Studio IDE'sinde bu işlem için proje seçeneklerinden
"Properties/C-C++ Preprocessor/Preprocessor Definitions" edit alanı yoluyla da yapılabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C standartlarında wchar_t isimli bir tür belirtilmiştir. Ancak wchar_t C'de bir anahtar sözcük değil typedef ismidir. Bu tür aşağıdaki
başlık dosyalarında typedef edilmiştir:
<stddef.h>
<stdlib.h>
<wchar.h>
C standartları bu türün herhangi bir tamsayı türü olarak typedef edilmesi gerektiğini belirtmektedir. Ancak hangi tür olarak typedef edileceği
derleyicileri yazanların isteğine bırakılmıştır. Microsoft C derleyicilerinde wchar_t türü unsigned short int olarak typedef edilmiştir.
gcc ve clang derleyicilerinde ise wchar_t türü unsigned int olarak typedef edilmiştir.
wchar_t türü karakterleri bir byte'tan uzun olan karakter tablolarının karakterlerini temsil etmek için düşünülmüştür. Ancak C standartları
wchar_t türünün hangi karakter tablosunu temsil edeceği konusunda bir belirlemede bulunmamıştır. Microsoft'un C ve C++ derleyicilerinde
wchar_t türü UNICODE UTF-16 encoding'ini temsil etmektedir. gcc ve clang derleyicilerinde ise wchar_t türü UNICODE UTF-32 encoding'ini
temsil etmektedir. C standartlarında wchar_t türünden karakter sabitleri tek tırnağa bitişik L ile ifade edilmektedir. Örneğin:
wchar_t ch = L'ş';
Benzer biçimde wchar_t türünden yazılar da yine iki tırnağa bitişik L harfile temsil edilmektedir. Örneğin:
wchar_t s[] = L"ağrı dağı";
char_t *ps = L"kpnya ovası";
Aşağıdaki ilkdeğer vermeler geçersizdir:
wchar_t s[] = "ağrı dağı"; /* geçersiz! */
char k[] = L"ağrı dağı"; /* geçersiz! */
O halde bir fonksiyon bizden bir UNICODE yazı isteyecekse onu "char *" türü ile değil "wchar_t *" türüyle istemelidir. Bizim de o
fonksiyona yazıyı L önekli bir string ile vermemiz gerekir.
C'deki klasik string fonksiyonları bir byte'lık karakter tabloları için düşünülmüştür. Örneğin strlen fonksiyonu null karakter görene
kadar birer byte'lık karakterlerin sayısını bize verir. printf fonksiyonu bir byte'lık karakter tablosuna ilişkin yazıları stdout
dosyasına yazdırmaktadır. İşte C'de <wchar.h> dosyası içerisinde "wide character" türü ile çalışabilen standrat C fonksiyonları da
bulundurulmuştur. char türü ile çalışan standart C fonksiyonlarının isimleri strxxx olmak üzere wchar_t türü ile çalışan standart C
fonksiyonlarının isimleri wcsxxx biçimindedir. Örneğin bir bir UNICODE yazının uzunluğunu strlen ile elde edemeyiz. Bunun için bizim
wcslen fonksiyonunu kullanmamız gerekir. printf fonksiyonun wchar_t ile çalışan biçiminin ismi wprintf biçimindedir. C standartlarından
ya da ilgili dokğmanlardan char türü ile çalışan fonksiyonların wchar_t türü ile çalışan versiyonlarının isimlerine bakabilirsiniz.
Örneğin:
wchar_t s[] = L"ali";
wprintf(L"%zd\n", wcslen(s));
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Eskiden Windows'un 3'lü versiyonlarında işletim sistemi tamamen ASCII yazılarla çalışıyordu. Windows 95, Windows 98 ve Milenium versiyonlarında
kernel ASCII olarak çalışıyordu. UNICODE yazılar ASCII'ye dönüştürülüp işlem yapılıyordu. Ancak Windows NT grubu sistemlerin kernel kodları
tam tersine UNICODE olarak çalışıyordu. Bu kernel versiyonları ASCII yazıları UNICODE dönüştürerek işleme sokuyordu. Artık çok uzun bir süredir
Windows kernel kodları yalnızca UNICODE kullanmaktadır. Dolayısıyla ASCII yazılar alan API fonksiyonları kernel tarafından UNICODE yazılara
dönüştürülerek işleme sokulmaktadır.
Bugün Windows sistemlerinde bütün yazı parametresi alan API fonksiyonları A'lı ve W'lu iki ayrı versiyon olarak bulunmaktadır. Örneğin yazı
parametresi alan API fonksiyonunun ismi XXX olmak üzere aslında XXXA ve XXXW biçiminde iki ayrı API fonksiyonu vardır. XXX biçiminde bir API
fonksiyonu yoktur. Örneğin CreateFile isimli bir API fonksiyonu aslında yoktur. CreateFileA ya da CreateFileW isimli API fonksiyonları vardır.
İşte ileride açıklayacağımız biçimde biz kodumuzda CreateFile ismini kullandığımızda bu isim önişlemci tarafından aslında CreateFileA ya da
CreateFileW biçimine dönüştürülmektedir.
Pekiyi XXX isimli bir API fonksiyonu önişlemci tarafından nasıl XXXA ya da XXXW biçimine dönüştürülmektedir? İşte bunun için UNICODE isimli
bir sembolik sabit kullanılmaktadır. <windows.h> dosyası içerisinde fonksiyonların isimlerini dönüştüren aşağıdaki makrolar bulunmaktadır:
#ifdef UNICODE
#define CreateFile CreateFileW
#define CreateProcess CreateProcessW
#define SetCurrentDirecory SetCurrentDirectoryW
...
#else
#define CreateFile CreateFileA
#define CreateProcess CreateProcessA
#define SetCurrentDirecory SetCurrentDirectoryA
...
#endif
Görüldüğü gibi yazı parametresi alan API fonksiyonlarının isimleri UNICODE sembolik sabitine bağlı olarak A'lı ya da W'lu biçime dönüştürülmektedir.
UNICODE sembolik sabiti define edilmemişse program ASCII uyumlu define edilmişse UNICODE uyumlu hale gelmektedir. Eğer bu sembolik sabit
define edilecekse <windows.h> dosyasının yukarısında define edilmeli ya da "-D seçeneği ile predefined" yapılmalıdır. Ancak Visual Studio
IDE'sinde bu işlem de görsel olarak yapılabilmektedir. Proje seçeneklerine gelinip "Properties/Configuration Properties/Advanced"/Character Set"
eğer "Use Unicode Character Set" olarak girilirse aslında IDE "cl" derleyicisini "/D UNICODE" komut satırı argümanını da ekleyerek çalıştırmaktadır.
Dolayısıyla Visual Studio IDE'sinde çalışıyorsak bu seçeneği seçerek API fonksiyonlarının UNICODE versiyonlarının kullanılmasını sağlayabiliriz.
Tabii bu seçenek seçilmezse UNICODE sembolik sabiti define edilmemiş olacağı için API fonksiyonlarının ASCII versiyonları kullanılacaktır.
Ancak programımızı UNICODE uyumlu yazabilmek için yalnızca API fonksiyonlarının UNICODE versiyonlarının kullanılması yetmemektedir. Çünkü API
fonksiyonlarının ASCII ve UNICODE versiyonlarının yazı parametreleri farklı türlerdendir.
Aşağıda bir Windows API prgramının UNICODE olarak yazılmasını görüyorsunuz
-------------------------------------------------------------------------------------------------------------------------------------------*/
#define UNICODE
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCWSTR lpszMsg);
int main(void)
{
HANDLE hFile;
if ((hFile = CreateFile(L"test.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys(L"CreateFile");
printf("success...\n");
CloseHandle(hFile);
return 0;
}
void ExitSys(LPCWSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPWSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fwprintf(stderr, L"%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir Windows programını UNICODE uyumlu yazmak demek "yalnızca UNICODE ve _UNICODE sembolik sabitlerine dayalı olarak kodda hiçbir değişiklik
yapılmadan kodun hem ASCII hem de UNICODE derlemesinde sorun çıkarmaması" demektir. UNICODE uyumlu Windows API kodu yazabilmek için şunlara
dikkat etmek gerekmektedir:
1) Program içerisindeki tüm string'ler TEXT makrosuyla ya da _TEXT makrosuyla kullanılmalıdır. TEXT makrosu <windows.h> içerisinde _TEXT makrosu
ise <tchar.h> içerisinde bulunmaktadır. <tchar.h> dosyası da Microsoft spesifik bir dosyadır. Biz TEXT makrosunu kullanacaksak <windows.h> dosyasının
include edilmesi yeterlidir. Ancak biz _TEXT makrosunu kullanacaksak <tchar.h> dosyasının include edilmesi gerekir. Genel olarak programcılar
her iki dosyayı da include ederler. Başı "_" ile başlayan ve ilk harfi büyük olan tüm isimler "reserved" olduğu için _TEXT makrosunun kullanılması
C için daha uygun gibi görünmektedir. İki makronun da yaptığı işlem aynıdır. TEXT makrosu UNICODE sembolik sabitine, _TEXT makrosu ise _UNICODE
sembolik sabitine bakmaktadır. TEXT makrosu şöyle yazılmıştır:
#ifdef UNICODE
#define TEXT(s) L##s
#else
#define TEXT(s) s
#endif
_TEXT makrosu ise şöyle yazılmıştır:
#ifdef _UNICODE
#define _TEXT(s) L##s
#else
#define _TEXT(s) s
#endif
Genellikle programcıların hem UNICODE hem de _UNICODE sembolik sabitlerini define etmeleri gerekmektedir. Zaten Visual Studio IDE'sinde
"character set" UNICODE olarak değiştirildiğinde her iki sembolik sabit de define edilmektedir.
2) char türü yerine TCHAR isimli typedef türünün kullanılması gerekir. Çünkü TCHAR türü UNICODE sembolik sabitine göre char ya da wchar_t
türü anlamına gelmektedir. TCHAR makrosu <windows.h> içerisinde aşağıdaki gibi oluşturulmuştur:
#ifdef UNICODE
#define TCHAR wchar_t
#else
#define TCHAR char
#endif
Karakter sabitleri için de <tchar.h> içerisindeki _T makrosu kullanılmaktadır. Bu makro da şöyle oluşturulmuştur:
#ifdef _UNICODE
#define _T(c) L##c
#else
#define _T(c) c
#endif
3) LPCSTR const char * anlamına, LPSTR ise char * anlamına gelir. Bunların UNICODE versiyonları LPCWSTR ve LPWSTR biçimindedir. Ancak
T li versiyonlar yine UNICODE sembolik sabitine bağlı olarak char * ya da wchar_t * türünü temsil etmektedir. Bu nedenle yazılara ilişkin
gösterici parametreleri ya TCHAR * ve const TCHAR * biçiminde ya da LPTSTR ve LPCTSTR biçiminde belirtilmelidir. Burada örneğin LPCSTR
türü ile LPCTSTR türü arasındaki farka dikkat ediniz. LPCSTR türü her zaman const char * anlamına gelmektedir. Halbuki LPCTSTR türü duruma
göre const char * duruma göre ise const wchar_t * anlamına gelmektedir.
4) Standart C fonksiyonları için de aynı durum söz konusudur. Örneğin ASCII programlarda printf kullanılırken UNICODE programlarda wprintf
kullanılmaktadır. Ancak uyumlu printf ismi için yine <tchar.h> dosyası içerisindeki _xxx biçimindeki özel fonksiyon isimleri kullanılır.
Her standart C fonksiyonun Windows sistemlerindeki UNICODE uyumlu ismi bilinmelidir. Örneğin _tprintf ismi printf fonksiyonun UNICODE
uyumlu ismidir. Bu isim _UNICODE sembolik sabiti define edilmişse wprintf olarak, edilmemişse printf olarak değiştirilmektedir.
5) Microsoft aslında main fonksiyonun da iki versiyonunu bulundurmaktadır: main ve wmain. Her ne kadar C standartlarında wmain diye
bir fonskiyon yoksa da Microsoft Windows sistemlerinde main fonksiyonunun UNICODE versiyonu wmain biçimindedir. Yani siz eğer main fonksiyonunun
argv parametresinin UNICODE olmasını istiyorsanın artık main yerine wmain kullanmalısınız. main fonksiyonunun _UNICODE uyumlu ismi
_tmain biçimindedir. O halde main fonksiyonu da şöyle tanımlanabilir:
int _tmain(int args, TCHAR *argv[])
{
//...
}
Aşağıda basit bir Windows API programının UNICODE uyumlu yazımı verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <tchar.h>
void ExitSys(LPCTSTR lpszMsg);
int _tmain()
{
HANDLE hFile;
TCHAR path[MAX_PATH] = _TEXT("test.txt");
if ((hFile = CreateFile(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys(_TEXT("CreateFile"));
_tprintf(_TEXT("Ok"));
CloseHandle(hFile);
return 0;
}
void ExitSys(LPCTSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
_ftprintf(stderr, _TEXT("%s: %s"), lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Şimdi de daha önce ASCII uyumlu olarak yazdığımız proses listesini alan programı UNICODE uyumlu olarak yazalım. Aşağıdaki örneği inceleyiniz.
Programda yukarıda sıraladığımız maddelere uyulduğuna dikkat ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <psapi.h>
#include <tchar.h>
#define NPROCESSES 4096
#define NMODULES 4096
void ExitSys(LPCTSTR lpszMsg);
int _tmain(void)
{
DWORD dwProcessIds[NPROCESSES];
DWORD cbNeeded;
HANDLE hProcess;
HMODULE hModules[NMODULES];
DWORD dwProcessCount;
DWORD dwModuleCount;
TCHAR szModuleName[MAX_PATH];
if (!EnumProcesses(dwProcessIds, sizeof(dwProcessIds), &cbNeeded))
ExitSys(_TEXT("EnumProcesses"));
dwProcessCount = cbNeeded / sizeof(DWORD);
for (DWORD i = 0; i < dwProcessCount; ++i) {
if ((hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessIds[i])) == NULL)
continue;
if (!EnumProcessModules(hProcess, hModules, sizeof(hModules), &cbNeeded)) {
CloseHandle(hProcess);
continue;
}
dwModuleCount = cbNeeded / sizeof(DWORD);
for (DWORD k = 0; k < dwModuleCount; ++k) {
if (!GetModuleBaseName(hProcess, hModules[k], szModuleName, MAX_PATH))
continue;
if (k == 0) {
_tprintf(_TEXT("%s: "), szModuleName);
continue;
}
if (k != 1)
_tprintf(_TEXT(", "));
_tprintf(_TEXT("%s"), szModuleName);
}
_tprintf(_TEXT("\n\n"));
CloseHandle(hProcess);
}
_tprintf(_TEXT("%lu process(es) listed...\n"), dwProcessCount);
getchar();
return 0;
}
void ExitSys(LPCTSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
_ftprintf(stderr, _TEXT("%s: %s"), lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Programların UNICODE uyumlu yazılması konusu Microsoft Windows sistemlerine özgüdür. UNIX/Linux sistemlerinde böyle bir konu yoktur.
Bütün POSIX fonksiyonları ASCII uyumludur. Dolayısıyla Windows sistemlerinde olduğu gibi bir POSIX fonksiyonunun UNICODE ve ASCII versiyonları
yoktur. Zaten Linux sistemlerinde UNICODE gerektiği zaman UNICODE UTF-8 encoding'i kullanılmaktadır. Bu encoding de "multibyte" bir kodlama
oluşturduğu için char * türü ile ifade edilebilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir proses sonlandığında işletim sistemine "exit kodu" ya da "exit durumu" denilen bir kod iletmektedir. Proseslerin exit kodları tamsayı
bir değerdir. İşletim sistemleri bu exit kodunu alır. Ancak onun hangi değerde olduğuna bakmaz. Bu exit kod prosesi yaratan proses (yani üst
proses) isterse ona verilmektedir. Böylece biz bir programı çalıştırdıktan sonra onun işini başarılı bir biçimde yapıp yapmadığını exit koduna
bakarak anlayabiliriz. Proseslerin exkit kodları fonksiyonların geri dönüş değerlerine benzetilebilir. Geleneksel olarak işletim sistemlerinde
başarılı sonlanmalar için sıfır değeri başarısız sonlanmalar için sıfır dışı değerler kullanılmaktadır.
Bir C programında prosesin exit odu standart exit fonksiyonunun argümanı ile belirlenmektedir. exit fonksiyonunun prototipini anımsayınız:
void exit(int status);
Ayrıca C'de okunabilirliği artırmak için <stdlib.h> içerisinde EXIT_SUCCESS ve EXIT_FAILURE sembolik sabitleri de bulundurulmuştur. C
standartlarında bu sembolik sabitlerin değerleri belirtilmemiş olsa da tipik olarak 0 ve 1 biçimindedir:
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
Pekiyi biz hiç exit fonksiyonunu çağırmazsak prosesin exit kodu ne olacaktır? İşte bu durumda main fonksiyonun geri dönüş değeri prosesin
exit kodu olmaktadır. C standartları main fonksiyonunun exit(main(...)) biçiminde çağrıldığını belirtmektedir.
Pekiyi bir program normal dışı sonlanamaz mı? Örneğin standart abort fonksiyonunun bir exit kod parametresi yoktur:
void abort(void);
Bir program abort fonksiyonuyla sonlanırsa ya da çökerse exit kodu ne olacaktır? İşte bu tür durumlarda prosesin exit kodları belirsiz durumda
olur. Dolayısıyla programcının eğer program normal olarak sonlanmışsa exit koduna bakması uygun olur.
UNIX/Linux kabuk programlarında son çalıştırılan programın exit kodu $? kabuk değişkeni ile elde edilebilmektedir. Örneğin:
$ ./sample
$ echo $?
123
Aynı şey Windows kabuklarında errorlevel değişkeni ile yapılmaktadır. Örneğin:
C:\>sample
C:\>echo %errorlevel%
123
C standartlarında main fonksiyonuna özgü şöyle bir durum belirtilmiştir: Eğer main fonksiyonunda hiç return kullanılmamışsa akışi ana
bloğu bitirerek main sonlanmışsa sanki ana bloğun sonunda "return 0" deyim varmış gibi bir etki oluşmaktadır. Yani örneğin:
int main(void)
{
...
return 0;
}
ile aşağıdakinin arasında hiçbir farklılık yoktur:
int main(void)
{
...
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
44. Ders 19/11/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Tipik olarak bir prosesin sonlandırılması exit standart C fonksiyonuyla yapılmaktadır. (Anımsanacağı gibi eğer bu fonksiyon hiç çağrılmazsa
zaten main bittiğinde çağrılmaktadır.) exit fonksiyonu aslında sonlandırmayı işletim sisteminin prosesi sonlandıran sistem fonksiyonunu
çağırarak yapmaktadır. Bu sistem fonksiyonu Windows sistemlerinde ExitProcess, UNIX/Linux ve macOS sistemlerinde _exit isimli fonksiyondur.
Ancak exit standart C fonksiyonu bundan önce sırasıyla şu işlemleri de yapmaktadır:
1) atexit fonksiyonuyla kaydettirilmiş olan fonksiyonları ters sırada çağırır.
2) Açık olan bütün dosyaların stdio tamponlarını flush eder ve bu dosyaları kapatır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde prosesi sonlandıran API fonksiyonu ExitProcess isimli fonksiyondur. Zaten Windows sistemlerinde C'nin standart exit
fonksiyonu bu fonksiyonu çağırmaktadır. Fonksiyonun prototipi şöyledir:
void ExitProcess(UINT uExitCode);
Fonksiyon parametre olarak prosesin exit kodunu almaktadır. Bu fonksiyon başarısız olamamaktadır. Bu fonksiyon normal olarak standart C
fonksiyonları için herhangi bir sonlandırma işlemi yapmaz. (Microsoft standart C kütüphanesinde bir süredir ExitProcess tarafından
çağrılan DLL_THREAD_DETACH bildirimi yoluyla stdio tamponlarını flush etmeye başlamıştır. Bu nedenle ExitProcess uygulandığında stdio
tamponları flush edilmektedir. Ancak atexit fonksiyonu ile kaydettirilen fonksiyonlar çağrılmamaktadır.)
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde bir proses bir program çalıştırdığında çalıştırdığı programın exit kodunu GetExitCodeProcess isimli API fonksiyonuyla
elde edebilmektedir. Bu işlemi yapabilen herhangi bir standart C fonksiyonu yoktur. Fonksiyonun prototipi şöyşedir:
BOOL GetExitCodeProcess(
HANDLE hProcess,
LPDWORD lpExitCode
);
Fonksiyonun birinci parametresi exit kodun elde edileceği prosesin handle değerini belirtmektedir. İkinci parametresi exit kodunun
yerleştirileceği DWORD türünden nesnenin adresini almaktadır. Fonksiyon başarı durumunda sıfır dışı bir değere başarısızlık durumunda
sıfır değerine geri dönmektedir.
Aşağıdaki örnekte "prog1" programı "prog2" programını CreateProcess ile çalıştırıp onun exit kodunu GetExitCodeProcess API fonksiyonu
ile elde etmiştir. Programlar UNICODE/ASCII uyumlu biçimde yazılmıştır. Burada "prog1" programı çalıştırdığı "prog2" programının bitmesini
WaitForSingleObject fonksiyonuyla beklemektedir. Bu fonksiyonu thread'ler konusunda ele alacağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* prog1.c */
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <tchar.h>
void ExitSys(LPCTSTR lpszMsg);
int main(void)
{
TCHAR szPath[] = _TEXT("prog2.exe");
STARTUPINFO si = {sizeof(STARTUPINFO)};
PROCESS_INFORMATION pi;
DWORD dwExitCode;
if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi))
ExitSys(_TEXT("CreateProcess"));
_tprintf(_TEXT("waiting for process to exit...\n"));
if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED)
ExitSys(_TEXT("WaitForSingleObject"));
if (!GetExitCodeProcess(pi.hProcess, &dwExitCode))
ExitSys(_TEXT("GetExitCodeProcess"));
if (dwExitCode == STILL_ACTIVE) {
_tprintf(_TEXT("Proces still active...\n"));
exit(EXIT_FAILURE);
}
_tprintf(_TEXT("Exit Code: %lu\n"), dwExitCode);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
void ExitSys(LPCTSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
_ftprintf(stderr, _TEXT("%s: %s"), lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/* prog2.c */
#include <stdio.h>
#include <tchar.h>
int _tmain(void)
{
_tprintf(_TEXT("press ENTER to exit...\n"));
getchar();
return 123;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
exit standart C fonksiyonu ve ExitProcess API fonksiyonu kendi prosesini sonlandırmaktadır. Ancak bazen bir prosesi zorla sonlandırmak
da isteyebiliriz. Bunun için Windows sistemlerinde TerminateProcess isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL TerminateProcess(
HANDLE hProcess,
UINT uExitCode
);
Fonksiyonun birinci parametresi sonlandırılacak prosesin handle değerini, ikinci parametresi sonlandırılacak prosesin exit kodunu belirtmektedir.
Fonksiyon başarı durumunda sıfır dışı bir değere başarıızlık durumunda sıfır değerine geri dönmektedir.
TerminateProcess fonksiyonu geri döndüğünde proses sonlanmış olmak zorunda değildir. Ancak sonlanma süreci başlatılmıştır. Sonlanmanın yine
WaitForSingleObject gibi bir fonksiyonla beklenmesi uygun olur.
Her proses handle değerini bildiği her prosesi sonlandıramaz. Ancak üst proses genel olarak kendi alt proseslerini sonlandırabilmektedir.
Windows'taki yetkisel özellikler karmaşık bir konusudur. Bu kursta ele alınmamaktadır. ("Windows Sistem Programalama" kursunda bu konu
ele alınmaktadır.)
TerminateProcess fonksiyonu son çare olarak uygulanmalıdır. Çünkü programın ansızın sonlandırılması o programın çalışması sırasında
olumsuzluklar oluşturabilir. Proses kritik birtakım işlemleri yaparken sonlandırılırsa sorunlar oluşabilmektedir.
Aşağıdaki örnekte "prog1" programı ENTER tuşuna basıldığında "prog2" programını zorla TerminateProcess API fonksiyonuyla sonlandırmaktadır.
kod yine UNICODE/ASCII uyumlu yazılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* prog1.c */
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <tchar.h>
void ExitSys(LPCTSTR lpszMsg);
int main(void)
{
TCHAR szPath[] = _TEXT("prog2.exe");
STARTUPINFO si = {sizeof(STARTUPINFO)};
PROCESS_INFORMATION pi;
DWORD dwExitCode;
if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi))
ExitSys(_TEXT("CreateProcess"));
_tprintf(_TEXT("Press ENTER to to terminate child process...\n"));
getchar();
if (!TerminateProcess(pi.hProcess, 999))
ExitSys(_TEXT("TerminateProcess"));
if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED)
ExitSys(_TEXT("WaitForSingleObject"));
if (!GetExitCodeProcess(pi.hProcess, &dwExitCode))
ExitSys(_TEXT("GetExitCodeProcess"));
if (dwExitCode == STILL_ACTIVE) {
_tprintf(_TEXT("Proces still active...\n"));
exit(EXIT_FAILURE);
}
_tprintf(_TEXT("Exit Code: %lu\n"), dwExitCode);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
void ExitSys(LPCTSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
_ftprintf(stderr, _TEXT("%s: %s"), lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/* prog2.c */
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(void)
{
for (int i = 0; i < 100; ++i) {
_tprintf(_TEXT("%d\n"), i);
Sleep(1000);
}
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux ve macOS sistemlerinde her prosesin sistem genelinde tek olan (unique) bir "proses id" değeri vardır. Bu sistemlerde Windows
sistemlerindeki gibi ayrıca proseslerin bir handle değerleri yoktur. Bu sistemlerde bir prosesi teşhis etmek için ve onun üzerinde işlem
yapmak için proses id değeri yetmektedir.
UNIX/Linux ve macOS sistemlerinde proseslerin id değerleri pid_t türü ile temsil edilmektedir. POSIX standartlarına göre pid_t türü
işaretli bir tamsayı türü olmak üzere işletim sistemlerini yazanlar tarafından herhangi bir olarak <sys/types.h> ve <unistd.h> dosyalarında
typedef edilmek zorundadır. Bu tür genellikle "int" ya da "long" olarak typedef edilmektedir.
UNIX/Linux sistemleri boot edildiğinde boot kodu kendini bir proses yapmakta ve 0 numaralı id'ye sahip olmaktadır. Bu prose "swapper" ya da
"pager" da denilmektedir. Bu proses "init" isimli prosesi yaratmaktadır. Bu "init" prosesi her zaman 1 numaralı id'ye sahip olur. Bu sistemler
her yaratılan proses için sırasıyla bir proses id değeri üretirler. Üst limite ulaşıldığında yeniden başa dönülmektedir.
UNIX/Linux sistemlerinde çekirdek proses id verildiğinde çok hızlı bir biçimde o prorsese ilişkin proses kontrol bloğuna erişebilmektedir.
Yani bu sistemlerde proses id'ler proses kontrol bloğuna erişmekte kullanılan bir handle değeri gibidir. Bunun için UNIX/Linux çekirdekleri
tipik olarak bir hash tablosu kullanmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde proseslerin yaratılması Windows sistemlerindekinde oldukça farklıdır. UNIX/Linux sistemlerinde yeni bir proses
yaratmak için fork isimli POSIX fonksiyonu kullanılmaktadır. fork POSIX fonksiyonu genel olarak doğrudan işletim sisteminin ilgili sistem
fonksiyonunu çağırmaktadır. Bu sistemlerde proses yaratmanın başkaca yolu yoktur. (fork benzeri vfork isimli bir fonksiyon olsa da bu
fonksiyonun artık bir kullanım gerekliliği kalmamıştır.) fork işlemi UNIX/Linux ve macOS sistemlerindeki en önemli kavramsal süreçlerden
biridir.
fork fonksiyonu bir prosesin özdeş bir kopyasını oluşturmaktadır. fork işlemi sırasında kabaca şunlar yapılmaktadır:
1) Proses kontrol bloğunun yeni bir kopyası oluşturulur. Yani yeni bir proses kontrol bloğu yaratılıp üst prosesin proses kontrol bloğu
içeriği bazı istisnalarla alt prosesin pross kontrol bloğuna kopyalanır. Dolayısıyla bu işlemden sonra üst ve alt proseslerin gerçek ve
etkin kullancı ve grup id'leri, çalışma dizinleri vs. tamamen aynı olmaktadır.
2) Üst prosesin bellek alanı da tamamen alt proses için alt prosesin bellek alanına kopyalanmaktadır. Artık üst proses ve alt proses bağımsız
iki ayrı proses olmaktadır. Bu iki prosesin geçmişleri aynı gibidir ancak birbirinden bağımsızdırlar. Burada üst proses ile alt prosesin
aynı program koduna sahip olacağına da dikkat ediniz.
3) Yeni bir çizelgele elemanı oluşturulup alt proses de zaman paylaşımlı biçimde bağımısz çalışmaya devam eder.
4) Çatallanma fork fonksiyonun içinde olmaktadır. Böylece yeni proses (yani alt proses) hayatına fork fonksiyonun içinden başlamaktadır.
Böylece hem üst proses hem de alt proses fork fonksiyonundan çıkacaktır.
fork fonksiyonun prototipi şöyledir:
#include <unistd.h>
pid_t fork(void);
Hem üst proses hem de alt proses fork fonksiyonundan çıkamktadır. Üst proseste fork fonksiyonu alt prosesin proses id değeri ile,
alt proseste ise 0 değeriyle geri dönmektedir. (Alt prosesin fork fonksiyonundan 0 ile geri dönmesi onun proses id'sinin 0 olduğu
anlamına gelmemektedir.) fork başarısız olabilir. Bu durumda fork -1 değerine geri döner.
fork fonksiyonunun kullaımına ilişkin tipik kalıp şöyledir:
pid_t pid;
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid != 0) { /* parent process */
...
}
else { /* chile process */
...
}
Üst prosesin ve alt prosesin her ikisin de fork fonksiyonundan çıktığını söylemiştik. Ancak hangi prosesin fork fonksiyonundan önce
çıkacağının hiçbir garantisi yoktur.
Aşağıda tipik bir fork uygulama kalıbı görülmektedir
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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 process...\n");
}
else {
printf("child process...\n"); /* child process */
}
printf("common code...\n");
sleep(1);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
fork işleminden sonra artık üst proses ile alt proses arasında hiçbir bağlantı kalmamıştır. Dolayısıyla birinin yaptığı bir şey diğerini
etkilemez. Örneğin aşağıdaki kodda üst proses g_x global değişkenine 10 değerini atamıştır ancak bu g_x üst prosesin kendi kopyasındaki
g_x'tir. Dolayısıyla alt proses bu g_x nesnesinin değerini değişmiş görmez.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int g_x;
int main(void)
{
pid_t pid;
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid != 0) { /* parent process */
g_x = 10;
printf("parent: %d\n", g_x);
}
else {
sleep(1);
printf("child: %d\n", g_x);
}
printf("common code...\n");
sleep(1);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki kodda ekrana (stdout dosyasına) kaç tane "common code..." yazısı çıkacaktır (kontroller yapılmamıştır)?
fork();
fork();
fork();
printf("common code...\n");
Birinci fork'a bir proses girer ondan 2 proses çıkar. İkinci fork'a 2 proses girer ondan 4 proses çıkar. Üçüncü fork'a 4 proses girer
ondan 8 proses çıkar. Yani bu yazı toplamda 8 kere ekrana basılacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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 = fork()) == -1)
exit_sys("fork");
if ((pid = fork()) == -1)
exit_sys("fork");
printf("common code...\n");
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
45. Ders 25/11/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
fork fonksiyonu başka bir programın çalışmasına yol açmamaktadır. Aynı programın özdeş bir kopyasının çalışmasına yol açmaktadır.
Oysa Windows'taki CreateProcess başka bir programı çalıştırarak proses oluşturmaktadır. O halde UNIX/Linux sistemlerinde fork ne işe
yaramaktadır ve başka bir program nasıl çalıştırılmaktadır?
Sonraki paragraflarda açıklayacağımız gibi aslında genellikle fork fonksiyonu tek başına değil exec fonksiyonlarıyla birlikte kullanılmaktadır.
Eskiden thread'ler yoktu. Bir işi birden fazla akışa yaptırabilmek için yeni prosesler yaratmak gerekiyordu. Böylece üst proses ile
alt proses aynı işin farklı kısımlarını birlikte yapabiliyordu. Örneğin:
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid != 0) { /* üst proses */
/* üst proses işin bir parçasını yapıyor */
}
else { /* üst proses */
/* üst proses işin diğer bir parçasını yapıyor */
}
Ancak artık bu tür işlemler için thread'ler kullanılmaktadır. Thread'ler proseslere göre çok daha az sistem kaynağı harcamaktadır. Dolayısıyla
yaratılmaları ve yok edilmeleri de proseslere kıyasla daha hızlıdır. Ayrıca bir işi birden fazla akışa yaptırırken onların koordine edilmesi
de gerekmektedir. Bu koordinasyon için fork modelinde proseslerarası haberleşme yöntemleri kullanılmaktadır. Bunun da ek maliyetleri vardır.
Halbuki thread'ler global değişkenler yoluyla haberleşebilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Başka program dosyasını çalıştırabilmek için UNIX/Linux sistemlerinde "exec fonksiyonları" diye isimlendirilen POSIX fonksiyonları
kullanılmaktadır. exec fonksiyonları bir aile belirtmektedir. Aslında bu fonksiyonların hepsi aynı işlemleri biraz değişik parametrelerle
yapmaktadır. exec ailesinde 7 farklı fonksiyon vardır:
execl
execlp
execv
execvp
execle
execvpe
execve (Linux'ta yalnızca bu bir sistem fonksiyonu olarak gerçekleştirilmiştir)
Aslında yalnızca execve bir sistem fonksiyonu olarak gerçekleştirilmiştir. Diğer exec fonksiyonları kütüphane fonksiyonlarıdır ve aslında
bu execve fonksiyonunu çağırarak işlemlerini yaparlar.
exec fonksiyonları prosesin bellek alanını boşaltıp başka bir program dosyasını o bellek alanına yükleyip onu çalıştırmaktadır. exec
fonksiyonları proses yaratmazlar. Mevcut prosesin başka bir kodla çalışmaya devam etmesini sağlarlar. Yani exec işlemi yapıldığında artık
exec yapan kod yok olacak exec işleminde belirtilen program dosyası çalıştırılacaktır. Ancak exec işlemi proses kontrol bloğunu değiştirmemektedir.
Dolayısıyla exec işleminden sonra prosesin id'si, etkin kullanıcı id'si, etkin grup id'si, çalışma dizini vs. aynı kalır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
En çok kullanılan exec fonksiyonlarından biri "execl" isimli fonksiyondur. Fonksiyonun prototipi şöyledir:
#include <unistd.h>
int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
Fonksiyonun birinci parametresi çalıştırılacak olan program dosyasının yol ifadesini belirtmektedir. Diğer parametreler o programa
geçirilecek olan komut satırı argümanlarıdır. Fonksiyonun "..." parametresi aldığına dikkat ediniz. C'de bu parametre fonksiyonun istenildiği
kadar çok argümanla çağrılabileceğini belirtmektedir. Tabii buradaki "..." için girilen argümanların hepsi komut satırı argüman yazılarının
adresleri olmalıdır. Ancak execl fonksiyonunun argüman listesinin bittiğini anlayabilmesi için buradaki argüman listesinin sonu NULL
adresle bitirilmelidir. Ancak burada NULL adesi düz sıfır olarak ya da NULL sembolik sabitiyleoluşturmayınız. Çünkü C'de "..." için kullanılan
argümanlarda düz sıfır int olarak ele alınacaktır. Benzer biçimde NULL sembolik sabitide düz sıfır olarak define edilmiş olabileceği için
benzer soruna yol açabilmektedir. Bu nedenle programcının argüman listesinin sonuna açıkça NULL adresi (char *)0 biçiminde yerleştirmesi
uygun olmaktadır. Programın ilk komut satırı argümanının program simi olması zorunlu değildir. Ancak genel beklenti bu yöndedir.
execl fonksiyonu başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyon başarı durumunda geri dönmez. Çünkü zaten başarı durumunda
başka bir program kodu çalışmaya başlayacaktır. Örneğin:
if (execl("/bin/ls", "/bin/ls", "-l", (char *)0) == -1)
exit_sys("execl");
Burada "/bin/ls" programı (yani ls komutu uygulandığında çalıştırılan program) çalıştırılmak isteniştir. Programın ilk komut satırı
argümanı kendisi olmalıdır. Komut satırı argüman listesinin (char *)0 ifadesi ile sonlandırıldığına dikkat ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
printf("main begins...\n");
if (execl("/bin/ls", "/bin/ls", "-l", (char *)0) == -1)
exit_sys("execl");
/* unreachable code */
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte "sample" programı execl fonksiyonu ile aşağıdaki gibi mample programını çalıştırmaktadır:
if (execl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1)
exit_sys("execl");
"mample" programı komut satırı argümanlarını yazdıran bir programdır. mample programının çıktısı şöyledir:
main begins...
argv[0] ==> mample
argv[1] ==> ali
argv[2] ==> veli
argv[3] ==> selami
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* sample.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
printf("main begins...\n");
if (execl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1)
exit_sys("execl");
/* unreachable code */
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* mample.c */
#include <stdio.h>
int main(int argc, char *argv[])
{
for (int i = 0; i < argc; ++i)
printf("argv[%d] ==> %s\n", i, argv[i]);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Tek başına fork bir prosesin özdeş kopyasını oluştup çalıştırmaktadır. Tek başına exec ise prosesin başka bir kodla çalışmasını sağlamaktadır.
O halde hem üst prosesin kodu çalışsın hem de başka bir programın kodu çalışsın isteniyorsa fork ve exec birlikte kullanılmalıaıdır. Yani
önce bir kez fork yapılır. Alt proseste exec uygulanır. fork sırasında üst prosesin kopyasından çıkartılacaktır. Bu sırada üst prosesin
bellek alanın da kopyasından çıkarılır. Alt proseste exec yapıldığında o kod bellekten yok edilip exec yapılan programın kodu belleğe
yüklenip çalıştırılır. Tipik fork/exec kalıbı şöyledir:
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0) {
if (execl(....) == -1)
exit_sys("execl"); /* dikkat burada sonlanan alt proses */
/* unreachable code */
}
/* buraya yalnızca üst proses akışı gelecektir */
&& operatörünün kısa devre özelliği kullanılarak bu işlem daha kompakt biçimde de ifade edilebilirdi:
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execl(....) == -1) {
exit_sys("execl"); /* dikkat burada sonlanan alt proses */
/* unreachable code */
}
/* buraya yalnızca üst proses akışı gelecektir */
Bazı programcılar ise aşağıdaki kalıbı tercih etmektedir:
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0) {
execl(....);
exit_sys("execl"); /* exec başarısızsa akış buraya gelecektir */
}
/* buraya yalnızca üst proses akışı gelecektir */
Microsoft sistemlerindeki CreateProcess API fonksiyonunun fork/exec ikilisi gibi işlem gördüğüne dikkat ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
pid_t pid;
printf("main begins...\n");
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execl("/bin/ls", "/bin/ls", "-l", (char *)0) == -1)
exit_sys("execl");
for (int i = 0; i < 10; ++i) {
printf("%d\n", i);
sleep(1);
}
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
fork/exec modelinde etkin olmayan şöyle bir durum kişilerin aklına gelmektedir: fork işleminden sonra alt proseste exec uyguladığımızda
üst prosesin bellek alanının gereksiz bir kopyası oluşturulmuş olmuyor mu? İlk bakışta burada gerçekten bir performans sorunu olduğu
düşünülmektedir. Eski sistemlerde gerçekten bu bir sorundu. Bu nedenle fork fonksiyonun vfork isminde bir kardeşi bulundurulmuştu.
vfork fonksiyonunun fork fonksiyonundan tek farkı üst prosesin bellek alanının kopyasından çıkarmamasıdır. Tabii vfork fonksiyonundan
sonra artık kesinlikle exec işlemi yapılmalıdır. Eğer vfork fonksiyonundan sonra exec işlemi yapılmazsa "tanımsız davranış" oluşmaktadır.
Bugün vfork fonksiyonu POSIX standratlarında hala muhafaza ediliyor olsa da faydalı bir kullanımı kalmamış gibidir. Çünkü uzun süredir
UNIX/Linux sistemlerinin çalıştığı makinelerdeki işlemcilerin "sayfalama (paging) ve sanal bellek (virtual memory) mekanizması" vardır.
Bu mekanizma sayesinde aslında üst prosesin bellek alanın kopyası zaten gerektiğinde çıkartılmaktadır. Bu mekanizmaya "copy on write"
denilmektedir. Dolayısıyla bugün zaten bir fork yapsak bile alt proseste henüz bir işlem yapmadıktan sonra ciddi bir kopyalama yapılmamaktadır.
Bu nednele artık fork/exec işleminde yukarıdaki gibi bir performans problemi oluşmayacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte sample programı mample programını çalıştırmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* sample.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
pid_t pid;
printf("main begins...\n");
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1)
exit_sys("execl");
for (int i = 0; i < 10; ++i) {
printf("%d\n", i);
sleep(1);
}
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* mample.c */
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("mample running...\n");
for (int i = 0; i < argc; ++i)
printf("argv[%d] ==> %s\n", i, argv[i]);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
execv (v vector'den geliyor) fonksiyonu execl fonksiyonun komut satırı argümanlarını tek tek değil de bir gösterici dizisi biçiminde
tek bir parametreyle alan biçimidir. Yani çalıştırılacak programın komut satırı argümanları char türünden bir gösterici dizisine yerleşltirilip
bu dizinin adresi execv fonksiyonuna verilmektedir. Fonksiyonun prototipi şöyledir:
#include <unistd.h>
int execv(const char *path, char * const argv[]);
Fonksiyonun birinci parametresi çalıştırılacak program dosyasının yol ifadesini almaktadır. İkinci parametre komut satırı argümanlarının
bulunduğu dizinin başlangıç adresini alır. Bu gösterici dizisinin son elemanı NULL adres olmalıdır. Yine fonksiyon başarısızlık durumunda
-1 değerine geri döner. Başrı durumunda zaten çağrıldığı yere geri dönememektedir. Örneğin:
const char *args[] = {"ls", "-l", NULL};
...
if (execv("/bin/ls", args) == -1)
exit_sys("execv");
Aşağıda execv fonksiyonunun kullanımına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
pid_t pid;
char *args[] = {"ls", "-l", NULL};
printf("main begins...\n");
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execv("/bin/ls", args) == -1)
exit_sys("execl");
for (int i = 0; i < 10; ++i) {
printf("%d\n", i);
sleep(1);
}
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bazen execv fonksiyonu execl fonksiyonundan daha uygun olabilmektedir. Örneğin komut satırı argümanlarıyla aldığımız bir propgramı çalıştırmak
istesek execl işimizi çok zorlaştıracaktır. Burada execv çok daha uygundur. "sample" programının komut satırı argümanlarıyla aldığı programı
çalıştırdığını düşünelim:
./sample /bin/ls -l -i
Burada çalıştırılacak program "/bin/ls" programıdır. Diğerleri onun komut satırı argümanlarıdır. İlk komut satırı argümanının program ismiyle
aynı olması gerektiğini de anımsayınız. Burada "sample" programının argv[1] parametresi "/bin/ls" yazısını göstermektedir. &argv[1] ise
buradan başlayan bir gösterici dizisi gibidir. argv listesinin sonunda zaten NULL adres bulunmaktadır. O halde exec işlemi şöyle basit
bir biçimde yapılabilir:
if (execv(argv[1], &argv[1]) == -1)
exit_sys("execv");
Aşağıda programın nasıl yazıldığı görülmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
pid_t pid;
if (argc < 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execv(argv[1], &argv[1]) == -1)
exit_sys("execl");
sleep(1);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
exec fonksiyonlarının p'li versiyonları (yani sonunda p soneki olan versiyonları) eğer yol ifadesinde hiç '/' karakteri yoksa ilgili
dosyayı PATH çevre değişkeni ile belirtilen dizinlerde tek tek aramaktadır. Eğer dosyanın yol ifadesinde en az bir '/' karakteri varsa bu
durumda dosya belirtilen yol ifadesinde aranır. Yani bu durumda exec fonksiyonlarının p'li versiyonlarıyla p'siz versiyonları arasında
bir fark olmaz.
execlp ve execvp fonksiyonlarının prototipleri exel ve execv ile aynıdır. Yalnızca kavramsal farkılığı vurgulamak için birinci parametre
"path" yerine "file" olarak isimlendirilmiştir:
#include <unistd.h>
int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
int execvp(const char *file, char *const argv[]);
PATH çevre değişkeni UNIX/Linux sistemlerinde ':' karakterinden ayrıştırılmaktadır. (Windows sistemlerinde ';' karakteri ile ayrıştırıldığını
anımsayınız.) Linux sistemlerindeki örnek bir PATH değişkeninin değerine bakınız:
/home/kaan/anaconda3/bin:/home/kaan/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
"/bin", "/usr/bin", "/usr/local/bin" gibi prgram dosyalarının bulunduğu dizinlerin PATH çevre değişkeninde bulunduğuna dikkat ediniz.
Artık UNIX/Linux sistemlerinde bir programı çalıştırırken neden "./sample" biçiminde program isminin önüne "./" getirdiğimizi anlamış
olmalısınız. Kabul programları exec fonksiyonlarının p'li versiyonlarını kullanmaktadır. execv fonksiyonlarının p'li versiyonları
eğer çalıştırılacak program dosyasına ilişkin yoli fadesinde hiç '/' karakteri yoksa onu PATH çevre değişkeni ile belirtilen dizinlerde
sırasıyla aramaktadır. Bu durumda prosesin çalışma dizinin de hiç arama yapmamaktadır. (Halbuki Windows'ta benzer durumda dosya önce
çalışma dizininde arandıktan sonra PATH çevre değişkenine bakılmaktadır.) Örneğin biz bulunduğumuz dizindeki "sample" programını şöyle
çalıştırmaktayız:
./sample
Bu programı biz aşağıdaki gibi çalıştırsaydık prgram dosyası bulunamaz:
sample
Çünkü burada exec fonksiyonlarının p'li versiyonları dosya yol ifadesinde hiç '/' karakteri geçmediği için onu yalnızca PATH çevre değişkeni
ile belirtilen dizinlerde arayacaktır. Halbuki biz programı "./sample" biçiminde çalıştırmak istediğimizde artık işin içine bir '/' karakteri
sokulduğu için exec fonksiyonlarının p'li versyionları PATH çevre değişkenine bakmayacaktır. '.' karakterinin "prosesin çalışma dizini"
anlamına geldiğine dikkat ediniz. Biz çalıştırmayı örneğin "/sample" biçiminde yapamazdık. Çünkü bu durumda "sample" programı kök dizinde
aranırdı.
Pekiyi exec fonksiyonlarının p'li biçimleri neden Windows gibi önce prosesin çalışma dizinine bakmamaktadır? Ya da kabuk programları
neden prosesin çalışma dizinine de bakmamaktadır? İşte çok eskiden kabuk programları Windows'taki gibi kabuğun çalışma dizinine de
bakıyordu. Ancak bu tasarımın bazı kötüye kullanımlarıyla karşılaşıldı. (Örneğin birisi birisinin dizinine bir komutla aynı isimli
bir program dosyası yerleştirebilir ve kişi komutu çalıştırdığını sanırken başkasının programını çalıştırabilir. Ya da kişi bir komutun
ismini yanlış yazarak yanlışlıkla çalışma dizinindeki başka bir programı da çalıştırabilir.) Bugün artıl bu sistemlerde kabuk programlarının
exec fonksiyonlarının p'li versiyonlarıyla exec yapması oturmuş bir kural biçimindedir.
Aşağıda komut satırı argümanlarıyla aldığı programı çalıtıran programı bu kez execvp fonksiyonunu kullanarak yazıyoruz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
pid_t pid;
if (argc < 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0) {
execvp(argv[1], &argv[1]);
exit_sys("execvp");
}
sleep(1);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
46. Ders 02/12/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında UNIX/Linux sistemlerinde shell programlarındaki komutların çok büyük çoğunluğu çalıştırılabilir dosyalardır. Yani örneğin "ls"
komutu aslında "/bin/ls" programıdır, "cat" komutu aslında "/bin/cat" programıdır. Çok az sayıda "internal" komut vardır. Bunlardan biri
"cd" komutudur. (Zaten "cd" komutu bir program olarak yazılamazdı. Eğer "cd" bir program olsaydı üst prosesin çalışma dizinini değil
(yani shell'in değil) kendi prosesinin çalışma dizinini değiştirirdi. Hiçbir proses üst prosesinin çalışma dizinini değiştirememektedir.
Aşağıda daha önce yazmış olduğumuz basit shell programının komutları external biçimde çalıştıran versiyonunu veriyoruz. Bu programda
henüz görmediğimiz wait isimli bir fonksiyonu kullandık. Bu fonksiyon alt proses bitene kadar üst prosesi blokede bekletmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* myshell.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_CMD_LINE 4096
#define MAX_CMD_PARAMS 128
#define BUFFER_SIZE 8192
#define MAX_PATH_SIZE 4096
void parse_cmdline(void);
void cd_proc(void);
void exit_sys(const char *msg);
typedef struct tagCMD {
char *cmd_name;
void (*cmd_proc)(void);
} CMD;
char g_cmdline[MAX_CMD_LINE];
char *g_params[MAX_CMD_PARAMS];
int g_nparams;
CMD g_cmds[] = {
{"cd", cd_proc},
{NULL, NULL}
};
char g_cwd[MAX_PATH_SIZE];
int main(void)
{
char *str;
int i;
pid_t pid;
if (getcwd(g_cwd, MAX_PATH_SIZE) == NULL)
exit_sys("getcwd");
for (;;) {
printf("CSD:%s>", g_cwd);
if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL)
continue;
if ((str = strchr(g_cmdline, '\n')) != NULL)
*str = '\0';
parse_cmdline();
if (g_nparams == 0)
continue;
if (!strcmp(g_params[0], "exit"))
break;
for (i = 0; g_cmds[i].cmd_name != NULL; ++i)
if (!strcmp(g_cmds[i].cmd_name, g_params[0])) {
g_cmds[i].cmd_proc();
break;
}
if (g_cmds[i].cmd_name == NULL) {
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execvp(g_params[0], &g_params[0]) == -1) {
printf("invalid command: %s\n", g_params[0]);
exit(EXIT_FAILURE);
}
if (wait(NULL) == -1)
exit_sys("wait");
}
}
return 0;
}
void parse_cmdline(void)
{
char *str;
g_nparams = 0;
for (str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t"))
g_params[g_nparams++] = str;
g_params[g_nparams] = NULL;
}
void cd_proc(void)
{
if (g_nparams == 1) {
printf("argument missing!..\n");
return;
}
if (g_nparams > 2) {
printf("too many arguments!..\n");
return;
}
if (chdir(g_params[1]) == -1) {
printf("%s: %s!..\n", g_params[1], strerror(errno));
return;
}
if (getcwd(g_cwd, MAX_PATH_SIZE) == NULL)
exit_sys("getcwd");
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
exec fonksiyonlarının bir de e'li biçimleri vardır. Bunlar exec işlemi sırasında çalıştırılacak program için yeni bir çevre değişken
takımı oluşturmakltadır. execle ve execve fonksiyonlarının prototipleri şöyledir:
#include <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[]);
Bu fonksiyonlar p'li olmadığı için PATH çevre deişkenine hiç bakmamaktadır. Her iki fonksiyonun da ilk parametresi çalıştırılacak programın
yol ifadesini belirtmektedir. execle fonksiyonu önce komut satırı argümanlarını bir liste olarak alır. Bu argümanların sonunda NULL adres
bulunmalıdır. Bu NULL adresten sonra çevre değişkenleri "anahtar=değer" yazıları biçiminde sonu NULL adresle biten bir gösterici dizisi biçiminde
girilmelidir. execve fonksiyonu ise hem komut satırı argümanlarını ehm de çevre değişkenlerini gösterici dizisi biçiminde almaktadır.
Her iki fonksiyon da yine başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir.
UNIX/Linux sistemlerinde çevre değişkenlerinin genel olarak prsoesin bellek alanı içerisinde tutulduğunu anımsayınız. Çevre değişkenleri
aslında fork işlemi sırasında üst prosesin tüm bellek alanı alt prosese kopyalandığından dolayı alt prosese geçirilmektedir. Ancak exec
fonksiyonları prosesin bellek alanını yok edip onun yerine başka bir programı bu bellek alanına yüklediklerinden dolayı bu çevre değişkenlerinin
kaybolması beklenir. İşte exec fonksiyonlarının e'siz biçimleri fork sonrasında bu eçvre değişkenlerini saklayıp exec işlemi ile çalıştırılan
program için ayrılan bellek alanına aktarmaktadır. Yani exec fonksiyonlarının e'siz versiyonlarında biz programı çalıştırdığımızda çevre
değişkenleri üst prosesle aynı olmaktadır. Ancak exec fonksiyonlarının e'li biçimleri programcının belirlediği çevre değişkenlerini exec
işlemi sırasında yeni programın bellek alanına aktarmaktadır.
execle fonksiyonu tipik olarak şöyle kullanılmaktadır:
pid_t pid;
char *env[] = {"city=eskişehir", "plate=26", NULL};
...
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execle("mample", "mample", "ali", "veli", "selami", (char *)NULL, env) == -1)
exit_sys("execl");
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* 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 *envs[] = {"city=eskişehir", "plate=26", NULL};
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execle("mample", "mample", "ali", "veli", "selami", (char *)NULL, envs) == -1)
exit_sys("execl");
if (wait(NULL) == -1)
exit_sys("wait");
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* mample.c */
#include <stdio.h>
extern char **environ;
int main(int argc, char *argv[])
{
printf("Command line arguments:\n");
for (int i = 0; i < argc; ++i)
printf("%s\n", argv[i]);
printf("\nEnvironment Variables:\n");
for (int i = 0; environ[i] != NULL; ++i)
puts(environ[i]);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda execve fonksiyonunun kullanımına bir örnek verilmiştir. execve fonksiyonunda hem komut satırı argümanlarının hem de çevre
değişkenlerinin gösterici dizisi biçiminde verildiğine dikkat ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* sample.c */
#include <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 *args[] = {"mample", "ali", "veli", "selami", NULL};
char *envs[] = {"city=eskişehir", "plate=26", NULL};
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execve("mample", args, envs) == -1)
exit_sys("execl");
if (wait(NULL) == -1)
exit_sys("wait");
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* mample.c */
#include <stdio.h>
extern char **environ;
int main(int argc, char *argv[])
{
printf("Command line arguments:\n");
for (int i = 0; i < argc; ++i)
printf("%s\n", argv[i]);
printf("\nEnvironment Variables:\n");
for (int i = 0; environ[i] != NULL; ++i)
puts(environ[i]);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında UNIX/Linux sistemlerinde genel olarak yalnızca execve bir sistem fonksiyonu olarak kernel içerisinde bulundurulmuştur. Diğer exec
fonksiyonları user mod fonksiyonlardır. Çeşitli düzenlemeleri yaptıktan sonra execve fonksiyonu çağırmaktadır. execve fonksiyonu komut satırı
argümanlarını ve çevre değişkenleribi gösterici dizisi biçiminde bizden ister. Aşağıda bir execve kullanımı görülmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* 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 *args[] = {"./mample", "mample", "ali", "veli", "selami", NULL};
char *env[] = {"city=eskişehir", "plaka=26", NULL};
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execve("./mample", args, env) == -1)
exit_sys("execve");
if (wait(NULL) == -1)
exit_sys("wait");
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* mample.c */
#include <stdio.h>
extern char **environ;
int main(int argc, char *argv[])
{
int i;
printf("Command line arguments:\n");
for (i = 0; i < argc; ++i)
printf("%s\n", argv[i]);
printf("Environment Variables:\n");
for (i = 0; environ[i] != NULL; ++i)
puts(environ[i]);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
exec fonksiyonlarının sonuncusu fexecve isimli fonksiyondur. Bu fonksiyonun execve fonksiyonundan tek farkı çalıştırılacak dosyanın
yol ifadesini değil onun dosya betimleyicisini almasıdır. Bu fonksiyon çok seyrek kullanılmaktadır. Prototipi şöyledir:
#include <unistd.h>
int fexecve(int fd, char *const argv[], char *const envp[]);
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında pek çok UNIX türevi sistemde yalnızca execve fonksiyonu bir sistem fonksiyonu olarak yazılmıştır. Başka bir deyişle yalnızca execve
fonksiyonu çekirdeğin içerisindedir. Diğer exec fonksiyonlarının hepsi bir çeşit "sarma (wrapper) fonksiyon" gibidir ve normal user mod
kütüphane içerisinde bulunmaktadır. Örneğin biz execl fonksiyonunu çağırdığımızda bu komut satırı argümanları bir gösterici dizisine
yerleştirilip environ değişkenini kullanarak execve fonksiyonunu çağırmaktadır. Örneğin execlp fonksiyonu programı PATH çevre değişkeni ile
belirtilen dizinlerde tek tek execve uygulayarak aramaktadır. Yani aslında PATH çevre değişkenine çekirdek kodları bakmamaktadır. Bu işlem
kütüphane fonksiyonu tarafından user modda yapılmaktadır. exec fonksiyonlarının execve fonksiyonu çağrılarak nasıl gerçekleştirildiğine yönelik
"Advanced Programming in the UNIX Environment" kitabının 254'üncü sayfasındaki şekli inceleyebilirsiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Çeşitli uzantılara sahip dosyaların önceden belirlenmiş bir formatı vardır. Buna "dosya formatı (file format)" denilmektedir. Dosya formatları
bazen standardizasyon kurumları tarafından çoğu zaman ise şirketler ve kurumlar tarafından oluşturulmaktadır. Dünyada çeşitli konulara
ilişkin yüzlerce dosya formatı bulunmaktadır. Bir dosya formatını anlayabilmek için o konu hakkında bilgiye sahip olmamız gerekir. Hiç
Autocad kullanmamış birisi Autocad dosya formatını anlayamaz.
Dosya formatlarının başında hemen her zaman bir "başlık kısmı (header" bulunmaktadır. Dosyaya ilişkin kritik "meta-data" bilgileri bu
başlık kısmında tutulmaktadır. Genellikle dosya formatlarının ilk birkaç byte'ı "magic number" denilen özel bazı değerler içermektedir.
Bunun nedeni o dosya formatını okuyup anlamlandıracak programların dosyanın doğru formatta olup olmadığını kabaca (ama kesin değil)
test etmesini sağlamaktır.
İşte derleyicilerin ürettikleri "amaç dosyaların (object files)" ve bağlayıcıların ürettikleri "çalıştırılabilir dosyaların (executable files)"
da belli formatları vardır. Tabii bu dosya formatlarını anlayabilmek için yine aşağı seviyeli pek çok kavramın bilinmesi gerekmektedir.
Microsoft'un kullandığı amaç dosya formatına "COFF (Common Object File Format)" denilmektedir. Microsoft daha önce DOS zamanlarında
"OMF (Object Module Format)" basit bir format kullanıyodu. Microsoft'un bugün Windows sistemlerinde kullandığı "çalıştırılabilir (executable)"
dosya formatına "PE (Portable Executable)" dosya formatı denilmektedir. COFF formatı ile PE formatı birbirine çok benzerdir.
Bugün Linux sistemleri ve diğer UNIX türevi sistemler amaç dosya formatı olarak ve çalıştırılabilir dosya formatı olarak "ELF (Executable
and Linkable Format)" denilen formatı kullanmaktadır. ELF hem bir amaç dosya formatı hem de çalıştırılabilir dosya formatıdır.
Uzun süredir macOS sistemlri Mach-O isimli bir amaç dosya ve çalıştırılabilir dosya formatı kullanmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
exec fonksiyonları ile aslında çalıştırılabilir olmayan dosyalar da (örneğin text dosyalar) çalıştırılmak istenebilir. Bu durumda exec
fonksiyonları ilgili dosyayııp onun ilk iki karakterine bakmaktadır. Eğer dosyada ilk iki karakter #! biçimindeyse bu karakterlere
"shebang" denilmektedir. shebang karakterlerini gerçek çalıştırılabilir bir dosyanın yol ifadesi izlemelidir. (Shebang'ten sonra boşluk
karakterleri bulunabilir.) İşte exec fonksiyonları aslında burada belirtilen dosyayı çalıştırılar. Çalıştırdıkları dosyaya da exec yapılan
dosyayı komut satır argümanı olarak geçirirler. Yukarıda da belirttiğimiz gibi UNIX/Linux sistemlerinde yalnızca execve fonksiyonu bir
sistem fonksiyonu olarak gerçekleştirilmiştir. Bu shebang kontrolü kernel tarafından bu execve içerisinde yapılmaktadır. execve burada
belirtilen çalıştılabilir dosyayı bir yol ifadesi olarak kabul eder. Genel olarak buradaki dosyanın mutlak yol ifadesi belirtmesi
istenmektedir. Ancak bugünkü sistemler göreli yol ifadelerini de kabul etmektedir. Buradaki dosya execve tarafından PATH çevre değişkenine
bakılmadan doğrudan ele alınmaktadır. Burada belirtilen dosyadan sonra yazılan komut satırı argümanları buradaki çalıştırılabilen dosyaya
tek bir komut satırı argümanı biçiminde aktarılmaktadır. exec fonksiyonlarının kendisinde belirtilen argümanlar ise son komut satırı
argümanları olarak kullılmaktadır.
Aşağıdaki örnekte bir shebang mekanizması uygulanmıştır. "sample.c" programı komut satırı argümanlarıyla alınan dosyayı execv ile çalıştırmaktadır.
Biz bu programla "test.txt" dosyasını aşağıdaki gibi çalıştıracağız:
#! /home/kaan/Study/SysProg/mample
Burada execp fonksiyonunda programın komut satırı argümanları "test.txt", "xxx" ve "yyy" durumundadır. "test.txt" dosyasının başındaki shebang
kısmı da şöyle olsun:
#! #! /home/kaan/Study/SysProg/mample
mample programı da komut satırı argümanlarını ekrana yazan program olsun. Bu durumda ekrana şunlar çıkacaktır:
argv[0]: /home/csd/Study/SysProg-2020/mample
argv[1]: ali veli selami
argv[2]: test.txt
argv[3]: xxx
argv[4]: yyy
Shebang içeren dosyanın ne olursa olsun x özelliklerine sahip olması gerekir. Çünkü exec fonksiyonları bunu kontrol etmektedir. Bir dosyaya
x hakları vermenin en pratik yolu şöyledir:
chmod +x test.txt
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* 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);
}
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execv(argv[1], &argv[1]) == -1)
exit_sys("execv");
if (wait(NULL) == -1)
exit_sys("wait");
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
nt main(int argc, char* argv[])
{
int i;
for (i = 0; i < argc; ++i)
printf("argv[%d]: %s\n", i, argv[i]);
return 0;
}
test.txt
#!mample
/*------------------------------------------------------------------------------------------------------------------------------------------
Tabii aslında text dosyalar eğer "x" hakkına sahipse doğrudan kabuk üzerinden de çalıştırılabilmektedir. Zaten bu durumda kabuk programları
bunları exevp ile çalıştırmaktadır. Örneğin "text.txt" dosyası aşağıdaki gibi olsun:
#! /home/kaan/Study/SysProg/mample
Şimdi biz bu dosyayı komut satırından çalıştırmak istediğimizde aslında "mample" programı çelıştırılacaktır. "mample" programına da "ali veli selami"
ve "test.txt" parametre olarak geçirilecektir. Örneğin:
$ ./test.txt
argv[0]: /home/kaan/Study/SysProg/mample
argv[1]: ./test.txt
$ ./test.txt xxx yyy
argv[0]: /home/kaan/Study/SysProg/mample
argv[1]: ./test.txt
argv[2]: xxx
argv[3]: yyy
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Shebang kullanımının en önemli faydası script dosyalarının sanki çalıştırılabilir bir dosya gibi çalıştırılmasını sağlamaktır. Örneğin
aşağıdaki gibi "sample.py" isminde bir python programı bulunyor olsun:
#! /usr/bin/python3
for i in range(10) :
print(' {}'.format(i))
Biz bu "sample.py" dosyasına "x" hakkı vererek onu çalıştırdığımızda aslında "/usr/bin/python3" programı çalıştırılacaktır. Bu programa da
"sample.py" komut satırı argümanı olarak verildiği için sanki çalıştırma aşağıdaki gibi yapılıyormuş etkisi oluşacaktır:
python3 sample.py
Windows sistemlerinde shebang gibi bir kullanım yoktur. Benzer işlemler dosya ilişkilendirmesi yoluyla yapılmaktadır. ShellExecute fonksiyonun
dosya ilişkilendirmesine baktığını biliyorsunuz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#!/usr/bin/python3
for i in range(10) :
print(' {}'.format(i))
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında kabuk komutları kendi içlerinde yorumlayıcı (interpreter) da içermektedir. Yani biz kabuk komutlarını bir dosyada bulundurup onları
sanki bir program gibi çalıştırabiliriz. Kabukların ayrı bir script dili vardır. Shell scirpt dilleri basit olsa da ayrıca öğrenilmesi
gerekir. Örneğin aşağıdaki gibi "sample.sh" isminde bir bash script dosyası oluşturup ona "x" hakkı vererek komut satırından çalıştırabiliriz:
#!/bin/bash
for n in {1..10};
do
echo $n
done
Çalıştırma şöyle yapılabilir:
./sample.sh
Aslında bu script bash tarafından aşağıdaki gibi çalıştırılmaktadır:
/bin/bash sample.sh
-------------------------------------------------------------------------------------------------------------------------------------------*/
#!/bin/bash
for n in {1..10};
do
echo $n
done
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir çalıştırılabilir text dosyanın başında shebang bölümü yoksa bu durumda bu dosya kabuk programı tarafından bir kabuk scipt dosyası
gibi çalıştırılmaktadır. Örneğin:
for n in {1..10};
do
echo $n
done
Buradaki dosyanın başında "shebang" karakterşeri yoktur. Bu dosya çalıştırılmak istendiğinde doğrudan sanki bir shell script gibi
çalıştırılacaktır. Burada aslında kabuk önce dosyayı "execvp" ile çalıştırmak ister. Dosya çalıştırılamazsa (örneğin dosyanın başında shebang)
yoksa bu kez onu script dosyası gibi kendisi çalıştırır. Ancak bu tarzda çalıştırma daha maliyetlidir. Bu nedenle shell script dosyalarının
başında "gerekmese bile" shebang bulundurulmalıdır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
47. Ders 03/12/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Biz Windows sistemlerinde prosesin exit kodunu GetExitCodeProcess isimli API fonksiyonuyla elde etmiştik. WaitForSingleObject fonksiyonunu
ise Proses sonlanana kadar beklemek için kullanmıştık. Şimdi de UNIX/Linux sistemlerinde benzer işlemlerin nasıl yapılacağını göreceğiz.
UNIX/Linux sistemlerinde alt proses sonlanana kadar bekleme işlemi ve alt prosesin exit kodunu alma işlemi wait, waitpid ve waitid isimli
üç POSIX fonksiyonu kullanılmaktadır. Linux sistemlerinde wait3 ve wait4 isimli wait fonksiyonları da vardır. wait fonksiyonlarının iki
önemli işlevi vardır: Alt proses bitene kadar üst prosesi blokede bekletmek ve alt prosesin exit kodunu elde etmek. Çok eskidne yalnızca wait
fonksiyonu vardı. Sonra bu fonksiyonun yetersizlikleri nedeniyle waitpid ve sonra da waitid fonksiyonları tasarlandı. Bu fonksiyonunların
hepsinin prototipleri <sys/wait.h> dosyası içerisindedir. wait fonksiyonunun prototipi şöyleid:r
#include <sys/wait.h>
pid_t wait(int *status);
Fonksiyon parametre olarak alt prosesin exit kodunun ve sonlanma biçiminin (buna "status" de denilmektedir) yerleştirileceği int nesnenin
adresini almaktadır. Fonksiyon başarı durumunda beklenen alt prosesin id değeri ile başarısızlık durumunda -1 değeri ile geri dönmektedir.
Fonksiyon çağrıldığında bir ya da birden fazla ilt prosesin hiçbiri henüz sonlanmamış olabilir. Bu durumda fonksiyon ilk sonlanacal
alt prosese kadar bekleme yapar. Eğer fonksiyon çağrıldığında zaten bir ya da birden fazla alt proses sonlanmış durumdaysa fonksiyon bunlardan
herhangi birinin durumunu (status) elde ederek hemen geri döner. Fonksiyonun ilk sonlanmış olan prosesin durumuyla geri dönmesi garanti
edilmemiştir. Fonksiyonun parametresi NULL adres de geçilebilir. Bu durumda fonksiyon yine bekleme işlevini yerine getirir ancak alt prosesin
durum bilgisini programcıya iletmez. (Yani programcı yalnızca alt prosesi beklemek istiyorsa, onun durum bilgisini elde etmek istemiyorsa
parametre olarak NULL adres geçebilir.)
Fonksiyonun bize verdiği durumsal bilgide hem prosesin nasıl sonlandığı hem de exit kodu bulunmaktadır. Bu bilgilerin int nesnenin
neresinde depolandığı standart olarak belirlenmemiştir. Bunun için WIFEXITED ve WIFSIGNALED makroları kullanılmaktadır. Bu iki makro da
wait fonksiyonun parametresine geçirilen int nesneyi parametre olarak almaktadır. WIFEXITED normal sonlanma durumunu, WIFSIGNALED
sinyal dolayısıyla normal olmayan sonlanma durumunu test etmektedir. Bu makrolar sıfır ya da sıfır dışı değere geri dönmektedir.
int nesne içerisindeki prosesin exit kodu WEXITSTATUS makrosuyla elde edilmektedir. Tabii programcı ancak proses normal sonlanmışsa
exit kodunu bu makroyla almaya çalışmalıdır. Örneğin:
int status;
...
if (wait(&status) == -1)
exit_sys("wait");
if (WIFEXITED(status))
printf("Exit code: %d\n", WEXITSTATUS(status));
else /* if (WIFSIGNALED(status)) */
printf("child terminates via signal!..\n");
Aşağıdaki örnekte "sample" programı komut satırı argümanlarıyla aldığı programı fork/exec ile çalıştırıp onun sonlanmasını beklemektedir.
"sample" programını aşağıdaki gibi çalıştırabilirsiniz:
$ ./sample mample
Buradaki "mample" programı birer saniye aralıklarla ekrana sayıları basıp 123 exit koduyla geri dönmektedir.
Sonra da "sample" programını standart komutlar için çalıştırarak durumu gözlemleyiniz:
$ ./sample /bin/ls
Pek çok UNIX/Linux sisteminde prosesin exit kodu 1 byte uzunlukta işaretsiz tamsayı türündendir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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;
int status;
if (argc == 1) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0 && execv(argv[1], &argv[1]) == -1)
exit_sys("execv");
printf("parent process waiting for the child to exit...\n");
if (wait(&status) == -1)
exit_sys("wait");
if (WIFEXITED(status))
printf("child exited with %d\n", WEXITSTATUS(status));
else /* if (WIFSIGNALED(status)) */
printf("child terminates via signal!..\n");
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* mample.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
for (int i = 0; i < 10; ++i) {
printf("%d\n", i);
sleep(1);
}
exit(123);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte üst proses üç farklı alt proses yaratmıştır ve hepsini wait fonksiyonu ile beklemiştir. Ekranda aşağıdaki gibi bir çıktı
göreceksiniz:
child created: 3471
child running...
child created: 3472
child running...
child created: 3473
child (3472) terminated with 20
child running...
child (3471) terminated with 10
child (3473) terminated with 30
Tabii buradaki alt proseslerin sonlanma sıraları birbirlerinden farklı olabilecektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <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 pid1, pid2, pid3, pid;
int status;
if ((pid1 = fork()) == -1)
exit_sys("fork");
if (pid1 == 0) {
printf("child running...\n");
exit(10);
}
else
printf("child created: %jd\n", (intmax_t)pid1);
if ((pid2 = fork()) == -1)
exit_sys("fork");
if (pid2 == 0) {
printf("child running...\n");
exit(20);
}
else
printf("child created: %jd\n", (intmax_t)pid2);
if ((pid3 = fork()) == -1)
exit_sys("fork");
if (pid3 == 0) {
printf("child running...\n");
exit(30);
}
else
printf("child created: %jd\n", (intmax_t)pid3);
for (int i = 0; i < 3; ++i) {
if ((pid = wait(&status)) == -1)
exit_sys("wait");
if (WIFEXITED(status))
printf("child (%jd) terminated with %d\n", (intmax_t)pid, WEXITSTATUS(status));
else /* if (WIFSIGNALED(status)) */
printf("child terminates via signal!..\n");
}
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
waitpid 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 prosesin id'sini belirtmektedir. Anımsanacağı gibi wait fonksiyonu herhangi bir alt prosesi
bekliyordu. Oysa waitpid fonksiyonunda biz fonksiyonun hangi alt prosesi bekleyeceğini belirleyebiliyoruz. Fonksiyonun ikinci parametresi
yine durum bilgisinin yerleştirileceği int nesnesnenin adresini almaktadır. Üçüncü parametre aşağıdaki aşağıdkai sembolik sabitlerin bir
ya da birden fazlasının bit OR ile işleme sokulmasıyla oluşturulabilir:
WCONTINUED
WNOHANG
WUNTRACED
Bu parametre 0 da geçilebilmektedir. WNOHANG bekleme yapmadan alt prosesin sonlanıp sonlanmadığını belirlemek amacıyla kullanılmaktadır.
Fonksiyonun birinci parametresi özel olarak -1 biçiminde girilirse herhangi bir alt proses beklenir. Başka bir deyişle wait(&status) çağrısı
ile waitpid(-1, &status, 0) çağrısı eşdeğerdir. Eğer bu parametre 0 girilirse fonksiyonu çağıran proses ile aynı proses grubunundaki alt
proseslerden herhangi bir beklenmektedir. Eğer pid değeri -1 değerindne küçükse fonksiyon bu negatif değerin mutlak değerine ilişkin proses
grubunundaki herhangi bir alt prosesi beklemektedir. Proses grupları konusu bu kursta ele alınmayacaktır. waitpid fonksiyonun da ikinci
parametresi NULL adres geçilebilir. Bu durumda alt proses beklenir ancak durumsal bilgi elde edilmez.
waitpid fonksiyonu da başarı durumunda beklenen prosesin id değerine başarısızlık durumunda -1 değerine geri dönmektedir.
Aşağıdaki örnekte üst proses ğüç alt proses yaratmıştır. Bu alt prosesler farklı sürelerde sleep uygulamaktadır. Ancak waitpid fonksiyonu
ile bekleme alt proseslerin yaratılma sırasına göre yapılmıştır. Programı çalıştırınca aşağıdaki gibi bir çıktı elde edeceksiniz:
$ ./sample
child created: 3313
child created: 3314
child running...
child created: 3315
child running...
child running...
child (3313) terminated with 10
child (3314) terminated with 20
child (3315) terminated with 30
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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 pids[3], pid;
int status;
if ((pids[0] = fork()) == -1)
exit_sys("fork");
if (pids[0] == 0) {
printf("child running...\n");
sleep(3);
exit(10);
}
else
printf("child created: %jd\n", (intmax_t)pids[0]);
if ((pids[1] = fork()) == -1)
exit_sys("fork");
if (pids[1] == 0) {
printf("child running...\n");
sleep(2);
exit(20);
}
else
printf("child created: %jd\n", (intmax_t)pids[1]);
if ((pids[2] = fork()) == -1)
exit_sys("fork");
if (pids[2] == 0) {
printf("child running...\n");
sleep(1);
exit(30);
}
else
printf("child created: %jd\n", (intmax_t)pids[2]);
for (int i = 0; i < 3; ++i) {
if ((pid = waitpid(pids[i], &status, 0)) == -1)
exit_sys("wait");
if (WIFEXITED(status))
printf("child (%jd) terminated with %d\n", (intmax_t)pid, WEXITSTATUS(status));
else /* if (WIFSIGNALED(status)) */
printf("child terminates via signal!..\n");
}
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux dünyasında "hortlak (zomibie)" proses denilen bir kavram vardır. Üst proses fork ile alt prosesi yarattıktan sonra alt proses
üst prosesten önce sonlanırsa işletim sistemi üst prosesin wait fonksiyonlarıyla sonlanmış olan alt prosesin durum bilgisini alabileceğini
düşünerek onun proses kontrol bloğunu ve dolayısıyla da proses id'sini boşaltmaz. Proseslerin durum bilgisi genel olarak proses kontrol
bloklarında tutulmaktadır. Ancak sonlanmış olan alt prosesler için üst proses wait fonksiyonlarını uygulamazsa alt prosesin proses kontrol
bloğu ve proses id'si sistem tarafından boşaltılmadığından dolayı bir sızıntı (leak) oluşturmaktadır. Bu sızıntı birkaç proses için önemsiz
olsa da binlerce alt proses yaratan ve bunları wait ile beklemeyen uzun ömürlü prosesler söz konusu olduğunda sistemi çalışamaz hale
getirecek derecede ciddi sonuçlar oluşturabilmektedir. İşte sonlandığı halde üst prosesin wait fonksiyonlarını uygulamadığı alt proseslere
UNIX/Linux dünyasında "hortlak (zomibe)" proses denilmektedir. Zombie proses sonlanmıştır ancak proses kontrol bloğu tam olarak boşaltılamamıştır.
Zaten zombie sözcüğü tam ölememiş, yaşamakla ölmek arasında kalmış canlılar için uydurulmuş bir sözcüktür. Bu anlamda zombie insan
bulunmamaktadır.
Zombie'lik ancak üst proses devam ederken alt proses sonlanmışsa ve üst proses alt prosesi wait fonksiyonlarıyla beklememişse bu süreç
içerisinde oluşmaktadır. Üst prosesin de sonlanmasıyla artık alt prosesin durum bilgisini alacak bir proses kalmadığı için işletim sistemi
alt prosesin kaynaklarını (proses kontrol bloğunu) boşaltır ve alt prosesin zombie'lik durumu sona erer.
Eğer üst proses alt prosesten daha önce sonlanırsa bu durumdaki alt proseslere "öksüz (orphan)" prosesler denilmektedir. İşletim sistemi
bir proses öksüz duruma geldiğinde 1 numaralı proses id'ye sahip olan "init" prosesini öksüz prosesin üst prosesi yapmaktadır. "init"
prosesi de öksüz proses sonlandığında wait işlemi uygulayarak onun kaynaklarını boşaltmaktadır.
Aşağıdaki örnekte üst proses fork ile alt proses yaratmış ancak alt proses hemen sonlanmıştır. Üst proses ise getchar fonksiyonunda bekletilmiştir.
Başka bir ekrandan girip ps komutuyla bu proseslere bakıldığında alt prosesin durum bilgisinin 'Z' harfi ile gösterildiğini ve onun yanında
<defunct> ibaresinin bulunduğunu göreceksiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.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) { /* child process */
exit(EXIT_SUCCESS);
}
printf("Press ENTER to exit...\n");
getchar();
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bazen üst proses alt prosesi yarattıktan sonra onu beklemeden bazı şeyler yapmak isteyebilir. Bu tür durumlarda üst proses wait fonksiyonlarını
uygulamadığından dolayı alt proses zombie durumda kalacaktır. Zombie oluşmasının wait ile bekleme yapmadan otomatik engellenmesinin bu
sistemlerde iki yolu vardır:
1) Üst proses SIGCHLD sinyalini set eder. Alt proses sonlandığında işletim sistemi bu sinyali üst prosese göndermektedir. Üst proses de bu
sintyalde asenkron biçimde wait uygular.
2) Üst proses işin başında SIGCHLD sinyalini "ignore" edebilir. Bu durumda alt proses sonlanır sonlanmaz işletim sistemi alt prosesin
kaynaklarını yok etmektedir.
Biz bu kursumuzda UNIX/Linux sistemlerinde "sinyal (signal)" kavramını görmeyeceğiz. Bu konu "UNIX/Linux Sistem Programlama" kurslarında
ele alınmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Anımsayacağınız gibi open fonksiyonu dosya başarılı bir biçimde açıldığında "dosya betimleyici (file descriptor)" denilen int türden bir
handle değeri vermektedir. Bu dosya betimleyicisi read, write, lseek, close fonksiyonlarında hangi dosya üzerinde işlem yapılacağını belirlemek
için kullanılmaktadır. UNIX/Linux sistemlerinde sistem programcılarının "dosya betimleyicilerinin (file descriptors)" ne anlam ifade
ettiğini bilmesi gerekmektedir.
UNIX/Linux sistemlerinde proses kontrol blok içerisinde "dosya betimleyici tablosu (file descriptor table)" bir tablonun adresini tutan
bir eleman vardır. Dosya betimleyici tablosu dosya nesnelerini gösteren bir gösterici dizisidir. Dosya nesneleri (file object) çekirdeğin
ık bir dosya üzerinde işlem yapabilmesi için gereken bilgileri tutmaktadır. Bu durumu aşağıdaki şekille temsil edebiliriz.
Proses Kontrol Blok
...
... Dosya Betimleyici Tablosu
...
pfds ----------------> 0 adres --------------------------> Dosya Nesnesi
... 1 adres --------------------------> Dosya Nesnesi (aşağıdaki ile aynı nesneyi gösteriyor)
... 2 adres --------------------------> Dosya Nesnesi (yukarıdaki ile aynı nesneyi gösteriyor)
... 3 BOŞ
4 BOŞ
5 BOŞ
...
1022 BOŞ
1023 BOŞ
Örneğin Linux çekirdeğinde proses kontrol blok "task_struct" isimli yapı ile temsil edilmiştir. Dosya nesneleri de "file" isimli yapı ile
temsil edilmiş durumdadır. Dosya betimleyici tablosu da aslında file türünden adreslerden olulan bir gösterici dizisidir. Bu gösterici dizisindeki
her elemana bir slot da diyebiliriz. Buradaki her elemanın bir indeksi numarası vardır. Proses çalışmaya başladığında hemen her zaman
dosya betimleyici tablosunun ilk üç slotu (yani 0, 1 ve 2 numaralı indeks elemanları) doludur. İşte "dosya betimleyicisi (file descriptor)"
aslında dosta betimleyici tablosunda bir indeks belirtmektedir. 0 numaralı betimleyici (yani dizinin ilk slotu) "stdin dosyası" dediğimiz
klavyeyi temsil eden terminal aygıt sürücüsüne ilişkin dosya nesnesini göstermektedir. 1 ve 2 numaralı betimleyiciler de ekranı
temsil eden terminal aygıt sürücüsüne ilişkin aynı dosya nesnesini göstermektedir. Yani 0 numaralı betimleyici ile okuma yapıldığında
klavyeden okuma yapılacak, 1 ve 2 numaralı betimleyici kullanılarak yazma yapıldığında yazılanlar ekrana çıkartılacaktır.
open fonksiyonu ile bir dosya açıldığında işletim sistemi önce bir dosya nesnesi oluşturur. Sonra dosya betimleyici tablosundaki ilk
boş slotun bu nesneyi göstermesini sağlar ve dosya betimleyicisi olarak bu slotun numarasıyla yani (dizideki indeks numarasıyla) geri döner.
Örneğin yukarıdaki şekli temel alarak prosesin open fonksiyonuyla bir dosya açmış olduğunu varsayalım:
Proses Kontrol Blok
...
... Dosya Betimleyici Tablosu
...
pfds ----------------> 0 adres --------------------------> Dosya Nesnesi
... 1 adres --------------------------> Dosya Nesnesi (aşağıdaki ile aynı nesneyi gösteriyor)
... 2 adres --------------------------> Dosya Nesnesi (yukarıdaki ile aynı nesneyi gösteriyor)
... 3 adres --------------------------> Dosya Nesnesi
4 BOŞ
5 BOŞ
...
1022 BOŞ
1023 BOŞ
Burada bize dosya betimeleyicisi olarak 3 değeri verilecektir.
open fonksiyonunun dosya betimelyici tablosundaki ilk boş betimleyiciyi vermesi garanti edilmiştir. Yukarıda da belirttiğimiz gibi genellikle
UNIX/Linux sistemlerinde proses çalışmaya başladığında 0, 1 ve 2 numaralı betimleyiciler dolu durumdadır. Dolayısıyla ilk boş betimleyici
3 numaralı betimleyicidir.
Aşağıdkai programda önce bir dosya açılmış ve oradan 3 numaralı betimleyici elde edilmiştir. Sonra bir dosya daha açılmış oaradan da 4
numaralı betimleyici elde edilmiştir. Sonra 3 numaralı betimelyici kapatılıp yenidne bir dosya açıldığında 3 numaralı betimeleyici elde
edilmiştir. Çünkü open fonksiyonu her zaman eldeki ilk boş betimleyiciyi vermektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
int fd1, fd2, fd3;
if ((fd1 = open("test.txt", O_RDONLY)) == -1)
exit_sys("open");
printf("%d\n", fd1); /* 3 */
if ((fd2 = open("test.txt", O_RDONLY)) == -1)
exit_sys("open");
printf("%d\n", fd2); /* 4 */
close(fd1);
if ((fd3 = open("test.txt", O_RDONLY)) == -1)
exit_sys("open");
printf("%d\n", fd3); /* 3 */
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
48. Ders 09/12/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir dosya ile ilgili işlem yaapabilmek için gereken her şey "dosya nesnesi (file object)" içerisinde bulunmaktadır. Dosya nesneleri diskteki
normal bir dosyaya ilişkin olabildiği gibi aygıt sürücü dosyalarına da ilişkin olabilmektedir. Aslında dosya nesnelerinin içerisinde
read, write, lseek, close gibi işlemlerde çağrılacak fonksiyonaların adresleri bulunmaktadır.
fd ----> Dosya Nesnesi
...
okuma fonksiyonunun adresi
yazma fonksiyonun adresi
konumlandırma fonksiyonun adresi
kapatma fonksiyonun adresi
...
Böylece örneğin bir betimleyici kullanılarak read fonksiyonu çağrıldığında aslında read fonksiyonu dosya nesnesinin içerisinde adresi
bulunan okuma fonksiyonunu çağırmaktadır. Tabii işlemlerin bazı yarıntıları vardır. Biz burada bu ayrıntıları basitleştirerek genel
mekanizmayııklamak istiyoruz:
ssize_t sys_read(int fd, ...)
{
1) prosesin dosya betimeleyici tablosunun fd numaralı elemanından dosya nesnesine eriş
2) Dosya nesnesinde belirtilen okuma fonksiyonunu çağır
}
write fonksiyonu ve temel dosya fonksiyonları da benzerdir.
Aygıt sürücüler kernel modda çalışan modüllerdir. Bir aygıt sürücüyü yazan programcı "read ile okuma yapıldığında şu fonksiyonum çalışsın,
write ile yazma yapıldığında şu fonksiyonu çalışsın, lseek ile konumlandırma yapıldığında şu fonksiyonun çalışsın, close ile kapatma
yapıldığında şu fonksiyonu çalışsın" biçiminde yazar. Aygıt sürücüler dosya gibi açılırlar. Bir aygıt sürücü açıldığında onun dosya nesnesinde
artık okuma, yazma, konumlandırma kapatma dosya göstericileri o aygıt sürücünün içerisindeki fonksiyonları göstermiş olur. Başka bir deyişle
aslında biz bir aygıt sürücüden okuma ve yazma yaptığımızda o aygıt sürücüdeki ilgili fonksiyonları çağırmış oluyoruz. Örneğin 1 numaralı
betimelyici ekranı kontrol eden terminal aygıt sürücüsü ile ilgilidir. Biz write(1, .....) çağrısı yaptığımızda aslında terminal aygıt sürücüsü
içerisindeki bir fonksiyonu çağırmış oluruz. O fonksiyon yazıyı ekrana yazar. Aygıt sürücü içerisinde okuma, yazma, konumlandırma ve
kapatma işlemlerinin dışında da yararlı başka fonksiyonlar bulunabilmektedir. Aygıt sürücüleri yazanlar bunların da user mode programlar
tarafından çağrılmasını mümkün hale getirmişlerdir. Bunun için UNUX/Linux sistemlerinde ioctl POSIX fonsiyonu, Windows sistemlerinde de
DeviceIOControl isimli API fonksiyonu bulunmaktadır. Yani yalnızca aygıt sürücülerdeki okuma yazma fonksiyonları değil diğer bazı fonksiyonlar
da user mode programlar tarafından çağrılabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Linux sistemlerinde bir prosesin dosya betimleyici tablosu default olarak 1024 elemanlıdır. Dolayısıyla proses hiç kapatmadan en fazla
1024 dosyayı aynı anda açık tutabilir. Tabii işin başında dosya betimelyici tablosunun ilk üç girişi zaten dolu biçimdedir. Bu durumda
proses onları kapamazsa ancak 1021 dosya açabilir. Aşağıda bu testi yapan bir program verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
int fd;
int i;
for (i = 0;; ++i) {
if ((fd = open("test.txt", O_RDONLY)) == -1) {
perror("open");
break;
}
printf("%d\n", fd);
}
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Linux sistemlerinde bir proses isterse dosya betimelyici tablosunu 1048576 kadar büyütebilir. Bunun için setrlimit POSIX fonksiyonu
kullanılmaktadır. Dosya betiemleyici tablosunun uzunluğu ise getrlimit ya da sysconf fonksiyonu ile alınabilmektedir. Ancak bu konular
kursumuzun kapsamı dışındadır. Aşağıda prosesin dosya betimelyici tablosunu 5000 uzunluğunda yapan örnek bir program verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/resource.h>
void exit_sys(const char *msg);
int main(void)
{
int fd;
struct rlimit rl;
rl.rlim_cur = 5000;
rl.rlim_max = 5000;
if (setrlimit(RLIMIT_NOFILE, &rl) == -1)
exit_sys("setrlimit");
printf("%ld\n", sysconf(_SC_OPEN_MAX));
for (int i = 0;; ++i) {
if ((fd = open("sample.c", O_RDONLY)) == -1)
exit_sys("open");
printf("%d\n", fd);
}
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Biz daha önce standart C fonksiyonlarının bir tampon (cache) kullandığını belirtmiştik. Aslında UNIX/Linux sistemlerinde tüm dosya
işlemlerinin wninde sonında read ve write POSIX fonksiyonlarıyla yapıldığını biliyorsunuz. Bu POSIX fonksiyonları da zaten ilgili sistemdeki
sistem fonksiyonlarını çağırmaktadır. O halde örneğin C'deki stdout dosyasına yazma yapan fonksiyonlar eninde sonunda write fonksiyonunu
1 numaralı betimelyici ile çağırarak bu işlemi yapacaklardır. Benzer biçimde stdin dosyasından okuma yapan C fonksiyonları da aslında
read fonksiyonunu 0 numaralı betimelyici ile çağırarak okumayı yapmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Mademki UNIX/Linux sistemlerinde open fonksiyonu en düşük boş betimleyiciyi bize vermektedir. O halde biz stdout aygıt sürücüne ilişkin
1 numaralı betimleyiciyi kapatıp hemen arkasından open fonksiyonu ile bir dosya açarsak open bize 1 numaralı betimleyiciyi verecektir.
Yani artık 1 numaralı betimleyici stdout dosyasına ilişkin dosya nesnesini değil, disk dosyasına ilişkin dosya nesnesini gösteriyor durumda
olur. İşte dosya yönlendirmeleri böyle bir mekanizmayla yapılmaktadır. Yukarıda da belirttiğimiz gibi printf, puts vs. gibi tüm standart
C fonksiyonları ve diğer dillerdeki (Java, C#, Python vs.) ekrana yazan tüm fonksiyonlar eninde sonunda aslında write fonksiyonuyla 1
numaralı betimleyiciyi kullanarak yazımı yaparlar. Dolayısıyla bu yönlendirmeden sonra ekrana yazdırma için kullanılan tüm fonksiyonlar
aslında yönlendirilen bu dosyaya yazacaktır.Aşağıda bu işleme bir örnek verilmiştir.
Örnekte açtığımız dosyayı biz kapatmadık. 0, 1 ve 2 numaralı betimelyiciler zaten program sona erdiğinde exit fonksiyonunda kapatılmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <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;
close(1);
if ((fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
exit_sys("open");
for (int i = 0; i < 10; ++i)
printf("Test: %d\n", i);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte aynı fikirle stdin dosyası yönlendirilmiştir. Burada "test.txt" dosyası içerisinde boşluk karakterleriyle ayrılmış
sayıların olduğunu varsayıyoruz. Burada scanf fonksiyonu klavyedne değil bu dosyadan okuma yapacaktır
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
int fd;
int val;
close(0);
if ((fd = open("test.txt", O_RDONLY)) == -1)
exit_sys("open");
while (scanf("%d", &val) == 1)
printf("%d\n", val);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir dosya betimelyicisnin gösterdiği dosya nesnesi başka bir betimelyici tarafında gösteriliyorsa ne olur? Örneğin fd1 betimelyeicisi ile
fd2 betiemleyicisinin aynı dosya nesnesini gösterdiğini varsayalım. Bu durumda read, write, lseek gibi dosya fonksiyonlarına fd1 ya da fd2
betimeleyicilerinin geçirilmesinde bir fark oluşmayacaktır. Çünkü dosya işlemleri neticede bu dosya nesnesinin içerisindeki bilgilerden
hareketle yapılmaktadır. Pekiyi böyle bir durumun bir faydası olabilir mi?
Dosya betimleyicilerini çiftlemek (duplicate etmek) için dup ve dup2 isimli iki POSIX fonksiyonu kullanılmaktadır. Tabii bu POSIZ fonksiyonları da
aslında doğrudan işletim sisteminin sistem fonksiyonlarını çağırmaktadır. dup fonksiyonunun prototipi şöyledir:
#include <unistd.h>
int dup(int fd);
Fonksiyon parametresi ile belirtilen dosya betimleyicisinin gösteridği dosya nesnesini gösteren yeni bir betimelyici tahsis etmektedir.
Yani fonksiyon başarılı olursa fonksiyonun geri döndürdüğü dosya betimleyicisi ile parametrede belirtilen betimleyicinin aynı dosya
nesnesini gösterir durumda olur. dup başarısızlık durumunda -1 değerine geri dönmektedir. dup fonksiyonunun dosya betimelyici tablosundaki
ilk boş betimelyiciyi vermesi garanti edilmiştir.
Aşağıdaki örnekte önce test.txt dosyasıılmış sonra da bu betimleyici çiftlenmiştir. Dosya göstericisinin dosya nesnesi içerisinde
bulunduğunu anımsayınız. Dolayısıyla aşağıdaki örnekte dosyadan ıkuma yaparken hangi dosya betimleyicisinin kullanıldığının bir önemi
kalmamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(void)
{
int fd1;
int fd2;
char buf[10 + 1];
ssize_t n;
if ((fd1 = open("test.txt", O_RDONLY)) == -1)
exit_sys("open");
if ((fd2 = dup(fd1)) == -1)
exit_sys("dup");
if ((n = read(fd1, buf, 10)) == -1)
exit_sys("read");
buf[n] = '\0';
puts(buf);
if ((n = read(fd2, buf, 10)) == -1)
exit_sys("read");
buf[n] = '\0';
puts(buf);
close(fd1);
close(fd2);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
dup2 fonksiyonu da dup fonksiyonu gibi dosya betimleyicisini çiftlemekte kullanılır. Fonksiyonun prototipi şöyleidR.
#include <unistd.h>
#include <unistd.h>
int dup2(int fd, int fd2);
Fonksiyon birinci parametresiyle belirtilen dosya betimleyicisi ile aynı dosya nesnesini gösteren bir betimleyici oluşturur. Ancak bu betimleyici
en düşük boş betimleyici değil ikinci parametrede belirtilen betimleyicidir. Yani fonksiyon başarılı olduğunda birinci ve ikinci parametresiyle
belirtilen dosya betimleyicileri aynı dosya nesnesini gösteriyor durumda olur. Eğer ikinci parametreyle verilen betimleyici zaten açık bir
dosyanın betimleyicisi ise bu durumda o dosya önce kapatılır, betimelyici boşaltılır sonra bu betimleyicinin birinci parametresiyle belirtilen
betimelycinin gösterdiği dosya nesnesini göstermesi sağlanır. dup2 fonksiyonu başarısızlık durumunda yine -1 değerine geri dönmektedir.
Dosya yönlendirmeleri genellikle dup2 fonksiyonuyla yapılmaktadır. Çünkü close işlemi ve open işlemi arasında prosese ilişkin bir
thread varsa ve o thread de tesadüfen open fonksiyonunu çağırırsa ilk boş betimelyiciyi o thread elde edebilir. Ayrıca ilk boş betimeleyici
duruma göre farklılıklar da gösterebilir. Örneğin stdout betimeleyicisini bir dosyaya yönlendirmek isteyelim. Bu işlemi şöyle yapabiliriz
kontroller uygulanmamıştır):
fd = open(....);
dup2(fd, 1);
close(fd);
Aşağıda stdout dosyasının dup2 ile doğru teknik kullanılarak yönlendirilmesi örneği verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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;
if ((fd = open("test.txt", O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1)
exit_sys("open");
if (dup2(fd, 1) == -1)
exit_sys("dup2");
close(fd);
for (int i = 0; i < 10; ++i)
printf("test: %d\n", i);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
49. Ders 10/12/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi kabuk programları nasıl IO yönlendirmesi yapmaktadır. Kabuk programları tipik olarak komut satırından girilen program için bir kez
fork uygulayıp alt yarattıktan sonra, alt proseste IO yönlendirmesini yapıp sonra exec uygulamaktadır. exec işlemi sırasında prsesin
kontrol bloğu değişmediği için dosya betimleyici tablosu da değişmemektedir. Dolayısıyla exec sonrasında çalıştırılan program aslında
IO yönlendirmesinin etkisi altında kalacaktır.
UNIX/Linux sistemlerinde exec yapıldığı zaman o ana kadar açılmış olan dosyaların exec işleminden sonra açık olarak kalması istenmeyebilir.
Örneğin biz 100 tane dosya açmış olalım. Sonra fork ve exec uygulamış olalım. Şimdi exec yaptığımız program çalışırken aslında kendisinin
ilgilenmediği 100 dosyayıık olarak görecektir. Bu da exec yapan kodun dosya betimleyici tablosunu boş değil kısmen dolu olarak
çalışması anlamına gelecektir. İşte UNIX/Linux sistemlrinde her betimleyici için "close on exec" isimli bir bayrak tutulmaktadır. Eğer bu
bayrak set edilmişse bu durumda exec işlemi sırasında o betimleyici ekrnel taarafından otomatik olarak kapatılmaktadır. Dosyalar açıldığında
default durumda betimleyicinin "close on exec" bayrığı "reset" durumdadır. Yani dosya exec işlemleri sırasında kapatılmayacaktır. Betimleyicinin
"close on exec" bayrağını set etmek için open fonksiyonunda O_CLOEXEC baurağı kullanılabilir. Ya da fcntl fonksiyonu ile bu işlem yapılabilir.
Biz kursumuzda bu konunun ayrıntılarına girmeyeceğiz.
Aşağıdaki örnekte "redirect" isimli bir program yazılmıştır. Program kabuk programının yaptığı yönlendirmenin benzerini yapmaktadır.
Programın komut satırı argümanı kabukta girilen yönlendirme komutunu almaktadır. Örneğin:
./redirect "ls -l -i > test.txt"
Programın içerisinde check_arg isimli fonksiyon '>' karakterinden yazıyı iki parçaya ayırmış ve soldaki programın komut satırı argümanlarını
da ayrıştırmıştır. Yönlendirmenin aşağıdaki gibi yapıldığına dikkat ediniz:
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0) { /* child process */
if ((fd = open(rargs.redirect_path, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1)
exit_sys("open");
if (dup2(fd, 1) == -1)
exit_sys("dup2");
close(fd);
if (execvp(rargs.exe_args[0], rargs.exe_args) == -1)
exit_sys("execvp");
/* unreacable code */
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* redirect.c */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_ARGS 1024
typedef struct tagREDIRECT_ARGS {
char *exe_args[MAX_ARGS];
char *redirect_path;
} REDIRECT_ARGS;
bool check_arg(char *arg, REDIRECT_ARGS *rargs);
void exit_sys(const char *msg);
/* ./redirect "./sample > test" */
int main(int argc, char *argv[])
{
char *arg;
REDIRECT_ARGS rargs;
pid_t pid;
int fd;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((arg = strdup(argv[1])) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
if (!check_arg(arg, &rargs)) {
fprintf(stderr, "invalid argument: \"%s\"\n", argv[1]);
exit(EXIT_FAILURE);
}
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0) { // child process
if ((fd = open(rargs.redirect_path, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1)
exit_sys("open");
if (dup2(fd, 1) == -1)
exit_sys("dup2");
close(fd);
if (execvp(rargs.exe_args[0], rargs.exe_args) == -1)
exit_sys("execvp");
/* unreacable code */
}
if (wait(NULL) == -1)
exit_sys("wait");
free(arg);
return 0;
}
bool check_arg(char *arg, REDIRECT_ARGS *rargs)
{
char *str;
size_t i = 0;
if ((str = strchr(arg, '>')) == NULL || strchr(str + 1, '>') != NULL)
return false;
*str = '\0';
if ((rargs->redirect_path = strtok(str + 1, " \t")) == NULL)
return false;
for (str = strtok(arg, " \t"); str != NULL; str = strtok(NULL, " \t"))
rargs->exe_args[i++] = str;
if (i == 0)
return false;
rargs->exe_args[i] = NULL;
return true;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
fork işlemi sırasında üst prosesin dosya betimleyici tablosu "sığ kopyalama (shallow copy)" yoluyla alt prosese kopyalanmaktadır. Sığ
kopyalama bir nesnenin (yapının) elemanlarının diğerine kopyalanması anlamına gelmektedir. Eğer yapının bir elemanı başka bir nesneyi
gösteriyorsa o nesnenin kopyasından çıkartılmamaktadır. Yalnızca ana nesnenin kopyasından çıkartılmaktadır. Dolayısıyşa sığ kopyalama
sonucunda iki nesnenin gösterici elemanları aynı nesneyi gösteriyor durumda olur.
fork işlemi sırasında alt prosesin kontrol bloğunda yeni bir dosya betimelyici tablosu oluşturulur. Üst prosesin dosya betimleyici tablosundaki
dosya nesnelerinin adresleri alt prosesin dosya betimeleyici tablosuna kopyalanır. Böylece prosesle alt proses aynı dosya nesnesini
gösteriyor durumda olur. Tabii bu surada dosya nesnelerinin referans sayaçları da 1 artırılmaktadır.
fork işlemi sırasında işlemlerin bu biçimde yapıldığını şöyle ispatlayabiliriz. Örneğin dosya göstericisi dosya nesnesinin içerisinde
tutulmaktadır. Bu durumda üst proses dosya nesnesini konumlandırırsa alt proses de onu konumlandırılmış görecektir. Aşağıdaki örnekte
üst proses açtığı dosyanın dosya göstericisini 10'uncu offset'e konumlandırmıştır. Alt proses o betimelyciden okuma yaptığında 10 numaralı
offset'ten itibaren okuma yapmış olacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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[])
{
int fd;
pid_t pid;
char buf[10 + 1];
ssize_t result;
if ((fd = open("test.txt", O_RDONLY)) == -1)
exit_sys("open");
if ((pid = fork()) == -1)
exit_sys("");
if (pid != 0) { /* parent process */
lseek(fd, 10, SEEK_CUR);
}
else { /* child process */
sleep(1);
if ((result = read(fd, buf, 10)) == -1)
exit_sys("read");
buf[result] = '\0';
puts(buf);
close(fd);
exit(EXIT_SUCCESS);
}
if (waitpid(pid, NULL, 0) == -1)
exit_sys("waitpid");
close(fd);
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'de bir fonksiyonun istenildiği kadar çok argümanla çağrılmasını sağlamak için fonksiyon prototoipinde ve/veya tanımlamasında ... (ellipsis)
atmonun bulundurulması gerekir. Örneğin:
void foo(int a, ...);
Burada foo fonksiyonu en azından bir argümanla çağrılmak zorundadır. Ancak istenildiği kadar çok argümanla çağrılabilir. Örneğin:
foo(10); /* geçerli */
foo(10, 20); /* geçerli */
foo(10, 20, 30); /* geçerli */
foo(10, 20, 30, 40); /* geçerli */
foo(10, 20, 30, 40, 50); /* geçerli */
Fonksiyonun ... parametresi parametre listesinin sonunda bulunmak zorundadır. Örneğin aşağıdkai fonksiyon bildirimi geçersizdir:
void bar(int a, ..., int b); /* geçersiz! ... parametresi parametre listesinin sonunda bulunmak zorunda */
Fonksiyonun ... parametresinden önce en az bir parametresi olmak zorundadır. Örneğin:
void tar(...); /* geçersiz! ... parametresinden önce en az bir parametrenin olması gerekirdi */
Değişken sayıda argüman alan fonksiyonlar denildiğinde akla printf, fprintf, scanfi fscanf, sprintf, snprintf gibi fonksiyonlar gelmektedir.
Örneğin printf fonksiyonunun prototipi şöyledir:
int printf(const char *format, ...);
Görüldüğü gibi printf en azından char türden bir adresle çağrılmak zorundadır. Ancak bu argümandna sonra sıfır tane ya da n tane argüman
girilebilir. printf fonksiyonu stdout dosyasına yazılan karakter sayısı ile geri dönmektedir. Tabii aslında printf de başarısız olabilir.
Bu durumda negatif herhangi bir değere geri döner.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi değişken sayıda parametre alabilen fonksiyonlar nasıl yazılmaktadır? Bu fonksiyonların yazımındaki temel sorun ... parametresine
karşılık gelen argümanların elde edilmesidir. Örneğin:
void foo(int a, ...)
{
/* ... */
}
foo fonksiyonunu şöyle çağırmış olalım:
foo(10, 20, "ankara", 40.5, 50);
Biz fonksiyon içerisinde a parametre değişkeni yoluyla yalnızca 10 değerini elde edebiliriz. Pekiyi ya diğer değerler?
İşte ... parametresine karşılık gelen argümanların elde edilmesi için <stdarg.h> dosyası içerisinde bulunan aşağıdaki makrolar kullanılmaktadır:
void va_start(va_list ap, argN);
type va_arg(va_list ap, type);
void va_end(va_list ap);
Bu makrolar va_list isimli bir tür ile çalışmaktadır. Programcı önce va_start makrosunu çağırmalıdır. va_start makrosunun birinci parametresi
va_list türünden bir nesne, ikinci parametresiise ... parametresinden bir önceki parametreyi almaktadır. Örneğin:
void foo(int a, ...)
{
va_list va;
va_start(va, a);
/* ... */
}
Programcı argümanlarla işini bitirdikten sonra va_list türünden nesne ile son kez va_end makrosunu çağırmalıdır.
void foo(int a, ...)
{
va_list va;
va_start(va, a);
/* ... */
va_end(va);
}
... parametresine karşı gelen argümanların elde edilmesi için argümanların türlerinin biliniyor olması gerekmektyedir. Argümanları elde
eden asıl makro va_arg isimli makrodur. Bu makronun birinci parametresi va_list nesnesini, ikinci parametresi argümanın türünü almaktadır.
Bu makro her çağrıldığında bize sırasıyla argümanların değerleri verecektir. Programcı ... parametresi için geçilen argümanaların sayısını
da bilmemektedir. Bu sayıyı programcı bir biçimde ... parametresinden önceki parametreler için geçilen argümanlardan elde edebilir. va_arg
makrosunun kullanımı şöyledir:
arg1 = va_arg(va, int);
arg2 = va_arg(va, const char *);
...
Programcının va_arg makrosuyla eksik argüman argüman çekmesinde bir sorun yoktur. Ancak fazla sayıda argüman çekildiğinde "tanımsız davranış
(undefined behavior)" oluşmakatadır. Bu tür durumlarda bir çökmeyle karşılaşılmayabilir. Ancak çöp değerler elde edilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte add isimli fonksiyonun birinci parametresi ... parametresi için girilen argümanların sayısını belirtmektedir. Örnekte tüm
argümanların int türden olduğu varsayılmaktadır. Fonksiyonun prototipi şöyledir:
int add(int count, ...);
Fonksiyon argümanların toplamına geri dönmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdarg.h>
int add(int count, ...)
{
va_list va;
int total, val;
va_start(va, count);
total = 0;
for (int i = 0; i < count; ++i) {
val = va_arg(va, int);
total += val;
}
va_end(va);
return total;
}
int main(void)
{
int total;
total = add(5, 10, 20, 30, 40, 50);
printf("%d\n", total);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda birden fazla yazıyı alt alta yazdıran vputs isimli bir fonksiyon örneği verilmiştir. Fonksiyonun prototip şöyledir:
void vputs(const char *str, ...);
Fonksiyonu çağıracak kişinin argüman listesinin sonuna NULL adres yerleştirmesi gerekmektedir. Çünkü fonksiyon NULL adres görene kadar
va_arg makrosuyla argüman çekmektedir. Örneğin:
vputs("ali", "veli", "selami", "ayse", "fatma", (char *)0);
execl ve execlp fonksiyonlarının da bu biçimde olduğunu anımsayınız.
Burada NULL adres girilirken tür dönüştürmesi yapılmalıdır. Bu tür dönüştürmesinin neden gerektiğini exec fonksiyonlarının l'li versiyonlarında
ıklamıştık. Burada bir kez daha açıklamak istiyoruz. C'de argümana karşılık gelen parametre ... ise bu durumda derleyici "default argüman
dönüştürmesi (default argument conversion)" denilen bir dönüştürme yapmaktadır. Default argüman dönüştürmesinde int türünden küçük olan
türler int türüne (integer promotion), float türü double türüne ve 0 sabiti de int türden 0 olarak fonksiyona gönderilmektyedir. Yani
biz argümanda düz 0 kullanırsak bu artık NULL adres anlamına gelmez. Tabii sistemlerin hemen hepsinde NULL adres zaten tüm bitleri 0 olan
adrestir. Ancak int türü ile adres türlerinin farklı uzunluklarda olduğu 64 bit sistemlerde bu durumun soruna yol açma olasılığı yüksek
olmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdarg.h>
void vputs(const char *str, ...)
{
va_list va;
const char *arg;
va_start(va, str);
arg = str;
for (;;) {
if (arg == NULL)
break;
puts(arg);
arg = va_arg(va, const char *);
}
va_end(va);
}
int main(void)
{
vputs("ali", "veli", "selami", (char *)NULL);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda printf fonksiyonun nasıl yazılmış olabileceğine ilişkin bir ipucu vermek verilmiştir. Buradaki myprintf fonksiyonunda '%' karakteri
olmadığı sürece ilerlenmiş ve o karakterler stdout dosyasına yazdırılmıştır. '%' karakteri görüldüğünde onun yanındaki karaktere bakılıp
hangi türden argüman çekileceğine karar verilmiştir. Tabii orijinal printf fonksiyonu stdout dosyasının tamponuna yazmaktadır. Biz bu
örneğimizde putchar fonksiyonu ile yazdırma kısmını yaptık. Bu tür durumlarda elde genellikle bir karakterin ekrana yazdırılması için
temel bir fonksiyon zaten bulunmaktadır. Tabii putchar fonksiyonu zaten standart C fonksiyonu olduğu için örneğimizde tampona yazmaktadır.
Zaten genel olarak standart C'deki <stdio> kütüphaneleri yazılırken önce tamponlu çalışacak tek bir karakteri yazan fonksiyon oluşturulur
(bizim örneğimizde bu putchar) sonra o fonksiyon kullanılarak diğer fonksiyonlar yazılır
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <math.h>
#include <stdarg.h>
void disp_int(int val)
{
if (val < 0) {
putchar('-');
val = -val;
}
if (val / 10)
disp_int(val / 10);
putchar(val % 10 + '0');
}
int myprintf(const char *format, ...)
{
va_list va;
int total;
int val_int;
const char *val_str;
va_start(va, format);
total = 0;
while (*format != '\0') {
if (*format == '%') {
switch (*++format) {
case 'd':
val_int = va_arg(va, int);
disp_int(val_int);
if (val_int < 0) {
val_int = -val_int;
++total;
}
total += log10(val_int) + 1;
break;
case 'c':
putchar(va_arg(va, int));
++total;
break;
case 's':
val_str = va_arg(va, const char *);
while (*val_str != '\0') {
putchar(*val_str++);
++total;
}
break;
/* ... */
}
}
else {
putchar(*format);
++total;
}
++format;
}
va_end(va);
return total;
}
int main(void)
{
int a = -10;
char c = 'x';
char s[] = "ankara";
int result;
result = myprintf("a = %d, ch = %c, s = %s\n", a, c, s);
myprintf("%d\n", result);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
50. Ders 16/12/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C'nin "stdio" kütüphanesinde printf ailesi fonksiyonların va_list parametreli başı "v" ile başlayan versiyonları vardır. Bu sayede
biz bu printf ailesi fonksiyonları sarmalayabilen fonksiyonlar yazabiliriz. Örneğin printf fonksiyonun v'li versiyonun ismi vprintf, fprintf
fonksiyonunun v'li versiyonun ismi vfprintf fonksiyonudur. Bu fonksiyonların listesi şöyledir:
printf ===> vprintf
scanf ===> vscanf
fprintf ===> vfprintf
fscanf ===> vfscanf
sprintf ===> vsprintf
snprintf ===> vsnprintf
sscanf ===> vsscanf
Bu fonksiyonların v'li versiyonları v'siz versiyonlarından bir parametre daha fazla parametreye sahiptir. Bu fazla parametre son parametredir
ve va_list türündendir. Örneğin vprintf fonksiyonunun parametrik yapısı ile printf fonksiyonunun parametrik yapısını karşılaştırınız:
int printf(const char *format, ...);
int vprintf(const char *format, va_list ap);
printf fonksiyonunu sarmalayan bir fonksiyon vprintf kullanılarak şöyle yazılabilir:
int wrapper_printf(const char *format, ...)
{
va_list va;
int result;
va_start(va, format);
result = vprintf(format, va);
va_end(va);
return result;
}
Aslında burada yapılan şey "..." parametresi ile alınan bütün argümanları doğrudan vprintf fonksiyonuna geçirmektir. Aşağıda buna bir
örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <math.h>
#include <stdarg.h>
int wrapper_printf(const char *format, ...)
{
va_list va;
int result;
va_start(va, format);
result = vprintf(format, va);
va_end(va);
return result;
}
int main(void)
{
int a = 10;
double b = 3.14;
wrapper_printf("%d, %f\n", a, b);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
printf ailesi fonksiyonların sarmalanmak istenmesinin en önemli nedeni araya girip bir şeyler yapmaktır. Örneğin biz UNIX/Linux sistemlerinde
bir hata olduğunda errno değişkenine karşı gelen yazıyı stderr dosyasına yazdırarak programı sonlandıran aşağıdaki gibi bir fonksiyon
kullanıyorduk:
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
Burada perror fonksiyonu önce yazıyı sonra ':' karakterini ve sonra da bir boşluk bırakıp errno değerine karşı gelen yazıyı stderr
dosyasına yazdırmaktadır. Nihayetinde program exit fonksiyonuyla sonlandırılmıştır. Biz burada bu fonksiyondan daha yetenekli olan
printf gibi kullanılan bir fonksiyonu vprintf fonksiyonunu sarmalayacak biçimde de yazabiliriz. Örneğin:
void exit_vsys(const char *format, ...)
{
va_list va;
va_start(va, format);
vfprintf(stderr, format, va);
fprintf(stderr, ": %s\n", strerror(errno));
va_end(va);
exit(EXIT_FAILURE);
}
Burada önce vfprintf fonksiyonu ile printf gibi bilgiler stderr dosyasına yazdırılmış sonra ':' karamteri ve boşluk karakterinden sonra
errno değerinin yazısı yazdırılmıştır. Fonksiyon bu haliyle daha yenekli hale gelmiştir. Örneğin artık biz fonksiyonu şöyle kullanabiliriz:
if ((fd = open(argv[1], O_RDONLY)) == -1)
exit_vsys("%s cannot open", argv[1]);
Hata durumunda stderr dosyasına aşağıdaki gibi bir yazı basılacaktır:
xxx cannot open: No such file or directory
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdarg.h>
void exit_vsys(const char *format, ...);
int main(int argc, char *argv[])
{
int fd;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((fd = open(argv[1], O_RDONLY)) == -1)
exit_vsys("%s cannot open", argv[1]);
close(fd) ;
return 0;
}
void exit_vsys(const char *format, ...)
{
va_list va;
va_start(va, format);
vfprintf(stderr, format, va);
fprintf(stderr, ": %s\n", strerror(errno));
va_end(va);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte exit_vsys fonksiyonunun tanımlaması "libsys.c" isimli dosyaya, prototipi de "libsys.h" isimli dosyaya yerleştirilmiştir.
Bu dosya bir kez derlenip aşağıdaki gibi link aşamasına dahil edilebilir:
gcc -c libsys.c
gcc -Wall -o sample sample.c libsys.o
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* sample.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include "libsys.h"
int main(int argc, char *argv[])
{
int fd;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((fd = open(argv[1], O_RDONLY)) == -1)
exit_vsys("%s cannot open", argv[1]);
close(fd);
return 0;
}
/* libsys.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdarg.h>
void exit_vsys(const char *format, ...)
{
va_list va;
va_start(va, format);
vfprintf(stderr, format, va);
fprintf(stderr, ": %s\n", strerror(errno));
va_end(va);
exit(EXIT_FAILURE);
}
/* libsys.h */
#ifndef LIBSYS_H_
#define LIBSYS_H_
/* Function Prototypes */
void exit_vsys(const char *format, ...);
#endif
/*------------------------------------------------------------------------------------------------------------------------------------------
Masaüstü (desktop) işletim sistemlerinin çalıştığı kapasiteli işlemlerin "sayflama (paging)" denilen bir mekanizması vardır. Sistem
programcılarının bu sayfalama mekanizmasını temel düzeyde biliyor olmaları gerekir. Sayfalama mekanizması "sanal bellek (virtual memory)"
denilen bellek yönetim tekniği için de bir araç durumundadır.
Intel işlemcileri 80386 ile birlikte (ilk kez 1985) sayfalama mekanizmasına sahip olmuştur. ARM işlemcilerinin A'lı (Application) serisi
cortex'leri bu mekanizmaya sahiptir. PwerPC; Itanimum, Alpha gibi yaygın işlemlerin de sayfalama mekanizması bulunmaktadır. Ancak düşük
kapasiteli işlemcilerde ve mikrodenetleyicilerde genel olarak bu mekanizma yoktur. Örneğin ARM işlemcilerinin M'li (Microcontoller) serisi
genel olarak sayfalama mekanizmasına sahip değildir.
Sayfa (page) bellekteki ardışıl byte topluluklarına denilmektedir. Sayfalama mekanizmasında işlemci RAM'e byte düzeyinde erişebilir. Ancak
aynı zamanda RAM'i sayfalardan (bloklardan) oluşan bir birim gibi ele almaktadır. Sayfa büyüklükleri farklı işlemcilerde farklı olabilmektedir.
Bazı işlemciler farklı büyüklükteki sayfaları destekleyebilmektedir. Ancak işlemcilerin desteklediği en yaygın kullanılan sayfa büyüklüğü
4K'dır. Örneğin Windows, Linux, macOS sistemleri işlemcinin 4K sayfalama mekanizmasını kullanmaktadır. Bu nedenle biz kursumuzda sayfa
denildiğinde onun büyüklüğünün default olarak 4K olduğunu varsayacağız.
İşlemciler RAM'in her sayfasına ilk sayfa 0 olmak üzere bir numara verirler. Örneğin RAM'in tepesindeki ilk 4096 byte 0'ıncı sayfa,
sınraki 4096 byte 1'inci sayfadır ve bu böyle devam etmektedir. Bir adres aslında RAM'de belirli bir sayfanın belirli bir offset'indedir.
Burada "offset" demekle ilgili sayfanın başından itibaren olan uzaklığı kastediyoruz. Sayfa uzunluğunun 4K olduğunu varsayalım (gerçekte
uygulanan tipik durum). İşlemcinin belli bir adresin hangi sayfada o sayfanın hangi offset'inde olduğunu anlaması oldukça kolaydır. Adresin
sağdaki 12 biti sayfa offsetini verir. Adres değeri 12 kere sağa ötelenirse bu da adresin sayfa numarasını verecektir. Örneğin 3F7C42
biçiminde bir adres olsun. Bu adres RAM'in 3F7'inci sayfasındadır ve bu sayfanın C42'inci offset'indedir:
3F7 ==> sayfa numarası C42 ==> Sayfa offset'i
Sayfalama mekanizmasını kullanan işletim sistemleri programları ardşıl bir biçimde RAM'e yüklememektedir. Program işletim sistemi tarafından
sayfa büyüklüklerine göre parçalara ayrılır, parçalar boş sayfalara yğklenir. Örneğin elimizde 20K'lık bir program olsun. Eğer sistemde
sayfalama mekanizması olmasaydı bu 20K ardışıl bir biçimde RAM'e yüklenecekti. Ancak sayfalama mekanizması söz konusu olduğunda bu 20K'lık
program 4K'lık 5 parçaya ayrılır ve bu 5 parça ardışıl olmayabilen 5 boş syfaya yüklenir. Böylece 20K'lık programın 4'er K'lık parçaları
farklı sayfaalarda bulunuyor olacaktır. Örneğin fiziksel belleğin belli bir bölümünün sayfaları aşağıdaki gibi olsun:
Sayfa Numarası Sayfanın Durumu
... ...
3FC5 <DOLU>
3FC6 <BOŞ>
3FC7 <DOLU>
3FC8 <DOLU>
3FC9 <BOŞ>
3FCA <BOŞ>
3FCB <DOLU>
3FCC <DOLU>
3FCD <BOŞ>
3FCE <DOLU>
3FCF <BOŞ>
3FD0 <DOLU>
... ....
Şimdi işletim sistemi programın sayfalarını boş sayfalara yğkleyecektir. Şöyle bir durum oluşabilecektir:
Sayfa Numarası Sayfanın Durumu
... ...
3FC5 <DOLU>
3FC6 Programın 1'inci Parçası
3FC7 <DOLU>
3FC8 <DOLU>
3FC9 Programın 2'inci Parçası
3FCA Programın 3'üncü Parçası
3FCB <DOLU>
3FCC <DOLU>
3FCD Programın 4'üncü Parçası
3FCE <DOLU>
3FCF Programın 5'inci Parçası
3FD0 <DOLU>
... ....
Burada iki soru karşımıza çıakacaktır: Birincisi programın ardışıl yüklenmemesinin avantajı nedir? İkincisi de programın hangi parçasının
hangi sayfalarda olduğunu işletim sistemi nasıl bilmektedir?
Sayfalama mekanizmasına sahip işlemciler çalışırken ismine "sayfa tablosu (page table)" denilen bir tabloya balarak çalışırlar. Sayfa tablosu
işletim sistemi tarafından her proses için ayruı bir biçimde oluşturulur ve Proses Kontrol Bloğunda tutulur. İşlemciler sayfa tablolarını
belli bir yazmacın gösteridği yerde ararlar. Böylece işletim sistemi prosesin sayfa tablosunu oluşturur. İşlemcinin ilgili yazmacına
sayfa tablosunun adresini yerleştirir. İşlemci de o sayfa tablosunu kullanır. Bugünkü işletim sistemleri daha önceden de belirttiğimiz gibi
thread temelinde preemptive zaman paylaşımlı sistemlerdir. Bir thread işlemciye atanır. Belli bir süre çalıştırılır, sonra çalışmasına
ara verilip diğer bir thread işlemciye atanır. Eğer threadler arası geçiş (context switch) sırasında ara verilen thread ile geçilen thread
farklı proseslerin thread'leri ise işletim sistemi işlemcinin ilgili yazmacındaki değeri değiştirerek işlemcinin yeni geçilen thread'in
ilişkin olduğu prosesin sayfa tablosunu kullanmasını sağlar. Yani belli bir anda hangi program çalışıyorsa işlemci onun sayfa tablosunu
kullanıyor durumda olur. Intel İşlemcilerinde sayfa tablosunun yeri CR3 yazmacıyla belirlenmektedir.
Sayfa tablosu aslında sanal sayfaları fiziksel sayfalara eşleyen bir tablodur. Sayfa tablosunun kaba biçimi aşağıdaki gibidir:
Sanal Sayfa No Fiziksel Sayfa No
... ...
4000 3FC6
4001 3FC9
4002 3FCA
4003 3FCB
4004 3FCF
... ....
Örneğin 32 bitlik bir sistemde derleyici sanki program 4GB'lik boş bir RAM'e tek başına yüklenecekmiş gibi kod üretmektedir. Örneğin
32 bit Windows sistemlerinde derleyici ve linker sanki program boş bir 4GB'lik belleğin 4MB'sinden itibaren yüklenecekmiş gibi kod
üretir. İşletim sistemi programı 4K'lık sayfalara ayırıp anları o parçaları boş sayfalara yerleştirmektedir. Böylece programın sanal
adres alanı ardışılmış gibi bir durum oluşturulur. İşlemci sayfa tablosuna bakarak çalıştığı için bir sorun çıkmamaktadır. İşlemci
karşılaştığı her adresi "sayfa numarası" ve "sayfa offset'i" biçiminde iki parçaya ayırmakta, sonra sayfa numarasını sayfa tablosundan
fiziksel sayfa numarasına dönüştürüp aslında o fiziksel sayfada ilgili offset'e erişmektedir. Örneğin işlemci aşağıdaki gibi bir makine
komutu ile karşılaşmış olsun:
MOV EAX, [4003F12]
Burada 4003F12 adresi 4003 sayfa numarasının F12 offset'ini belirtmektedir. İşlemci sayfa tablosunda 4003 numaralı sayfanın 3FCB numaralı
fiziksel sayfayla eşleştirildiğini görür aslında fiziksel bellekte 3FCBF12 adresine erişir. Program içerisindeki bütün adresler gerçek
fiziksel adresler değildir. Bunlara "sanal adres (virtaul address)" ya da "doğrusal adres (linear address)" denilmektedir. İşlemci sanal
adresleri fiziksel adreslere dönüştürüp fiziksel RAM'de gerçek yere erişmektedir. Prosesin sanal adres alanının ardışıl olduğuna ancak
fiziksel adres alanının ardışıl olmadığına dikkat ediniz. Bu durumda iki programdaki aynı sanal adresler aslında aynı fiziksel adres
belirtmemektedir. Çünkü işletim sistemi her prosesin sayfa tablosunda sanal adresleri farklı fiziksel sayfalara yönlendirmektedir.
Örneğin aynı programı ikinci kez çalıştırdığımızda aslında program içerisindeki sanal adresler değişmez. Çünkü her program sanal belleğe
onun belli bir noktasından itibaren tek başına yüklenecekmiş gibi koda sahiptir. Aşağıdaki örnekte 20K uzunluğunda bir programın ikinci kez
çalıştırımasındaki sayfat atblosu temsili olarak verilmiştir.
Sanal Sayfa No Fiziksel Sayfa No
... ...
4000 3FC6
4001 3FC9
4002 3FCA
4003 3FCB
4004 3FCF
... ....
Sanal Sayfa No Fiziksel Sayfa No
... ...
4000 4F12
4001 7B14
4002 F12A
4003 47C8
4004 16CB
... ....
Burada örneğin 40013FC adresi her iki proseste aynı sanal adres olsa da farklı fiziksel adres belirtmektedir.
Sayfalama mekanizmasının kullanıldığı sistemlerde işletim sistemi zaten prosesleri sayfa tabloları yoluyla izole etmektedir. Yani bir
iki prosesin sayfa tablolarındaki fiziksel sayfa adresleri zaten farklıdır. Bu durumda bir proses diğerinin fiziksel sayfalarına
zaten erişemez.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
51. Ders 17/12/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Sayfalama "sanal bellek (virtual memory)" denilen bellek yönetim tekniğini uygulanması için gerekli bir mekanizmadır. Sanal bellek bir
programın tamamının değil yalnızca belli sayfalarının RAM'e yüklenip disk ile RAM arasında yer değiştirmeli biçimde çalıştırılması
anlamına gelmektedir. Bugün Windows, Linux, macOS gibi işletim sistemleri sanal bellek mekanizmasını kullanmaktadır.
Sanal bellek mekanizmasında işletim sistemi prosesin tüm sayfalarını fiziksel RAM'e yüklemez. Yalnızca bazı sayfalarını yükleyerek
programı çalıştırır. Yani sayfa tabşlosunda bazı sanal sayfalara fiziksel bir sayfa karşılık düşürülmemiştir. Örneğin:
Sanal Sayfa No Fiziksel Sayfa No
... ...
4000 3FC6
4001 -
4002 3FCA
4003 -
4004 3FCF
... ....
Buradaki '-' karakterleri ilgili sanal sayfa için bir fiziksel sayfanın karşı düşürülmediğ anlamına gelmektedir. Pekiyi işlemci
böyle bir durumla karşılaştığında ne yapmaktadır? Örneğin işlemci aşağıdaki gibi bir makine komutuyla karşılaşsın:
MOV EAX, [400117C]
Bu makine komutunda belleğin 400117C adresindeki 4 byte CPU yazmacına çekilmek istenmektedir. Komuttaki adresin sanal sayfa numarası
4001'dir. İşlemci sayfa tablosuna başvurup bu sanal sayfa için bir fiziksel sayfanın karşı gelmediğini gördüğünde bir ""içsel kesme
(internal interrupt)" oluşturmaktadır. Bu içsel kesmeye "page fault" da denilmektedir. Page fault oluşturğunda işlemci sistem programcısının
belirlediği bir kodu çalıştırır. Bu koda "page fault handler" denilmektedir. Page fault handler işletim sistemini yazanlar tarafından
yazılmıştır. Böylece page fault oluştuğunda aslında otomatik olarak işletim sisteminin bir kodu devreye girmektedir. İşletim sisteminin
bu page fault kodu önce buna yol açan adresi incelemektedir. Bu kod söz konusu adresteki adresteki sanal sayfa numarısına bakar. Bunun
programın neresine (yani hangi 4K'lık kısmına) karşı geldiğini tespit eder. Programın o 4K'lık kısmını diskten alarak boş bir sayfaya
yükler. Sonra sayfa tablosunu düzeltir. Artık sayfa tablosunda ilgili sayfaya karşı bir fiziksel sayfa bulunmaktadır. Sonra kesme kodunu
sonlandırır. Page fault kodunun çalışması bittiğinde işlemci bu fault'a yol açan makine komutuyla çalışmasına devam eder. Dolayısıyla
artık ilgili adresin sanal sayfa numarası fiziksel bir sayfa numarasına yönlendirilmiş durumdadır. Kullanıcılar programın kesiksiz çalıştığını
sanmaktadır. Aslında program ilgili sayfaların gerektiğinde diskten RAM'e yüklenmesiyle çalışmaktadır. (Örneğin diskinizin ışığına bakarsanız
onun sürekli yanıp sçmdüğünü fark edersiniz. Çünkü programlar çalışırken sürekli "pgae fault" oluşmaktadır.) Yukarıdaki örneğimizde 400117C
adresine erişldiğinde oluşan page fault'ta işletim sisteminin programın ilgili 4K'lık parçasını RAM'de boş olan 418F numaralı fiziksel
sayfaya yüklediğini düşünelim. Artık sayfa tablosunun ilgili kısmı şöyle olacaktır:
Sanal Sayfa No Fiziksel Sayfa No
... ...
4000 3FC6
4001 418F
4002 3FCA
4003 -
4004 3FCF
... ....
Pekiyi page fault oluştuğunda fiziksel RAM'deki tüm sayfalar doluysa ne olacaktır? Bu durumda işletim sistemi dolu olan bir sayfayı fiziksel
RAM'den atacak ve programa ilişkin 4K'lık kısmı bu bu fiziksel sayfaya yükleyecektir. Tabii işletim sistemi "ileride en az kullanılabilecek"
bir sayfayı RAM'den atmaya çalışmaktadır. Bunun için kullanılan algprtimalara "page replacement" algoritmaları denilmektedir. En çok tercih
edilen "page replacment" algoritması "LRU (Least Recently Used)" algoritmasıdır. Bit fiziksel sayfanın RAM'den atılmasına İngilizce "swap-out",
diskteki bir sayfanın RAM'e çekilmesine ise "swap-in" denilmektedir. Disk ile RAM arasındaki bu tür değiştirmelere ise "swapping" ismi
verilmektedir.
Pekiyi swap-out işlemi yapılacakken ya RAM'deki sayfanın içeriği değişmişse ne olacaktır? Eğer RAM'deki sayfanın içeriği değişmemişse
ve bu sayfa çalıştırılabilir dosyanın (executable file) içerisinde zaten varsa o sayfa doğrudan RAM'den atılabilir. Çünkü gerektiğinde
yine çalıştırılabilir dosyanın içerisinde alınabilecektir. Ancak eğere RAM'deki sayfa değişmişse (buna "dirty" hale gelmek de denilmektedir)
bu durumda o sayfa daha sonra yeniden swap-in yapılabileceği için sayfanın diskte saklanması gerekir. İşte güncellenmiş olan (kirlenmiş olan)
bu tür sayfaların diskte saklanabilmesi için işletim sistemleri "swap dosyası (swap files)" oluşturmaktadır. Tabii işletim sistemlerinin
oluşturduğu swap dosyalarının da bir büyüklüğü vardır. Eğer o büyüklük aşılırsa sanal belleğin de limitine ulaşılmış olur. O halde kabaca
aslında sanal belleğin toplam büyüklüğü kabaca "fiziksel RAM + swap dosyası" kadardır. Swap dosyaları genellikle işletim sisteminin çalışma
zamanı sırasında dinamik olarak büyütülmezler. Bunlar için genellikle baştan bir yer ayrılmaktadır. Bu yerin büyüklüğünü sistem yöneticisi
belirleyebilmektedir. Örneğin Windows sistemlerinde "Denetim Masası/System/Advanced System Settings/Performance/Advanced/Virtual Memory"
penceresinden değiştirilebilmektedir. Linux sistemleri bu bakımdan da esnektir. Genellikle Linux sistemlerinde swap dosyası bir "disk
bölümü (disk paritition)" olarak ayrılır. Ancak sistem yöneticisi isterse disk dosyası yaratıp onu da swap alanına dahil edebilmektedir.
Bu sistemlerde "free" komutu ile ya da "swapon -s" komutu ile swap alanlarının listesi alınabilmektedir. Linux'ta bir dosyanın swap alanına
dahil edilmesi için ne yapılması gerektiğine ilgili dokümanlardan erişebilirsiniz.
Pekiyi sanal bellek kullanan sistemlerde dinamik bellek tahsisatı nasıl yapılmaktadır? Dinamik alan çalıştırılabilir dosyanın içerisinde
olmadığına göre ve bunun için swap dosyası içerisinde bir yer ayrılmalıdır. malloc gibi bir fonksiyonu çağırdığımızda bu fonksiyon swap dosyası
içerisinde sayfalar için yer ayırmakla birlikte gerçek fiziksel RAM'de henüz bir tahsisat yapmamaktadır. Dinamik tahsis edilen alanın
sayfalarına erişildiğinde swap-in yapılmaktadır. Biz eldeki fiziksel RAM'im ötesinde dinamik tahsisatlar yapabiliriz. Ancak tabii swap
dosyalarımızın toplam uzunluğu da bizim için bir limit oluşturmaktadır.
Pekiyi işletim sistemi bir sayfanın güncellendiğini (kirlendiğini) nasıl anlamaktadır? İşte aslında sayfa tablosunda her sayfanın bilgilerinin
tutulduğu bir yer de vardır. Yani sayfa tablosunun organizasyonu aşağıdaki gibidir:
Sanal Sayfa No Fiziksel Sayfa No Sayfa Özellikleri
... ... ...
4000 3FC1 read-write-user
4001 1FC2 read-kernel
4002 4D02 read-write-user
... ... ...
İşlemci ne zaman bir sayfaya erişse o sayfanın sayfa tablosundaki "D (Dirty)" bitini set etmektedir. İşletim sistemi de baştan bu reset
eder sonra swap-out yapacağı zaman bu bite bakar. eğer bu bit set edilmişse sayfanın güncellenmiş olduğunu düşünür.
Sayfalara "read only" ya da "read/write" biçiminde özellikler verilebilmektedir. "read-only" bir sayfaya yazma yapıldığında "page fault"
oluşmaktadır. Böyle bir durumda işletim sistemi prosesi cezalandırıp sonlandırmaktadır. Örneğin C derleyicileri string ifadelerini
"read-only" section'lara yerleştirmektedir. İşletim sistemi de bu sectionlar'ı RAM'e sayfa sayfa yüklemektedir. String'lerin bulunduğu
bu sayfaların sayfa özelliklerini "read-only" yapmaktadır. Dolayısıyla Windows sistemlerinde, Linux ve macOS sistemlerinde bir string'in
herhangi bir karakterini değiştirmek istediğimizde page fault oluşmakta bunun sonucu olarak da prosesimiz işletim sistemi tarafından
sonlandırılmaktadır. Zaten C'de bir string'in karakterlerini değiştirmek "tanımsız davranışa" yol açmaktadır.
Sayfaların diğer bir özelliği de "user mode/kernel mode" özelliğidir. Bir sayfa "user mode" özelliğindeyse o sayfaya user modda çalışan
prosesler ve kernel modda çalışan (örneğin işletim sistemi) prosesler erişebilir. Ancak sayfa eğer kernel modda ise o sayfaya yalnızca
kernel modda çalışan prosesler erişebilir. User modda çalışan prosesler kernel mod özelliğine sahip sayfalara erişmek istediğinde page
fault oluşmaktadır. İşletim sistemi de bu prosesleri cezalandırarak sonlandırmaktadır.
Bir proses çalışırken işletim sisteminin kodları da sayfa tablosunda fiziksel sayfalara yönlendirilmiş durumdadır. Çünkü proses sistem
fonksiyonlarını çağırdığında akışın işletim sisteminin içerisine girip o sistem fonksiyonun kodlarını çalıştırabilmesi için işletim sisteminin
kodlarının bulunduğu sayfalarında sayfa tablosunda bulunuyor olması gerekir. İşletim sistemi kendisini user mode proseslerden korumak
için kendi kodlarının bulunduğu sayfaları kernel mode sayfa olarak özelliklendirir. Böylece user mode programlar kernel mode sayafalara
erişmek istediğinde page fault oluşmakta ve proses sonlandırılmaktadır. Sistem fonksiyonları çağrıldığında prosesin çalışma modunun
user mode'dan kernel mode'a geçtiğini anımsayınız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
32 bit Windows sistemleri 32 bit işlemciler için yazılmıştır. Bu işlemcilerin toplam adresleyebildiği fiziksel RAM 4GB'dir. Dolayısıyla bu
sistemlerde zaten sayfa tabloları 4GB'lik bir fiziksel alanı haritalandırmaktadır. 32 bit Windows sistemlerinde her proses sanki 4GB'lik
bir sanal belleğe tek başına yükleniyormuş gibi sanal bellek kullanmaktadır. Ancak Windows 4GB alanın 2GB'sini user için 2GB'sini de
kernel için ayırmıştır. Bu durumda 32 bit Windows sistemlerinde normal bir prosesin kullanabileceği maksimum sanal bellek 2GB'dir.
64 bit Windows sistemleri 64 bit işlemciler için yazılmıştır. Bu işlemciler teorik olarak 16EB belleği adresleyebilmektedir. 64 bit Windows
sistemleri eskiden yalnızca belleğin ilk 16TB'sini kullanıyordu. Ancak Windows 10 ile birlikte bu sistemlerde 128TB alan user prosesler için
128TB alan da kernel tarafından kullanılmaktadır. Bu konun ayrıntıları vardır.
32 bit Linux sistemlerinde sanal bellek alanının ilk 3GB'si user prosesler tarafından kalan 1GB'si kernel tarafından ayrılmıştır. 64 bit Linux
sistemlerinde ise user alanı ve kernel alanı 128TB kadar olabilmektedir. İşlemci destekliyor olsa da işletim sistemleri gereksiz büyüklükleri
onları kontrol etmek için alan gerektiği için desteklememektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi işletim sistemi farklı iki prosesin farklı sanal sayfalarını aynı fiziksel sayfaya yönlendirirse ne olur? Örneğin aşağıda iki
prosesin sayfa tablosuna temsil bir örnek verilmiştir:
Sanal Sayfa No Fiziksel Sayfa No
... ...
4000 3FC6
4001 56F1
4002 3FCA
4003 26C3
4004 3FCF
... ....
Sanal Sayfa No Fiziksel Sayfa No
... ...
4000 246C
4001 17F3
4002 5421
4003 5421
4004 3FC6
... ....
Burada birinci prosesin 4000 numaralı sanal sayfası ve ikinci prosesin 4004 numaralı sanal sayfası 3FC6 fiziksel sayfsına yönlendirilmiştir.
O halde birinci proses 4000000-400FFF sanal adreslerine bir şey yazdığında diğer proses bu yazılanları 4004000-4004FFF sanal adreslerinden
okuyabilecektir. Yani iki prosesin farklı sanal sayfaları aslında aynı fiziksel sayfayı görmektedir. Bu biçimdeki proseslerarası haberleşme
yöntemine "shared memory" denilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Şimdi aynı programı ikinci kez çalıştırdığımızı düşünelim. Bu durumda aslında yanı kodlar farklı bir proses olarak kullanılacaktır. Bu tür
durumlarda işletim sistemi başlangıçta mümkün olduğunca ikinci kopyası çalıştırılan prosesin sayfa tablosundaki girişleri birinci kopyanın
kullandığı fiziksel sayfalara yönlendirmektedir. Yani işletim sistemi iki prosesin sayfa tablosundaki fiziksel sayfa numaralarını mümkün
olduğunca aynı yapar. Tabii bu prosesler biribirinden farklı olduğuna göre proseslerden birinin fiziksel sayfaya yazma yaptığında diğerinin
bu yazmadan etkilenmemesi gerekir. Pekiyi bu nasıl sağlanacaktır? İşte işletim sistemleri başlangıçta aynı fiziksel sayfaları kullanan
proseslerin birinin bu fiziksel sayfaya yazma yaptığı zaman bu fiziksel sayfanın kopyasını çıkarmaktadır. Yani iki proses (daha fazla da
olabilir) başlangıçta aynı fiziksel sayfayı kullanıyor olsa da bir yazma olayı gerçekleştiğinde artık bu fiziksel sayfalar birbirinden
ayrılmaktadır. Bu mekanizmaya işletim sistemlerinde "copy on write" denilmektedir.
Pekiyi "copy on write" mekanizmasını işletim sistemi nasıl gerçekleştirmektedir. Genellikle kullanılan yöntem şöyledir: İşletim sistemi
aslında read-arite özelliğe sahip olan sayfalara "copy on write" uygulayabilmek için "read-ony" özellik atamaktadır. Bu sayfaya proseslerden
biri yazma yapmaya çalıştığında "page fault" oluşmakta ve işletim sistemi devreye girip sayfanın kopyasını çıkartmaktadır. Tabii artık işletim
sistemi kopyası çıkartılan sayfanın özelliklerini "read-write" olarak değiştirecektir.
Örneğin UNIX/Linux sistemlerinde fork işlemi sırasında aslında baştan tüm prosesin bellek alanının gerçek bir kopyası oluşturulmamaktadır.
Alt prosesin sayfa tablosunun içeriği üzt prosesteki gibi yapılmaktadır. Ancak alt ya da üst proseslerden biri bu sayfalardan birine yazma
yaptığında o sayfanın kopyasından çıkarılmaktadır. Bu nedenle fork sonrasında alt proseste exec işlemi yapıldığında aslında bunun bellek
kopyalama maliyeti oluşmamaktadır. Zaten artık exec öncesinde cfork kullanılmamasının bir nedeni de budur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
52. Ders 23/12/2023 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir prosesin diğer bir prosese byte düzeyinde bir bilgi göndermesine v o prosesin de bu bilgiyi almasına "proseslerarası haberleşme
(inteprocess communication ya da IOPC) denilmektedir. Proseslerarsı haberleşme sistem programlamanın önemli konularındandır. Modern
işletim sistemlerinde proseslerin bellek alanları sayfa tabloları yoluyla biribirinden izole edildiği için bir prosesin diğerine bilgi
gönderip ondan bilgi alması ancak özel birtakım yöntemlerle gerçekleştirilmektedir. Proseslerarası haberleşme kendi içinde iki ana bölüme
ayrılabilir:
1) Aynı makinenin prosesleri arasında haberleşme
2) Farklı makinelerin prosesleri arasında haberleşme
Aynı makinenin prosesleri arasındaki haberleşmelerde farklı işletim sistemlerinde benzer mekanizmalar geliştirilmiştir. Farklı makinelerin
prosesleri arasındaki haberleşmenin farklı unsurları da bulunmaktadır. İki makinedeki farklı proseslerin haberleşebilmesi için bir haberleşme
ortamının oluşturulmuş olması gerekir. Genel olarak haberleşmede uyulması gereken kurallar topluluğuna "protokol (protocol)" denilmektedir.
Farklı maikenelerin prosesleri arasındaki haberleşmeler iyi tanıomlanmış protokoller yoluyla yapılmaktadır. Bunun için çeşitli protokol
aileleri geliştirilmiştir. Günümüzde en yaygın kullanılan protok ailesi IP denilen protokol ailesidir.
Biz kursumuzun bu bölümünde öne "aynı makinenin prosesleri arasındaki haberleşmeleri" inceleyeciz. Daha sonra başka bir bölümde IP protokol
ailesi ile haberleşme üzerinde duracağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aynı makinenin prosesleri arasındaki haberleşmelerde Windows sistemleri ile UNIX/Linux (ve macOS) sistemleri arasındaki yöntemler çok benzerdir.
Tabii bu yöntemler bu sistemlerde farklı fonksiyonlarla gerçekleştirilmektedir. Yöntemler tema olarak birbirilerine bzense de ayrıntılarda
farklılıklar bulunmaktadır. Biz de kurusumuzun bu bölümünde önce değişik yöntemleri önce UNIX/Linux (dolayısıla macOS) sistemlerinde sonra
da Windows sistemlerinde göreceğiz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Boru haberleşmesi en yalın ve en çok kullanılan proseslerarası haberleşme yöntemidir. Boru FIFO prensibiyle çalışan bir kuyruk sistemidir.
Borunun bir usu bir proseste diğer ucu diğer prosestedir. Proseslerden biri boruya yazma yaptığında diğeri yazılanları yazıldığı sırada
okumaktadır. Boruların belli bir uzunluğu vardır. Boruya yazma yapan proses eğer borudan okuma yapan proses yavaş kalırsa boruyu doldurabilir.
Dolu bir boruya yazma yapıldığında yazma yapan taraf bloke olur (yani CPU zamanı harcamadan bekler) ta ki okuyan taraf borıda yer açana kadar.
Benzer biçimde borudan okuma yapan taraf boru boşsa okunacek bir şey kalmadığı için bloke olmaktadır. Ta ki yazan taraf boruya bir şey
yazana kadar. Böylece boru haberleşmesinde yazan taraf okuyan tarafı okuyan taraf da yazan tarafı beklemektedir. Dolayısıyla bu yöntem kendi
içerisinde bir senkroniasyon da içermektedir. Boru haberleşmesi her zaman önce yazan tarafın boruyu kapatmasıyla sonlandırılır. Bu durumda
borudan okuma yapan taraf önce boruda kalanları okur, sonra okunacak bir şey kalmayınca o da boruyu kapatır. Haberşelmede önce okuyan tarafın
boruyu kapatması patolojik bir durumdur.
Boru haberleşmeleri kendi aralarında ikiye ayrılmaktadır:
1) İsimsiz Boru Haberleşmeleri (Unnamed Pipes ya da Anonymous Pipe)
2) İsimli Boru Haberleşmeleri (Named Pipes ya da FIFO)
İsimiz boru haberleşmesi üst ve alt prosesler arasında kullanılmaktadır. Ancak isimli boru haberleşmesi herhangi iki proses arasında
kullanılabilmektedir:
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux (ve macOS) sistemlerinde isimsiz boru haberleşmesi şu adımlardan geçilerek gerçekleştirilmektedir:
1) Önce üst proses pipe POSIX fonksiyonuyla isimsiz boruyu yaratır. pipe fonksiyonun prototipi şöyledir:
#include <unistd.h>
int pipe(int pipefd[2]);
pipe fonksiyonunun parametresi int türden göstericidir. Programcı 2 elemanlı int bir dizi açarak dizinin adresini fonksiyona geçirir.
(Prototipteki dizi sentaksının göstericiden hiçbir farkı yoktur. Dolayısıyla burada programcı aslında fonksiyona iki elemanlı int dizinin
adresini geçirmek zorunda dğeildir. Burada okunabilirliğin artırılması için böyle bir prototip yazılmıştır.) Fonksiyon boruyu yaratır.
Boruya ilişkin iki betimleyiciyi argüman olarak verdiğimiz int diziye yerleştirir. Dizinin ilk elemanındaki (0'ıncı indeksteki) betimleyici
"read-only" bir betimleyicidir ve borudan okuma yapmak için kullanılmalıdır. Dizinin ikinci elemanındaki (1 numaralı indeksindeki) betimleyici
"write only" bir betimleycidir boruya yazma yapmak için kullanılmalıdır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1
değerine geri döner. Örneğin:
int pfds[2];
if (pipe(pfds) == -1)
exit_sys("pipe");
/* pfds[0] betimleyicisi okuma yapmak için pfds[1] betimleyicisi yazma yapmak için kullanılmalıdır */
2) Artık alt proses yaratılır. Böylece üst prosesteki boru betimleyicileri de alt proses aktarlmış olur. Örneğin:
int pfds[2];
int pid;
if (pipe(pfds) == -1)
exit_sys("pipe");
if ((pid = fork()) == -1)
exit_sys("pid");
if (pid != 0) { /* parent process */
...
}
else { /* child process */
...
}
3) Boru ve alt proses yaratıldıktan sonra hangi tarafın okuma yapacağına ve hangi tarafın yazma yapacağına programcının karar vermesi
gerekir. Örneğin üst proses yazma yapacak, alt proses okuma yapacak olabilir. Ya da bunun tersi olabilir. Borular sanki birer dosyaymış
gibi ele alınmaktadır. Dolayısıyla borulardan okuma yapmak için read fonksiyonu, borulara yazma yapmak için write fonksiyonu kullanılmaktadır.
fork işlemi sonrasında üst prosesteki iki boru betimleyicisi alt prosese aktarılmış olur. Böylece hem üst proseste hem de alt proseste
okuma ve yazma betimleyicileri bulunacaktır. Normal olarak boruya yazma potansiyelinde olan ve borudan okuma potansiyelinde olan tek bir
betimleyici bulunmalıdır. Dolaysıyla yazan taraf okuma betimleyicisini, okuyan taraf ise yazma betimelyicisini kapatmalıdır. Örneğin
üst proses yazma yapacak olsun alt proses de okuma yapacak olsun:
int pfds[2];
int pid;
if (pipe(pfds) == -1)
exit_sys("pipe");
if ((pid = fork()) == -1)
exit_sys("pid");
if (pid != 0) { /* üst proses boruya yazma yapacak */
close(pfds[0]);
...
}
else { /* alt proses borudan okuma yapacak */
close(pfds[1]);
...
}
read fonksiyonu ile borudan n byte okuma yapılmak istendiğinde eğer boruda hiçbir byte yoksa read fonksiyonu bloke olur ve en az 1 byte
okuyana kadar beklemeye yol açar. Eğer borudan n byte okunmak istediniğinde boruda en az 1 byte bilgi oluşmuşsa bu durumda read fonksiyonu
n byte'ın tamamı okunan kadar blokede beklemez. Okuyabildiği kadar byte'ı okur ve okuyabildiği byte sayısı ile geri döner. Eğer boruda
okunacak bir şey yoksa ancak boruya yazma potansiyelinde olan tüm betimleyiciler de kapatılmışsa bu durumda read fonksiyonu bloke olmaz
ve 0 değeri ile geri döner. Yani read fonksiyonu 0 ile geri dönmüşse bu durum "boruda bir şey kalmadı yazan taraf da boruyu kapatmış"
anlamına gelmektedir.
write fonksiyonu ile boruya n byte yazma yapılmak istendiğinde write fonksiyonu n byte'ın hepsi yazlana kadar blokede beklemektedir.
Yani borulara kısmi yazım (partial write) mümkün değildir. Örneğin boruda 10 byte'lık boş alan olsun. Biz de write fonksiyonu ile boruya
15 byte yazmak isteyelim. Bu durumda bu 15 byte'ın tamamı yazılana kadar write fonksiyonu blokede bekleyecektir. Borudan okuma potansiyeline
sahip hiçbir betimleyici kalmadığı durumda boruya write fonksiyonu ile bir şey yazılmak istendiğinde UNIX/Linux sistemlerinde SIGPIPE
isimli sinyal oluşmaktadır. Bu sinyal de prosesin sonandırılmasına yol açmaktadır.
Aşağıdaki örnekte üst proses alt prosese 1000000 tane int değeri boruyu yoluyla iletmekte ve alt proses de bunları borudan alarak stdout
dosyasına yazdırmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void exit_sys(const char *msg);
int main(void)
{
int pfds[2];
int pid;
ssize_t result;
int val;
if (pipe(pfds) == -1)
exit_sys("pipe");
if ((pid = fork()) == -1)
exit_sys("pid");
if (pid != 0) { /* parent process writes to pipe */
close(pfds[0]);
for (int i = 0; i < 10; ++i)
if (write(pfds[1], &i, sizeof(int)) == -1)
exit_sys("write");
close(pfds[1]);
if (wait(NULL) == -1)
exit_sys("wait");
}
else { /* child process reads from pipe */
close(pfds[1]);
while ((result = read(pfds[0], &val, sizeof(int))) > 0) {
printf("%d ", val);
fflush(stdout);
}
if (result == -1)
exit_sys("write");
close(pfds[0]);
}
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
İsimsiz boru haberleşmesinde neden okuyan taraf yazma betimleyicini yazan taraf da okuma betimleyicini kapatmaktadır? Eğer okuyan taraf
yazma betimeleyicisini kapatmazsa yazab taraf yazma betimleyicisini kapatsa bile okuyan taraftaki read fonksiyonu "hala boruya yazma
potansiyelinde olan bir betimleyici bulunduğu için" 0 ile geri dönmeyecektir ve bloke oluşturacaktır. Yazan tarafın okuma betimelyicisini
kapatmaması önceki kadar önemlib bir probleme yol açmayacaksa da betimleyicilerin boşuna betimelyici tablosunda yer kaplaması iyi bir
teknik değildir. Yazna tarafın da okuma betimelyicisini kapatması en normal durumdur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte "sample" programı komut satırıyla aldığı programı (örneğimizde "mample") fork/exec yaparak çalıştırmaktadır. Ancak "sample"
programı fork işlemi öncesinde isimsiz boru yaratıp exec yapmaktadır. exec yapılan program kodu boru betimleyicisini bilemeyeceği için
"sample" programı bu betimelyiciyi çalıştırdığı programa ("mample") komut satırı argümanı olarak aktarmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* sample.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int pfds[2];
int pid;
ssize_t result;
int val;
char buf[10];
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if (pipe(pfds) == -1)
exit_sys("pipe");
if ((pid = fork()) == -1)
exit_sys("pid");
if (pid != 0) { /* parent process writes to pipe */
close(pfds[0]);
for (int i = 0; i < 1000000; ++i)
if (write(pfds[1], &i, sizeof(int)) == -1)
exit_sys("write");
close(pfds[1]);
if (wait(NULL) == -1)
exit_sys("wait");
}
else { /* child process reads from pipe */
close(pfds[1]);
sprintf(buf, "%d", pfds[0]);
if (execlp(argv[1], argv[1], buf, (char *)NULL) == -1)
exit_sys("execvp");
/* unreachable code */
}
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* mample.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int pfd;
ssize_t result;
int val;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
pfd = atoi(argv[1]);
while ((result = read(pfd, &val, sizeof(int))) > 0) {
printf("%d ", val);
fflush(stdout);
}
printf("\n");
if (result == -1)
exit_sys("read");
close(pfd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
53. Ders 24/12/2023 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Şimdi de kabuk programlarının boru işlemlerini nasıl yaptığınııklayan bir örnek yapalım. Örneğimizdeki "shellpipe.c" programı kabuk
programının yaptığı gibi boru işlemini yapmaktadır. Bu program aşağıdaki gibi çalıştırılmalıdır:
./shellpipe "ls -l | wc"
Programda önce '|' karakterinin yeri bulunmuş ve bu karakterin iki tarafı da parse edilmiştir. Daha sonra işlemler şu sırada yürütülmüştür:
1) Üst proses ("shellpipe.c") bir boru yaratmıştır. Burada iki betimleyici elde etmiştir.
2) Üst proses '|' karakterin solundaki program için fork yapmıştır. Bu durumda boru betimleyicileri ayaratılan alt prosese aktarılmıştır.
Sonra alt proseste okuma yapılmak için kullanılan boru betimelyeicisi kapatılmış stdout betimelyicisi de boruya yönlendirilmiştir. Tabii
diğer boru betimelyicisi de bu işlemden sonra kapatılmıştır. Nihayet bu işlemlerden sonra exec işlemi uygulanmıştır:
if (pid1 == 0) { /* first child */
close(pfds[0]);
if (dup2(pfds[1], 1) == -1)
exit_sys("dup2");
close(pfds[1]);
if (execvp(pargs.prog1[0], &pargs.prog1[0]) == -1)
exit_sys("execvp");
/* unreachable code */
}
3) Benzer işlemler '|' karakterinin sağındaki program için de yapılmıştır. Tabii '|' karakterinin sağındaki program için fork yapıldığında
artık alt proseste stin betimleyicisi boruya yönlendirilmiştir:
if ((pid1 = fork()) == -1)
exit_sys("fork");
if ((pid2 = fork()) == -1)
exit_sys("fork");
if (pid2 == 0) { /* first child */
close(pfds[1]);
if (dup2(pfds[0], 0) == -1)
exit_sys("dup2");
close(pfds[0]);
if (execvp(pargs.prog2[0], &pargs.prog2[0]) == -1)
exit_sys("execvp");
/* unreachable code */
}
4) Artık üst prosesin de boru betimelycisilerini kapatması gerekmektedir. Böylece toplamda boru ile ilgili iki betimleyici kalacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_ARGS 1024
typedef struct tagPIPE_ARGS {
char *prog1[MAX_ARGS];
char *prog2[MAX_ARGS];
} PIPE_ARGS;
bool check_arg(char *arg, PIPE_ARGS *rargs);
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
char *arg;
PIPE_ARGS pargs;
int pfds[2];
pid_t pid1, pid2;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((arg = strdup(argv[1])) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
if (!check_arg(arg, &pargs)) {
fprintf(stderr, "invalid argument: \"%s\"\n", argv[1]);
exit(EXIT_FAILURE);
}
if (pipe(pfds) == -1)
exit_sys("pipe");
if ((pid1 = fork()) == -1)
exit_sys("fork");
if (pid1 == 0) { /* first child */
close(pfds[0]);
if (dup2(pfds[1], 1) == -1)
exit_sys("dup2");
close(pfds[1]);
if (execvp(pargs.prog1[0], &pargs.prog1[0]) == -1)
exit_sys("execvp");
/* unreachable code */
}
if ((pid2 = fork()) == -1)
exit_sys("fork");
if (pid2 == 0) { /* first child */
close(pfds[1]);
if (dup2(pfds[0], 0) == -1)
exit_sys("dup2");
close(pfds[0]);
if (execvp(pargs.prog2[0], &pargs.prog2[0]) == -1)
exit_sys("execvp");
/* unreachable code */
}
free(arg);
close(pfds[0]);
close(pfds[1]);
if (waitpid(pid1, NULL, 0) == -1)
exit_sys("waitpid");
if (waitpid(pid2, NULL, 0) == -1)
exit_sys("waitpid");
return 0;
}
bool check_arg(char *arg, PIPE_ARGS *pargs)
{
char *pos, *str;
size_t i;
if ((pos = strchr(arg, '|')) == NULL || strchr(pos + 1, '|') != NULL)
return false;
*pos = '\0';
i = 0;
for (str = strtok(arg, " \t"); str != NULL; str = strtok(NULL, " \t"))
pargs->prog1[i++] = str;
pargs->prog1[i] = NULL;
if (i == 0)
return false;
i = 0;
for (str = strtok(pos + 1, " \t"); str != NULL; str = strtok(NULL, " \t"))
pargs->prog2[i++] = str;
pargs->prog2[i] = NULL;
if (i == 0)
return false;
return true;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux (ve macOS) sistemlerinde isimli borular sanki bir dosyaymış gibi oluşturulup kullanılmaktadır. Programcı önce bir isimli boru
dosyasını oluşturmalıdır. Bu sistemlerde isimli borulara "fifo" da dendiğini anımsayınız. İsimli boru dosyaları mkfifo isimli POSIX
fonksiyonuyla yaratılmaktadır. Fonksiyonun prototipi şöyledir:
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
Boru dosyaları gerçek birer dosya değildir. Bunların yalnızca dizin girişleri vardır. Bu dosylar açıldığında aslında boru yine işletim
sistemi tarafından kernel alanı içerisinde oluşturulmaktadır. Buradaki dizin girişi yalnızca farklı proseslerin aynı isim altında anlaşmaları
için kullanılmaktadır. mkfifo fonksiyonunun birinci parametresi yaratılacak boru dosyasının yol ifadesini belirtir. İkinci parametre ise
erişim haklarını belirtmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Örneğin:
if (mkfifo("myfifo", S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH) == -1)
exit_sys("mkfifo");
mkfifo fonksiyonu prosesin umask değerini dikkat ealmaktadır. Eğer yaratacağınız borunun tam olarak sizin belirlediğiniz umask değerine
sahip olmasını istiyorsanız önce umask(0) çağrısını yapmalısınız.
Boru dosyaları "ls -l" komutunda dosya türü olarak "p" ile temsil edilmektedir. Örneğin:
prw-r--r-- 1 kaan study 0 Ara 24 17:34 myfifo
Aşağıda örnek olarak "mkfifo" komutunun bir benzeri verilmiştir. Komuta isteğe bağlı olarak -m seçeneği girilebilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* mymkfifo.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <getopt.h>
bool check_octal(const char *arg);
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int m_flag, err_flag;
int result;
char *m_arg;
int mode;
m_flag = err_flag = 0;
mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH;
opterr = 0;
while ((result = getopt(argc, argv, "m:")) != -1) {
switch (result) {
case 'm':
m_flag = 1;
m_arg = optarg;
break;
case '?':
if (optopt == 'b')
fprintf(stderr, "-b option given without argument!..\n");
else
fprintf(stderr, "invalid option: -%c\n", optopt);
err_flag = 0;
break;
}
}
if (err_flag)
exit(EXIT_FAILURE);
if (argc - optind == 0) {
fprintf(stderr, "pipe name(s) must be specified!..\n");
exit(EXIT_FAILURE);
}
if (m_flag) {
if (!check_octal(m_arg)) {
fprintf(stderr, "invalid mode parameter: %s\n", m_arg);
exit(EXIT_FAILURE);
}
sscanf(m_arg, "%o", &mode);
}
umask(0);
for (int i = optind; i < argc; ++i)
if (mkfifo(argv[i], mode) == -1)
perror(argv[i]);
return 0;
}
bool check_octal(const char *arg)
{
if (strlen(arg) > 3)
return false;
for (int i = 0; arg[i] != '\0'; ++i)
if (arg[i] < '0' || arg[i] > '7')
return false;
return true;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
İsimli boru mkfifo fonksiyonuyla ya da mkfifo komutuyla yaratıldktan sonra artık haberleşecek iki proses de boruyu open fonksiyonuyla
açmalıdır. Boruya yazma yapacak prosese O_WRONLY bayrağını, borudan okuma yapacak proses O_RDONLY bayrağını kullanmalıdır. Boruların O_RDWR
modunda açılması Linux sistemlerinde geçerli olsa da POSIX standartlarında "tanımsız davranış" oluşturmaktadır.
İki proses de uygun bir biçimde open fonksiyonuyla boruyu açtıktan sonra artık tıpkı isimsiz borularda oludğu gibi write ve read fonksiyonlarıyla
haberleşme sağlanır. write ve read fonksiyonlarınınm davranışı isimsiz borularda olduğu gibidir. İsimli borular sanki bir dosyaymış gibi
kullanıldığı halde aslında isimsiz borulardaki gibi kernel alanında bir FIFO kuyruk sistemini kullanırlar. Boruyu yine yazan taraf kapatmalıdır.
Pkuyan taraf yine önce boruda kalanları okur sonra read fonksiyonu 0 ile geri döner. read fonksiyonu 0 ile geri döndüğünde okuyan taraf da
boruyu kapatır.
open fonksiyonuyla isimli boru O_WRONLY modunda açılmak istendiğinde open fonksiyonu boru başka bir proses tarafından O_RDONLY modunda
ılana kadar blokeye yol açmaktadır. Benzer biçimde boruyu bir proses O_RDONLY modunda açmaya çalıştığında başka bir proses boruyu
O_WRONLY modunda açana kadar open blokeye yol açmaktadır. Alında isimli borular da "blokesiz modda" açılabilmektedir. Ancak bu konu
kursumuzun kapsamı dışındadır.
Aşağıdaki örnekte "pwrite.c" ve "pread.c" isimli iki program verilmiştir. pwrite programı boruyu O_WRONLY modunda açıp boruya yazma
yapmaktadır. pread fonksiyonu ise boruyu O_RDONLY modunda açıp borudan okuma yapmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* pwrite.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int pfd;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((pfd = open(argv[1], O_WRONLY)) == -1)
exit_sys("open");
for (int i = 0; i < 1000000; ++i)
if (write(pfd, &i, sizeof(i)) == -1)
exit_sys("write");
close(pfd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* pread.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void exit_sys(const char *msg);
int main(int argc, char *argv[])
{
int pfd;
ssize_t result;
int val;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((pfd = open(argv[1], O_RDONLY)) == -1)
exit_sys("open");
while ((result = read(pfd, &val, sizeof(int))) > 0)
printf("%d ", val), fflush(stdout);
if (result == -1)
exit_sys("read");
printf("\n");
close(pfd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi iki proses aynı anda isimli boruya yazma yaapmak isterse ne olur? İşte POSIX standrtlarına göre eğer yazılmak istenen bilgi
<limits.h> dosyası içerisindeki PIPE_BUF sembolik sabitinden büyük olmadıktan sonra iç içe geçme oluşmamaktadır. Yani bu durumda iki proses
aynı anda boruya yazma yapmaya çalışsa bile önce birinin sonra diğerinin yazdıkları boruda görünür. Ancak PIPE_BUF değerinden daha büyük
miktarda bilgi boruya yazılmak istendiğinde iç içe geçme oluşabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde boru haberleşmesi UNIX/Linux sistemindekilere benzer bir biçimde yapılmaktadır. Bu sistemlerde de borular "isimsiz
(anonymous)" ve "isimli (named)" olmak üzere ikiye ayrılmaktadır. Windows sistemlerindek isimli borular farklı Windows makinelerindeki
proseslerin haberleşmesi için de kullanılabilmektedir. Bu nedenle Windows sistemlerindeki isimli borular bazı ayrıntılara sahiptir. Biz
bu kursumuzda Windows sstemlerindeki isimli borular üzerinde durmayacağız. Bu konu "Windows Sistem Programlama" kurslarında ele alınmaktadır.
Windows sistemlerinde isimsiz borular yine üst ve alt proseslerin haberleşmesinde kullanılmaktadır. Bu sistemlerde fork fonksiyonun olmadığını
CreateProcess API fonksiyonunun adresta fork/exec ikilisi gibi bir işleve sahip olduğunu anımsayınız. Dolayısıyla bu sistemlerde bizim
alt prosese boruların handle değerlerini de geçirmemiz gerekir. Windows sistemlerinde üst ve alt prosesler arasında isimsiz boru haberleşmesi
tipik olarak şu adımlardan geçilerek gerçekleştirilmektedir:
1) Üst proses boruyu CreatePipe isimli API fonksiyonuyla yaratır. Fonksiyonun prototipi şöyledir:
BOOL CreatePipe(
PHANDLE hReadPipe,
PHANDLE hWritePipe,
LPSECURITY_ATTRIBUTES lpPipeAttributes,
DWORD nSize
);
Fonksiyonun birinci ve ikinci parametresi boruya yazma yapmakta kullanılacak ve borudan okuma yapmakta kullanılacak HANDLE değerlerinin
yerleştirileceği nesnelerin adreslerini almaktadır. Windows sistemlerinde borular çift yönlüdür. Fonksiyonun üçüncü parametresi yaratılan
boru nesnesinin güvenlik bilgilerini içermektedir. Bu patametre NULL geçilebilir. Son parametre borunun byte cinsinden uzunluğunu belirtmektedir.
Bu parametre için girilecek değer yalnızca bir ipucu (hint) anlamındadır. Bu parametreye 0 girilirse bu durumda boru default uzunlukla
yaratılmaktadır.
Fonksiyon başarı durumunda sıfır dışı bir değre, başarısızlık durumunda sıfır değerine geri dönmektedir.
2) Borudan okuma için ReadFile fonksiyonu, boruya yazma için WriteFile API fonksiyonu kullanılmaktadır. Bu fonksiyonları zaten daha önce
görmüştük. ReadFile fonksiyonu ile borudan okuma yapılmak istendiğinde eğer boruda hiçbir bte yoksa ReadFile blokede bekler. Benzer biçimde
WriteFile fonksiyonu ile boruya yazma yapılırken eğer boruda yazılmak istenen kadar boş yer yoksa bu bilgilerin hepsinin yazılabilmesi
için gerekli alan açılana kadar WriteFile bloke oluşturmaktadır. Buradaki davranış UNIX/Linux sistemlerindeki write ve read fonksiyonlaırnın
davranışların gibidir. Yine haberleşme yazan tarafın boruyu CloseHandle fonksiyonu ile kapatmasıyla sonlandırılmalıdır. Bu durumda okuyan
taraf önce boruda kalanları okur, sonra ReadFile başarısız olarak 0 ile geri döner. Okuyan taraf da boruyu kapatır. Eğer önce okuyan taraf boruyu
kapatırsa (bu normal bir durum değildir) bu durumda yazan taraf boruya yazma yapmak istediğinde WriteFile fonksiyonu başarısız olmaktadır.
ReadFile ya da WriteFile fonksiyonlarında karşı taraf boruyu kapattığından dolayı bu fonksiyonlar başarısız olmuşsa GetLastError fonksiyonu
ERROR_BROKEN_PIPE değerini vermektedir.
3) Windows sistemlerinde üst prosesteki HANDLE değerleri alt prosese default durumda aktarılmamaktadır. (Halbuki UNIX/Linux sistemlerinde
default durumda üst prosesin betimeleyicilerinin exec işlemi sırasında korunduğunu anımsayınız.) Eğer üst prosesin HANDLE değerlerinin alt
prosese aktarılması isteniyorsa handle üzerinde SetHandleInformation fonksiyonu ile aşağda çağrı uygulanmalıdır:
if (!SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT))
ExitSys(TEXT("CreatePipe"));
Ayrıca aktarımın yapıması için ana şalter görevinde olan CreateProcess fonksiyonundaki bInheritHandles parametresinin TRUE olarak geçilmesi
gerekmektedir.
4) Artık hangi prosesin boruya yazma yapacağına, hangisinin buradan yazma yapacağına karar verilir. Yine her iki proses kullanmadıkları
betimelyicileri kapatmalıdır.
5) CreateProses fonksiyonu ile alt proses yaratılır. Ancak boru HANDLE değerinin alt prosese komut satırı argümanlarıyla aktarılması gerekir.
Aşağıdaki örnekte bir proses yukarıda belirtilen adımları uygulayarak alt prosesle boru haberleşmesi yapmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* Parent.c */
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define CHILD_PATH "Child.exe"
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hPipeRead, hPipeWrite;
DWORD dwWritten, dwRead;
char args[4096];
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi;
if (!CreatePipe(&hPipeRead, &hPipeWrite, NULL, 0))
ExitSys("CreatePipe");
if (!SetHandleInformation(hPipeRead, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT))
ExitSys("SetHandleInformation");
sprintf(args, "%s %p", CHILD_PATH, hPipeRead);
if (!CreateProcess(NULL, args, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
ExitSys("CreateProcess");
CloseHandle(hPipeRead);
for (int i = 0; i < 1000000; ++i)
if (!WriteFile(hPipeWrite, &i, sizeof(int), &dwWritten, NULL))
ExitSys("WriteFile");
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle(hPipeWrite);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/* Child.c */
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(int argc, char *argv[])
{
HANDLE hPipeRead;
DWORD dwRead;
int val;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
sscanf(argv[1], "%p", &hPipeRead);
while (ReadFile(hPipeRead, &val, sizeof(int), &dwRead, 0)) {
printf("%d ", val);
fflush(stdout);
}
if (GetLastError() != ERROR_BROKEN_PIPE)
ExitSys("ReadFile");
printf("\n");
CloseHandle(hPipeRead);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*-----------------------------------------------------------------------------------------------------------------------------------------
54. Ders 06/01/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------------------------------------------------------------------
Proseslerarası haberleşme yöntemlerinden bir diğeri de "paylaşılan bellek alanları (shared memory)" denilen yöntemdir. Aslında bu yöntem
hakkında biz sayfa tablolarını anlattığımız bölümde bazı ipuçları vermiştik. Bu yöntemde farklı proseslerin sayfa tablolarındaki farklı
sanal sayfa numaraları aynı fiziksel sayfaya yönlendirilmektedir. Böylece proseslerden biri o sanal sayfaya yazma yaptığında diğeri onu
diğer sanal sayfa yoluyla görebilmektedir. Örneğin:
Proses-1 Sayfa Tablosu
Sanal Sayfa No Fiziksel Sayfa No
.... ....
1784 3641
.... ....
Proses-2 Sayfa Tablosu
Sanal Sayfa No Fiziksel Sayfa No
.... ....
1432 3641
.... ....
Buradaki numaraların 16'lık sistemde olduğunu varsayalım. Burada Proses-1'in 1784 numaralı sanal sanal sayfa adresiyle [1784000-17844FFF]
yaptığı erişimlerle Proses-2'nin 1432 numaralı sanal sayfa adresiyle [1432000-1432FFF] yaptığı erişimler aslında aynı fiziksel bölgeyi
belirtmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux (ve macOS) sistemlerinde paylaşılan bellek alanlarının oluşturulması için iki farklı fonksiyon grubu kullanılabilmektedir.
Bunlardan biri eski System-5 fonksiyonlarıdır. Diğeri daha modern fonksiyonlardır. Bu modern fonksiyon grubuna halk arasında "POSIX Shared
Memory" fonksiyonları da denilmektedir. Aslında her iki fonksiyon grubu da POSIX standartlarında bulunmaktadır. System-5 paylaşılan bellek
aşanı fonksiyonları çok esikden beri UNIX türevi sistemlerde bulunmaktadır. POSIX paylaşılan bellek alanı fonksiyonları ise sistemlere
90'lı yılların ortalarında girmeye başlamıştır. Ancak bugün her iki grup fonksiyonun da kullanıldığını söyleyebiliriz. Biz kurusumuzda
yalnızca POSIX paylaşılan bellek alanı fonksiyonlarını göreceğiz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------------------------------------------------------------------
POSIX paylaşılan bellek alanları tipik olarak şu adımlardan geçilerek kullanılmaktadır:
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. Fonksiyonun 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. Bu ismi kök dizinde bir dosya ismi gibi verilmesi
gerekmektedir. (POSIX standartlarında bazı sistemlerin buradaki dosya isminin başka dizinlerde olmasına izin verebildiğini belirtmiştir.)
Fonksiyonun ikinci parametresi paylaşılan bellek alanının açış bayraklarını belirtmektedir. Bu bayraklar şunlardan birini içerebilir:
O_RDONLY: Bu durumda paylaşılan bellek alanından yalnızca okuma yapılabilir.
O_RDWR: Bu durumda paylaşılan bellek alanından hem okuma yapılabilir hem de oraya yazma yapılabilir.
Aşağıdaki bayraklar da açış moduna eklenebilir:
O_CREAT: Paylaşılan bellek alanı yoksa yaratılır, varsa olan açılır.
O_EXCL: O_CREAT bayrağı ile birlikte kullanılabilir. Paylaşılan bellek alanı zaten varsa fonksiyon başarısız olur.
O_TRUNC: Paylaşılan bellek alanı varsa sıfırlanarak açılır. Bu mod için O_RDWR bayrağının kullanılmış olması gerekmektedir.
Fonksiyonun üçüncü parametresi paylaşılan bellek alanının erişim haklarını belirtmektedir. Tabii ancak ikinci parametrede O_CREAT
bayrağı kullanılmışsa bu parametreye gereksinim duyulmaktadır. İkinci parametrede O_CREAT bayrağı kullanılmamışsa üçüncü parametre
fonksiyon tarafından hiç kullanılmamaktadır.
shm_open bize tıpkı bir disk dosyasında olduğu gibi bir dosya betimleyicisi vermektedir. Fonksiyon başarısız olursa yine -1 değerine geri
döner ve errno değişkeni uygun biçimde set edilir. Burada yaratılan paylaşılan bellek alanı nesnesi için bir dizin girişi oluşturulmamaktadır.
Yani paylaşılan bellek alanı nesnesi bir dosyaymış gibi ele alınmakla birlikte aslında bir dosya değildir.
Örneğin:
int fdshm;
if ((fdshm = shm_open(SHM_NAME, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1)
exit_sys("shm_open");
2) Paylaşılan bellek alanı yaratıldıktan sonra ftruncate fonksiyonu ile ona bir büyüklük vermek gerekir. Örneğin:
if (ftruncate(fdshm, SHM_SIZE) == -1)
exit_sys("ftruncate");
Tabii paylaşılan bellek alanı zaten yaratılmışsa ve biz onu açıyorsak ftruncate fonksiyonunu aynı uzunlukta çağırdığımızda aslında
fonksiyon herhangi bir şey yapmayacaktır. Yani aslında ftruncate fonksiyonu paylaşılan bellek alanı ilk kez yaratılırken bir kez çağrılır.
Ancak yukarıda da belirttiğimiz gibi aynı uzunlukta ftruncate işleminin bir etkisi yoktur.
ftruncate aslında bir dosyanın büyüklüğünü değiştirmek için kullanılan genel bir fonksiyondur. Fonksiyonun prototipi şöyledir:
#include <unistd.h>
int ftruncate(int fd, off_t length);
Fonksiyonun birinci parametresi dosya betimleyicisini ikinci parametresi dosyanın yeni uzunluğunu almaktadır. Fonksiyon başarı durumunda
0 değerine başarısızlık durumunda -1 değerine geri döner. ftruncate POSIX fonksiyonunun truncate isminde yol ifadeis ile çalışanm bir
biçimi de vardır.
POSIX paylaşılan bellek alanı nesneleri Linux'ta dosya sisteminde /dev/shm dizini içerisinde görüntülenmektedir. Yani programcı isterse
bu dizin içerisindeki nesneleri komut satırında rm komutuyla silebilir.
3) Artık paylaşılan bellek alanı nesnesinin belleğe "map" edilmesi gerekmektedir. Bunun için mmap isimli bir POSIX fonksiyonu kullanılmaktadır.
mmap fonksiyonu pek çok UNIX türevi sistemde bir sistem fonksiyonu olarak gerçekleştirilmiştir. mmap paylaşılan bellek alanlarının dışında
başka amaçlarla da kullanılabilen ayrıntılı bir sistem fonksiyonudur. Bu nedenle biz burada önce fonksiyonun paylaşılan bellek alanlarında
kullanımına kısaca değineceğiz. Sonra bu fonksiyonu ayrıca daha ayrıntılı biçimde ele alacağız. mmap fonksiyonunun prototipi şöyledir:
#include <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 sanal adresi belirtmektedir. Programcı belli bir sanal adresin mapping için
kullanılmasını isteyebilir. Ancak fonksiyon flags parametresinde MAP_FIXED geçilmemişse bu adresi tam (exact) olarak yani verildiği gibi
kullanmayabilir. Fonksiyon bu önerilen adresin yakınındaki bir sayfayı tahsis edebilir. Bu tahsisatın burada belirtilen adresin neresinde
yapılacağı garanti edilmemiştir. Yani buradaki adres eğer fonksiyonun flags parametresinde MAP_FIXED kullanılmamışsa bir öneri niteliğindedir.
Eğer bu adres NULL olarak geçilirse bu durumda mapping işlemi işletim sisteminin kendi belirlediği bir adresten itibaren yapılır. Tabii
en tipik durum bu parametrenin NULL adres olarak geçilmesidir.
Fonksiyonun ikinci parametresi, paylaşılan bellek alanının ne kadarının map edileceğini belirtir. Örneğin paylaşılan bellek alanı
nesnesi 1MB olabilir. Ancak biz onun 100K'lık bir kısmını map etmek isteyebiliriz. Ya da tüm paylaşılan bellek alanını da map etmek
isteyebiliriz. Bu uzunluk sayfa katlarında olmak zorunda değildir. Ancak pek çok sistem bu uzunluğu sayfa katlarına doğru yukarı
yuvarlamaktadır. Yani biz uzunluğu örneğin 100 byte verebiliriz. Ancak sistem 100 byte yerine sayfa uzunluğu olan 4096 byte'ı map edecektir.
Fonksiyonun üçüncü parametresi, mapping işleminin koruma özelliklerini belirtmektedir. Başka bir deyişle bu parametre
paylaşılan bellek alanı için ayrılacak fiziksel sayfaların işlemci düzeyinde koruma özelliklerini belirtir. Bu özellikler şunlardan
oluşturulabilir:
PROT_READ
PROT_WRITE
PROT_EXEC
PROT_NONE
PROT_READ sayfanın "read only" olduğunu belirtir. Böyle sayfalara yazma yapılırsa işlemci exception oluşturur ve program SIGSEGV
sinyali ile sonlandırılır. PROT_WRITE sayfaya yazma yapılabileceğini belirtmektedir. Örneğin PROT_READ|PROT_WRITE hem okuma hem de
yazma anlamına gelmektedir. PROT_EXEC ilgili sayfada bir kod varsa (örneğin oraya bir fonksiyon yerleştirilmişse) o kodun
çalıştırılabilirliği üzerinde etkili olmaktadır. Örneğin Intel ve ARM işlemcilerinde fiziksel sayfa PROT_EXEC ile özelliklendirilmemişse
o sayfadaki bir kod çalıştırılamamaktadır. PROT_NONE o sayfaya herhangi bir erişimin mümkün olamayacağını belirtmektedir. Yani
PROT_NONE olan bir sayfa ne okunabilir ne de yazılabilir. Bu tür sayfa özellikleri "guard page" oluşturmak için kullanılabilmektedir.
Tabii bir sayfanın koruma özelliği daha sonra da değiştirilebilir. Aslında bütün işlemciler buradaki koruma özelliklerinin hepsini
desteklemeyebilirler. Örneğin Intel işlemcilerinde PROT_WRITE zaten okuma özelliğini de kapsamaktadır. Bazı işlemciler sayfalarda
PROT_EXEC özelliğini hiç bulundurmamaktadır. Ancak ne olursa olsun programcı sanki bu özelliklerin hepsi varmış gibi bu parametreyi
oluşturmalıdır.
Fonksiyonun dördüncü parametresi olan flags aşağıdaki değerlerden yalnızca birini alabilir:
MAP_PRIVATE
MAP_SHARED
MAP_PRIVATE ile oluşturulan mapping'e "private mapping", MAP_SHARED ile oluşturulan mapping'e ise "shared mapping" denilmektedir.
MAP_PRIVATE "copy on write" denilen semantik için kullanılmaktadır. "Copy on write" işlemi "yazma yapılana kadar sanal sayfaların
aynı fiziksel sayfalara yönlendirilmesi ancak yazmayla birlikte o sayfaların bir kopyalarının çıkartılıp yazmanın o prosese
özel olarak yapılması ve yapılan yazmaların paylaşılan bellek alanına yansıtılmaması" anlamına gelmektedir. Başka bir deyişle
MAP_PRIVATE şunlara yol açmaktadır:
- Okuma yapılınca paylaşılan bellek alanından okuma yapılmış olur.
- Ancak yazma yapıldığında bu yazma paylaşılan bellek alanına yansıtılmaz. O anda yazılan sayfanın bir kopyası çıkartılarak
yazma o kopya üzerine yapılır. Dolayısıyla başka bir proses bu yazma işlemini göremez.
Bir proses ilgili paylaşılan bellek alanı nesnesini MAP_PRIVATE ile map ettiğinde diğer proses o alana yazma yaptığında onun yazdığını MAP_PRIVATE
yapan prosesin görüp görmeyeceği POSIX standartlarında belirsiz (unspecified) bırakılmıştır. Linux sistemlerinin man sayfasında da
aynı "unspecified" durum belirtilmiş olsa da mevcut Linux çekirdeklerinde başka bir proses private mapping yapılmış yere yazma yaptığında
bu yazma private mapping'in yapıldığı proseste görülmektedir. Ancak sayfaya yazma yapıldığında artık o sayfanın kopyasından
çıkartılacağı için bu yazma işleminden sonraki diğer prosesin yaptığı yazma işlemleri görülmemektedir.
MAP_SHARED ise yazma işleminin paylaşılan bellek alanına yapılacağını yani "copy on write" yapılmayacağını belirtmektedir.
Dolayısıyla MAP_SHARED bir mapping'te paylaşılan alana yazılanlar diğer prosesler tarafından görülür. Normal olarak programcılar
amaç doğrultusunda shared mapping kullanırlar. Private mapping (yani "copy on write") bazı özel durumlarda tercih edilmektedir.
Örneğin işletim sistemi (exec fonksiyonları) çalıştırılabilir dosyanın ".data" bölümünü private mapping yaparak belleğe mmap
ile yüklemektedirler.
Fonksiyonun flags parametresinde MAP_PRIVATE ve MAP_SHARED değerlerinin yalnızca biri kullanılabilir. Ancak bu değerlerden biri ile
MAP_FIXED değeri bit düzeyinde OR işlemine sokulabilmektedir. MAP_FIXED bayrağı, fonksiyonun birinci parametresindeki adres NULL geçilmemişse
bu adresin kendisinin aynen (hiç değiştirilmeden) kullanılacağını belirtmektedir. Yani bu adresin yakınındaki herhangi bir sayfa değil
kendisi tahsis edilecek ve fonksiyon bu adresin aynısıyla geri dönecektir. Eğer MAP_FIXED bayrağı belirtilmişse Linux sistemlerinde birinci
parametredeki adresin sayfa katlarında olma zorunlululuğu vardır. Ancak POSIX standartlarının son versiyonları "may require" ifadesiyle
bunun zorunlu olmayabileceğini belirtmektedir.
Fonksiyonun son iki parametresi dosya betimleyicisi ve bir de offset içermektedir. Paylaşılan bellek alanının belli bir offset'ten
sonraki kısmı map edilebilmektedir. Örneğin paylaşılan bellek alanı nesnemiz 4MB olsun. Biz bu nesnenin 1MB'sinden itibaren 64K'lık
kısmını map edebiliriz. O halde mmap fonksiyonunu örneğin şöyle çağırabiliriz:
shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, fdshm, 0);
Burada paylaşılan bellek alanı nesnesinin SHM_SIZE kadar alanı map edilmek istenmiştir. İlgili sayfalar PROT_WRITE özelliğine sahip
olacaktır. Yani bu sayfalara yazma yapılabilecektir. Bu sayfalara yazma yapıldığında paylaşılan bellek alanı nesnesi bundan etkilenecek
yani aynı nesneyi kullanan diğer proseslerde de eğer shared mapping yapılmışsa bu durum gözükecektir. Burada paylaşılan bellek alanı
nesnesinin 0'ıncı offset'inden itibaren SHM_SIZE kadar alanın map edildiğine dikkat ediniz.
mmap fonksiyonun son parametresindeki offset değeri MAP_FIXED belirtilmişse, birinci parametre ile son parametrenin sayfa katlarına
bölümünden elde edilen kalan aynı olmak zorundadır. (Yani örneğin POSIX standartlarında işletim sistemi eğer 5000 adresini kabul
ediyorsa 5000 % 4096 = 4'tür. Bu durumda son parametrenin de 4096'ya bölümünden elde edilen kalan 4 olmalıdır.) Ancak MAP_FIXED
belirtilmemişse POSIX standartları bu offset değerinin sayfa katlarında olup olmayacağını işletim sistemini yazanların isteğine
bırakmıştır. Linux çekirdeklerinde, MAP_FIXED belirtilsin ya da belirtilmesin bu offset değeri her zaman sayfa katlarında olmak zorundadır.
mmap fonksiyonu başarı durumunda mapping yapılan sanal bellek adresine geri dönmektedir. Fonksiyon başarısızlık durumunda
MAP_FAILED özel değerine geri döner. Pek çok sistemde MAP_FAILED bellekteki son adres olarak aşağıdaki biçimde define edilmiştir:
#define MAP_FAILED ((void *) -1)
Kontrol şöyle yapılabilir:
shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, fdshm, 0);
if (shmaddr == MAP_FAILED)
exit_sys("mmap");
Paylaşılan bellek alanı betimleyicisi mapping işleminden sonra close fonksiyonuyla kapatılabilir. Bu durum mapping'i etkilememektedir.
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 belirtir. İkinci parametre unmap edilecek alanın
uzunluğunu belirtmektedir. Fonksiyon birinci parametresinde belirtilen adresten itibaren ikinci parametresinde belirtilen miktardaki
byte'ı kapsayan sayfaları unmap etmektedir. (Örneğin buradaki adres bir sayfanın ortalarında ise ve uzunluk da başka bir sayfanın
ortalarına kadar geliyorsa bu iki sayfa da tümden unmap edilmektedir.) POSIX standartları işletim sistemlerinin birinci parametrede
belirtilen adresin sayfa katlarında olmasını zorlayabileceğini (may require) belirtmektedir. Linux'ta birinci parametrede belirtilen
adres sayfa katlarında olmak zorundadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner ve errno
değişkeni uygun biçimde set edilir. munmap ile zaten map edilmemiş bir alan unmap edilmeye çalışılırsa fonksiyon bir şey yapmaz. Bu durumda
fonksiyon başarısızlıkla geri dönmemektedir.
Paylaşılan bellek alanına ilişkin dosya betimleyicisi close fonksiyonu ile kapatılabilir. Paylaşılan bellek alanı betimleyicisi close
ile kapatıldığında munmap işlemi yapılmamaktadır. Zaten paylaşılan bellek alanı nesnesi map edildikten sonra hemen close ile kapatılabilir.
Bunun mapping işlemine bir etkisi olmaz.
Unmap işlemi mevcut mapping'in bir kısmına yapılabilmektedir. Bu durumda işletim sistemi mapping işlemini ardışıl olmayan
parçalara kendisi ayırmaktadır. Örneğin:
xxxxmmmmmmmmmxxxx
Burada "m" map edilmiş sayfaları "x" ise diğer sayfaları belirtiyor olsun. Biz de mapping'in içerisinde iki sayfayı unmap edelim:
xxxxmmmmxxmmmxxxx
Görüldüğü gibi artık sanki iki ayrı mapping varmış gibi bir durum oluşmaktadır.
Proses bittiğinde map edilmiş bütün alanlar zaten işletim sistemi tarafından unmap edilmektedir.
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. Eğer bu silme yapılmazsa sistem reboot edilene
kadar nesne hayatta kalmaya devam edecektir (kernel persistant). shm_unlink fonksiyonun prototipi şöyledir:
#include <sys/mman.h>
int shm_unlink(const char *name);
Fonksiyon paylaşılan bellek alanı nesnesinin ismini alır onu yok eder. Başarı durumunda 0 değerine, başarısızlık durumunda -1
değerine geri dönmektedir. Örneğin:
if (shm_unlink(SHM_PATH) == -1)
exit_sys("shm_unlink");
Aşağıdaki örnekte prog1 programı paylaşılan bellek alanına bir şeyler yazmakta prog2 programı da bunu okumaktadır. prog1
programı işlemini bitirince paylaşılan bellek alanı nesnesini shm_unlink fonksiyonuyla silmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* 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_PATH "/this_is_a_sample_shared_memory"
void exit_sys(const char *msg);
int main(void)
{
int fdshm;
char *shmem;
if ((fdshm = shm_open(SHM_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1)
exit_sys("shm_open");
if (ftruncate(fdshm, 4096) == -1)
exit_sys("ftruncate");
if ((shmem = (char *)mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED)
exit_sys("mmap");
printf("press ENTER to write shared memory...\n");
getchar();
strcpy(shmem, "this is a test...");
printf("press ENTER to exit...\n");
getchar();
if (munmap(shmem, 4096) == -1)
exit_sys("munmap");
close(fdshm);
if (shm_unlink(SHM_PATH) == -1)
exit_sys("shm_unlink");
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* prog2.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>
#define SHM_PATH "/this_is_a_sample_shared_memory"
void exit_sys(const char *msg);
int main(void)
{
int fdshm;
char *shmem;
if ((fdshm = shm_open(SHM_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1)
exit_sys("shm_open");
if (ftruncate(fdshm, 4096) == -1)
exit_sys("ftruncate");
if ((shmem = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED)
exit_sys("mmap");
printf("press ENTER to read shared memory...\n");
getchar();
puts(shmem);
printf("press ENTER to exit...\n");
getchar();
if (munmap(shmem, 4096) == -1)
exit_sys("munmap");
close(fdshm);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Paylaşılan bellek alanları çok hızlı bir haberleşme sağlıyor olsa da kendi içerisinde bir senkronizasyona sahip değildir. Bir prosesin
birden fazla bilgiyi farklı zamanlarda diğerine bu yöntemle iletmesi sırasında iki prosesin senkronize olması yani koordineli bir
biçimde çalışması gerekir. Bu senkronizasyon problemine "üretici tüketici problemi (producer-conseumer problem)" denilmektedir. Bu konu ileride
kursumuzun thread'ler kısmında ele alınacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Bellek tabanlı dosyalar (memory mapped files) 90'lı yıllarla birlikte işletim sistemlerine sokulmuştur. Microsoft ilk kez 32 bit Windows
sistemlerinde (Windows NT ve sonra da Windows 95) bellek tabanlı dosyaları işletim sisteminin çekirdeğine dahil etmiştir. 90'ların ortalarında
bellek tabanlı dosyalar POSIX IPC nesneleriyle birlikte UNIX türevi sistemlere de resmi olarak sokulmuştur. macOS sistemleri de bellek tabanlı
dosyaları desteklemektedir.
Bellek tabanlı dosyalar (memory mapped files) adeta diskte bulunan bir dosyanın prosesin sanal bellek alanına çekilmesi anlamına gelmektedir.
Biz bir disk dosyasını bellek tabanlı biçimde açıp kullandığımızda dosya sanki bellekteymiş gibi bir durum oluşturulur. Biz bellekte göstericilerle
dosyanın byte'larına erişiriz. Bellekte birtakım değişikler yapıldığında bu değişiklikler dosyaya yansıtılmaktadır. Böylece dosya üzerinde
işlemler yapılırken read ve write sistem fonksiyonları yerine doğrudan göstericilerle bellek üzerinde işlem yapılmış olur.
read fonksiyonu ile dosyanın bir kısmını okumak isteyelim:
result = read(fd, buf, size);
Burada genellikle işletim sistemlerinde arka planda iki işlem yapılmaktadır: Önce dosyanın ilgili bölümü işletim sisteminin çekirdeği içerisindeki
bir alana (bu alana buffer cache ya da page cache denilmektedir) çekilir. Sonra bu alandan bizim belirttiğimiz alana aktarım yapılır. Halbuki
bellek tabanlı dosyalarda genel olarak bu iki aktarım yerine dosya doğrudan prosesin bellek alanına map edilmektedir. Yani bu anlamda bellek
tabanlı dosyalar hız ve bellek kazancı sağlamaktadır. Ayrıca her read ve write işleminin kontrol edilme zorunluluğu da bellek tabanlı dosyalarda
ortadan kalkmaktadır. Bir dosya üzerinde dosyanın farklı yerlerinden okuma ve yazma işlemlerinin sürekli yapıldığı durumlarda bellek tabanlı
dosyalar klasik read/write sistemine göre oldukça avantaj sağlamaktadır.
Bu noktada kişilerin akıllarına şu soru gelmektedir? Biz bir dosyayı open ile açsak dosyanın tamamını read ile belleğe okusak sonra işlemleri
bellek üzerinde yapsak sonra da write fonksiyonu ile tek hamlede yine onları diske yazsak bu yöntemin bellek tabanlı dosyalardan bir farkı
kalır mı? Bu soruda önerilen yöntem bellek tabanlı dosya çalışmasına benzemekle birlikte bellek tabanlı dosyalar bu sorudaki çalışma biçiminden
farklıdır. Birincisi, dosyanın tamamının belleğe okunması yine iki tamponun devreye girmesine yol açmaktadır. İkincisi, bellek tabanlı
dosyaların bir mapping oluşturması ve dolayısıyla prosesler arasında etkin bir kullanıma yol açmasıdır. Yani örneğin iki proses aynı dosyayı
bellek tabanlı olarak açtığında işletim sistemi her proses için ayrı bir alan oluşturmamakta dosyanın parçalarını fiziksel bellekte bir
yere yerleştirip o proseslerin aynı yerden çalışmasını sağlamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Bir dosya bellek tabanlı (memory mapped) biçimde sırasıyla şu adımlardan geçilerek kullanılmaktadır:
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 belirteceğimiz gibi dosyalar bellek tabanlı olarak yaratılamamakta ve dosyalara bellek tabanlı biçimde eklemeler yapılamamaktadır.
Yani zaten var olan dosyalar bellek tabanlı biçimde kullanılabilirler.
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
bu dosyaya yansıtılmaz, "copy on write" mekanizması devreye girer. mmap fonksiyonun son iki parametresi dosya betimleyicisi ve dosyada bir offset
belirtmektedir. İşte dosya betimleyicisi olarak açmış olduğumuz dosyanın betimleyicisini verebiliriz. Offset olarak da dosyanın neresini map
edeceksek oranın başlangıç offsetini verebiliriz. Mapping sırasında dosya göstericisinin konumunun bir önemi yoktur. Örneğin:
char *maddr;
struct stat finfo;
...
if (fstat(fd, &finfo) == -1)
exit_sys("fstat");
if ((maddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
exit_sys("mmap");
Burada biz önce dosyanın uzunluğunu fstat fonksiyonu ile elde ettik sonra da mmap fonksiyonu ile dosyanın hepsini shared mapping yaparak map ettik.
Artık dosya bellektedir ve biz dosya işlemleri yerine gösterici işlemleri ile bellekteki dosyayı kullanabiliriz. Örneğin:
for (off_t i = 0; i < finfo.st_size; ++i)
putchar(maddr[i]);
mapping işleminden sonra artık dosya betimleyicisi close fonksiyonuyla kapatılabilir. Yani kapatım için unmap işleminin beklenmesine gerek
yoktur.
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;
struct stat finfo;
char *fmaddr;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((fd = open(argv[1], O_RDWR)) == -1)
exit_sys("open");
if (fstat(fd, &finfo) == -1)
exit_sys("fstat");
if ((fmaddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
exit_sys("mmap");
for (off_t i = 0; i < finfo.st_size; ++i)
putchar(fmaddr[i]);
putchar('\n');
memcpy(fmaddr, "XXXXXX", 6);
if (munmap(fmaddr, finfo.st_size) == -1)
exit_sys("munmap");
close(fd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerindeki bellek tabanlı dosyaları (memory mapped files) açarken ve kullanırken bazı ayrıntılara dikkat
edilmesi gerekir. Burada bu ayrıntılar üzerinde duracağız.
- Dosyayı bizim mmap fonksiyonundaki sayfa koruma özelliklerine uygun açmamız gerekmektedir. Örneğin biz dosyayı O_RDONLY modunda
ı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ıılırsa mmap fonksiyonun
başarısız olacağını ve errno değerinin EACCESS olarak set edileceğini belirtmektedir.
- Bir dosyanın uzunluğu 0 ise biz mmap fonksiyonunda length parametresini 0 yapamayız. Fonksiyon doğrudan başarısızlıkla sonlanıp errno
değeri EINVAL olarak set edilmektedir. Yani 0 uzunlukta bir dosya map edilememektedir.
- Anımsanacağı gibi mmap fonksiyonunun offset parametresi Linux sistemlerinde sayfa uzunluğunun katlarında olması gerekiyordu (POSIX bunu
"may require" biçimde belirtmiştir). Yani Linux'ta biz dosyayı zaten sayfa katlarından itibaren map edebilmekteyiz. Bu durumda Linux'ta
zaten map edilen adres sayfanın başında olmaktadır.
- Biz normal bir dosyayı büyütmek için dosya göstericisini EOF durumuna çekip yazma yapıyorduk. Ya da benzer işlemi truncate, ftruncate
fonksiyonlarıyla da yapabiliyorduk. Halbuki bellek tabanlı olarak açılmış olan dosyalar bellek üzerinde hiçbir biçimde büyütülememektedir.
Biz bir dosyayı mmap fonksiyonu ile dosya uzunluğundan daha fazla uzunlukta map edebiliriz. Örneğin dosya 10000 byte uzunlukta olduğu
halde biz dosyayı 20000 byte olarak map edebiliriz. Bu durumda dosyanın sonundan o sayfanın sonuna kadarki alana biz istediğimiz gibi
erişebiliriz. Dosyanın uzunluğu 10000 byte ise dosyanın son sayfasında dosyaya dahil olmayan 2288 byte bulunacaktır (3 * 4096 - 10000).
İşte bizim bu son sayfadaki 2288 byte'a erişmemizde hiçbir sakınca yoktur. Ancak bu sayfanın ötesinde erişim yapamayız. Yani bizim artık
3 * 4096 = 12288'den 20000'e kadarki alana erişmeye çalışmamamız gerekir. Eğer bu alana erişmeye çalışırsak SIGBUS sinyali oluşur ve
prosesimiz sonlandırılır. Pekiyi bir dosyanın uzunluğundan fazla yerin map edilmesinin bir anlamı olabilir mi? Daha önceden de belirttiğimiz
gibi bellek tabanlı dosyalar bellek üzerinde büyütülemezler. Yani biz dosyayı uzunluğunun ötesinde map ederek oraya yazma yapmak suretiyle
dosyayı büyütemeyiz. Ancak dosyalar dışarıdan büyütülebilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
55. Ders 07/01/2024 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Bellek tabanlı dosyaların çalışma mekanizmasının iyi anlaşılması için öncelikle dosya işlemlerinde read ve write işlemlerinin nasıl yapıldığı
hakkında bilgi sahibi olunması gerekmektedir.
Biz write POSIX fonksiyonuyla dosyaya bir yazma yaptığımızda write fonksiyonu genellikle doğrudan bu işlemi yapan bir sistem fonksiyonunu
çağırmaktadır. Örneğin Linux sistemlerinde write fonksiyonu, prosesi kernel moda geçirerek doğrudan sys_write isimli sistem fonksiyonunu
çağırır. Pekiyi bu sys_write sistem fonksiyonu ne yapmaktadır? Genellikle işletim sistemlerinde dosyaya yazma yapan sistem fonksiyonları
hemen yazma işlemini diske yapmazlar. Önce kernel içerisindeki bir tampona yazma yaparlar. Bu tampona Linux sistemlerinde eskiden "buffer cache"
denirdi. Sonradan sistem biraz değiştirildi ve "page cache" denilmeye başlandı. İşte bu tampon sistemi işletim sisteminin bir "kernel thread'i"
tarafından belli periyotlarla diske flush edilmektedir. Yani biz diske write fonksiyonu ile yazma yaptığımızda aslında bu yazılanlar önce kernel
içerisindeki bir tampona (Linux'ta page cache) yazılmakta ve işletim sisteminin bağımsız çalışan başka bir akışı tarafından çok bekletilmeden
bu tamponlar diske flush edilmektedir. Pekiyi neden write fonksiyonu doğrudan diske yazmak yerine önce bir tampona (page cache) yazmaktadır?
İşte bunun amacı performansın artırılmasıdır. Bu konuya genel olarak "IO çizelgelemesi (IO scheduling)" denilmektedir. IO çizelgelemesi
diske yazılacak ya da diskten okunacak bilgilerin bazılarının bir araya getirilerek belli bir sırada işleme sokulması anlamına gelmektedir.
(Örneğin biz dosyaya peşi sıra birkaç write işlemi yapmış olalım. Bu birkaç write işlemi aslında kernel içerisindeki page cache'e yapılacak
ve bu page cache'teki sayfa tek hamlede işletim sistemi tarafından diske flush edilecektir.) Tabii işletim sisteminin arka planda bu tamponları
flush eden kernel thread'i çok fazla beklemeden bu işi yapmaya çalışmaktadır. Aksi takdirde elektrik kesilmesi gibi durumlarda bilgi kayıpları
daha yüksek düzeyde olabilmektedir. Pekiyi biz write fonksiyonu ile yazma yaptığımızda mademki yazılanlar hemen diskteki dosyaya aktarılmıyor
o halde başka bir proses tam bu işlemden hemen sonra open fonksiyonu ile dosyayııp ilgili yerden okuma yapsa bizim en son yazdıklarımızı
okuyabilecek midir? POSIX standartlarına göre write fonksiyonu geri döndüğünde artık aynı dosyadan bir sonraki read işlemi ne olursa olsun
write yapılan bilgiyi okumalıdır. İşte işletim sistemleri zaten bir dosya açıldığında read işleminde de write işleminin kullandığı aynı
tamponu kullanmaktadır. Bu tasarıma "unified file system" da denilmektedir. Bu tasarımdan dolayı zaten ilgili dosya üzerinde işlem yapan
her proses işletim sistemi içerisindeki aynı tamponları kullanmaktadır. Yani işletim sisteminin sistem fonksiyonları önce bu tamponlara
bakmaktadır. Dolayısıyla bu tamponların o anda flush edilip edilmediğinin bir önemi kalmamaktadır. (Tabii bir proses işletim sistemini
bypass edip doğrudan disk sektörlerine erişirse bu durumda gerçekten henüz write fonksiyonu ile yazılanların dosyaya yazılmamış olduğunu
görebilir.)
Pekiyi biz bir dosyayı bellek tabanlı olarak açarak o bellek alanını güncellediğimizde oradaki güncellemeler başka prosesler tarafından
read işlemi sırasında görülecek midir? Ya da tam tersi olarak başka prosesler dosyaya write yaptığında bizim map ettiğimiz bellek otomatik
bu yazılanları görecek midir? İşte POSIX standartları bunun garantisini vermemiştir. POSIX standartlarında bellek tabanlı dosyanın bellek
içeriğinde değişiklik yapıldığında bu değişikliğin diğer prosesler tarafından görülebilmesi için ya da diğer proseslerin yaptığı write
işleminin bellek tabanlı dosyanın bellek alanına yansıtılabilmesi için msync isimli bir POSIX fonksiyonunun çağrılması gerekmektedir.
Her ne kadar POSIX standartları bu msync fonksiyonunun çağrılması gerektiğini belirtiyorsa da Linux gibi pek çok UNIX türevi sistem
"unified file system" tasarımı nedeniyle aslında msync çağrısına gereksinim duymamaktadır. Örneğin Linux'ta biz bir bellek tabanlı dosyayı
map ettiğimizde aslında sayfa tablosunda bizim map ettiğimiz kısım doğrudan zaten işletim sisteminin tamponunu (page cache) göstermektedir.
Yani zaten Linux sistemlerinde bütün prosesler dosya işlemlerinde önce bu page cahche'e bakmaktadır. Dolayısıyla biz bellek tabanlı dosyanın
bellekteki alanına yazma yaptığımızda diğer prosesler dosyayı bellek tabanlı açmamış olsa bile page cache olarak aynı alana baçvuracaklarından
dolayı bizim yazdıklarımızı hemen görecektir. Benzer biçimde başka bir proses dosyaya yazma yaptığında da aslında aynı tampona (page cache)
yazma yapmaktadır. Ancak ne olursa olsun taşınabilir programların bu msync fonksiyonunu aşağıda belirteceğimiz biçimde çağırması gerekmektedir.
Aşağıdaki örnekte "sample.c" programı bir dosyayı bellek tabanlı olarak açıp onun başına bir şeyler yazıp beklemektedir. "mample.c" programı
ise bir dosyanın ilk 32 karakterini ekrana (stdout dosyasına) yazdırmaktadır. Bu örnekten amaç Linux sistemlerinde hiç msync çağırması
yapılmadan bir bir prosesin bellek tabanlı dosyanın bellekteki alanına yazma yaptığında diğer bir prosesin dosyayı bellek tabanlı açmasa
bile o değişikliği gördüğünün ispat edilmesidir. Programları "test.txt" gibi örnek bir dosya oluşturup onun üzerinde farklı terminallerden
çalıştırarak deneyebilirsiniz. Örneğin:
./sample test.txt
./mample test.txt
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* sample.c */
#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;
struct stat finfo;
char *fmaddr;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((fd = open(argv[1], O_RDWR)) == -1)
exit_sys("open");
if (fstat(fd, &finfo) == -1)
exit_sys("fstat");
if ((fmaddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
exit_sys("mmap");
printf("press ENTER to write...\n");
getchar();
memcpy(fmaddr, "XXXXXX", 6);
printf("press ENTER to exit...\n");
getchar();
if (munmap(fmaddr, finfo.st_size) == -1)
exit_sys("munmap");
close(fd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* mample.c */
#include <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 buf[32 + 1];
ssize_t result;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((fd = open(argv[1], O_RDONLY)) == -1)
exit_sys("open");
if ((result = read(fd, buf, 32)) == -1)
exit_sys("read");
for (ssize_t i = 0; i < result; ++i)
putchar(buf[i]);
putchar('\n');
close(fd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz gibi her ne kadar Linux gibi "unified file system" tasarımını kullanan işletim sistemlerinde msync fonksiyonu
gerekmiyorsa da bellek tabanlı dosyada yapılan değişikliklerin diskteki dosyaya yansıtılması, diskteki dosyada yapılan değişikliklerin
bellek tabanlı dosyanın bellek alanına yansıtılması için msync isimli POSIX fonksiyonun çağrılması gerekmektedir. msync fonksiyonunun
prototipi şöyledir:
#include <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 <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;
struct stat finfo;
char *fmaddr;
if (argc != 2) {
fprintf(stderr, "wrong number of arguments!..\n");
exit(EXIT_FAILURE);
}
if ((fd = open(argv[1], O_RDWR)) == -1)
exit_sys("open");
if (fstat(fd, &finfo) == -1)
exit_sys("fstat");
if ((fmaddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED)
exit_sys("mmap");
printf("press ENTER to write...\n");
getchar();
memcpy(fmaddr, "XXXXXX", 6);
if (msync(fmaddr, 4096, MS_SYNC) == -1)
exit_sys("msync");
if (munmap(fmaddr, finfo.st_size) == -1)
exit_sys("munmap");
close(fd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows'ta paylaşılan bellek alanları yoluyla proseslerarası haberleşme UNIX/Linux sistemlerindekine benzer biçimde yürütlmektedir. Tabii
bu sistemlerde kullanılan API fonksiyonları farklıdır. Windows'ta Paylaşılan bellek alanları tipik olarak şu aşamalardan geçilerek
kullanılmaktadır:
1) Önce iki proses de CreateFileMapping API fonksiyonuyla bir "file mapping" nesnesi oluşturur. CreateFileMapping fonksiyonunun prototipi
şöyledir:
HANDLE CreateFileMappingA(
HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCSTR lpName
);
Fonksiyonun birinci parametresi bellek tabanlı dosya oluşturmak için kullanılmaktadır. Paylaşılan bellek alanları için bu parametre
için INVALID_HANDLE_VALUE özel değeri geçilmelidir. İkinci parametre kernel nesnesinin güvenlik bilgilerine ilişkindir. Bu parametre
NULL geçilebilir. Üçüncü parametre file mapping nesnesinin koruma özelliklerini belirtir. Bu parametre aşağıdakilerden biri olarak
girilmelidir:
PAGE_EXECUTE_READ
PAGE_EXECUTE_READWRITE
PAGE_EXECUTE_WRITECOPY
PAGE_READONLY
PAGE_READWRITE
PAGE_WRITECOPY
Burada PAGE_READONLY yalnıcz okuma yapmak için, PAGE_READWRITE hem okuma hem de yazma yapmak için, PAGE_WRITECOPY "copy on write" işlemi
için, PAGE_EXECUTE_XXX bayrakları ise paylaşılan alana yerleştirilecek programın çalıştırılabilmesi için kullanılmaktasır. Burada en yaygın
kullanılan bayrak PAGE_READWRITE bayrağıdır. Fonksiyonun sonraki iki parametresi file mapping nesnesinin 8 byte'lık uzunlupunun yüksek ve
düşük anlamlı dörder byte'lık değerlerini almaktadır. Bu iki parametre bellek tabanlı dosya oluştururken 0 geçilebilir. Bu durumda file mapping
nesnesinin uzunuğu ilgili dosyanın uzunluğu kadar olur. Fonksiyonun son parametresi proseslerin aynı file mapping nesnesini görebilmesi
için gerekli olan ismi belirtir. Bu isim bir dosya ismi değildir. Programcının uydurduğu herhangi bir isim olabilir. Eğer haberleşme üst ve
alt prosesler arasında yapılacaksa ya da bellek tabanlı dosya kullanılacaksa bu parametre NULL geçilebilir. Fonksiyon zaten bu isimde
bir file mapping nesnesi yoksa onu yaratır, varsa olanı açar. Fonksiyon başarı durumunda file mapping nesnesinin handle değerine başarısızlık
durumunda NULL adrese geri dönmektedir. Eğer file mapping nesnesi varsa ve biz onu açıyorsak fonksiyon başarılı olur, ancak GetLestError
fonksiyonu fonksiyonu çağrıldığında fonksiyon ERROR_ALREADY_EXISTS değeri ile geri döner.
CreateFileMapping fonksiyonu UNIX/Linux sistemlerindeki işlevsel olarak shm_open fonksiyonuna benzetilebilir.
Örneğin:
HANDLE hFileMapping;
...
if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL)
ExitSys("CreateFileMapping");
2) Artık file mapping nesnesi için sanal bellekte yer tahsis etmek gerekir. Yani başka bir deyişle nesneyi belleğe "map etmek" gerekir.
Bu işlem MapViewOfFile API fonksiyonuyla yapılmaktadır. Bu fonksiyonun prototipi şöyledir:
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap
);
Fonksiyonun birinci parametresi file mapping nesnesinin handle değerini almaktadır. Fonksiyonun ikinci parametresi map edilecek alandaki
sayfaların koruma özelliklerini belirtmektedir. (Yani örneğin biz file mapping nesnesini read/write olarak oluşturmuş olabiliriz. Ancak
"read only" yapabiliriz.) Buradaki bayraklar aşağıdı derlerin bit OR işlemine sokulmasıyla oluşturulmaktadır:
FILE_MAP_READ
FILE_MAP_WRITE
FILE_MAP_ALL_ACCESS
FILE_MAP_COPY
FILE_MAP_EXECUTE
FILE_MAP_LARGE_PAGES
FILE_MAP_TARGETS_INVALID
Bu parametre tipik olarak FILE_MAP_READ|FILE_MAP_WRITE biçiminde girilir. Tabii bu durumda file mapping nesnesinin de PAGE_READWRITE
biçiminde yaratılmış olması gerekir.
Fonksiyoun sonraki iki parametresi file mapping nesnesinin neresinin map edileceğini belirtmektedir. Bu parametreler 8 byte'lık uzunluk
değerinin yüksek anlamlı ve düşük anlamlı dört byte'ını almaktadır. Bu offset değerinin sayfa katlarında olması gerekmektedir. Son
parametre map edilecek uzunluğu belirtmektedir. Bu değer 0 girilirse file mapping nesnesinin belirtilen offset'ten itibaren geri kalan
hepsi map edilmektedir.
Fonksiyon başarı durumunda map edilen sanal bellek adresine, başarısızlık durumunda NULL adrese geri dönmektedir.
MapViewOfFile API fonksiyonu işlevsel olarak UNIX/Linux sistemlerindeki mmap POSIX fonksiyonuna benzetilebilir. Örneğin:
char *pcMapAddr;
...
if ((pcMapAddr = (char*)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL)
ExitSys("MapViewOfFile");
3) Haberleşme bittikten sonra artık iki proses de map edilen sanal bellek alanını UnmapViewOfFile API fonksiyonuyla serbest bırakabilir.
Fonksiyonun prototipi şöyledir:
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress
);
Fonksiyon parametre olarak MapViewOfFile fonksiyonu ile tahsis edilmiş olan sanal bellek adresini alır. Başarı durumunda sıfır dışı
bir değere başarısızlık durumunda sıfır değerine geri döner. Bu fonksiyon UNIX/Linux sistemlerindeki işlevsel olarak munmap fonksiyonuna
benzetilebilir. Ancak maunmap fonksiyonunun parçalı geri bırakmaya izin verdiğini anımsayınız. Bu fonksiyon ise tüm taksis edilen alanı
geri bırakmaktadır.
4) Nihayet CreateFileMapping fonksiyonu ile elde edilmiş olan file mapping nesnesi CloseHandle API fonksiyonuyle kapatılmalıdır. Fonkisyonun
prototipini daha önce vermiştik:
BOOL CloseHandle(
HANDLE hObject
);
FileMapping nesneleri son proses de nesneyi kapattığında otomatik olark sistemden silinmektedir. Halbuki UNIX/Linux sistemlerinde bu
nesnelerin reboot edilene kadar (kernel persistent) ya da silinene kadar kaldığını anımsayınız.
Aşağıdaki örnekte "Prog1.c" ve "Prog2.c" programları paylaşılan bellek alanları yoluyla haberleşmektedir. "Prog1.c" programı paylaşılan
bellek alanına 0'dan 100'e kadar int sayıları yerleştirir. "Prog2.c" programı da bunları okuyup ekrana (stdout dosyasına) yazdırmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* Prog1.c */
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#define FILE_MAPPING_NAME "TestFileSharedMemory"
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFileMapping;
int *mapAddr;
if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL)
ExitSys("CreateFileMapping");
if ((mapAddr = (int *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL)
ExitSys("MapViewOfFile");
for (int i = 0; i < 100; ++i)
mapAddr[i] = i;
printf("Press ENTER to EXIT...\n");
getchar();
UnmapViewOfFile(mapAddr);
CloseHandle(hFileMapping);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/* Prog2.c */
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#define FILE_MAPPING_NAME "TestFileSharedMemory"
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFileMapping;
int *mapAddr;
if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL)
ExitSys("CreateFileMapping");
if ((mapAddr = (int *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL)
ExitSys("MapViewOfFile");
printf("Press ENTER to read...\n");
for (int i = 0; i < 100; ++i)
printf("%d ", mapAddr[i]);
printf("\n");
UnmapViewOfFile(mapAddr);
CloseHandle(hFileMapping);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde bellek tabalı dosyaların oluşturulması da oldukça kolaydır. Sırasıyla şu işlemlerin yapılması gerekmektedir:
1) İlgili dosya CreateFile API fonksiyonuyla açılır.
2) Açılan dosyanın handle değeri verilerek CreateFileMapping fonksiyonu ile mapping nesnesi elde edilir. File mapping nesnesi oluşturulurken
artık buna bir isim verilmesine gerek yoktur.
3) File mapping nesnesinin handle değeri verilerek MapViewOfFile fonksiyonu çağrılır ve sanal bellek adresi elde edilir.
4) Kullanım bittikten sonra map edilen adres UnmapViewFile fonksiyonu ile serbest bırakılır. Yine file mapping nesnesi ve açılmış olan
dosya CloseHandle fonksiyonlarıyla kapatılır.
Windows sistemlerinde de "unified file system" kullanılmaktadır. Yani mapping yapılan dosya için verilen adres tıpkı Linux sistemlerinde
olduğu gibi işletim sisteminin kullandığı "page cache" buffer adresidir. Yani bir proses yine bu sistemlerde paylaşılan bellek alanına
bir şey yazdığı zaman diğer prosesler (eğer dosya sharing modda açılmışsa) bu yazılanı görmektedir. Bunun tersi de geçerlidir. Windows
sistemlerinde bu durum Microsoft tarafındna garanti edildiği için bu sistemlerde UNIX/Linux sistemlerinde olduğu gibi msync benzeri
bir fonksiyon yoktur.
Aşağıda Windows sistemlerinde bellek tabanlı dosya örneği verilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFile;
HANDLE hFileMapping;
char *mapAddr;
DWORD dwSize;
if ((hFile = CreateFile("test.txt", GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
if ((dwSize = GetFileSize(hFile, NULL)) == INVALID_FILE_SIZE && GetLastError() != NO_ERROR)
ExitSys("GetFileSize");
if ((hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL)) == NULL)
ExitSys("CreateFileMapping");
if ((mapAddr = (char *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL)
ExitSys("MapViewOfFile");
for (DWORD i = 0; i < dwSize; ++i)
putchar(mapAddr[i]);
putchar('\n');
memcpy(mapAddr, "XXXXX", 5);
UnmapViewOfFile(mapAddr);
CloseHandle(hFileMapping);
CloseHandle(hFile);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
56. Ders 13/01/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda bir BMP dosyasını bellek tabanlı olarak açıp ona ilişkin bilgileri alan bir Windows programı verişmiştir. BMP dosya formatı için
çeşitli kaynaklara başvurabilirsiniz. Örneğin aşağıdaki bağlantıda BMP dosya formatı temel düzeyde açıklanmıştır:
https://www.ece.ualberta.ca/~elliott/ee552/studentAppNotes/2003_w/misc/bmp_file_format/bmp_file_format.htm
Aşağıdaki programda Windows ile ilgili kısım Macar Notasyonu ile diğer kısımlar klasik C notasyonu ile yazılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <Windows.h>
void ExitSys(LPCSTR lpszMsg);
#define ROUND_UP(val, n) (((val) + (n) - 1) / (n) * (n))
#pragma pack(1)
typedef struct tagBITMAP_FILE_HEADER {
uint8_t signature[2];
uint32_t file_size;
uint32_t reserved;
uint32_t data_offset;
} BITMAP_FILE_HEADER;
typedef struct tagBITMAP_INFO_HEADER {
uint32_t header_size;
uint32_t image_width;
uint32_t image_height;
uint16_t plane;
uint16_t bits_per_pixel;
uint32_t compression;
uint32_t compressed_image_size;
uint32_t horizontal_res;
uint32_t vertical_res;
uint32_t color_used;
uint32_t important_colors;
} BITMAP_INFO_HEADER;
typedef struct tagBITMAP_FORMAT {
BITMAP_FILE_HEADER file_header;
BITMAP_INFO_HEADER info_header;
/* ... */
} BITMAP_FORMAT;
typedef struct tagRGB {
uint8_t red, green, blue;
} RGB;
void get_pixel24(uint8_t *dbmp, int width, int height, int row, int col, RGB *rgb);
int main(void)
{
HANDLE hFile;
HANDLE hFileMapping;
DWORD dwSize;
BITMAP_FORMAT *bmp;
const char *compression_types[] = {"NO COMPRESSION", "8 BIT RLE ENCODING", "4 BIT RLE_ENCODING"};
uint8_t *dbmp;
RGB rgb;
if ((hFile = CreateFile("test.bmp", GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)
ExitSys("CreateFile");
if ((dwSize = GetFileSize(hFile, NULL)) == INVALID_FILE_SIZE && GetLastError() != NO_ERROR)
ExitSys("GetFileSize");
if ((hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL)) == NULL)
ExitSys("CreateFileMapping");
if ((bmp= (BITMAP_FORMAT *)MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0)) == NULL)
ExitSys("MapViewOfFile");
printf("Signature: %c%c (%02X %02X)\n", bmp->file_header.signature[0], bmp->file_header.signature[1],
bmp->file_header.signature[0], bmp->file_header.signature[1]);
printf("File Size:%lu (%lX)\n", (unsigned long)bmp->file_header.file_size, (unsigned long)bmp->file_header.file_size);
printf("Data Offset: %lu (%lX)\n", (unsigned long)bmp->file_header.data_offset, (unsigned long)bmp->file_header.data_offset);
printf("Info Header Size: %lu (%lX)\n", (unsigned long)bmp->info_header.header_size, (unsigned long)bmp->info_header.header_size);
printf("Image Width: %lu (%lX)\n", (unsigned long)bmp->info_header.image_width, (unsigned long)bmp->info_header.image_width);
printf("Image Height: %lu (%lX)\n", (unsigned long)bmp->info_header.image_height, (unsigned long)bmp->info_header.image_height);
printf("Bits Per Pixel: %lu (%lX)\n", (unsigned long)bmp->info_header.bits_per_pixel, (unsigned long)bmp->info_header.bits_per_pixel);
printf("Compression: %s\n", compression_types[bmp->info_header.compression]);
/* .... */
dbmp = (uint8_t *)bmp + bmp->file_header.data_offset;
get_pixel24(dbmp, bmp->info_header.image_width, bmp->info_header.image_height, 46, 157, &rgb);
printf("Red: %d, Green: %d, Blue: %d\n", rgb.red, rgb.green, rgb.blue);
UnmapViewOfFile(bmp);
CloseHandle(hFileMapping);
CloseHandle(hFile);
return 0;
}
void get_pixel24(uint8_t *dbmp, int width, int height, int row, int col, RGB *rgb)
{
uint32_t destoff;
destoff = (height - row - 1) * ROUND_UP(width * 3, 4) + col * 3;
rgb->blue = dbmp[destoff];
rgb->green = dbmp[destoff + 1];
rgb->red = dbmp[destoff + 2];
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Diğer bir proseslerarası haberleşme yöntemi de "mesaj kuyrukları (message queues)" denilen yöntemdir. Mesaj kuyrukları UNIX/Linux (ve macOS)
sistemlerinde kullanılan bir yöntemdir. UNIX/Linux sistemlerinde mesaj kuyrukları tıpkı paylaşaılan bellek alanlarında olduğu gibi iki farklı
fonksiyon grubuyla kullanılabilmektedir. Eski mesaj kuyruklarına genellikle "Sistem 5 mesaj kuyrukları" denilmektedir. Modern mesaj kuyruklarına
POSIX mesaj kuyrukları denir. POSIX mesaj kuyrukları 90'lı yılların ortalarında tasarlanmıştir. Tabii aslında her iki grup fonksiyon da
POSIX standartlarında bulunmaktadır. Sistem 5 mesaj kuyrukları çok eskiden beri var olduğu için geniş bir taşınabilirliğe sahiptir.
Bugün programcılar her iki mesaj kuyruklarını da kullanmaktadır. Biz kursumuzda yalnızca POSIX mesaj kuyruklarını göreceğiz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Haberleşme sistemleri verilerin gönderilip alınma biçimine göre iki kısma ayrılmaktadır:
1) Stream Tarzı (Stream Oriented) Haberleşme
2) Paket ya da Mesaj Tarzı (Message Oriented) Haberşleme
Stream tarzı haberleşme denildiğinde gönderen ve alan tarafın istediği kadar byte'ı gönderip alabilmesi anlaşılmaktadır. Örneğin borular
stream tarzı bir haberleşme sunmaktadır. Yani borularda gönderen taraf istediği kadar byte'ı üst üste gönderebilir. Bunlar byte düzeyinde
sıraya dizilirler. Alan taraf da istediği kadar byte'ı alabilir. Gönderen taraf n byte gönderdiğinde alan taraf bu n byte'ı tek hamlede
almak zorunda değildir. Paket ya da mesaj tabanlı haberleşmede gönderen taraf bir grup bilgiyi bir paket ya da mesaj adı altında gönderir.
Alan taraf byte düzeyinde alma yapamaz. Gönderilen paketin hepsini almak zorundadır. Ağ haberleşmelerinde bu tarz haberleşmelere "datagram"
haberleşmesi de denilmektedir.
İşte borular stream tabanlı bir haberleşme sunarken mesaj kuyrukları mesaj tabanlı (ya da paket tabanlı da diyebiliriz) bir haberleşme
sunmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
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ılır. Eğer mesaj kuyruğunun yaratılması söz konusu
ise son iki parametreye sırasıyla IPC nesnesinin erişim hakları ve "özellikleri (attribute)" girilmelidir. Yani mesaj kuyruğu yaratılacaksa
adeta fonksiyonun parametrik yapısının aşağıdaki gibi olduğu varsayılmalıdır:
mqd_t mq_open(const char *name, int oflag, mode_t mode, const struct mq_attr *attr);
Fonksiyonun birinci parametresi IPC nesnesinin kök dizindeki dosya ismi gibi uydurulmuş olan ismini belirtir. İkinci parametre açış bayraklarını
belirtmektedir. Burada open fonksiyonundaki bayrakların bazıları kullanılmaktadır. Açış bayrakları aşağıdakilerden yalnızca birini içermek
zorundadır:
O_RDONLY
O_WRONLY
O_RDWR
ış bayraklarına aşağıdaki değerlerin bir ya da birden fazlası da bit OR işlemiyle eklenebilir:
O_CREAT
O_EXCL
O_NONBLOCK
O_CREAT bayrağı yine "yoksa yarat, varsa olanı aç" anlamına gelmektedir. O_EXCL yine O_CREAT birlikte kullanılabilir. Eğer nesne zaten varsa
bu durumda fonksiyonun başarısız olmasını sağlar. O_NONBLOCK blokesiz okuma-yazma yapmak için kullanılmaktadır.
Eğer açış bayrağında O_CREAT belirtilmişse bu durumda programcının fonksiyona iki argüman daha girmesi gerekir. Tabii eğer nesne varsa bu
iki argüman zaten kullanılmayacaktır. Yani bu argüman IPC nesnesi yaratılacaksa (yoksa) kullanılmaktadır. Mesaj kuyruğu yaratılırken erişim
haklarını tıpkı dosyalarda olduğu gibi kuyruğu yaratan kişi S_IXXX sembolik sabitleriyle (ya da 2008 sonrasında doğrudan sayısal biçimde)
vermelidir. Eğer mesaj kuyruğu yaratılacaksa son parametre mq_attr isimli yapı türünden bir nesnenin adresi biçiminde girilmelidir. mq_attr
yapısı şöyle bildirilmiştir:
struct mq_attr {
long mq_flags; /* Flags: 0 or O_NONBLOCK */
long mq_maxmsg; /* Max. # of messages on queue */
long mq_msgsize; /* Max. message size (bytes) */
long mq_curmsgs; /* # of messages currently in queue */
};
Yapının mq_flags parametresi yalnızca O_NONBLOCK içerebilir. max_msg elemanı kuyruktaki tutulacak maksimum mesaj sayısını belirtmektedir.
Yapının mq_msgsize elemanı bir mesajın maksimum uzunluğunu belirtmektedir. mq_curmsgs elemanı ise o anda kuyruktaki mesaj sayısını belirtmektedir.
Programcı mesaj kuyruğunu yaratırken yapının mq_maxmsg ve mq_msgsize elemanlarına uygun değerler girip mesaj kuyruğunun istediği gibi
yaratılmasını sağlayabilir. Yani mesaj kuyruğu yaratılırken programcı mq_attr yapısının yalnızca mq_maxmsg ve mq_msgsize elemanlarını doldurur.
Yapının diğer elemanları mq_open tarafından dikkate alınmamaktadır. Ancak bu özellik parametresi NULL adres biçiminde de geçilebilir.
Bu durumda mesaj kuyruğu default değerlerle yaratılır. Bu default değerler değişik sistemlerde değişik biçimlerde olabilir. Linux sistemlerinde
genel olarak default durumda maksimum mesaj mq_maxmsg değeri 10, mq_msgsize değeri ise 8192 alınmaktadır. mq_open fonksiyonunda kuyruk özelliklerini
girerken mq_maxmsg ve mq_msgsize elemanlarına girilecek değerler için işletim sistemleri alt ve üst limit belirlemiş olabilirler. Eğer yapının bu
elemanları bu limitleri aşarsa mq_open fonksiyonu başarısız olur ve errno değişkeni EINVAL olarak set edilir. Örneğin Linux sistemlerinde
sıradan prosesler (yani root olmayan prosesler) mq_maxmsg değerini 10'un yukarısına çıkartamamaktadır. Ancak uygun önceliğe sahip prosesler
bu değeri 10'un yukarısında belirleyebilmektedir. Ancak POSIX standartları bu default limitler hakkında bir şey söylememiştir. Linux sistemlerinde
sonraki paragraflarda açıklanacağı gibi bu limitler proc dosya sisteminden elde edilebilmektedir.
mq_open fonksiyonu başarı durumunda yaratılan mesaj kuyruğunu temsil eden betimleyici değeriyle, başarısızlık durumunda -1 değeriyle geri
dönmektedir. Fonksiyonun geri döndürdüğü "mesaj kuyruğu betimleyicisi (message queue descriptor)" diğer fonksiyonlarda bir handle değeri
gibi kullanılmaktadır. Linux çekirdeği aslında mesaj kuyruklarını tamamen birer dosya gibi ele almaktadır. Yani mq_open fonksiyonu Linux
sistemlerinde dosya betimleyici tablosunda bir betimleyici tahsis edip ona geri dönmektedir. Ancak POSIX standartları, fonksiyonun geri
dönüş değerini mqd_t türüyle temsil etmiştir. Bu durum değişik çekirdeklerde mesaj kuyruklarının dosya sisteminin dışında başka biçimlerde
de gerçekleştirilebileceği anlamına gelmektedir. POSIX standartlarına göre mqd_t herhangi bir tür olarak (yapı da dahil olmak üzere)
<mqueue.h> dosyasında typedef edilebilir.
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
dizin'in adresini almaktadır. Ancak bu parametrenin void bir adres olmadığına dikkat ediniz. Eğer mesaj başka türlere ilişkinse
tür dönüştürmesinin yapılması gerekmektedir. Üçüncü parametre gönderilecek mesajın uzunluğunu belirtir. Dördüncü parametre
mesajın öncelik derecesini belirtmektedir. Bu öncelik derecesi >= 0 bir değer olarak girilmelidir. POSIX mesaj kuyruklarında
öncelik derecesi yüksek olan mesajlar FIFO sırasına göre önce alınmaktadır. Bu mesaj kuyruklarının klasik Sistem 5 mesaj kuyruklarında
olduğu gibi belli bir öncelik derecesine sahip mesajları alabilme yeteneği yoktur. Buradaki öncelik derecesinin <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 bloke olmaktadır. Ancak açış sırasında O_NONBLOCK bayrağı belirtilmişse mq_send kuyruk doluysa bloke olmaz. Kuyruğa
hiçbir şey yazmadan başarısızlıkla (-1 değeriyle) geri döner ve errno EAGAIN değeri ile set edilir.
Örneğin:
for (int i = 0; i < 100; ++i) {
if (mq_send(mqdes, (const char *)&i, sizeof(int), 0) == -1)
exit_sys("mq_send");
}
Burada 0'dan 100'e kadar 100 tane int değer mesaj kuyruğuna mesaj olarak yazılmıştır.
Mesaj kuyruklarının kendi içerisinde bir senkronizasyon da içerdiğine dikkat ediniz. Kuyruğa yazan taraf kuyruk dolarsa
(Linux'taki default değerin 10 olduğunu anımsayınız) blokede beklemektedir. Ta ki diğer taraf kuyruktan mesajı alıp kuyrukta
yer açana kadar.
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 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.
Aşağıda tipik bir POSIX mesaj kuyruğu örneği verilmiştir. Bu örnekte "prog1" programı stdin dosyasındna okuduklaırnı mesaj kuyruğuna
yazmaktadır. "prog2" programı da mesaj kuyruğdan mesajları alarak stdout dosyasına yazmaktadır. "prog1" programı "quit" mesajını
kuyruğa yazdıktan sonra işlemini sonlandırmaktadır. Benzer biçimde "prog2" programı da bu mesajı aldıktan sonra işlemini sonlandırır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* prog1.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mqueue.h>
#define MESSAGE_QUEUE_NAME "/TestMessageQueue"
#define BUFFER_SIZE 8192
void exit_sys(const char *msg);
int main(void)
{
mqd_t mqd;
char buf[BUFFER_SIZE];
char *str;
if ((mqd = mq_open(MESSAGE_QUEUE_NAME, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, NULL)) == -1)
exit_sys("mq_open");
for (;;) {
printf("Message:");
if (fgets(buf, BUFFER_SIZE, stdin) == NULL)
continue;
if ((str = strchr(buf, '\n')) != NULL)
*str = '\0';
if (mq_send(mqd, buf, strlen(buf), 0) == -1)
exit_sys("mq_send");
if (!strcmp(buf, "quit"))
break;
}
mq_close(mqd);
if (mq_unlink(MESSAGE_QUEUE_NAME) == -1)
exit_sys("mq_unlink");
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* prog2.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mqueue.h>
#define MESSAGE_QUEUE_NAME "/TestMessageQueue"
#define BUFFER_SIZE 8192
void exit_sys(const char *msg);
int main(void)
{
mqd_t mqd;
char buf[BUFFER_SIZE];
ssize_t result;
if ((mqd = mq_open(MESSAGE_QUEUE_NAME, O_RDONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, NULL)) == -1)
exit_sys("mq_open");
for (;;) {
if ((result = mq_receive(mqd, buf, BUFFER_SIZE, NULL)) == -1)
exit_sys("mq_receive");
buf[result] = '\0';
if (!strcmp(buf, "quit"))
break;
printf("%s\n", buf);
}
mq_close(mqd);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Kurusumuzun bu bölümünde dikkatimizi veri yapıları ve algoritmalar konusuna yönelteceğiz. Bir sistem programcınının temel veri yapıları
ve algoritmalar konusunda belli düzeyde bilgi sahibi olması gerekmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aralarında fiziksel ya da mantıksal ilişki olan bir grup nesnenin oluşturduğu topluluğa "veri yapısı (data structure)" denilmektedir.
Veri yapısı denildiğinde tekil bir nesne değil belli bir düzen içerisinde bulunan bir grup nesne anlaşılmaktadır. Çok temel veri
yapıları programlama dillerinin sentaksı tarafından built-in biçimde desteklenmektedir. Örneğin C'de "diziler (arrrays)", "yapılar (structures)",
"birlikler (unions)" dilin sentaksı tarafından desteklenen built-in veri yapılarıdır. Ancak C tarafından doğrudan desteklenen bu veri
yapıları dış dünyadaki olayları modellemekte yetersiz kalmaktadır. Programcının dilin sentaksı ile doğrudan desteklenen veri yapılarını
kullanarak diğer veri yapılarını oluşturması gerekebilmektedir.
Nesne Yönelimli Programlama Dillerinde genellikle o dillerin kütüphanelerinde pek çok veri yapısı sınıflar biçiminde zaten hazır olarak
bulunudurlmaktadır. Örneğin C++'ın standart kütüphanesinde, C# (.NET) ve Java'nın temel kütüphanelerinde "kuyruk sistsmleri (queues)" gibi,
"bağlı listeler (linked lists)" gibi "hash tabloları (hash tables)" gibi veri yapıları hazır bir biçimde bulunmaktadır. Halbuki C'de bu veri
yapıları C'nin standart kütüphanesi tarafından desteklenmemektedir. Her ne kadar pek çok nesne yönelimli programlama dilinin kütüphanelerinde
bu bölümde ele alacağımız veri yapıları zaten hazır bir biçimde bulunuyorsa da sistem programcısının bu veri yapılarını yazabilecek düzeyde
tanıması önemli olmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir problemi kesin çözüme götüren adımlar topluluğuna "algoritma (algorithm)" denilmektedir. Algoritma sözcüğü Fars bilim adamı Ebû Ca'fer
Muhammed bin Mûsâ el-Hârizmî'nin isminden hareketle (bazı yanlış anlaşılmalarla) uydurulmuş bir sözcüktür. Algoritmaların problemi kesin
çözüme götürmesi gerekir. Eğer söz konusu adımlar problemi kesin çözüme götürmüyor ancak iyi bir çözüm sunuyorsa buna "algoritma"
yerine "sezgisel yöntemler (heuristic)" denilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi algoritmalar nasıl ifade edilmektedir? Örneğin "bir dizinin en büyük elemanını bulan algoritma" teknik olarak nasıl açıklanabilir?
Algoritmaları cümlelerle sözel biçimde açıklamak hala kullanılan temel yöntemlerden biridir. Örneğin "dizini ilk elemanını en büyük kabul
edilip bir değişkende saklanır. Sonra diğer tüm elemanlar bir döngü içerisinde bu elemanla karşılaştırılır. Daha büyük eleman görülürse bu
değişkene yerleştirilir" gibi. Ancak sözel açıklama teknik anlamda belirsiz olabilmektedir. Sözükler yerine sembollerin kullanılması daha kesin
belirlemelerin yapılmasına olanak sağlamaktadır. İşte algoritmalar "pseude kodlar" yoluyla, "akış diyagramları" yoluyla ya da "belli bir
programlama dilinde yazılmış kodlar" yoluyla da ifade edilebilmektedir. Eskiden "pseudo kodlar" ve akış diyagramları daha çok kullanılırdı.
Artık algoritmanın ifadesi için mevcut programlama dillerinden biri seçilmektedir. Tabii her zaman sözel açıklama da sürece eşlik etmektedir.
Pekiyi algoritmaları ifade etmek için hangi programlama dili tercih edilmelidir? Çok yüksek seviyeli diller bu iş için uygun değildir.
Dil ne kadar alçak seviyeli olursa algoritmadaki inceliği o kadar iyi yansıtabilmektedir. Bu bakımdan C Programalama Dili tercihler arasında
ön plandadır. Ancak son 20 yıldır algoritmaların ifade edilmesinde Java gibi, C# gibi nesne yönelimli diller de yoğun olarak kullanılmaktadır.
Python kanımızca algoritmaları ifade etmek için fazlaca yüksek seviyeli bir dildir. Ancak Python üzeirnden de algoritmaları anlatan kitaplar
ve dokümanlar bulunmaktadır. Gerçekten de popüler algoritma kitaplarına bakıldığında "Algorithms in C" gibi, "Algorithms in Java" gibi, anlatımın
mevcut bir dil kullanılarak yapıldığı görülmektedir. Özete günümüzde algoritmalar hem sözel olarak hem de bir programlama dilinde yazılmış
kodlar biçiminde ifade edilmektedir.
Algoritmalar konusunda en bilindik kişi Donald Knuth isimli araştırmacıdır. Knuth'un ansiklopedik nitelikte üç ciltlik klasikleşmiş
"The Art of Compuer Programming" isimli kitabı vardır. Daha sonra Knuth bu seriye birkaç ince fasikül de eklemiştir. Knuth bu önemli
başvuru kitabında algoritmaları ifade etmek için kendisinin uydurduğu MIX isimli bir sembolik makine dili kullanmıştır. Daha sonra bu
MIX dilinin RMIX isimli RISC tabanlı biçimini de geliştirmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir problemi çözebilen pek çok alternatif algoritmalar söz konusu olabilmektedir. Bunların hangisinin daha iyi olduğunun belirlenmesi
gerekir. Tabii bunun için bir "iyilik ölçütü" tespit edilmelidir. Algoritmaları kıyaslamak için ölçüt ne olmalıdır? Kıyaslama için iki
önemli ölçüt "hız" ve "kaynak kullanımıdır". Ancak baskın ve default ölçüt hızdır. Yani genellikle hangi alternatif algoritma daha hızlı
sonuç veriyorsa o algoritma tercih edilmektedir. Tabii bazen kısıtlı kaynakların söz konusu olduğu durumlarda programcı hız yerine kaynak
kullanımı (tipik olarak bellek kullanımı)" ölçütünü de ön plana alabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
İki algoritmanın hızlarını kıyaslamak kolay bir işlem değildir. Çünkü algoritmada koşula dayalı işlemler yapılyor olabilr. Bu koşulların
sağlanması ya da sağlanmaması girdilere bağlı olabilir. Algoritmaların önemli bir bölümü dizi gibi veri yapıları üzerinde işlemler yapmaktadır.
Bu durumda örneğin dizinin dağılımı hız üzerinde etkili olabilmektedir.
Algoritmaların hızlarını kıyaslamak için "simülasyon yöntemi" kullanılabilir. Bu yöntemde alternatif algoritmaların kodları yazılır.
Bu algoritmalar rastgele girdilerle çok fazla kez çalıştırılır, bir ortalama zaman hesaplanır. Simülasyon yöntemi bazen başvurulan yardımcı
yöntemlerden biridir. Ancak simülasyon yöntemini uygulamak oldukça zordur. Her algoritmayı simüle edecek kodun yazılması ve bunun çok kez
çalıştırılması zor bir süreçtir. Bunun yerine tamamen cebrik yöntemlerin kullanılması daha uygundur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
58. Ders 20/01/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Algoritmaların cebrik yöntemlerle hızlarının ve kaynak kullanımlarının analizine ilişkin sürece "algoritma analizi (analysis of algorithms)"
denilmektedir. Pekiyi algoritma analizi nasıl yapılmaktadır? Bunun temel yolu algoritmanın çözümü için gereken işlem sayısının bulunmasıdır.
Tabii bu işlem sayısı girdi büyüklüğüne bağlı olarak parametrik bir biçimde hesaplanacaktır. Ancak yukarıda da belirttiğimiz gibi algortimadaki
işlemlerin sayısı girdilere bağlı olarak da değişebilmektedir. (Tabii aslında her işlem aynı sürede yapılmayabilir. Örneğin pek çok işlemcide
çarpma işlemi toplama işlemine göre çok daha yavaş yapılmaktadır.) Örneğin bir dizi içerisinde var olduğunu bildiğimiz belli bir değerin kaç
işlemde bulunacağını hesaplamak isteyelim. Algoritma şöyle olsun:
int a[N];
for (int i = 0; i < N; ++i)
if (a[i] == VAL)
break; /* bulundu */
Burada işlem sayısına şu işlemler dahildir:
int i = 0
i < N
++i
a[i] == VAL
Ancak burada aranacak olan VAL değerinin dizinin kaçıncı elemanında olduğu diziye bağlıdır. Algoritmaya bağlı değildir. İşte bu tür durumlarda
algoritma analiz edilirken tipik olarak iki durum için hesaplama yapılır:
1) En kötü durum analizi (worst case analyisis)
2) Ortalama durum analizi (average case analysis)
En iyi durum analizi de söz konusu olabilirse de pratikte çok iyimser bir yaklaşım olduğu için uygulanmamaktadır.
Yukarıdaki sıralı aramamın en kötü durum analizini yapmaya çalışalım. En köü durumda eleman dizinin son elemanı olarak bulunur. Böylece
en kötü durumdaki işlem sayısı 1 + 3N kadardır. Bu algoritmadaki ortalama işlem sayısı aranacak değerin 1'inci, 2'inci, 3'üncü, ... N'inci
indekslerde bulunması durumunun toplamının N'e bölünmesiyle elde edilebilir. Bunu matematiksel olarak şöyle yazabiliriz:
1 + (3 * (1 + 2 + 3 + ... + N) / N)
1'den N'e kadar sayıların toplamı N * (N + 1) olduğuna göre buradan şu değer elde edilir:
1 + (3 * (N + 1) / 2)
Tabii bu tür hesaplamalarda bizim sabit olmayan kısmın artışına bakmamız doğru olur. Buradaki 1 toplamı, 3 çarpımı ve 2'ye bölüm sabittir.
Bu tür durumlarda bizim için önemli olan bu N değerinin artması ile algoritmanın davranışıdır.
Algoritmalar dünyasında girdi büyüklüğüne (örneğin dizinin uzunluğuna) bağlı olarak algoritmada yapılan işlemlerin sayısını elde etmekte kullanılan
ifadelere "algoritmanın karmaşıklığı (complexity of algorithms)" denilmektedir. Yani algoritmanın karmaşıklığı girdi büyüklüüne bağlı
olarak algortimanın hızının betimlenmesi için kullanılan ifadelere denilmektedir. Örneğin yukarıdaki sıralaı aramanın karmaşıklığı
1 + (3 * (N + 1) / 2) olarak belirtilebilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir algoritmanın en kötü durum ve özellikle de ortalama durumdaki karmaşıklığının (yani girdiye dayalı olarak işlem sayısının) hesaplanması
oldukça zor olabilmektedir. Bu nedenle genellikle karmaşıklık için kesin bir değer bulma yerine karmaşıklıkları kategorilere ayırıp karşılaştırma
yoluna gidilmektedir. Bu tür kategorik karşılaştırmalar her ne kadar algoritmanın aldığı zaman konusunda ince bir değer vermese de onun
karakteri hakkında kısa sürece içerisinde önemli bir bilgi vermektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Algoritmaları kategorik olarak sınıflandırmak için "asimtotik notasyonlar" denilen özel notasyonlar kullanılmaktadır. Asimtotik notasyonlar
aslında matematikte programlamanın dışında ele alınıp kullanılmış olan tekniklerdir. Başka bir deyişle asimtoto,k notasyonlar bir fonksiyonun
büyüme karakteristiği ile ilgili çalışmalardan kaynaklanmaktadır. Ancak bu konu algoritma analizinde önemli bir uygulama alanı bulmuştur.
Algoritma karmaşıklığında kullanılan asimtotik notasyonlar şunlardır:
- Big O Notasyonu
- Büyük Teta Notasyonu
- Büyük Omega Notasyonu
- Küçük o Notasyonu
Ancak Big O notasyonu en çok kullanılan notasyondur.
Bu notasyonlar aslında bir grup fonksiyonu karakterize etmek için düşünülmüştür.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Big O notasyonu aşağıdaki gibi belirtilmektedir:
f(x) = O(g(x))
Burada f(x) ve g(x) iki fonksiyondur. Notasyondaki eşittir karakteri eleştirilmektedir. Buradaki eşittir karakteri aslında matematikteki
eşitliği anlatmamaktadır. Burada aslında "f(x) fonksiyonunun Big O notasyonuna göre g(x) kategorisinden olduğu" anlatılmaktadır. Örneğin:
3x = O(X^2)
5X^2 = O(X^2)
Burada 3x ve 5X^2 fonksiyonları big O notasyonuna göre X^2 kategorisindendir.
Big O notasyonunun matematiksel sembollerle biçimsel (formal) açıklaması biraz ayrıntılıdır. Bu açıklamalar için Ebook klasöründeki
"Algorithms and Complexity" kitabının Birinci bölümüne başvurabilirsiniz. Biz burada önce biçimsel olmayan açıklama yapacağız. Sonra
biçimsel açıklama üzerinde duracağız.
Big O notasyonunda kategori olarak O(gx(x)) ifadesiyle belirtilmiş olan fonksiyon ile üstel bakımdan aynı olan ve bundan küçük olan tüm
fonksiyonlar kstedilmektedir. Örneğin O(x^2) kategorisi üssü en fazla iki olan tüm fonksiyonları ve üssü bundan küçük olan tüm fonksiyonları
belirtmektedir. Örneğin aşağıdaki fonksiyonların hepsi O(x^2) kategorisine sahiptir, dolayısıyla f(x) = O(x^2) biçiminde ifade edilebilir:
f(x) = 100
f(x) = 3x
f(x) = 3x^2
f(x) = 100x^2 + 80x +5
f(x) = 2X^2 - 5x - 8
Burada x^2'nin katsayısının bir önemi olmadığına dikkat ediniz.
Başka bir deyişle O(x^2) kategorisi k x^2 ve x^1 ve sabitin tüm toplamlarına ilişkin fonksiyonları belirtmektedir. Benzer biçimde
O(x) kategorisine ilişkin bazı fonksiyonlar şunlardır:
f(x) = 3x
f(x) 0 100x - 5
f(x) = x - 1
f(x) = 10
Burada da x'in katsayısının bir önemi yoktur.
Big O notasyonunda O(1) karmaşıklığına sahip (Burada 1 yerine başka bir sayıda kullanılabilir. Ancak en düşük sayı 1 olduğu için 1 tercih edilmektedir.
Yani O(5) ile O(1) aynı anlamdadır.) fonksiyonlardan bazıları şunlardır:
f(x) = 123
f(x) = 1234567
f(x) = 1
Big O notasyonunda g(x) fonksiyonundaki çarpanın önemli olmadığına dikkat ediniz. Örneğin O(x^2) kategorisi ile O(3x^2) kategorisi
arasında hiçbir farklılık yoktur. Benzer biçimde bunlarla O(5X^2 + 2) kategorisi arasında da bir farklılık yoktur. Benzer biçimde
Bu nedenle kategoriler belirtilirken gereksiz biçimde g(x) fonksiyonunda çarpan ve toplam değerler belirtilmez.
Big O natsyonunun biçimsel açıklaması şöylşe yapılabilir:
f(x) = O(g(x)) (x → ∞) if ∃ C, x0 öyle ki |f(x)| < Cg(x) tüm x > x0 değerleri için
Burada şu belirtilmektedir: f(x) = O(g(x)) olabilmesi için |f(x)| < Cg(x) eşitliğini sağlayabilecek bir C değerinin bulunması gerekir.
Örneğin 3x^2 + 5x + 2 = O(x^2) yazılabilir. Çünkü burada örneğin C = 5 gibi bir C değeri bulunabilir. Böylece 5X^2 belli bir x0 değerinde
sonra her zaman 3X^2 + 5x + 2 değerinden büyük kalacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Algoritmaları kıyaslamak için iyiden kötüye doğru çeşitli Big O kategorileri kullanılmaktadır. Bu kategoriler şunlardır (Burada x yerine N
gösterimini kullanacağız):
O(1) Sabit Karmaşıklık
O(log log N) Çifte Logaritmik Karmaşıklık
O(log N) Logaritmik Karmaşıklık
O((log N)^c) c > 1 olmak üzere Polylogaritmik Karmaşıklık
O(N^c) c < 0 < 1 olmak üzere Oransal Kuvvet Karmaşıklığı
O(N) Doğrusal Karmaşıklık
O(N log N) N Log N karmaşıklığı
O(N^2) Karesel Karmaşıklık
O(N^3) Küpsel Karmaşıklık
O(N^c) Polinomsal Karmaşıklık
O(c^N) c > 1 olmak üzere Üstel Karmaşıklık
O(N!) Faktöriyel Karmaşıklığı
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Algoritmaları Big O notasyonuna göre kategorik olarak karşılaştırmak kolaydır. Alternatif algoritmaların kategorileri belirlenir.
Hangi kategorinin diğerinden daha iyi olup olmadığına bakılır. Aynı kategoriye giren algoritmalar arasında bir farklılık söz konusu olsa da
bu farklılık kategorik bir farklılık değildir. Biz aynı Big O kategorisindeki algoritmaları aynı karakterde kabul edebiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi bir algoritmanın yukarıda belirttiğimiz Big O kategorisi nasıl tespit edilir? Algoritmanın girdisinin N olduğunu varsayalım.
Örneğin N bir dizinin uzunluğu olabilir. Bazı kategorilerin tespiti şöyle yapılabilir:
- Tekil ifadeler içeren ve N değrine bağlı olmayan sabit uzunluklu döngüler içeren algoritmalar O(1) karmaşıklıktadır. Örneğin üçgenin
alanın hesaplanması, dikdörtgenin çevresinin hesaplanması, bir yılın artık yıl olup olmadığının tespit edilmesi gibi algoritmalar
O(1) yani sabit karmaşıklıktadır. Sabit karmaşıklık en iyi karmaşıklık kategorisidir. Örneğin bir problemde "bunun için sabit karmaşıklığa
sahip bir algoritma kullanın" ifadesi "N değerine dayalı döngü kullanmadan tekil ifadelerle çözümü bulun" anlamına gelmektedir. Örneğin
dizinin bir elemanına erişilmesi sabit karmaşıklıklı bir işlemdir.
- Bir algoritmada bir döngü varsa ancak döngü N'in logaritması kadar dönüyorsa (tabii algoritma tekiş ilemler de içerebilir) bu algoritma
O(log N) karmaşıklıktadır. Örneğin "ikili arama (binary search)" algoritmasında N değeri arttıkça döngü log2 N kadar artmaktadır.
- Bir algoritma N'e dayalı tekil döngüler içeriyorsa ve tekil işlemler içeriyorsa bu algoritma O(N) karmaşıklıktadır. Bu tür karmaşıklıklara
sözel olarak "doğrusal karmaşıklık" da denilmektedir. Örneğin "dizinin en büyük elemanının bulunması", "dizinin aritmetik ortalamasının
bulunması", "dizide bir elemanın bulunması" gibi algoritmalar O(N) karmaşıklıktadır. Sıralı bir dizi bulunyor olsun. Biz de bu sıralı dizide
belli bir elemanı bulmak isteyelim. İkili arama (binary search) O(log N) karmaşıklıkta, sıralı arama O(N) karmaşıklıktadır. O halde ikili
arama sıralı aramdan daha iyidir.
- O(N log N) karmaşıklıkta iç içe iki döngü bulunabilir. Dıştaki döngü N'e dayalı dönerken içteki dönü N'nin logaritmasına dayalı dönmektedir.
Tabii algoritma başka N'e dayalı döngüler ve tekil işlemler içerebilir. Uick Sort gibi, Heap sort gibi kaliteli sort algoritmaları
O(N log N) karmaşıklıktadır.
- N'e dayalı iç içe iki döngü varsa (tabii ayrık N'e dayalı döngüler ve tekil işlemler de olabilir) bu tür algoritmalar O(N^2) karmaşıklıktadır.
Örneğin "boubble sort", "selection sort" gibi algoritmalar O(N^2) karmaşıklıktadır. Bu karmaşıklığa "karesel karmaşıklık" da denilmektedir.
Bu durumda örneğin "selection sort" algoritması "quick sort" algoritmasından daha kötüdür.
- N'e dayalı iç içe üç döngü varsa (tabii N'e dayalı iç içe döngüler, tekil döngüler ve tekil işlemler de olabilir) bu tür algoritmalar
O(N^3) yani küpsel karmaşıklığa sahiptir. Örneğin tipik matris çarpımı küpsel karmaşıklığa sahiptir. Ancak karesel kamaşıklığa sahip
çözümleri de vardır.
- İç içe k tane döngü içerebilen tüm algoritmalara genel olarak "polinomsal karmaşıklıktaki (polynomial complexity) algortimalar" denilmektedir.
- Alt küme işlemlerine ilişkin algoritmalar tipik olarak O(c^n) yani üstel karmaşıklıktadır. Bir kümenin tüm alt kğmelerinin sayısının 2^n
olduğunu anımsayınız.
- Bazı graf ptoblemleri O(N!) yani faktöriyelsel karmaşıklıktadır. Örneğin "gezgin satıcı problemi (traveling salesmen problem)" faktöriyelsel
karmaşıklıktadır. Faktöriyelsel karmaşıklık en kötü karmaşıklık kategorisidir.
Üstel karmaşıklığa ve faktöriyelsel karmaşıklığa "polinomsal olmayan (nonpolynomial)" karmaşıklar denilmektedir. Polinomsal olmayan karmaşıklık
kısaca "NP karçaıklık" olarak da ifade edilmektedir. NP karmaşıklıktaki problemler bugünkü bilgisayarlarda bile binlerce sene zaman alabilecek
boyutlara gelebilmektedir. Pek çok NP algoritma için polinomsal karmaşıklığa sahip daha makul algoritmalar aranmaktadır. Ancak bu konuda
önemli başarılar elde edilmemiştir. Pekiyi NP karmaşıklıktaki problemler için neler yapılabilir? İşte bu alanda "sezgisel yöntemler (heuristic)"
denilen yöntemler kullanılmaktadır. Anımsanacağı gibi "sezgisel yöntemler (heuristic)" en iyi çözümü hedeflemeyen ancak tatmin edici bir çözümü
makul bir zaman içerisinde bulmayı sağlayan yöntemlerdir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Algoritma analizinde en çok Big O notasyonu kullanılmaktadır. Big O notasyonu aslında bir üst sınır belirtmektedir. Algoritmalarda da
üst sınır tespit etmek önemli bir konudur.
Büyük omega notasyonu Big O notasyonunun tersi gibidir. Yani bu notasyondan amaç alt sınır tespit etmektir. Örneğin Ω(x^2) kategorisi demek
x^2 den daha yüksek üslü polinomların kategorisi demektir. Örneğin Ω(x^2) kategorisine ilişkin bazı fonksiyonlar şunlardır:
3x^3 + 4x = Ω(x^2)
3x^5 = Ω(x^2)
7x^5 + 6x^2 + 4x = Ω(x^2)
3x^2 gibi bir fonksiyonun Ω(x^2) kategorisinde olamayacağınz dikkat ediniz. Görüldüğü gibi Büyük omega notasyonu "alt sınır belirtmektedir".
Yukarıda da belirttiğimiz gibi algoritmalarda alt sınır yerine üst sınır tespit etmek genel olarak daha önemlidir.
Büyük Teta notasyonu tam sınır belirtmek için kullanılmaktadır. Yani polinomun derecesi büyük teta ifadesinden büyük ya da küçük olamaz.
Ancak katsayılar farklı olabilir. Örneğin Θ(x^2) için aşağıdaki kategoriler geçerlidir:
3x^2 = Θ(x^2)
7x^2 + 4x = Θ(x^2)
10x^2 + 5x + 2= Θ(x^2)
Ancak aşağıdaki kategoriler Θ(x^2) için geçerli değildir:
7x
5x^3
7x^4 + 3X^2 + 2
100
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C'de "diziler", "yapılar" ve "birlikler" built-in veri yapılarıdır. Yani dilin sentaksı taarafından birnci elde desteklenmektedir.
Ancak bu veri yapıları bazı uygulamaları gerçekleştirmek için yetersiz kalmaktadır. İşte bu bölümde C'nin sentaksı tarafından doğrudan
desteklenmeyen ancak uygulamalarda sıkça karşılaşılan veri yapılarını inceleyeceğiz. Bu veri yapıları pek çok nesne yönelimli programlama
dilinin temel kütüphanelerinde birer sınıf biçiminde bulunmaktadır. Yani eğer C++, Java ve C# fillerle çalışıyorsanız burada göreceğimiz
veri yapıları zaten o dilelrin temel kütüphanlerinde hazır bir biçimde bulunmaktadır. Ancak ne olursa olsun bu veri yapılarının nasıl
çalıştığının ve nasıl gerçekleştirildiğinin sistem programcıları tarafından bilinmesi gerekmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Veri yapıları dünyasında bir süredir çok karşılaşılan terimlerden biri de "abstract data type" terimidir. Bu terimi Türkçe'ye "soyut veri
türü" biçiminde çevirebiliriz. Abstract data type denildiğinde Belli bir veri yapısını gerçekleştiren data ve fonksiyon grubu anlaşılmalıdır.
Burada "abstract" sözcüğü "soyutlama" yani "aytrıntıların göz ardı edilerek işlevlere dikkatin yöneltilmesi" anlamına gelmektedir. Abstract
data type denildiğinde bir veri yapısı üzerinde işlem yapan API arayüzü anlaşılır. Tipik olarak bu kavran nesne yönelimli programlama
tekniğinde bir sınıf ve o sınıfın desteklediği arayüz biçiminde oluiturulmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
En çok kullanılan veri yapılarından biri "dinamk dizi (dynamic array)" denilen veri yapısıdır. Dinamik dizi "gerektiğinde büyütülen" dizi
anlamına gelmektedir. Bu veri yapısını kullanan programcı veri yapısına bir eleman eklediğinde ekleme fonksiyonu eğer gerekiyorsa tahsis
etmiş olduğu diziyi büyütmektedir. Ancak veri yapısını kullanan kişi işin bu kısmıyla uğraşmamaktadır. Bu veri yapısı C++'ın standart
kütüphanesinde "vector" ismi ile C# ve Java ortamlarının temel kütüphanelerinde "ArrayList" ismiyle bulunmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Dinamik dizilerin gerçekleştirilmesinde önemli olan birkaç nokta vardır. Bunlardan en önemlisi dinamik dizi için ayrılan alan yetmediğinde
dizinin ne kadar büyütüleceğidir. Diziyi birer birer büyütmek iyi bir fikir değildir. Çünkü dinamik tahsisatlar yavaş olma eğilimindedir.
Genellikle büyütme "eskisinin iki katı olacak" biçimde yapılmaktadır. Böylece büyütme (reallocation) dizi uzunluğuna göre logaritmik
karmaşıklıkta (yani O(log N) karmaşıklıkta) yapılmış olur. Gerçekleştirimde programcının o anda dinamk dizi içerisinde kaç elemanın bulunduğunu
ve o anda dizi için tahsis edilen elemanın kaç eleman uzunluğunda olduğunu tutması gerekir. Dizide var olan eleman sayısına "count" ya da
"size" denilmektedir. Dizi için tahsis edilmiş olan eleman sayısına ise genellikle "capacity" debilmektedir.
Dinamik dizinin sonuna eleman eklerken eleman count (ya da size) ile belirtien indekse eklenir ve count değer bir artılır. count değeri
capacity değerine ulaştığında yeniden tahsisat (reallocation) yapılarak dinamik dizi büyütülmelidir. Dinamik dizilerde araya eleman da
benzer biçimde eklenmektedir. Genel olarka dinamik dizilerin gerçekleştirimlerinde eleman silindiğinde kapasite azaltılması yapılmamaktadır.
Dinamik dizilerin gerçekleştirilmesinde kullanıcılara sunulacak temel işlevler şunlardır:
- Dinamik dizinin yaratılması
- Dinamik dizinin yok edilmesi
- Dinamik dizinin sonuna eleman eklenmesi
- Araya elemanın insert edilmesi
- Count ve capacity değerleriin dışarıya verilmesi
- Elemanların get ve set edilmesi
- Belli bir indeksteki elemanın silinmesi
- Dizideki tüm elemanların silinmesi
- Capacity değerinin büyütülmesi
- Capacity değerinin count değerine çekilmesi
Genel olarak dinamik büyütülen dizilerde bir elemanın silinmesi ya da tüm elemanların silinmesi capaciry değerinde bir değişiklik
yaratmamaktadır. Genellikle bu tür veri yapılarında capacity değerinin büyütülmesine izin verilir. Ancak küçültülmsine izin verilmez.
Ancak özel bir durum olarak capacity değeri count değerine çekilebilmektedir.
Aşağıdaki dinamik dizilerin gerçekleştirimine ilişkin bir örnek verilmiştir. Bu örnekte bazı küçük fonksiyonlar makro yerine static inline
fonksiyon biçiminde başlık dosyasının içerisinde tanımlanmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* dynamicarray.h */
#ifndef DYNAMICARRAY_H_
#define DYNAMICARRAY_H_
/* Symbolic Constants */
#define DARRAY_DEF_CAPACITY 8
#define DARRAY_FAILED ((size_t)-1)
/* Type Declaratrions */
typedef int DATATYPE;
typedef struct tagDARRAY {
DATATYPE *darray;
size_t capacity;
size_t count;
} DARRAY, *HDARRAY;
/* Function Prototypes */
HDARRAY create_darray(void);
void destroy_darray(HDARRAY hdarray);
size_t add_darray(HDARRAY hdarray, DATATYPE val);
size_t multiadd_darray(HDARRAY hdarray, const DATATYPE *vals, size_t size);
size_t addp_darray(HDARRAY hdarray, const DATATYPE *val);
size_t insert_darray(HDARRAY hdarray, size_t index, DATATYPE val);
size_t remove_darray(HDARRAY hdarray, size_t index);
size_t reserve_darray(HDARRAY hdarray, size_t newcapacity);
/* inine function definitions */
static inline size_t capacity_darray(HDARRAY hdarray)
{
return hdarray->capacity;
}
static inline size_t count_darray(HDARRAY hdarray)
{
return hdarray->count;
}
static inline DATATYPE get_darray(HDARRAY hdarray, size_t index)
{
return hdarray->darray[index];
}
static inline void set_darray(HDARRAY hdarray, size_t index, DATATYPE val)
{
hdarray->darray[index] = val;
}
static inline void setp_darray(HDARRAY hdarray, size_t index, const DATATYPE *val)
{
hdarray->darray[index] = *val;
}
static inline void getp_darray(HDARRAY hdarray, size_t index, DATATYPE *val)
{
*val = hdarray->darray[index];
}
static inline void clear_darray(HDARRAY hdarray)
{
hdarray->count = 0;
}
#endif
/* dynamic.arry.c */
#include <stdio.h>
#include <stdlib.h>
#include "dynamicarray.h"
static int reallocate(HDARRAY hdarray, size_t newcapacity);
HDARRAY create_darray(void)
{
HDARRAY hdarray;
if ((hdarray = (HDARRAY)malloc(sizeof(DARRAY))) == NULL)
return NULL;
if ((hdarray->darray = (DATATYPE *)malloc(sizeof(DATATYPE) * DARRAY_DEF_CAPACITY)) == NULL) {
free(hdarray);
return NULL;
}
hdarray->capacity = DARRAY_DEF_CAPACITY;
hdarray->count = 0;
return hdarray;
}
void destroy_darray(HDARRAY hdarray)
{
free(hdarray->darray);
free(hdarray);
}
size_t add_darray(HDARRAY hdarray, DATATYPE val)
{
if (hdarray->count == hdarray->capacity && reallocate(hdarray, hdarray->capacity * 2) == -1)
return DARRAY_FAILED;
hdarray->darray[hdarray->count++] = val;
return hdarray->count - 1;
}
size_t multiadd_darray(HDARRAY hdarray, const DATATYPE *vals, size_t size)
{
if (hdarray->count + size > hdarray->capacity && reallocate(hdarray, hdarray->capacity * 2 + size) == -1)
return DARRAY_FAILED;
memmove(hdarray->darray + hdarray->count, vals, sizeof(DATATYPE) * size);
hdarray->count += size;
return hdarray->count - 1;
}
size_t addp_darray(HDARRAY hdarray, const DATATYPE *val)
{
if (hdarray->count == hdarray->capacity && reallocate(hdarray, hdarray->capacity * 2) == -1)
return DARRAY_FAILED;
hdarray->darray[hdarray->count++] = *val;
return hdarray->count - 1;
}
size_t insert_darray(HDARRAY hdarray, size_t index, DATATYPE val)
{
if (index > hdarray->count)
return DARRAY_FAILED;
if (hdarray->count == hdarray->capacity && reallocate(hdarray, hdarray->capacity * 2) == -1)
return DARRAY_FAILED;
memmove(hdarray->darray + index + 1, hdarray->darray + index, (hdarray->count - index) * sizeof(DATATYPE));
hdarray->darray[index] = val;
++hdarray->count;
return index;
}
size_t insertp_darray(HDARRAY hdarray, size_t index, const DATATYPE *val)
{
if (index > hdarray->count)
return DARRAY_FAILED;
if (hdarray->count == hdarray->capacity && reallocate(hdarray, hdarray->capacity * 2) == -1)
return DARRAY_FAILED;
memmove(hdarray->darray + index + 1, hdarray->darray + index, (hdarray->count - index) * sizeof(DATATYPE));
hdarray->darray[index] = *val;
++hdarray->count;
return index;
}
size_t remove_darray(HDARRAY hdarray, size_t index)
{
if (index >= hdarray->count)
return DARRAY_FAILED;
memmove(hdarray->darray + index, hdarray->darray + index + 1, (hdarray->count - index - 1) * sizeof(DATATYPE));
--hdarray->count;
return index;
}
size_t reserve_darray(HDARRAY hdarray, size_t newcapacity)
{
if (newcapacity <= hdarray->capacity)
return 0;
if (reallocate(hdarray, newcapacity) == -1)
return 0;
return newcapacity;
}
static int reallocate(HDARRAY hdarray, size_t newcapacity)
{
DATATYPE *new_darray;
if ((new_darray = (DATATYPE *)realloc(hdarray->darray, sizeof(DATATYPE) * newcapacity)) == NULL)
return -1;
hdarray->darray = new_darray;
hdarray->capacity = newcapacity;
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
60. Ders 27/01/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aralarında öncelik-sonralık ilişkisi olan veri yapılarına "liste (list)" de denilemektedir. Örneğin bu tanıma göre diziler de "liste tarzı"
veri yapılarıdır. Liste tarzı veri yapılarının en yaygın kullanılanlarında biri "bağlı liste (linkes list)" denilen veri yapısıdır. Önceki
elemanının sonraki elemanın yerini gösterdiği dolayısıyla elemanların ardışıl olma zorunluluğunun ortadan kaldırıldığı dizilere "bağlı liste"
denilmektedir. Dizi elemanlarının bellekte fiziklsel olarak ardışıl biçimde bulunduğunu anımsayınız. Bağlı listeler adeta "elemanları bellekte
ardışıl olmayan diziler" gibidir.
Bağlı listelerin her elemanına "düğüm (node)" denilmektedir. Biz kursumuzda bağlı liste elemanlarına "düğüm" ya da "eleman" diyeceğiz. Bağlı
listelerde her düğüm sonraki düğümün yerini tutarsa ve ilk elemanın yeri de biliniyorsa liste elemanlarının hepsine erişilebilmektedir.
Örneğin:
head ----> node ---> node ---> node ----> node (NULL)
Bağlı listelerde her düğüm hem elemanın değerini hem de sonraki elemanın adresini tutmaktadır. Bunun için bağlı liste deüğümleri bir yapı
ile ifade edilmektedir. Örneğin:
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
Bağlı listelerde ilk elemanın yeri bir biçimde bir gösterici tutulmalıdır. Genellikle ilk elemanın yerini tutan göstericiye "head göstericisi"
denilmektdir. Örneğin:
NODE *head; /* ilk elemanın yeri */
Bağlı listenin sonuna elemanın hızla eklenebilmesi için genellikle son elemanın yeri de tutulmaktadır. Son elemanın yerini tutan göstericiye
de "tail göstericisi" denilmektedir.
NODE *tail; /* son elemanın yeri */
Tipik olarak bağlı listenin son elemanın (düğümünün) next göstericisinde NULL adres bulunur. Bu durum listenin sonuna gelindiğini belirtmektedir.
Bağlı listenin tüm elemanlarını gözden geçirmek basit döngüyle yapılabilir:
NODE *node;
...
node = head;
while (node != NULL) {
...
node = node->next;
}
Tabii bu dolaşımı for döngüsüyle daha kolay da ifade edebiliriz:
for (NODE *node = head; node != NULL; node = node->next) {
...
}
Her düğümün yalnızca sonraki düğümün değil aynı zamanda önceki düğümün de yerini tuttuğu bağlı listelere "çift bağlı listeler
(double linked list)" denilmektedir. Çift bağlı listeler belli bir düğümün adresini biliyorsak yalnızca ileriye doğru değil, geriye doğru
da gidebiliriz. Çift bağlı listelerin düğümleri de aşağıdaki gibi bir yapıyla temsil edilebilir:
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
struct tagNODE *prev;
} NODE;
Çift bağlı listelerin bir düğümünün bellekte daha fazla yer kaplayacağına dikkat ediniz. Çift bağlı listelerin tek bağlı listelere göre
en önemli özelliği "adresi bilinen bir düğümün" silinebilmesidir. Tek bağlı listelerde bu durum mümkün değildir. Uygulamalarda buna çok
sık gereksinim duyulmaktadır.
Eğer bir bağlı listede son eleman ilk elemanı gösteriyorsa bu tür bağlı listelere de "döngüsel bağlı listeler (circular linkes lists)"
denilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi bağlı listelere neden gereksinim duyulmaktadır? Diziler varken bağlı listelere gerek var mıdır? Dizilerle bağlı listeler arasındaki
farklılkları, benzerlikleri ve bağlı listelere neden gereksinim duyulduğunu birkaç maddede açıklayabiliriz:
1) Diziler ardışıl alana gereksinim duymaktadır. Ancak belleğin bölündüğü (fragmente olduğu) durumlarda bellekte yeteri kadar küçük boş
alanlar olduğu halde bunlar ardışıl olmadığı için dizi tahsisatı mümkün olamamaktadır. Bu tür durumlarda ardışıllık gereksinimi olmayan
bağlı listeler kullanılabilir. Özellikle heap gibi bir alanda çok sayıda dinamik diziler bellek kullanımıısından verimsizliğe yol
açabilmektedir. Bu dinamik diziler zamanla büyüdükçe birbirini engeller hale gelebilmektedir. İşte uzunluğu baştan belli olmayan çok sayıda
dizinin oluşturulacağı durumlarda dinamik dizi yerine bağlı listeler toplamda daha iyi performans gösterebilmektedir. Dinamik dizilerde
dinamik dizinin büyütülmesi yavaş bir işlemdir. Çünkü büyütme sırasında bloklar yer değiştirebilmektedir.
2) Dizilerde araya eleman ekleme (insert etme) ve aradaki bir elemanı silme dizinin kaydırılmasına (expand ve shrink edilmesine) yol
açacağından yavaş bir işlemdir. Teknik olarak dizilerde eleman isert etme ve eleman silme O(N) karmaşıklıkta bir işlemdir. Halbuki bağlı
listelerde eğer düğümün yerini biliyorsak bu işlem O(1) karmaşıklıkta (yani döngü olmadan tekil işlemlerle) yapılabilmektedir. O halde
araya eleman eklemenin ve aradan eleman silmenin çok yapıldığı sistemlerde bağlı diziler yerine bağlı listeler tercih edilebilmektedir.
3) Bağlı listelerde belli bir indeksteki elemana erişmek O(N) karmaşıklıkta bir işlemdir. Halbuki dizilerde elemana erişim O(1) karmaşıklıkta
yani çok hızlıdır. O halde belli bir indeks değeri ile elemana erişimin yoğun yapıldığı durumlarda bağlı listeler yerine diziler tercih
edilmelidir.
4) Bağlı listeler toplamda bellekte daha fazla yer kaplama eğilimindedir. Çünkü bağlı listenin her düğümü sonraki (ve duruma göre önceki)
elemanın yerini de tutmaktadır.
O halde bağlı listeler tipik şu durumlarda dizilere tercih edilmelidir:
- Eleman insert etmenin ve eleman silmenin çok yapıldığı durumlarda
- Uzunluğu baştan belli olmayan çok sayıda dizinin kullanıldığı durumda
- İndeks yoluyla erişimin az yapıldığı durumda
- Bellek miktarının yeteri kadar fazla olduğu sistemlerde
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Soyut bir veri türü (abstract data type) olarak bağlı listeler üzerinde şu işlemlerin yapılması beklenmektedir:
- Bağlı listenin başına ve sonuna eleman eklenmesi
- Bağlı listenin dolaşılması ve belli bir indeksteki elemanın elde edilmesi
- Adresi bilinen bir düğümün önüne ya da arkasına eleman insert edilmesi
- Adresi bilinen bir düğümün silinmesi
Aşağıda çift bağlı listelerin gerçekleştirilmesine ilişkin bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* llist.h */
#ifndef LLIST_H_
#define LLIST_H_
#include <stddef.h>
#include <stdbool.h>
/* Type Declarations */
typedef int DATATYPE;
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
struct tagNODE *prev;
} NODE;
typedef struct tagLLIST {
NODE *head;
NODE *tail;
size_t count;
} LLIST, *HLLIST;
/* Function Prototypes */
HLLIST create_llist(void);
NODE *add_tail(HLLIST hllist, DATATYPE val);
NODE *addp_tail(HLLIST hllist, const DATATYPE *val);
NODE *add_head(HLLIST hllist, DATATYPE val);
NODE *addp_head(HLLIST hllist, const DATATYPE *val);
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val);
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val);
NODE *insert_prev(HLLIST hllist, NODE *node, DATATYPE val);
NODE *insertp_prev(HLLIST hllist, NODE *node, const DATATYPE *val);
void remove_node(HLLIST hllist, NODE *node);
DATATYPE *getp_item(HLLIST hllist, size_t index);
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *));
bool walk_llist_rev(HLLIST hllist, bool (*proc)(DATATYPE *));
void clear_llist(HLLIST hllist);
void destroy_llist(HLLIST hllist);
/* inline Function Definitions */
static inline size_t count_llist(HLLIST hllist)
{
return hllist->count;
}
#endif
/* llist.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
/* static Functions Prototypes */
static bool disp(DATATYPE *val);
/* Function Definitions */
HLLIST create_llist(void)
{
HLLIST hllist;
if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL)
return NULL;
hllist->head = hllist->tail = NULL;
hllist->count = 0;
return hllist;
}
NODE *add_tail(HLLIST hllist, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
if (hllist->head != NULL) /* is list is empty? */
hllist->tail->next = new_node;
else
hllist->head = new_node;
new_node->prev = hllist->tail;
new_node->next = NULL;
hllist->tail = new_node;
++hllist->count;
return new_node;
}
NODE *addp_tail(HLLIST hllist, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
if (hllist->head != NULL) /* is list is empty? */
hllist->tail->next = new_node;
else
hllist->head = new_node;
new_node->prev = hllist->tail;
new_node->next = NULL;
hllist->tail = new_node;
++hllist->count;
return new_node;
}
NODE *add_head(HLLIST hllist, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
new_node->next = hllist->head;
new_node->prev = NULL;
if (hllist->head == NULL)
hllist->tail = new_node;
else
hllist->head->prev = new_node;
hllist->head = new_node;
++hllist->count;
return new_node;
}
NODE *addp_head(HLLIST hllist, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
new_node->next = hllist->head;
new_node->prev = NULL;
if (hllist->head == NULL)
hllist->tail = new_node;
else
hllist->head->prev = new_node;
hllist->head = new_node;
++hllist->count;
return new_node;
}
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
if (node != hllist->tail)
node->next->prev = new_node;
else
hllist->tail = new_node;
new_node->next = node->next;
node->next = new_node;
new_node->prev = node;
++hllist->count;
return new_node;
}
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
if (node != hllist->tail)
node->next->prev = new_node;
else
hllist->tail = new_node;
new_node->next = node->next;
node->next = new_node;
new_node->prev = node;
++hllist->count;
return new_node;
}
NODE *insert_prev(HLLIST hllist, NODE *node, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
if (node != hllist->head)
node->prev->next = new_node;
else
hllist->head = new_node;
new_node->next = node;
new_node->prev = node->prev;
node->prev = new_node;
++hllist->count;
return new_node;
}
NODE *insertp_prev(HLLIST hllist, NODE *node, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
if (node != hllist->head)
node->prev->next = new_node;
else
hllist->head = new_node;
new_node->next = node;
new_node->prev = node->prev;
node->prev = new_node;
++hllist->count;
return new_node;
}
void remove_node(HLLIST hllist, NODE *node)
{
if (node == hllist->head)
hllist->head = node->next;
else
node->prev->next = node->next;
if (node == hllist->tail)
hllist->tail = node->prev;
else
node->next->prev = node->prev;
--hllist->count;
free(node);
}
DATATYPE *getp_item(HLLIST hllist, size_t index)
{
NODE *node;
if (index >= hllist->count)
return NULL;
node = hllist->head;
for (size_t i = 0; i < index; ++i)
node = node->next;
return &node->val;
}
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *))
{
bool retval = true;
bool def_flag = false;
if (proc == NULL) {
proc = disp;
def_flag = true;
}
for (NODE *node = hllist->head; node != NULL; node = node->next)
if (!proc(&node->val)) {
retval = false;
break;
}
if (def_flag)
putchar('\n');
return retval;
}
bool walk_llist_rev(HLLIST hllist, bool (*proc)(DATATYPE *))
{
bool retval = true;
bool def_flag = false;
if (proc == NULL) {
proc = disp;
def_flag = true;
}
for (NODE *node = hllist->tail; node != NULL; node = node->prev)
if (!proc(&node->val)) {
retval = false;
break;
}
if (def_flag)
putchar('\n');
return retval;
}
void clear_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head;
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
hllist->head = hllist->tail = NULL;
hllist->count = 0;
}
void destroy_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head;
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
free(hllist);
}
static bool disp(DATATYPE *val)
{
printf("%d ", *val);
fflush(stdout);
return true;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
int main(void)
{
HLLIST hllist;
NODE *node, *pos_node;
DATATYPE *val;
if ((hllist = create_llist()) == NULL) {
fprintf(stderr, "cannot create linked list...\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 10; ++i) {
if ((node = add_tail(hllist, i)) == NULL) {
fprintf(stderr, "cannot add item!..\n");
exit(EXIT_FAILURE);
}
if (i == 9)
pos_node = node;
}
walk_llist(hllist, NULL);
remove_node(hllist, pos_node);
walk_llist(hllist, NULL);
if ((val = getp_item(hllist, 5)) == NULL) {
fprintf(stderr, "invalid index!..\n");
exit(EXIT_FAILURE);
}
printf("%d\n", *val);
clear_llist(hllist);
for (int i = 0; i < 10; ++i)
if (add_tail(hllist, i) == NULL) {
fprintf(stderr, "cannot add item!..\n");
exit(EXIT_FAILURE);
}
walk_llist(hllist, NULL);
destroy_llist(hllist);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Çift bağlı liste gerçekleştiriminde handle alanında head ve tail göstericilerini ayrı ayrı tutmak yerine NODE nesnesi tutulursa özel
durumlar ortadan kaldırılabilir ve gerçekleştirim daha sade hale getirilebilir. Bu tasarımda ilk düğümün prev göstericisi ve son düğümün
next göstericisi handle alanındaki düğümü göstermelidir. Benzer biçimde handle alanındaki düğümün next göstericisi ilk düğümün yerini
prev göstericisi ise son düğümün yerini göstermelidir.
Tabii bu durumda handle alanında tutulan NODE nesnesinin val elemanı aslında listeye dahil olmadığı
için kullanılmayacaktır. Dolayısıyla bu eleman boşa yer kaplayacaktır. Ancak genellikle bu durum öncemsizdir. Bu tarzda gerçekleştirim
programcılar tarafından daha fazla tercih edilmektedir. Bu biçimdeki tasarımda handle alanı aşağıdaki gibi olacaktır:
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
struct tagNODE *prev;
} NODE;
typedef struct tagLLIST {
NODE head;
size_t count;
} LLIST, *HLLIST;
Başlangıç durumunda handle alanı içerisindeki düğümün next ve prev göstericilerinin kendisini göstermesi gerekir. Tabii dolaşım
yapılırken artık dolaşımın biteceği son düğümdeki NULL adresinden değil next göstericisinin handle alanı içerisindeki düğümün adresine
eşit olmaması ile tespit edilebilecektir.
Aşağıda çift bağlı listelerde handle alanında düğüm tutma yoluyla özel durumların elimine edilmesine bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* llist.h */
#ifndef LLIST_H_
#define LLIST_H_
#include <stddef.h>
#include <stdbool.h>
/* Type Declarations */
typedef int DATATYPE;
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
struct tagNODE *prev;
} NODE;
typedef struct tagLLIST {
NODE head;
size_t count;
} LLIST, *HLLIST;
/* Function Prototypes */
HLLIST create_llist(void);
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val);
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val);
NODE *insert_prev(HLLIST hllist, NODE *node, DATATYPE val);
NODE *insertp_prev(HLLIST hllist, NODE *node, const DATATYPE *val);
NODE *add_tail(HLLIST hllist, DATATYPE val);
NODE *addp_tail(HLLIST hllist, const DATATYPE *val);
NODE *add_head(HLLIST hllist, DATATYPE val);
NODE *addp_head(HLLIST hllist, const DATATYPE *val);
void remove_node(HLLIST hllist, NODE *node);
DATATYPE *getp_item(HLLIST hllist, size_t index);
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *));
bool walk_llist_rev(HLLIST hllist, bool (*proc)(DATATYPE *));
void clear_llist(HLLIST hllist);
void destroy_llist(HLLIST hllist);
/* inline Function Definitions */
static inline size_t count_llist(HLLIST hllist)
{
return hllist->count;
}
#endif
/* llist.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
/* static Functions Prototypes */
static bool disp(DATATYPE *node);
/* Function Definitions */
HLLIST create_llist(void)
{
HLLIST hllist;
if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL)
return NULL;
hllist->head.next = &hllist->head;
hllist->head.prev = &hllist->head;
hllist->count = 0;
return hllist;
}
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
node->next->prev = new_node;
new_node->next = node->next;
node->next = new_node;
new_node->prev = node;
++hllist->count;
return new_node;
}
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
node->next->prev = new_node;
new_node->next = node->next;
node->next = new_node;
new_node->prev = node;
++hllist->count;
return new_node;
}
NODE *insert_prev(HLLIST hllist, NODE *node, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
node->prev->next = new_node;
new_node->next = node;
new_node->prev = node->prev;
node->prev = new_node;
++hllist->count;
return new_node;
}
NODE *insertp_prev(HLLIST hllist, NODE *node, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
node->prev->next = new_node;
new_node->next = node;
new_node->prev = node->prev;
node->prev = new_node;
++hllist->count;
return new_node;
}
NODE *add_tail(HLLIST hllist, DATATYPE val)
{
return insert_prev(hllist, &hllist->head, val);
}
NODE *addp_tail(HLLIST hllist, const DATATYPE *val)
{
return insertp_prev(hllist, &hllist->head, val);
}
NODE *add_head(HLLIST hllist, DATATYPE val)
{
return insert_next(hllist, &hllist->head, val);
}
NODE *addp_head(HLLIST hllist, const DATATYPE *val)
{
return insertp_next(hllist, &hllist->head, val);
}
void remove_node(HLLIST hllist, NODE *node)
{
node->prev->next = node->next;
node->next->prev = node->prev;
--hllist->count;
free(node);
}
DATATYPE *getp_item(HLLIST hllist, size_t index)
{
NODE *node;
if (index >= hllist->count)
return NULL;
node = hllist->head.next;
for (size_t i = 0; i < index; ++i)
node = node->next;
return &node->val;
}
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *))
{
bool retval = true;
bool def_flag = false;
if (proc == NULL) {
proc = disp;
def_flag = true;
}
for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next)
if (!proc(&node->val)) {
retval = false;
break;
}
if (def_flag)
putchar('\n');
return retval;
}
bool walk_llist_rev(HLLIST hllist, bool (*proc)(DATATYPE *))
{
bool retval = true;
bool def_flag = false;
if (proc == NULL) {
proc = disp;
def_flag = true;
}
for (NODE *node = hllist->head.prev; node != &hllist->head; node = node->prev)
if (!proc(&node->val)) {
retval = false;
break;
}
if (def_flag)
putchar('\n');
return retval;
}
void clear_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head.next;
while (node != &hllist->head) {
temp_node = node->next;
free(node);
node = temp_node;
}
hllist->head.next = &hllist->head;
hllist->head.prev = &hllist->head;
hllist->count = 0;
}
void destroy_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head.next;
while (node != &hllist->head) {
temp_node = node->next;
free(node);
node = temp_node;
}
free(hllist);
}
static bool disp(DATATYPE *val)
{
printf("%d ", *val);
fflush(stdout);
return true;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
int main(void)
{
HLLIST hllist;
NODE *node, *pos_node;
DATATYPE *val;
if ((hllist = create_llist()) == NULL) {
fprintf(stderr, "cannot create linked list...\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 10; ++i) {
if ((node = add_tail(hllist, i)) == NULL) {
fprintf(stderr, "cannot add item!..\n");
exit(EXIT_FAILURE);
}
if (i == 0)
pos_node = node;
}
walk_llist(hllist, NULL);
remove_node(hllist, pos_node);
walk_llist(hllist, NULL);
if ((val = getp_item(hllist, 5)) == NULL) {
fprintf(stderr, "invalid index!..\n");
exit(EXIT_FAILURE);
}
printf("%d\n", *val);
clear_llist(hllist);
for (int i = 0; i < 10; ++i)
if (add_tail(hllist, i) == NULL) {
fprintf(stderr, "cannot add item!..\n");
exit(EXIT_FAILURE);
}
walk_llist(hllist, NULL);
destroy_llist(hllist);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Tek bağlı listelerde her düğüm yalnızca sonraki düğümün adresini tutmaktadır. Dolayısıyla geriye doğru ilerleme mümkün olmaz. Tak bağlı
listelerde adresini bildiğimiz düğümün yalnızca önüne eleman insert edebiliriz. Benzer biçimde adresini bildiğimiz düğümü silemeyiz.
Ancak adresini bildiğimiz düğümün önündeki düğümü silebiliriz. Bunun için oluşturulacak handle alanı şöyle olabilir:
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagLLIST {
NODE *head;
NODE *tail;
size_t count;
} LLIST, *HLLIST;
Soyut bir veri türü olarak tek baalı listelerde gerçekleştirilebilecek işlemler şunlar olabilir:
- Listenin başına ve sonuna eleman eklenmesi
- Listenin dolaşılması ve belli bir indeksteki elemanın elde edilmesi
- Adresi bilinen bir düğümün önüne eleman insert edilmesi
- Adresi bilinen bir düğümün önündeki elemanın silinmesi
Aşağıdaki örnekte bir tek bağlı liste gerçekleştirimi verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* llist.h */
#ifndef LLIST_H_
#define LLIST_H_
#include <stddef.h>
#include <stdbool.h>
/* Type Declarations */
typedef int DATATYPE;
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagLLIST {
NODE *head;
NODE *tail;
size_t count;
} LLIST, *HLLIST;
/* Function Prototypes */
HLLIST create_llist(void);
NODE *add_tail(HLLIST hllist, DATATYPE val);
NODE *addp_tail(HLLIST hllist, const DATATYPE *val);
NODE *add_head(HLLIST hllist, DATATYPE val);
NODE *addp_head(HLLIST hllist, const DATATYPE *val);
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val);
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val);
void remove_next(HLLIST hllist, NODE *node);
DATATYPE *getp_item(HLLIST hllist, size_t index);
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *));
void clear_llist(HLLIST hllist);
void destroy_llist(HLLIST hllist);
/* inline Function Definitions */
static inline size_t count_llist(HLLIST hllist)
{
return hllist->count;
}
#endif
/* llist.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
/* static Functions Prototypes */
static bool disp(DATATYPE *val);
/* Function Definitions */
HLLIST create_llist(void)
{
HLLIST hllist;
if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL)
return NULL;
hllist->head = hllist->tail = NULL;
hllist->count = 0;
return hllist;
}
NODE *add_tail(HLLIST hllist, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
if (hllist->tail != NULL)
hllist->tail->next = new_node;
else
hllist->head = new_node;
hllist->tail = new_node;
new_node->next = NULL;
++hllist->count;
return new_node;
}
NODE *addp_tail(HLLIST hllist, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
if (hllist->tail != NULL)
hllist->tail->next = new_node;
else
hllist->head = new_node;
hllist->tail = new_node;
new_node->next = NULL;
++hllist->count;
return new_node;
}
NODE *add_head(HLLIST hllist, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
new_node->next = hllist->head;
if (hllist->head == NULL)
hllist->tail = new_node;
hllist->head = new_node;
++hllist->count;
return new_node;
}
NODE *addp_head(HLLIST hllist, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
new_node->next = hllist->head;
if (hllist->head == NULL)
hllist->tail = new_node;
hllist->head = new_node;
++hllist->count;
return new_node;
}
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
if (node == hllist->tail)
hllist->tail = new_node;
new_node->next = node->next;
node->next = new_node;
++hllist->count;
return new_node;
}
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
if (node == hllist->tail)
hllist->tail = new_node;
new_node->next = node->next;
node->next = new_node;
++hllist->count;
return new_node;
}
void remove_next(HLLIST hllist, NODE *node)
{
NODE *next_node;
if (node == hllist->tail)
return;
if (node->next == hllist->tail)
hllist->tail = node;
next_node = node->next;
node->next = next_node->next;
--hllist->count;
free(next_node);
}
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *))
{
bool retval = true;
bool def_flag = false;
if (proc == NULL) {
proc = disp;
def_flag = true;
}
for (NODE *node = hllist->head; node != NULL; node = node->next)
if (!proc(&node->val)) {
retval = false;
break;
}
if (def_flag)
putchar('\n');
return retval;
}
DATATYPE *getp_item(HLLIST hllist, size_t index)
{
NODE *node;
if (index >= hllist->count)
return NULL;
node = hllist->head;
for (size_t i = 0; i < index; ++i)
node = node->next;
return &node->val;
}
void clear_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head;
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
hllist->head = hllist->tail = NULL;
hllist->count = 0;
}
void destroy_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head;
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
free(hllist);
}
static bool disp(DATATYPE *val)
{
printf("%d ", *val);
fflush(stdout);
return true;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
int main(void)
{
HLLIST hllist;
NODE *node, *pos_node;
int *val;
if ((hllist = create_llist()) == NULL) {
fprintf(stderr, "cannot create linked list...\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 10; ++i) {
if ((node = add_tail(hllist, i)) == NULL) {
fprintf(stderr, "cannot add item!..\n");
exit(EXIT_FAILURE);
}
if (i == 8)
pos_node = node;
}
walk_llist(hllist, NULL);
if ((val = getp_item(hllist, 5)) == NULL) {
fprintf(stderr, "invalid index!..\n");
exit(EXIT_FAILURE);
}
printf("%d\n", *val);
remove_next(hllist, pos_node);
walk_llist(hllist, NULL);
clear_llist(hllist);
for (int i = 0; i < 10; ++i)
if ((node = add_tail(hllist, i)) == NULL) {
fprintf(stderr, "cannot add item!..\n");
exit(EXIT_FAILURE);
}
walk_llist(hllist, NULL);
destroy_llist(hllist);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Tek bağlı listelerde de handle alanı içerisinde bir düğüm tutulursa özel durumlar elimine edilebilir. Ancak bunun sağlayacağı fayda çift
bağlı listelerdeki gibi olmayacaktır. Çünkü tek bağlı listelerde düğüm içerisinde yalnızca next göstericisi bulunduğu için tail göstericisinin
ayrıca handle alanında tutulması gerekecektir. Burada da son düğümün next göstericisinin artık NULL adresi değil handle alanındaki düğümü
göstermesi gerekir. Tabii işin başında hem handle alanındaki düğümün next göstericisi hem de tail göstericisi handle alanındaki düğümün
kendisini göstermlidir. Bu tasarımda handla alanı aşağıdaki gibi oluşturuabilir:
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagLLIST {
NODE head;
NODE *tail;
size_t count;
} LLIST, *HLLIST;
Yapının head elemanının bir gösterici olmadığına NODE nesnesi olduğuna dikkat ediniz.
Aşağıda bu tasarıma ilişkin bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* llist.h */
#ifndef LLIST_H_
#define LLIST_H_
#include <stddef.h>
#include <stdbool.h>
/* Type Declarations */
typedef int DATATYPE;
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagLLIST {
NODE head;
NODE *tail;
size_t count;
} LLIST, *HLLIST;
/* Function Prototypes */
HLLIST create_llist(void);
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val);
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val);
NODE *add_tail(HLLIST hllist, DATATYPE val);
NODE *addp_tail(HLLIST hllist, const DATATYPE *val);
NODE *add_head(HLLIST hllist, DATATYPE val);
NODE *addp_head(HLLIST hllist, const DATATYPE *val);
void remove_next(HLLIST hllist, NODE *node);
void remove_head(HLLIST hllist);
DATATYPE *getp_item(HLLIST hllist, size_t index);
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *));
void clear_llist(HLLIST hllist);
void destroy_llist(HLLIST hllist);
/* inline Function Definitions */
static inline size_t count_llist(HLLIST hllist)
{
return hllist->count;
}
#endif
/* llist.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
/* static Functions Prototypes */
static bool disp(DATATYPE *val);
/* Function Definitions */
HLLIST create_llist(void)
{
HLLIST hllist;
if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL)
return NULL;
hllist->head.next = &hllist->head;
hllist->tail = &hllist->head;
hllist->count = 0;
return hllist;
}
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
if (node == hllist->tail)
hllist->tail = new_node;
new_node->next = node->next;
node->next = new_node;
++hllist->count;
return new_node;
}
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
if (node == hllist->tail)
hllist->tail = new_node;
new_node->next = node->next;
node->next = new_node;
++hllist->count;
return new_node;
}
NODE *add_tail(HLLIST hllist, DATATYPE val)
{
return insert_next(hllist, hllist->tail, val);
}
NODE *addp_tail(HLLIST hllist, const DATATYPE *val)
{
return insertp_next(hllist, hllist->tail, val);
}
NODE *add_head(HLLIST hllist, DATATYPE val)
{
return insert_next(hllist, &hllist->head, val);
}
NODE *addp_head(HLLIST hllist, const DATATYPE *val)
{
return insertp_next(hllist, &hllist->head, val);
}
void remove_next(HLLIST hllist, NODE *node)
{
NODE *next_node;
if (node == hllist->tail)
return;
if (node->next == hllist->tail)
hllist->tail = node;
next_node = node->next;
node->next = next_node->next;
--hllist->count;
free(next_node);
}
void remove_head(HLLIST hllist)
{
remove_next(hllist, &hllist->head);
}
DATATYPE *getp_item(HLLIST hllist, size_t index)
{
NODE *node;
if (index >= hllist->count)
return NULL;
node = hllist->head.next;
for (size_t i = 0; i < index; ++i)
node = node->next;
return &node->val;
}
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *))
{
bool retval = true;
bool def_flag = false;
if (proc == NULL) {
proc = disp;
def_flag = true;
}
for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next)
if (!proc(&node->val)) {
retval = false;
break;
}
if (def_flag)
putchar('\n');
return retval;
}
void clear_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head.next;
while (node != &hllist->head) {
temp_node = node->next;
free(node);
node = temp_node;
}
hllist->head.next = &hllist->head;
hllist->tail = &hllist->head;
hllist->count = 0;
}
void destroy_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head.next;
while (node != &hllist->head) {
temp_node = node->next;
free(node);
node = temp_node;
}
free(hllist);
}
static bool disp(DATATYPE *val)
{
printf("%d ", *val);
fflush(stdout);
return true;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
int main(void)
{
HLLIST hllist;
NODE *node, *pos_node;
int *val;
if ((hllist = create_llist()) == NULL) {
fprintf(stderr, "cannot create linked list...\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 10; ++i) {
if ((node = add_tail(hllist, i)) == NULL) {
fprintf(stderr, "cannot add item!..\n");
exit(EXIT_FAILURE);
}
if (i == 5)
pos_node = node;
}
walk_llist(hllist, NULL);
if ((val = getp_item(hllist, 5)) == NULL) {
fprintf(stderr, "invalid index!..\n");
exit(EXIT_FAILURE);
}
printf("%d\n", *val);
remove_next(hllist, pos_node);
walk_llist(hllist, NULL);
remove_head(hllist);
walk_llist(hllist, NULL);
clear_llist(hllist);
for (int i = 0; i < 10; ++i)
if ((node = add_tail(hllist, i)) == NULL) {
fprintf(stderr, "cannot add item!..\n");
exit(EXIT_FAILURE);
}
walk_llist(hllist, NULL);
destroy_llist(hllist);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Tabii bağlı listenin tuttuğu elemanlar (yani DATATYPE türü) int yerine başka türlerden de olabilir. Eğer DATATYPE türü bir yapı ise
fonksiyonların p'li versiyonlarını kullanmak daha uygun olacaktır. Aşağıdaki tek bağlı liste örneğinde DATATYPE bir yapı biçiminde
oluşturulmuştur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* llist.h */
#ifndef LLIST_H_
#define LLIST_H_
#include <stddef.h>
#include <stdbool.h>
/* Type Declarations */
typedef struct tagPERSON {
char name[32];
int no;
} PERSON;
typedef PERSON DATATYPE;
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagLLIST {
NODE head;
NODE *tail;
size_t count;
} LLIST, *HLLIST;
/* Function Prototypes */
HLLIST create_llist(void);
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val);
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val);
NODE *add_tail(HLLIST hllist, DATATYPE val);
NODE *addp_tail(HLLIST hllist, const DATATYPE *val);
NODE *add_head(HLLIST hllist, DATATYPE val);
NODE *addp_head(HLLIST hllist, const DATATYPE *val);
void remove_next(HLLIST hllist, NODE *node);
void remove_head(HLLIST hllist);
DATATYPE *getp_item(HLLIST hllist, size_t index);
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *));
void clear_llist(HLLIST hllist);
void destroy_llist(HLLIST hllist);
/* inline Function Definitions */
static inline size_t count_llist(HLLIST hllist)
{
return hllist->count;
}
#endif
/* llist.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
/* static Functions Prototypes */
static bool disp(DATATYPE *node);
/* Function Definitions */
HLLIST create_llist(void)
{
HLLIST hllist;
if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL)
return NULL;
hllist->head.next = &hllist->head;
hllist->tail = &hllist->head;
hllist->count = 0;
return hllist;
}
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
if (node == hllist->tail)
hllist->tail = new_node;
new_node->next = node->next;
node->next = new_node;
++hllist->count;
return new_node;
}
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
if (node == hllist->tail)
hllist->tail = new_node;
new_node->next = node->next;
node->next = new_node;
++hllist->count;
return new_node;
}
NODE *add_tail(HLLIST hllist, DATATYPE val)
{
return insert_next(hllist, hllist->tail, val);
}
NODE *addp_tail(HLLIST hllist, const DATATYPE *val)
{
return insertp_next(hllist, hllist->tail, val);
}
NODE *add_head(HLLIST hllist, DATATYPE val)
{
return insert_next(hllist, &hllist->head, val);
}
NODE *addp_head(HLLIST hllist, const DATATYPE *val)
{
return insertp_next(hllist, &hllist->head, val);
}
void remove_next(HLLIST hllist, NODE *node)
{
NODE *next_node;
if (node == hllist->tail)
return;
if (node->next == hllist->tail)
hllist->tail = node;
next_node = node->next;
node->next = next_node->next;
--hllist->count;
free(next_node);
}
void remove_head(HLLIST hllist)
{
remove_next(hllist, &hllist->head);
}
DATATYPE *getp_item(HLLIST hllist, size_t index)
{
NODE *node;
if (index >= hllist->count)
return NULL;
node = hllist->head.next;
for (size_t i = 0; i < index; ++i)
node = node->next;
return &node->val;
}
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *))
{
bool retval = true;
bool def_flag = false;
if (proc == NULL) {
proc = disp;
def_flag = true;
}
for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next)
if (!proc(&node->val)) {
retval = false;
break;
}
if (def_flag)
putchar('\n');
return retval;
}
void clear_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head.next;
while (node != &hllist->head) {
temp_node = node->next;
free(node);
node = temp_node;
}
hllist->head.next = &hllist->head;
hllist->tail = &hllist->head;
hllist->count = 0;
}
void destroy_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head.next;
while (node != &hllist->head) {
temp_node = node->next;
free(node);
node = temp_node;
}
free(hllist);
}
static bool disp(DATATYPE *val)
{
printf("%s, %d\n", val->name, val->no);
fflush(stdout);
return true;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include "llist.h"
bool disp_person(PERSON *per);
void clear_stdin(void);
int main(void)
{
HLLIST hllist;
PERSON per;
char *str;
if ((hllist = create_llist()) == NULL) {
fprintf(stderr, "cannot create linked list...\n");
exit(EXIT_FAILURE);
}
for (;;) {
printf("Adi soyadi:");
if (fgets(per.name, 32, stdin) == NULL)
continue;
if ((str = strchr(per.name, '\n')) != NULL)
*str = '\0';
if (!strcmp(per.name, "quit"))
break;
printf("No:");
scanf("%d", &per.no);
clear_stdin();
addp_tail(hllist, &per);
}
walk_llist(hllist, disp_person);
destroy_llist(hllist);
return 0;
}
bool disp_person(PERSON *per)
{
printf("%s, %d\n", per->name, per->no);
return true;
}
void clear_stdin(void)
{
int ch;
while ((ch = getchar()) != '\n' && ch != EOF)
;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Çok karşılaşılan diğer bir veri yapısı da "kuyruk (queue)" veri yapısıdır. Kuyruklar FIFO ve LIFO olmak üzere ikiye ayrılmaktadır. Ancak
kuyruk denildiğinde default olarak FIFO kuyruklar anlaşılmaktadır.
Kuyruk veri yapısında iki eylem vardır: Kuyruğua eleman yerleştirmek ve kuyruktan eleman almak. Kuyruğun arasına eleman insert etmenin bir
anlamı yoktur. Aradan eleman silmenin de bir anlamı yoktur. Kuyruklara yalnızca eleman yerleştirilip kuyruklardan yalnızca eleman alınmaktadır.
FIFO kuyruk sistemine eleman yerleştirileceği zaman eleman her zaman sona yerleştirilmektedir. Bu kuyruk sisteminden eleman alınacağı
zaman eleman baştan alınmaktadır.
FIFO kuyruk sistemleri gerçek hayatta çokça karşımıza çıkmaktadır. Örneğin gerçek hayattaki kuyrukların çoğu bir FIFO kuyruk sistemidir.
Yemekhanede kuyruğun sonuna gireriz. Kuyruğun başındaki kişiye yemek verilir. Bu yönüyle FIFO kuyruk sistemi adil bir kuyruk sistemidir.
FIFO kuyruk sistemleri "bilgilerin sırası bozulmadan geçici olarak bekletileceği" durumlarda kullanılmaktadır. Yani tampon sistemleri
genellikle FIFO kuyruk sistemi biçiminde oluşturulmaktadır. Örneğin bir yerden bilgi geliyor olsun. Bu bilgiyi o anda işleyemiyor olalım.
O zaman gelen bilgiyi geçici süre bir FIFO kuyruk sisteminde tutabiliriz. Daha sonra oradan alarak geldiği sırada işleyebiliriz. Aslında
daha önce görmüş olduğumuz borularda bir FIFO kuyruk sistemi ile gerçekleştirilmektedir. Örneğin boruya br şey yazıldığında bu şeyler
borunun sonuna yazılır. Biz de boruyu okuduğumuzda başındakileri alırız.
Kuyruk sistemine eleman yerleştirmeye İngilizce genellikle "put" ya da "enqueue" işlemi denilmektedir. Kuyruktan eleman alamaya da İngilizce
genellikle "get" ya da "dequeue" işlemi denilmektedir.
Kuyruk sistemlerinin bir uzunlukları olabilir. Kuyruk dolduğunda artık kuyruğa put yapılamamaktadır. Benzer biçimde kuruk tamamen boşaldığında
artık "get" yapılamamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
62. Ders 03/02/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
FIFO kuyruk sistemlerinin gerçekleştirilmesi için üç yöntem kullanılmaktadır:
1) Dizi Kaydırması Yöntemi: Bu yöntem ilk akla gelen yöntemdir. Ancak diğer yöntemlerden bariz kötü olduğu için tercih edilmemektedir.
Bu yöntemde kuyruk için bir dizi yaratılır. Dizinin sonu (elemanların bittiği yer dizinin gerçek sonu değil) bir indeksle ya da gösterici
ile tutulur. Çrneğin:
abcdexxxxxxxxxxxxx
^
Burada a, b, c, d, e kuruktaki elemanları x'ler ise dizide henüz kullanılmayan boşi alanları belirtmektedir. Bu yöntemde kuyuğa eleman
ekleneceği zaman sona ekleme yapılır. Örneğin:
abcdefxxxxxxxxxxxx
^
Kuyruktan eleman alınacağı zaman eleman kuruğun başından alınır ve kuyruktaki elemanlar bir kaydırılır. Örneğin;
bcdefxxxxxxxxxxxx
^
Bu tasarımda kuyuğa eleman yerleştirmek O(1) karmaşıklıktayken kuyruktan eleman almak O(N) karmaşıklıktadır.
2) İndeks Kaydırması Yöntemi: En çok kullanılan yöntemlerden biridir. Bu yöntemde kuyruğun başı ve sonu iki indeks ya da gösterici ile
tutturulur. Kuruğun bgaşını gösteren indeks ya da göstericiye genellikle İngilizce "head" göstericisi, kuyrun sonunu gösteren indeks ya da
göstericiye de "tail" göstericisi denilmektedir. Örneğin:
xxxxabcdefgxxxxxx
^(h) ^(t)
Eleman tail göstericisinin gösterdeiği yere yerleştirilir ve tail göstericisi 1 artırılır. Ancak eğer tail göstericisi dizinin sonuna
gelmişse yeniden başa geçirilir. Örneğin:
xxxxabcdefghxxxxxxx
^(h) ^(t)
Eleman head göstericisinin gösteridği yerden alınır ve head göstericisi 1 artırılır. Tabii yine head göstericisi kuyruğun sonuna geldiğinde
yeniden başa geçirilir. Bu yöntemde head ve tail göstericileri aynı yeri gösteriyorsa ya kuyruk tamamen doludur ya da kuyruk tamamen boştur.
Genellikle kuyruktaki eleman sayısı da bir değişkenle tutulmaktadır. Kuyruğun tam dolu ya da tam boş olduğuna bu değişkene bakılarak karar
verilir. Bu yöntemde eleman ekleme de eleman alma da O(1) karmaşıklıkta yapılabilmektedir.
İndeks kaydırma yönteminde kuyruk dolduğunda tıpkı dinamik dizilerde olduğu gibi kuyruk büyütülebilir. Ancak kuyruğun büyütülüp büyütülmeyeceği
uygulamadan uygulamaya değişebilmektedir.
3) Bağlı Liste Yöntemi: Bu yöntemde bir bağlı liste oluşturulur. Eleman bağlı listenin sonuna eklenir ve başından alınır. Bağlı liste
tek bağlı liste biçiminde oluşturulabilir. Bu yöntemde sanki eleman ekleme ve alma O(1) karmaşıklıkta yapılıyormuş gibi gözükse de
eğer elemanlar dinamik olarak tahsis edilip free edeilecekse genellikle bu işlemler O(N) karmaşıklıkta yapılmaktadır. (Tabii O(1) karmaşıklıkta
çalışan tahsisat algoritmaları da vardır. Ancak Windows ve UNIX/Linux sistemlerindeki malloc v free algoritmaları O(N) karmaşıklıkta
gerçekleştirilmiştir. Bu yöntemin avantajı baştan kuyruk uzunluğunun belirli olmasına gerek kalmamasıdır.)
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağda indeks kaydırma yöntemiyle bir kuyruk sistemi oluşturulmuştur. Kuyruk bilgileri aşağdaki yapıyla temsil edilmiştir:
typedef struct tagQUEUE {
DATATYPE *queue;
size_t head;
size_t tail;
size_t size;
size_t count;
} QUEUE, *HQUEUE;
Handle alanı içerisinde kuyruk verilerinin bulunduğu dizi, head ve tail indeksleri, kuyruğun toplam uzunluğu ve kuyruktaki dolu elemanların
sayısının tutulduuna dikkat ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* queue.h */
#ifndef QUEUE_H_
#define QUEUE_H_
#include <stddef.h>
#include <stdbool.h>
typedef int DATATYPE;
typedef struct tagQUEUE {
DATATYPE *queue;
size_t head;
size_t tail;
size_t size;
size_t count;
} QUEUE, *HQUEUE;
/* Function Prototypes */
HQUEUE create_queue(size_t size);
bool put_queue(HQUEUE hqueue, DATATYPE val);
bool putp_queue(HQUEUE hqueue, const DATATYPE *val);
bool get_queue(HQUEUE hqueue, DATATYPE *val);
bool resize_queue(HQUEUE hqueue, size_t newsize);
void clear_queue(HQUEUE hqueue);
void destroy_queue(HQUEUE hqueue);
/* inline Function Definitions */
static inline bool isempty_queue(HQUEUE hqueue)
{
return hqueue->count == 0;
}
static inline bool count_queue(HQUEUE hqueue)
{
return hqueue->count;
}
#endif
/* queue.c */
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
HQUEUE create_queue(size_t size)
{
HQUEUE hqueue;
if ((hqueue = (HQUEUE)malloc(sizeof(QUEUE))) == NULL)
return NULL;
if ((hqueue->queue = (DATATYPE *)malloc(sizeof(DATATYPE) * size)) == NULL) {
free(hqueue);
return NULL;
}
hqueue->head = hqueue->tail = 0;
hqueue->size = size;
hqueue->count = 0;
return hqueue;
}
bool put_queue(HQUEUE hqueue, DATATYPE val)
{
if (hqueue->count == hqueue->size)
return false;
hqueue->queue[hqueue->tail++] = val;
hqueue->tail %= hqueue->size;
++hqueue->count;
return true;
}
bool putp_queue(HQUEUE hqueue, const DATATYPE *val)
{
if (hqueue->count == hqueue->size)
return false;
hqueue->queue[hqueue->tail++] = *val;
hqueue->tail %= hqueue->size;
++hqueue->count;
return true;
}
bool get_queue(HQUEUE hqueue, DATATYPE *val)
{
if (hqueue->count == 0)
return false;
*val = hqueue->queue[hqueue->head++];
hqueue->head %= hqueue->size;
--hqueue->count;
return true;
}
bool resize_queue(HQUEUE hqueue, size_t newsize)
{
DATATYPE *new_queue;
if (newsize <= hqueue->size)
return false;
if ((new_queue = (DATATYPE *)malloc(newsize * sizeof(DATATYPE))) == NULL)
return false;
for (size_t i = 0, head = hqueue->head; i < hqueue->size; ++i) {
new_queue[i] = hqueue->queue[head++];
head %= hqueue->size;
}
hqueue->head = 0;
hqueue->tail = hqueue->count;
hqueue->size = newsize;
free(hqueue->queue);
hqueue->queue = new_queue;
return true;
}
void clear_queue(HQUEUE hqueue)
{
hqueue->count = 0;
hqueue->head = 0;
hqueue->tail = 0;
}
void destroy_queue(HQUEUE hqueue)
{
free(hqueue->queue);
free(hqueue);
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
int main(void)
{
HQUEUE hqueue;
DATATYPE val;
if ((hqueue = create_queue(10)) == NULL) {
fprintf(stderr, "cannot create queue!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 8; ++i)
if (!put_queue(hqueue, i)) {
fprintf(stderr, "cannot put queue!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 5; ++i)
get_queue(hqueue, &val);
if (!resize_queue(hqueue, 20)) {
fprintf(stderr, "cannot resize queue!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 15; ++i)
if (!put_queue(hqueue, i)) {
fprintf(stderr, "cannot put queue!..\n");
exit(EXIT_FAILURE);
}
while (!isempty_queue(hqueue)) {
get_queue(hqueue, &val);
printf("%d ", val);
fflush(stdout);
}
destroy_queue(hqueue);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte FIFO kuyruk bağlı liste yöntemi gerçekleştirilmiştir. Bu örnekte handle alanı aşağıdaki gibi bir yapı ile temsil edilmiştir:
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagQUEUE {
NODE *head;
NODE *tail;
size_t count;
} QUEUE, *HQUEUE;
Handle alanı içerisinde tek bağlı listenin head ve tail düğümleri ve kuyruktaki eleman sayısı tutulmuştur. Örneğimizde kuyruğa eleman
yerleştirirken bağlı listenin sonuna elemanı ekledik. Kuyruktan eleman alınırken de bağlı listenin başından elemanı aldık.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* queue.h */
#ifndef QUEUE_H_
#define QUEUE_H_
#include <stddef.h>
#include <stdbool.h>
typedef int DATATYPE;
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagQUEUE {
NODE *head;
NODE *tail;
size_t count;
} QUEUE, *HQUEUE;
/* Function Prototypes */
HQUEUE create_queue(voids);
bool put_queue(HQUEUE hqueue, DATATYPE val);
bool putp_queue(HQUEUE hqueue, const DATATYPE *val);
bool get_queue(HQUEUE hqueue, DATATYPE *val);
void clear_queue(HQUEUE hqueue);
void destroy_queue(HQUEUE hqueue);
/* inline Function Definitions */
static inline bool isempty_queue(HQUEUE hqueue)
{
return hqueue->count == 0;
}
static inline bool count_queue(HQUEUE hqueue)
{
return hqueue->count;
}
#endif
/* queue.c */
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
HQUEUE create_queue(void)
{
HQUEUE hqueue;
if ((hqueue = (HQUEUE)malloc(sizeof(QUEUE))) == NULL)
return NULL;
hqueue->head = hqueue->tail = NULL;
hqueue->count = 0;
return hqueue;
}
bool put_queue(HQUEUE hqueue, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return false;
new_node->val = val;
new_node->next = NULL;
if (hqueue->tail != NULL)
hqueue->tail->next = new_node;
else
hqueue->head = new_node;
hqueue->tail = new_node;
++hqueue->count;
return true;
}
bool putp_queue(HQUEUE hqueue, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return false;
new_node->val = *val;
new_node->next = NULL;
if (hqueue->tail != NULL)
hqueue->tail->next = new_node;
else
hqueue->head = new_node;
hqueue->tail = new_node;
++hqueue->count;
return true;
}
bool get_queue(HQUEUE hqueue, DATATYPE *val)
{
NODE *node;
if (hqueue->head == NULL)
return false;
node = hqueue->head;
*val = node->val;
if (node->next == NULL)
hqueue->tail = NULL;
hqueue->head = node->next;
--hqueue->count;
free(node);
return true;
}
void clear_queue(HQUEUE hqueue)
{
NODE *node, *temp_node;
node = hqueue->head;
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
hqueue->head = hqueue->tail = NULL;
hqueue->count = 0;
}
void destroy_queue(HQUEUE hqueue)
{
NODE *node, *temp_node;
node = hqueue->head;
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
free(hqueue);
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
int main(void)
{
HQUEUE hqueue;
DATATYPE val;
if ((hqueue = create_queue(10)) == NULL) {
fprintf(stderr, "cannot create queue!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 8; ++i)
if (!put_queue(hqueue, i)) {
fprintf(stderr, "cannot put queue!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 15; ++i)
if (!put_queue(hqueue, i)) {
fprintf(stderr, "cannot put queue!..\n");
exit(EXIT_FAILURE);
}
while (!isempty_queue(hqueue)) {
get_queue(hqueue, &val);
printf("%d ", val);
fflush(stdout);
}
destroy_queue(hqueue);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
LIFO (Last In First Out) kuyruk sistemlerine "Stack Veri Yapısı" da denilmektedir. Stack veri yapısında yine iki eylem vardır. Stack'e
eleman eklemek ve stack'ten eleman almak. Geleneksel olarak stack'e eleman yerleştirmeye "push" işlemi, stack'ten eleman almaya da "pop"
işlemi denilmektedir. LIFO kuyruk sistemleri FIFO kuyruk sistemlerine göre daha seyrek kullanılmaktadır. Dış dünyada seyrek de olsa stack
sistemleriyle karşılaşılmaktadır. Örneğin:
- Tabaklar üst üste konulduğunda en üsttek ilk alınmaktadır.
- Asansöre son binen çoğu kez ilk inmektedir.
- Bazı oyun programlarında oyun kağıtları üst üste yere atıldığında oyuncular son atılan kağıdı yerden alabilmektedir.
Bilgisayar dünyasında da stack sistemleriyle karşılaşılmaktadır. Örneğin "undo mekanizması" stack veri yapısıyla gerçekleştirilmektedir.
Yani örneğin biz bir editörde birtakım şeyler yaptıktan sonra "Ctrl+Z" tuşlarına basarsak son yapılandna ilk yapılana doğru eylemler geri
alınmaktadır. Mikroişlemciler de stack sistemini fonksiyon çağrılarında ve yerel değişkenleri depolamada kullanmaktadır. Örneğin birkaç
fonksiyon peşi sıra çağrıldığında geri dönüşler son çağırmadan ilk çağırmaya doğru yapılmaktadır. Parsing algoritmalarında stack veri
yapısı yoğun olarak kullanılmaktadır. Örneğin RPN (Rverse Polish Notation) hesap makineleri stack veri yapısı kullanmaktadır. Stack bir bilgiyi
ters yüz etmek için kullanılabilir.
Stack veri yapısı yine "dizi yoluyla" ya da "bağlı liste yoluyla" gerçekleştirilebilmektedir. Dizi gerçekleştiriminde belli uzunlukta
bir dizi yaratılır. Stack'in aktif noktası bir göstericiyle (ya da indeksle) belirlenir. Stack'in aktif noktasını tutan bu göstericiye
geleneksel olarak "stack göstericisi (stack pointer)" denilmektedir. Başlangıçta stack boştur. Stack göstericisi dizinin soununu göstermektedir.
Örneğin:
x
x
x
x
x
SP -->
Push işleminde stack göstericisi önce bir azaltılır. Sonra push edilecek değer göstericisinin gösterdiği yere yerleştirilir. Örneğin
a değerini yukarıdaki stack'e yerleştirelim:
x
x
x
x
x
SP ---> a
Şimdi de b değerine stack'e push edelim:
x
x
x
x
SP ---> b
a
Şmdi de c değerini push edelim:
x
x
x
SP ---> c
b
a
Pop işleminde tam tersi yapılır. Yani Stack göstericisinin gösterdiği yerdne bilgi alınır ve stack göstericisi 1 ilerletilir. Örneğin yukarıdaki
durumda pop işlemi yapalım:
x
x
x
c
SP ---> b
a
Şimdi bir daha pop yapalım:
x
x
x
c
b
SP ---> a
Eğer stack uzunluğundan daha fazla psuh işlemi yapılırsa (yani stack doluyken push işlemi yapılırsa) stack için ayrılan alanı yukarından
taşırmış oluruz. Bu duruma geleneksel olarak "stack'in yukarıdan taşması (stack overflow)" denilmektedir. eğer çok fazla pop işlemi yaparsak
(yani boş bir stack'ta pop işlemi yaparsak) bu durumda stack için ayrılan diziyi aşağıdan taşırmış oluruz. Buna da geleneksel olarak
"stack underflow" denilmektedir.
Stack sistemleri tek bağlı listelerle de gerçekleştirilebilir. Bir bağlı listenin elemanlar önünne eklenip öününden alınırsa zaten
bu stack olur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda stack veri yapısının dizi kullanılarak gerçekleştirimine bir örnek verilmiştir. Bu örnekte stack veri yapısı (handle alanı) STACK
isimli bir yapı ile temsil edilmiştir:
typedef struct tagSTACK {
DATATYPE *stack;
DATATYPE *sp;
size_t size;
size_t count;
} STACK, *HSTACK;
Yapı içerisinde stack için kullanılacak dizinin başlangıç adresi, stack göstericinin durumu, stack dizinin uzunluğu ve stack'teki eleman
sayısı tutulmuştur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* stack.h */
#ifndef STACK_H_
#define STACK_H_
#include <stddef.h>
#include <stdbool.h>
typedef int DATATYPE;
typedef struct tagSTACK {
DATATYPE *stack;
DATATYPE *sp;
size_t size;
size_t count;
} STACK, *HSTACK;
/* Function Prototypes */
HSTACK create_stack(size_t size);
bool push_stack(HSTACK hstack, DATATYPE val);
bool pushp_stack(HSTACK hstack, const DATATYPE *val);
bool pop_stack(HSTACK hstack, DATATYPE *val);
void clear_stack(HSTACK hstack);
void destroy_stack(HSTACK hstack);
/* inline Function Definitions */
static inline size_t count_stack(HSTACK hstack)
{
return hstack->count;
}
static inline bool isempty_stack(HSTACK hstack)
{
return hstack->count == 0;
}
#endif
/* stack.c */
#include <stdio.h>
#include <stdlib.h>
#include "stack.h"
HSTACK create_stack(size_t size)
{
HSTACK hstack;
if ((hstack = (HSTACK)malloc(sizeof(STACK))) == NULL)
return NULL;
if ((hstack->stack = (DATATYPE *)malloc(sizeof(DATATYPE) * size)) == NULL) {
free(hstack);
return NULL;
}
hstack->sp = hstack->stack + size;
hstack->size = size;
hstack->count = 0;
return hstack;
}
bool push_stack(HSTACK hstack, DATATYPE val)
{
if (hstack->count >= hstack->size)
return false;
*--hstack->sp = val;
++hstack->count;
return true;
}
bool pushp_stack(HSTACK hstack,const DATATYPE *val)
{
if (hstack->count >= hstack->size)
return false;
*--hstack->sp = *val;
++hstack->count;
return true;
}
bool pop_stack(HSTACK hstack, DATATYPE *val)
{
if (hstack->count == 0)
return false;
*val = *hstack->sp++;
--hstack->count;
return true;
}
void clear_stack(HSTACK hstack)
{
hstack->sp = hstack->stack + hstack->size;
hstack->count = 0;
}
void destroy_stack(HSTACK hstack)
{
free(hstack->stack);
free(hstack);
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "stack.h"
int main(void)
{
HSTACK hstack;
DATATYPE val;
if ((hstack = create_stack(10)) == NULL) {
fprintf(stderr, "cannot create stack!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 10; ++i)
if (!push_stack(hstack, i)) {
fprintf(stderr, "cannot push stack!..\n");
exit(EXIT_FAILURE);
}
while (!isempty_stack(hstack)) {
pop_stack(hstack, &val);
printf("%d ", val);
fflush(stdout);
}
printf("\n");
destroy_stack(hstack);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda stack veri yapısının tek bağlı liste ile gerçekleştirime ilişkin bir örnek verilmiştir. Burada NODE yapısı ve handle alanını temsil
eden STACK yapısı aşağıdaki gibi bildirilmiştir:
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagSTACK {
NODE *head;
size_t count;
} STACK, *HSTACK;
Bu gerçekleştirimde düğüm bağlı listenin önüne eklenip önünden alınmaktadır. Handle alanında tail göstericisinin tutulmasına gerek olmadığına
dikkat ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* stack.h */
#ifndef STACK_H_
#define STACK_H_
#include <stddef.h>
#include <stdbool.h>
typedef int DATATYPE;
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagSTACK {
NODE *head;
size_t count;
} STACK, *HSTACK;
/* Function Prototypes */
HSTACK create_stack(void);
bool push_stack(HSTACK hstack, DATATYPE val);
bool pushp_stack(HSTACK hstack, const DATATYPE *val);
bool pop_stack(HSTACK hstack, DATATYPE *val);
void clear_stack(HSTACK hstack);
void destroy_stack(HSTACK hstack);
/* inline Function Definitions */
static inline size_t count_stack(HSTACK hstack)
{
return hstack->count;
}
static inline bool isempty_stack(HSTACK hstack)
{
return hstack->count == 0;
}
#endif
/* stack.c */
#include <stdio.h>
#include <stdlib.h>
#include "stack.h"
HSTACK create_stack(void)
{
HSTACK hstack;
if ((hstack = (HSTACK)malloc(sizeof(STACK))) == NULL)
return NULL;
hstack->head = NULL;
hstack->count = 0;
return hstack;
}
bool push_stack(HSTACK hstack, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return false;
new_node->val = val;
new_node->next = hstack->head;
hstack->head = new_node;
++hstack->count;
return true;
}
bool pushp_stack(HSTACK hstack,const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return false;
new_node->val = *val;
new_node->next = hstack->head;
hstack->head = new_node;
++hstack->count;
return true;
}
bool pop_stack(HSTACK hstack, DATATYPE *val)
{
NODE *node;
if (hstack->head == NULL)
return false;
node = hstack->head;
hstack->head = node->next;
*val = node->val;
free(node);
--hstack->count;
return true;
}
void clear_stack(HSTACK hstack)
{
NODE *node, *temp_node;
node = hstack->head;
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
hstack->head = NULL;
hstack->count = 0;
}
void destroy_stack(HSTACK hstack)
{
NODE *node, *temp_node;
node = hstack->head;
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
free(hstack);
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "stack.h"
int main(void)
{
HSTACK hstack;
DATATYPE val;
if ((hstack = create_stack()) == NULL) {
fprintf(stderr, "cannot create stack!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 10; ++i)
if (!push_stack(hstack, i)) {
fprintf(stderr, "cannot push stack!..\n");
exit(EXIT_FAILURE);
}
while (!isempty_stack(hstack)) {
pop_stack(hstack, &val);
printf("%d ", val);
fflush(stdout);
}
printf("\n");
for (int i = 0; i < 10; ++i)
if (!push_stack(hstack, i)) {
fprintf(stderr, "cannot push stack!..\n");
exit(EXIT_FAILURE);
}
while (!isempty_stack(hstack)) {
pop_stack(hstack, &val);
printf("%d ", val);
fflush(stdout);
}
printf("\n");
destroy_stack(hstack);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
64. Ders 10/02/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Diğer önemli bir veri yapısı da "çift yönlü dinamik diziler (double-ended queue)" denilen veri yapılarıdır. Çift yönlü dinamik diziler
normal dinamik dizilere benzemekle birlikte bunların başına ve sonuna eleman eklenmesi, baştan ve sondan eleman silinmesi O(1) karmaşıklıktadır.
(Halbuki normal dinamik dizilerde başa eleman eklenmesi ve baştaki elemanın silinmesi dizinin tümden kaydırılmasını gerektirdiği için
O(N) karmaşlıklıktadır.)
Çift yönlü dizmaik dizilere kısaca İngilizce "deque" de denilmektedir. Bu sözcük "deck" gibi okunmaktadır. Bazı yazarlar bu veri yapısının
kısa ismi için "dequeue" ismini kullanıyorsa da bu isim "kuyruktan veri almak" anlamına gelen sözcükle aynı olduğu karışıklığa yol açmaktadır.
Bu veri yaısının yaygın kısa ismi "deque" biçimindedir.
Çift yönlü dinamik dizilerde elemana erişim yine sabit karmaşıklıkta yapılmaktadır. Araya eleman insert edilmesi ve aradan eleman silinmesi
O(N) karmaşıklıktadır. O halde bu veri yapısının en önemli özelliği başa eleman eklemenin ve baştaki elemanı silmenin çok hızlı olmasıdır.
Çift yönlü dinamik diziler pek çok kütüphanede (örneğin C++'ın Standart Kütüphanesinde) "kuyruk" ve "stack" veri yapısının gerçekleştiriminde
temel bir veri yapısı olarak kullanılmaktadır. Örneğin C++ Standart Kütüphanesinde FIFO kuyruk sistemi "bir deque'in sonuna eleman insert
edip başındaki elemanı almak" biçiminde gerçekleştirilmiştir. Bnezer biçimde "stack" veri yapısı da "deque'in başına eleman yerleştirip
başından, eleman almakla gerçekleştirilmiştir.
Çift yönlü dinamik diziler tipik olarak üç biçimde gerçekleştirilmektedir. Bu gerçekleştirimlerin etkinliği birbirine yakındır. Özel durumlar
dikkat alınarak hangi gerçekleştirimin tercih edileceğine karar verilebilir.
İndeks Kaydırma Yöntemi ile Gerçekleştirim: Burada bir kuyruk sistemi oluşturulur. Yine kuyruğun başı ve sonu birer gösterici ya da indeks
ile tutulur. Sona ekleme tail göstericisinin gösteriği yere, başa ekleme head göstericisinin gösterdiği yere yapılır. Diğer işlemler kuyruk
sistemlerindeki gerçekleştirim ile benzerdir. Kuyruk dolduğunda yine normal dinamik dizilerde olduğu gibi iki kat artırım yoluna gidilir.
Başa ve sona eleman ekleme, baştaki ve sondaki elemanın silinmesi O(1) karmaşıklıkta gerçekleştirilebilir. Elemana O(1) karmaşıklıkta
erişilebilir.
Dinamik Dizi Yoluyla Gerçekleştirim: Bu gerçekleştirimde bir dinamik dizi oluşturulur. Başlangıçta head ve tail göstericileri bu dinamik
dizinin ortasında bir yeri gösterir. Sona eklemeler yine tail göstericisinin bulunduğu yere, başa eklemeler ise head göstericisinin bulunduğu
yere yapılır. Yani eklemelerde tail sağa doğru, head sola doğru ilerler. Baştan eleman silinmesinde ise head sağa doğru, sondan eleman
silinmesinde ise tail sola doğru ilerleyecektir. head ve tail göstericileri iki uçtan herhangi birine eriştiğinde dizi büyütülecektir.
Bu gerçekleştirimde kullanılmayan boş alanların bulunma olasılığı artmaktadır. Bazen çift yönlü dinamik dizi yalnızca bir yöne ilerleyebilir.
Yani sona ekleme ve baştan silme işlemleri yoğun olabilir. Bu durumda head ve tail göstericileri dizinin sonlarına yakın bölgelerde konumlanabilirler..
Ancak yeniden tahsisat yapıldığında bunlar yeni uygun konumlarına taşınabilirler.
Birden Fazla Dizinin Kullanılması Yöntemi: Bu gerçekleştirimde tek bir dizi değil belli uzunluklarda birden fazla dizi kullanılır. Örneğin
her biri 64 byte uzunluğunda dizilerin kullanıldığını varsayalım. Bir dizi yetmeyince yeni bir 64 byte'lık dizi tahsis edilecektir.
Tabii bu dizilerin adreslerinin de bir yerde saklanması gerekir. Tipik olarak bu dizilerin adresleri dinamik bir gösterici dizisinde
saklanmaktadır. Tabii bu durumda bu dinamik gösterici dizisinin kaydırılması gerekecektir. Ancak bu dizi çok bütük olmazsa bu kaydırma
önemli bir zamana yol açmayabilir. Bu gerçekleştirimde yine elemana erişim O(1) karmaşıklıkta tutulabilir. Başa ve sona eleman ekleme,
baştan ve sondan eleman silme yine O(1) karmaşıkıkta yapılabilir. Ancak eleman ekleme sırasında dizilerin adreslerini tutan dizide
kaydırmalar söz konusu olabilecektir. Bu nedenle eleman ekleme ve silme işlemi aslında tam O(1) karmaşıklıkta değil "amortized O(1)"
karmaşıklıktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
65. Ders 11/02/2024 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda bir deque gerçekleştirimine örnek verilmiştir. Bu gerçekleştirimde deque bir juruk sistemi gibi oluturulmuştur. Veri yaosının
bilgilerini tutan handle alanı şöyledir:
typedef int DATATYPE;
typedef struct tagDEQUE {
DATATYPE *deque;
size_t capacity;
size_t count;
size_t head;
size_t tail;
} DEQUE, *HDEQUE;
Bu gerçekleştirimde deque için başlangıçta DEQUE_DEF_CAPACITY (8) elemanlık yer ayrılmaktadır. Deque dolunca öncekinin iki katı uzunluğunda
yeni bir aalan tahsis edilmiş ve eski alandan yeni alana kopyalama yapılmıştır. Deque veri yapısında başa ve sona eleman eklemek, baştan
ve sondan eleman silmek O(1) karmaşıklıktadır. Ancak araya eleman insert etmek ya da aradan eleman silmek O(N) karmaşıklıktadır. Biz
aşağıdaki gerçekleştirimde araya eleman ekleme ve aradan eleman işlemini kuyruğun sonunu referans alarak kaydırma yoluyla yaptık.
Aslında insert ve remove pozisyonları hangi uca daha yakınsa kaydırma ona göre de yapılabilirdi.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* deque.h */
#ifndef DEQUE_H_
#define DEQUE_H_
#include <stddef.h>
#include <stdbool.h>
/* Symbolic Constants */
#define DEQUE_DEF_CAPACITY 8
#define DEQUE_FAILED ((size_t)-1)
#define MIN(a, b) ((a) < (b) ? (a) : (b))
/* Type Declerations */
typedef int DATATYPE;
typedef struct tagDEQUE {
DATATYPE *deque;
size_t capacity;
size_t count;
size_t head;
size_t tail;
} DEQUE, *HDEQUE;
/* Function Prototypes */
HDEQUE create_deque(void);
size_t add_back_deque(HDEQUE hdeque, DATATYPE val);
size_t add_backp_deque(HDEQUE hdeque, const DATATYPE *val);
size_t add_front_deque(HDEQUE hdeque, DATATYPE val);
size_t add_frontp_deque(HDEQUE hdeque, const DATATYPE *val);
DATATYPE at_deque(HDEQUE hdeque, size_t index);
void atp_deque(HDEQUE hdeque, size_t index, DATATYPE *val);
DATATYPE pop_front_deque(HDEQUE hdeque);
void pop_frontp_deque(HDEQUE hdeque, DATATYPE *val);
size_t insert_deque(HDEQUE hdeque, size_t index, DATATYPE val);
size_t remove_deque(HDEQUE hdeque, size_t index);
DATATYPE *set_capacity_deque(HDEQUE hdeque, size_t new_capacity);
void clear_deque(HDEQUE hdeque);
void destroy_deque(HDEQUE hdeque);
/* inline Function Definitions */
static inline size_t count_deque(HDEQUE hdeque)
{
return hdeque->count;
}
static inline size_t capacity_deque(HDEQUE hdeque)
{
return hdeque->capacity;
}
#endif
/* deque.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "deque.h"
HDEQUE create_deque(void)
{
HDEQUE hdeque;
if ((hdeque = (HDEQUE)malloc(sizeof(DEQUE))) == NULL)
return NULL;
if ((hdeque->deque = (DATATYPE *)malloc(DEQUE_DEF_CAPACITY * sizeof(DATATYPE))) == NULL) {
free(hdeque);
return NULL;
}
hdeque->count = 0;
hdeque->capacity = DEQUE_DEF_CAPACITY;
hdeque->head = hdeque->tail = 0;
return hdeque;
}
size_t add_back_deque(HDEQUE hdeque, DATATYPE val)
{
if (hdeque->count == hdeque->capacity && set_capacity_deque(hdeque, hdeque->capacity * 2) == NULL)
return DEQUE_FAILED;
hdeque->deque[hdeque->tail++] = val;
hdeque->tail %= hdeque->capacity;
++hdeque->count;
return hdeque->count - 1;
}
size_t add_backp_deque(HDEQUE hdeque, const DATATYPE *val)
{
if (hdeque->count == hdeque->capacity && set_capacity_deque(hdeque, hdeque->capacity * 2) == NULL)
return DEQUE_FAILED;
hdeque->deque[hdeque->tail++] = *val;
hdeque->tail %= hdeque->capacity;
++hdeque->count;
return hdeque->count - 1;
}
size_t add_front_deque(HDEQUE hdeque, DATATYPE val)
{
if (hdeque->count == hdeque->capacity && set_capacity_deque(hdeque, hdeque->capacity * 2) == NULL)
return DEQUE_FAILED;
if (hdeque->head == 0)
hdeque->head = hdeque->capacity - 1;
else
--hdeque->head;
hdeque->deque[hdeque->head] = val;
++hdeque->count;
return 0;
}
size_t add_frontp_deque(HDEQUE hdeque, const DATATYPE *val)
{
if (hdeque->count == hdeque->capacity && set_capacity_deque(hdeque, hdeque->capacity * 2) == NULL)
return DEQUE_FAILED;
if (hdeque->head == 0)
hdeque->head = hdeque->capacity - 1;
else
--hdeque->head;
hdeque->deque[hdeque->head] = *val;
++hdeque->count;
return 0;
}
DATATYPE *set_capacity_deque(HDEQUE hdeque, size_t new_capacity)
{
size_t size1, size2;
DATATYPE *new_deque;
if ((new_deque = (DATATYPE *)malloc(new_capacity * sizeof(DATATYPE))) == NULL)
return NULL;
size1 = MIN(hdeque->capacity - hdeque->head, hdeque->count);
size2 = hdeque->count - size1;
memcpy(new_deque, &hdeque->deque[hdeque->head], size1 * sizeof(DATATYPE));
if (size2 != 0)
memcpy(new_deque + size1, hdeque->deque, size2 * sizeof(DATATYPE));
free(hdeque->deque);
hdeque->deque = new_deque;
hdeque->capacity = new_capacity;
hdeque->head = 0;
hdeque->tail = hdeque->count;
return new_deque;
}
DATATYPE at_deque(HDEQUE hdeque, size_t index)
{
return hdeque->deque[(hdeque->head + index) % hdeque->capacity];
}
void atp_deque(HDEQUE hdeque, size_t index, DATATYPE *val)
{
*val = hdeque->deque[(hdeque->head + index) % hdeque->capacity];
}
DATATYPE pop_front_deque(HDEQUE hdeque)
{
size_t head;
head = hdeque->head++;
hdeque->head %= hdeque->capacity;
return hdeque->deque[head];
}
void pop_frontp_deque(HDEQUE hdeque, DATATYPE *val)
{
*val = hdeque->deque[hdeque->head++];
hdeque->head %= hdeque->capacity;
}
size_t insert_deque(HDEQUE hdeque, size_t index, DATATYPE val)
{
size_t k;
if (index > hdeque->count)
return DEQUE_FAILED;
if (hdeque->count == hdeque->capacity && set_capacity_deque(hdeque, hdeque->capacity * 2) == NULL)
return DEQUE_FAILED;
k = hdeque->tail;
for (size_t i = 0; i < hdeque->count - index; ++i) {
if (k == 0) {
k = hdeque->capacity - 1;
hdeque->deque[0] = hdeque->deque[k];
}
else {
hdeque->deque[k] = hdeque->deque[k - 1];
--k;
}
}
hdeque->deque[k] = val;
hdeque->tail = (hdeque->tail + 1) % hdeque->capacity;
++hdeque->count;
return index;
}
size_t remove_deque(HDEQUE hdeque, size_t index)
{
size_t k;
if (index >= hdeque->count)
return DEQUE_FAILED;
k = (hdeque->head + index) % hdeque->capacity;
for (size_t i = 0; i < hdeque->count - index - 1; ++i) {
if (k == hdeque->capacity - 1) {
hdeque->deque[k] = hdeque->deque[0];
k = 0;
}
else {
hdeque->deque[k] = hdeque->deque[k + 1];
++k;
}
}
hdeque->tail = (hdeque->tail + hdeque->capacity - 1) % hdeque->capacity;
--hdeque->count;
return index;
}
void clear_deque(HDEQUE hdeque)
{
hdeque->head = hdeque->tail = 0;
hdeque->count = 0;
}
void destroy_deque(HDEQUE hdeque)
{
free(hdeque->deque);
free(hdeque);
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "deque.h"
void disp_deque(HDEQUE hdeque);
int main(void)
{
HDEQUE hdeque;
if ((hdeque = create_deque()) == NULL) {
fprintf(stderr, "cannot create deque!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 10; ++i)
add_back_deque(hdeque, i);
disp_deque(hdeque);
remove_deque(hdeque, 1);
disp_deque(hdeque);
destroy_deque(hdeque);
return 0;
}
void disp_deque(HDEQUE hdeque)
{
DATATYPE val;
for (int i = 0; i < count_deque(hdeque); ++i) {
val = at_deque(hdeque, i);
printf("%d ", val);
fflush(stdout);
}
printf("\n");
printf("head: %zd, tail: %zd, count: %zd, capacity: %zd\n", hdeque->head, hdeque->tail, count_deque(hdeque), capacity_deque(hdeque));
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Algoritmalar ve veri yapıları konusunun en önemli alt konularından biri "arama (searching)" işlemleridir. Bir öğenin hızlı bir biçimde
bulunması bazı uygulamaların performasını önemli ölçüde etkileyebilmektedir.
Arama işlemleri doğrudan ana bellek (RAM) üzerinde ya da diskteki dosyalar üzerinde yapılabilir. Ana belek üzeirndeki aramalara "içsel aramalar
(internal searches)" işlemleri denilmektedir. Diskteki dosyalar üzerinde yapılan aramalara ise "dışsal arama (external searches)" işlemleri
denilmektedir. Örneğin bir dizideki elemanın aranması içsel aramaya örnektir. Ancak bir dosya üzerinde yapılan arama dışsal aramaya örnektir.
Veritabanı işlemlerini gerçekleştiren araçlar dışsal aramayı etkin bir biçimde yapmaya çalışırlar. Dışsal arama yöntemlerinde içsel arama
yöntemlerinden farklı algoritmalar kullanılabilmektedir. Biz bu kurusumuzda "içsel arama yöntemleri" üzerinde duracağız. Dışsal arama
yöntemleri "Sistem Programlama ve İleri C Uygulamarı II" kursunda ele alınmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Elimizde liste tarzı bir veri yapısı varsa ilk akla gelen arama yöntemi "sıralı arama (sequential search)" denilen yöntemdir. Eğer listenin
elemanları arasında hiçbir ilişki yoksa (yani elemanlar gelişi güzel biçimde sıralanmışsa) sıralı aramadan başka bir yol yoktur. Sıralı aramda
listenin başından itibaren her elemana ilgili bulunana kadar bakılır. Eğer aranacak eleman liste içerisinde varsa buna "başarılı arama successful
search" denilmektedir. Eğer aranacak eleman listede yoksa buna da "başrısız arama (unsuccessful search)" denilmektedir.
Başarılı sıralı aramada dizinin uzunluğu N olmak üzere ortalama (N + 1) / 2 karşılaştırma yapılmaktadır. Tabii en kötü durum senaryosunda
bu değer N olacaktır. Big O notasyonuna göre arama işlemi O(N) karmaşıklıktadır. O(N) karmaşıklık arama işlemleri için kötü bir karmaşıklıktır.
Örneğin 1000000 elamanın bulunduğu bir listede elemanın bulunması için ortalama 500000 karşılaştırma gerekmektedir. Ancak eleman sayısının çok
az olduğu listelerde (örneğin eleman sayısının 20'den az olduğu listelerde) en hızlı arama yöntemi yine de sıralı aramadır.
Aşağıda tipik bir sıralı arama işlemini yapan fonksiyon örneği verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
typedef int DATATYPE;
DATATYPE *linear_search(const DATATYPE *array, size_t size, DATATYPE val)
{
for (size_t i = 0; i < size; ++i)
if (array[i] == val)
return (DATATYPE *)&array[i];
return NULL;
}
int main(void)
{
DATATYPE array[10] = {4, 67, 34, 12, 45, 32, 98, 11, 9, 85};
DATATYPE *val;
if ((val = linear_search(array, 10, 98)) == NULL) {
fprintf(stderr, "cannot find!..\n");
exit(EXIT_FAILURE);
}
printf("%d\n", *val);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında Knuth "The Art of Computer Programming" kitabının "The Sorting and Searching" isimli üçüncü cildinde sıralı arama sırasında
her defasında "dizi bitti mi karşılaştırmasını" elimine etmek için bir yöntem önermiştir. Bu yönteme göre aranacak değer önce dizinin
sonuna yerleştirilir. Sonra gereksiz biçimde her yinelemede bu kontrol yapılmaz. Çünkü eleman en kötü olasılıkla dizinin sonuna gelindiğinde
bulunmuş olacaktır. Tabii bunun için dizinin bir fazl uzunlukta açılmış olması gerekir. Aşağıda bu yöntem uygulanmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
typedef int DATATYPE;
DATATYPE *linear_search(DATATYPE *array, size_t size, DATATYPE val)
{
size_t i;
array[size] = val;
for (i = 0;; ++i)
if (array[i] == val)
break;
return i != size ? &array[i] : NULL;
}
int main(void)
{
DATATYPE array[10 + 1] = {4, 67, 34, 12, 45, 32, 98, 11, 9, 85};
DATATYPE *val;
if ((val = linear_search(array, 10, 98)) == NULL) {
fprintf(stderr, "cannot find!..\n");
exit(EXIT_FAILURE);
}
printf("%d\n", *val);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Eğer aramanın yapılacağı liste sıraya diziliyse ve O(1) karmaşıklıktakli erişime izin veriyorsa (yani başka bir deyişle arama sıralı bir
dizi üzerinde yapılacaksa) bu durumda "ikili arama (binary search)" denilen yöntem tercih edilmelidir. İkili arama yönteminde aralık
sürekli bir biçimde ikiye bölünerek daraltılır. İkili aramanın en kötü durumdaki karşılaştırma sayısı log2 N kadardır. (Yani örneğin 1
milyon eleman için 20 karşılaştırma). Big O notasyonuna göre en kötü durumdaki algoritma karçalıklığı O(log N) biçimindedir.
Bir diziyi önce sıraya dizip onun üzerinde ikili arama uygulamak çoğu kez uygun bir yöntem değildir. Çünkü sıraya dizmenin maliyeti sıralı
aramadan daha yüksektir. En iyi sıraya dizme algoritmaları O(N log N) karmaşıklıktadır. Tabii eğer dizi güncellenmeyecekse ve çok sayıda
arama yaapılacaksa bir kez O(N log N) maliyeti karşılanıp diğer aramalar O(log N) karmaşıklıkta yürütülebilir.
İkili arama yapılırken tipik olarak left ve right biçiminde iki çubuk alınır. Bunun orta noktası bulunur. Orat noktasındaki dizi elemanı
aranacak elemanla karşılaştırılır. Eğer aranacak eleman orat noktadaki elemandan büyükse soldaki çubuk, küçükse sağdaki çubuk orta noktanın
yanına çekilir. Başarısız aramda sağ çubuk sol çubuğun soluna gelmektedir. Burada çubukların ora noktasının şöyle bulunduğuna dikkat ediniz:
mid = (left + right) / 2;
Bu işlem aslında aşağıdakiyle eşdeğerdir:
mid = left + (right - left) / 2;
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
typedef int DATATYPE;
DATATYPE *binary_search(const DATATYPE *array, size_t size, DATATYPE val)
{
size_t left, right, mid;
left = 0, right = size - 1;
while (left <= right) {
mid = (right + left) / 2;
if (val > array[mid])
left = mid + 1;
else if (val < array[mid])
right = mid - 1;
else
return (DATATYPE *)&array[mid];
}
return NULL;
}
int main(void)
{
DATATYPE array[10] = {4, 11, 18, 24, 38, 43, 52, 68, 74, 89};
DATATYPE *val;
if ((val = binary_search(array, 10, 100)) == NULL) {
fprintf(stderr, "cannot find!..\n");
exit(EXIT_FAILURE);
}
printf("%d\n", *val);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Eğer sıralı dizinin eleman sayısı bilinmiyorse (unbounded array) bu durumda önce elemanın bulunduğu bölge üstel bir biçimde (2 ile
çarpılarak) belirlenir. Sonra o bölgede ikili arama uygulanabilir. Bu yönteme literatürde "üstel arama(exponential serach)" de denilmektedir.
Yukarıda da belirttiğimiz gibi üstel arama üst sınıfın bilinmediği sıralı dizilerde özellikle uygulanmaktadır. Sınırın bilindiği durumlarda
doğrudan ikili aramaya geçilebilir. Örneğin:
1 3 6 9 11 17 23 28 36 41 48 54 61 67 72 78 82 86 90 92 .....
Böyle bir dizide bir sınır olmadığı için ikili armanın sağ çubuğunun yerini de eblirleyemeyiz. İşte onu belirleyebilmek için önce 1'den
başlatılan bir index sürekli iki ile çarpılarak ilerlenir. Örneğin:
right = 1;
while (array[right] < val && right < size)
right *= 2;
left = right / 2;
Burada artık döngüden çıkıldığında aranacak eleman left ile right arasındadır. Bu noktada klasik ikili arama uygulaanabilir. Örneğin
yukarıdaki örnek dizide aranacak eleman 78 olsun. Önce 1'inci indeksteki elemana bakılır (3). Sonra 2'inci indeksteki elemana bakılır (6)
Ondan sonra 4'üncü indeksteki sonra 8'inci (36) sonra 16'ıncı indeksteki (82) elemanlara bakılır. 16'ıncı indeksteki eleman artık aranan
eleman olan 78'den büyüktür. Döngü çıkılır ve dizinin 8'inci ve 16'ıncı indeksi arasında ikili arama uygulanır.
Aşağıda üstel aramaya bir örnek verilmiştir. Her ne kadar bu arama yöntemi aslında sınırı bilinmeyen bir dizi için tercih ediliyorsa da
sıralı normal dizilerde de kullanılabilir. (Normal sıralı ve sınırlı dizilerde bu algoritmanın kullanılması gerçek anlamda bir fayda
sağlamamaktadır.)
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
typedef int DATATYPE;
DATATYPE *binary_search(const DATATYPE *array, size_t size, DATATYPE val)
{
size_t left, right, mid;
left = 0, right = size - 1;
while (left <= right) {
mid = (right + left) / 2;
if (val > array[mid])
left = mid + 1;
else if (val < array[mid])
right = mid - 1;
else
return (DATATYPE *)&array[mid];
}
return NULL;
}
DATATYPE *exponential_search(const DATATYPE *array, size_t size, DATATYPE val)
{
size_t left, right;
if (array[0] == val)
return (DATATYPE *)&array[0];
right = 1;
while (right < size && val > array[right])
right *= 2;
if (right >= size)
right = size - 1;
left = right / 2;
return binary_search(array + left, right - left + 1, val);
}
int main(void)
{
DATATYPE array[] = {1, 3, 6, 9, 11, 17, 23, 28, 36, 41, 48, 54, 61, 67, 72, 78, 82, 86, 90, 92};
DATATYPE *val;
if ((val = exponential_search(array, sizeof(array) / sizeof(*array), 61)) == NULL) {
fprintf(stderr, "cannot find!..\n");
exit(EXIT_FAILURE);
}
printf("%d\n", *val);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Sıralı dizilerde arama yapmak için kullanılan diğer bir yöntem de "enterpolasyon araması (interpolation search)" denilen yöntemdir. Bu
yöntem ikili arama gibidir ancak aralık orta noktadan değil daha uygun yerden daraltılmaya çalışılır. Örneğin elimizde bir sözlük olsun.
Bu sözlükte y harfi ile başlayan biz sözcüğü aramak isteyelim. Aramayı sözlüğün ortasından mı yoksa sonlarına doğru bir noktadan mı
başlatırız? İşte ikili aramada arama orta noktalar temelinde yapılır. Ancak enterpolasyon aramasında arama orta nokta temelinde değil
aranack anahtarla oranlı bir biçimde yapılmaktadır. Bu yöntemde de yine left ve right biçiminde iki çubuk alınır. Bu yöntemin ikili
aramadan farkı orta noktalar yerine otantılı npktalara bakılmasıdır. Örneğin aranacak değer val olsun. Aranak ye şu orantıyla tespit edilir:
mid = left + ((right - left) /(array[right] - array[left]) * (val - array[left]))
İfadedeki şu kısma dikkat ediniz:
(right - left) / (array[right] - array[left])
Burada yapılmak istenen şey dizi elemanlarındaki bir birim artımın kaç indeks artırımına karşı geldiğinin tespit edilmesidir. Bu tespit
edildikten sonra bu değer (val - array[left]) değeri ile çarpılmıştır. Böylece orta nokta değil orantılı bir nokta elde edilmiştir.
Pekiyi bu yöntem ikili aramadan daha mı iyidir? Eğer dizideki elemanların arasındaki artırım miktarı n,speten stabil ise yani düzgün
bir artırım söz konusu ise bu yöntem ikili aramadan daha hızlı bir aramaya yol açar. Ancak uç değerlerin olduğu durumda bu yöntemin performansı
çok düşmektedir. Örneğin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1000000
Bu gibi durumlarda enterpolasyon araması O(N) karmaşıklığa kadar gerilemektedir. O halde sıralı dizinin elemanlarının genel artırımı
hakkında bir bilgi sahibi değilsek ikili aramayı tercih etmeliyiz. Ancak dizideki elemanlar nispeten birbirine yakın artırımlarla
ilerliyorsa ve dizide uç değerler yoksa bu ymntem daha iyi performans gösterebilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
typedef int DATATYPE;
DATATYPE *interpolation_search(const DATATYPE *array, size_t size, DATATYPE val)
{
size_t left, right, mid;
left = 0, right = size;
while (left <= right) {
mid = left + ((right - left) / (array[right] - array[left]) * (val - array[left]));
if (val > array[mid])
left = mid + 1;
else if (val < array[mid])
right = mid - 1;
else
return (DATATYPE *)&array[mid];
}
return NULL;
}
int main(void)
{
DATATYPE array[] = {1, 3, 6, 9, 11, 17, 23, 28, 36, 41, 48, 54, 61, 67, 72, 78, 82, 86, 90, 92};
DATATYPE *val;
if ((val = interpolation_search(array, sizeof(array) / sizeof(*array), 12)) == NULL) {
fprintf(stderr, "cannot find!..\n");
exit(EXIT_FAILURE);
}
printf("%d\n", *val);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aramalar aslında anahtara göre yapılıp aramanın sonucunda o anahtara ilişkin değer elde edilir. Biz yukarıdaki örneklerde yalnızca
anahtarların bulunduğu dizide anahtar aradık. Dolayısıyla yukarıdaki örneklerde elde edebileceğimiz tek bilgi "anahtarın dizide var olup
olmadığı" bilgisidir. Halbuki aramalarda önce anahtar-değer çiftleri veri yapısına yerleştirilir. Sonra anahtar verildiğinde onun değeri
elde edilir. Örneğin öğrencilerin bilgileri numaralarına göre aranabilir. Böylece aramayı yapacak kişi öğrencinin numarasını verir.
Arama sonucunda o numaralı öğrencinin bilgileri elde edilir.
Veri yapıları dünyasında "bir anahtar verildiğinde ona iliştirilmiş olan değerin elde edilmesini" sağlayan veri yapılarına "sözlük
(dictionary)" tarzı veri yapıları denilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
66. Ders 17/02/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Sözlük tarzı veri yapılarının en önemli özelliği anahtar verildiğinde değeri çok hızlı bir biçimde bulmasıdır. Anahtar ve değerlerin bir
dizide tutulması ve onların sıralı bir biçimde aranması çok yavaş bir yöntemdir. Bu tür durumlarda hızlı aramalar için "algoritmik arama
yöntemleri" kullanılmaktadır. Algoritmik arama yötemlerinin en çok kullanılanları "hash tabloları (hash tables)" ve "arama ağaçları (search
trees)" denilen yöntemlerdir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
İdeal bir anahtar-değer araması nasıl olabilir? Şüphesiz ideal durumda aramın O(1) karmaşıklıkta yapılması istenir. Anahtarın bir int
değer olduğunu ve kişinin numarasını belirttiğini düşünelim. Biz de numara verildiğinde o kişinin bilgilerini elde etmek isteyelim. Kişilerin
bilgilerini PERSON isimli bir yapıyla temesil edebiliriz:
struct PERSON {
....
};
Sonra da PERSON türünden büyük bir dizi açabiliriz:
struct PERSON people[MAX_SIZE];
Sonra da kişilerin numaralarını indeks yaparak bu diziye yerleştirebiliriz. Örneğin numarası 123 olan kişinin bilgileri diziye şöyle
yerleştirilebilir:
people[123] = person_info;
Numarası 123 olan kişinin bilgilerini O(1) karmaşıklıkta çok hızlı bir biçimde aşağıdaki gibi elde edebiliriz:
person_info = people[123];
Bu yöntem ilk bakışta çok iyi bir yöntem gibi gözükse de genellikle kullanılabilir bir yöntem değildir. Çünkü burada anahtar int türdendir.
Ancak anahtarlar farklı tür olabilir. Örneğin anahtar kişinin adı soyadı olabilir. Yazısal bir bilgi indeks belirtmemektedir. Bu yöntemin
diğer bir sakıncası örneğin kişi numaralarının yüksek basamaklardan oluştuğu durumda o kadar büyük bir dizinin açılması gerekliliğidir.
Örrneğin kişinin TC numarasına göre onun bilgilerinin elde edileceği durumda TC numarası 11 digit bir sayıdır. Yani skalası 100 milyar
sınırındadır. 100 milyarlık bir yapı dizisini bu amaçla oluşturmak ya mümkün değildir, mümkün olsa da etkin değildir. Bu yönteme "indeskli
arama index search" denilmektedir. Ancak çok özel durumlarda bu yöntem kullanılabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Algoritmik aramalarda en çok kullanılan yöntemlerden biri "hash tabloları (hash tables)" denilen yöntemdir. Hash tabloları aslında yukarıda
belirttiğimiz indeksli arama ile sıralı aramanın hibrit bir biçimidir. Yöntemde ismine "hash tablosu (hash table)" makul bir uzunlukta dizi
oluşturulur. Sonra anahtarlar ismine "hash fonksiyonu (hash function)" denilem bir fonksiyona sokularak dizi indeksine dönüştürülür. Sonra da
dizinin o indeksteki elemanına başvurulur. Örneğinkişinin bilgilerini TC numaralarına göre saklayıp geri almak isteyelim. Hash tablomuzun
uzunluğu da 1000 olsun. Hash fonksiyonunun "1000'e bölümden elde edilen kalan" değerini veren fonksiyon olduğunu varsayalım. Bu durumda
örneğin 2566198712 TC kimlik numarasına sahip kişinin bilgileri hash tablosunun 712'inci indeksteki elemanında saklanabilir. 72484926820
TC kimlik numarasına sahip kişinin belgileri de dizinin 820'inci indeksteki elemanında saklanacktır. Ancak farklı kişilerin TC numaraları
hash fonksiyonuna sokuldupunda aynı değer elde edilebilir. Örneğin 6238517712 TC numarasına sahip kişi de dizinin 712'inci indeskteki
elemanına yerleşmek isteyecektir. İşte tablosu yönteminde bu duruma "çakışma durumu (collison)" denilmektedir. Hash tablosu yöntemi
çakışma durumunda izlenecek stratejiye göre değişik varyasyonlara sahiptir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Hash tabloları yönteminde çakışma durumunda bu sorunu çözmek için iki ana yöntem grubu kullanılmaktadır: Ayrı zincir oluşturma yöntemi
(separate chaining) ve açık adresleme (open addresiing) yöntemi. Açık adresleme yöntemi de kendi aralarında "doğrusal yoklama (linear probing)",
"karesel yoklama (quadratic probing)", "çift hash'leme (double hasing)" gibi alternatif alt yöntemlere ayrılmaktadır. Ayrı zincir oluşturma
ve açık adresleme yöntemlerinin dışında başka çakışma çözümleme stratejileri de vardır. Ancak ağırlıklı olarak bu ikisi tercih edilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Ayrı zincir oluşturma yönteminde (separate chaining) hash tablosu aslında bir bağlıntı liste dizisi gibi oluşturulur. Yani hash tablosunun
her elemanı bağlı listenin ilk elemanını (head pointer) göstermektedir. Eklenecek anahtar hash fonksiyonuna sokulur ve bağlı listenin
hemen önüne (ya da duruma göre arkasına) eklenir. Eleman aranırken yine anahtar hash fonksiyonuna sokulur ve dizinin ilgili indeksindeki
bağlı listede sıralı arama yapılır.
Hash tablolarına eleman insert etmek O(1) karmaşıklıktadır. Tabii burada kullanılacak hash fonksiyonu da önemlidir. Küçük dönüler içeren
hash fonksiyonları O(1) karmaşıklığı yükseltmemektedir. elemanın silinmesi de benzer biçimdedir. Eleman aramanın O(1) karmaşıklıkta
olabilmesi için bağlı listelerdeki zincir uzunluklarının kısa olması gerekir. 10 kadar eleman için en hızlı arama yöntemi sıralı aramdır.
Bu koşulda sıralı aramanın O(1) karmaşıkta oludğu söylenebilir. O halde eğer zincirlerdeki oratalama eleman 10 civarında makul bir düzeyde
tutulursa arama işleminin de O(1) karmaşıklıkta yapılabileceği söylenebilir.
Sözlük tarzı veri yapılarında genel olarak aynı anahtara ilişkin birden fazla anahtar-değer çifti veri yapısına yerleştirilememektedir.
Bazı kütüphanelerde buna izin verilebilmektedir. Eğer aynı anahtara ilişkin yeni bir değer insert edilmeye çalışılırsa eski değer yeni
değerle yer değiştirmektedir. Yani başka bir deyişle anahtarın değeri değişitirilmektedir. Bazı tasarımlar ise aynı anahtara ilişkin
insert yapmayı engellemektedir. Yani bu tasarımlarda yalnızca olmayan elemanı insert edebiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi hash tablolarında kullanılacak iyi bir hash fonksiyonu nasıl olmalıdır? İyi bir hash fonksiyonunun "hızlı" olması gerekir. Çünkü
her türlü insert gibi arama gibi işlemlerde hash fonksiyonu kullanılacaktır. İyi bir hash fonksiyonunun "anahtarlar yanlı bile olsa" tabloya
onları iyi bir biçimde yaydırması gerekir. Örneğin aslında sayısal anahtarlar için "bölümden elde edilen kalan" iyi bir hash fonksiyonu
değildir. Hash tablolarında tablonun asal sayı uzunluğunda olması hash fonksiyonlarının daha iyi yaydırmasına yardımcı olmaktadır. (Örneğin
tablo uzunluğu için 100 yerine 101 değeri tercih edilmelidir.) Hash fonksiyonları "sayıyı indekse dönüştüren" ve "yazıyı indekse" dönüştüren
fonksiyonlar biçiminde oluşturulabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Hash tablolarının büyütülmesi önemli bir zaman kaybına yol açabilmektedir. Çünkü tablodaki tüm elemanların yeni tablo uzunluğuna göre
yeniden hash'lenip yeni tablodaki uygun slotlara yerleştirilmesi gerekmektedir. Pekiyi büyütme ne zaman yapılamalıdır? Tablodaki toplam
eleman sayısının tablo büyüklüğüne oranına "yükleme faktörü (load factor)" denilmektedir. Genellikle yükleme faktörü 0.75 gibi çok
küçük bir değerde tutulur. Ancak yukarıda da belirttiğimiz gibi zincirlerdeki ortalama eleman sayısı 10 civarında olduğunda hash
tabloları yine çok hızlı çalışmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda ayrı zincir oluşturma yöntemi (separate chaining) için bir örnek verilmiştir. Bu örnekte çift bağlı liste kullanılmıtır. Aslında
çift bağlı liste kullanılmasının bu örnekte bize bir faydası yoktur. Daha önceden de belirttiğimiz gibi çift bağlı listeler özellike düğüm
adresi verildiğinde hızlı silme yapmak için tercih edilmektedir. Örneğimizde düğüme dayalı silme yapılamamaktadır. Çünkü silinecek düğüm
eğer ilk düğümse tablonun güncellenmesi gerekir. Onun için de anahtarın bilinmesi gerekir. Tabii biz tabloda bağlı listelerin ilk düğümlerinin
adreslerini tutmak yerine doğrudan bir düğüm de tutabilirdik. Bu durumda düğüme dayalı silmeyi yapabilirdik.
Örneğimizde hash tablosunu eleman sayısı yükleme faktörüne eriştiğinde büyüttük. Default yükleme faktörünü 0.75 aldık. Yani örneğin
tablonun uzunluğu 100 olsun. Tablodaki eleman sayısı 75'e geldiğinde iki kat büyütme sağlanmaktadır. Tabii aslında yükleme faktörünü bu
kadar düşük tutmayabiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* htable.h */
#ifndef HTABLE_H_
#define HTABLE_H_
#include <stddef.h>
#include <stdbool.h>
/* Type Decalarations */
typedef struct tagPERSON {
char name[32];
int city;
int no;
} PERSON;
typedef struct tagNODE {
char key[32];
PERSON value;
struct tagNODE *next;
struct tagNODE *prev;
} NODE;
typedef struct tagHTABLE {
NODE **table;
size_t tsize;
size_t count;
double lf;
} HTABLE, *HHTABLE;
/* Function Prototypes */
HHTABLE create_lf_ht(size_t tsize, double lf);
NODE *insert_ht(HHTABLE hhtable, const char *key, const PERSON *value);
NODE *update_ht(HHTABLE hhtable, const char *key, const PERSON *value);
PERSON *find_ht(HHTABLE hhtable, const char *key);
bool remove_ht(HHTABLE hhtable, const char *key);
bool resize_ht(HHTABLE hhtable, size_t new_size);
void clear_ht(HHTABLE hhtable);
void destroy_ht(HHTABLE hhtable);
/* inline Function Definitions */
static inline size_t count_ht(HHTABLE hhtable)
{
return hhtable->count;
}
static inline size_t tsize_ht(HHTABLE hhtable)
{
return hhtable->tsize;
}
static inline HHTABLE create_ht(size_t tsize)
{
return create_lf_ht(tsize, 0.75);
}
#endif
/* htable.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "htable.h"
static size_t hash_func(const char *str, size_t tsize);
HHTABLE create_lf_ht(size_t tsize, double lf)
{
HHTABLE hhtable;
if ((hhtable = (HHTABLE)malloc(sizeof(HTABLE))) == NULL)
return NULL;
if ((hhtable->table = (NODE **)malloc(tsize * sizeof(NODE *))) == NULL) {
free(hhtable);
return NULL;
}
for (size_t i = 0; i < tsize; ++i)
hhtable->table[i] = NULL;
hhtable->tsize = tsize;
hhtable->count = 0 ;
hhtable->lf = lf;
return hhtable;
}
NODE *insert_ht(HHTABLE hhtable, const char *key, const PERSON *value)
{
NODE *new_node;
size_t hash;
hash = hash_func(key, hhtable->tsize);
for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next)
if (!strcmp(key, node->key))
return NULL;
if (((double)hhtable->count / hhtable->tsize) >= hhtable->lf)
if (!resize_ht(hhtable, hhtable->tsize * 2))
return NULL;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
strcpy(new_node->key, key);
new_node->value = *value;
new_node->next = hhtable->table[hash];
hhtable->table[hash] = new_node;
++hhtable->count;
return new_node;
}
NODE *update_ht(HHTABLE hhtable, const char *key, const PERSON *value)
{
NODE *new_node;
size_t hash;
hash = hash_func(key, hhtable->tsize);
for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next)
if (!strcmp(key, node->key)) {
node->value = *value;
return node;
}
if (((double)hhtable->count / hhtable->tsize) >= hhtable->lf)
if (!resize_ht(hhtable, hhtable->tsize * 2))
return NULL;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
strcpy(new_node->key, key);
new_node->value = *value;
new_node->next = hhtable->table[hash];
hhtable->table[hash] = new_node;
++hhtable->count;
return new_node;
}
PERSON *find_ht(HHTABLE hhtable, const char *key)
{
size_t hash;
hash = hash_func(key, hhtable->tsize);
for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next)
if (!strcmp(key, node->key))
return &node->value;
return NULL;
}
bool remove_ht(HHTABLE hhtable, const char *key)
{
size_t hash;
NODE *node, *prev_node;
hash = hash_func(key, hhtable->tsize);
prev_node = NULL;
node = hhtable->table[hash];
while (node != NULL) {
if (!strcmp(key, node->key)) {
if (hhtable->table[hash] == node)
hhtable->table[hash] = node->next;
else
prev_node->next = node->next;
free(node);
--hhtable->count;
return true;
}
prev_node = node;
node = node->next;
}
return false;
}
bool resize_ht(HHTABLE hhtable, size_t new_size)
{
NODE **new_table;
NODE *node, *temp_node;
size_t hash;
if (new_size <= hhtable->tsize)
return false;
if ((new_table = (NODE **)malloc(new_size * sizeof(NODE *))) == NULL)
return false;
for (size_t i = 0; i < new_size; ++i)
new_table[i] = NULL;
for (size_t i = 0; i < hhtable->tsize; ++i) {
node = hhtable->table[i];
while (node != NULL) {
temp_node = node->next;
hash = hash_func(node->key, new_size);
node->next = new_table[hash];
new_table[hash] = node;
node = temp_node;
}
}
free(hhtable->table);
hhtable->table = new_table;
hhtable->tsize = new_size;
return true;
}
void clear_ht(HHTABLE hhtable)
{
NODE *node, *temp_node;
for (size_t i = 0; i < hhtable->tsize; ++i) {
node = hhtable->table[i];
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
hhtable->table[i] = NULL;
}
hhtable->count = 0;
}
void destroy_ht(HHTABLE hhtable)
{
NODE *node, *temp_node;
for (size_t i = 0; i < hhtable->tsize; ++i) {
node = hhtable->table[i];
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
}
free(hhtable);
}
static size_t hash_func(const char *str, size_t tsize)
{
size_t hash = 0;
while (*str != '\0') {
hash = (13 * hash + *str) % tsize;
++str;
}
return hash;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "htable.h"
void get_random_record(char *key, PERSON *per);
int main(void)
{
HHTABLE hhtable;
char name[32];
PERSON per;
PERSON specific_per = {"Kaan Aslan", 34, 123456};
PERSON *retper;
srand((unsigned)time(NULL));
if ((hhtable = create_ht(10)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 1000; ++i) {
if (i == 500) {
if (insert_ht(hhtable, "Kaan Aslan", &specific_per) == NULL) {
fprintf(stderr, "cannot insert item!..\n");
exit(EXIT_FAILURE);
}
}
else {
get_random_record(name, &per);
if (insert_ht(hhtable, name, &per) == NULL) {
fprintf(stderr, "cannot insert item!..\n");
exit(EXIT_FAILURE);
}
}
}
if ((retper = find_ht(hhtable, "Kaan Aslan")) != NULL)
printf("Found: %s, %d, %d\n", retper->name, retper->city, retper->no);
else
printf("cannot find key!..\n");
strcpy(per.name, "Kaan Aslan");
per.city = 35;
per.no = 1000;
if (update_ht(hhtable, "Kaan Aslan", &per) == NULL)
printf("Update failed!..\n");
if ((retper = find_ht(hhtable, "Kaan Aslan")) != NULL)
printf("Found: %s, %d, %d\n", retper->name, retper->city, retper->no);
else
printf("cannot find key!..\n");
printf("count: %zd\n", count_ht(hhtable));
printf("tsize: %zd\n", tsize_ht(hhtable));
destroy_ht(hhtable);
return 0;
}
void get_random_record(char *key, PERSON *per)
{
int i;
for (i = 0; i < 31; ++i)
key[i] = per->name[i] = rand() % 26 + 'A';
per->name[i] = key[i] = '\0';
per->city = rand() % 81;
per->no = rand() % 1000000;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıdaki örneği tek bağlı liste kullanarak aşağıdaki gibi yeniden yazabiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* htable.h */
#ifndef HTABLE_H_
#define HTABLE_H_
#include <stddef.h>
#include <stdbool.h>
#define HT_DEF_LOAD_FACTOR 0.75
/* Type Decalarations */
typedef struct tagPERSON {
char name[32];
int city;
int no;
} PERSON;
typedef struct tagNODE {
char key[32];
PERSON value;
struct tagNODE *next;
} NODE;
typedef struct tagHTABLE {
NODE **table;
size_t tsize;
size_t count;
double lf;
} HTABLE, *HHTABLE;
/* Function Prototypes */
HHTABLE create_lf_ht(size_t tsize, double lf);
NODE *insert_ht(HHTABLE hhtable, const char *key, const PERSON *value);
NODE *update_ht(HHTABLE hhtable, const char *key, const PERSON *value);
PERSON *find_ht(HHTABLE hhtable, const char *key);
bool remove_ht(HHTABLE hhtable, const char *key);
bool resize_ht(HHTABLE hhtable, size_t new_size);
void clear_ht(HHTABLE hhtable);
void destroy_ht(HHTABLE hhtable);
/* inline Function Definitions */
static inline size_t count_ht(HHTABLE hhtable)
{
return hhtable->count;
}
static inline size_t tsize_ht(HHTABLE hhtable)
{
return hhtable->tsize;
}
static inline HHTABLE create_ht(size_t tsize)
{
return create_lf_ht(tsize, HT_DEF_LOAD_FACTOR);
}
#endif
/* htable.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "htable.h"
static size_t hash_func(const char *str, size_t tsize);
HHTABLE create_lf_ht(size_t tsize, double lf)
{
HHTABLE hhtable;
if ((hhtable = (HHTABLE)malloc(sizeof(HTABLE))) == NULL)
return NULL;
if ((hhtable->table = (NODE **)malloc(tsize * sizeof(NODE *))) == NULL) {
free(hhtable);
return NULL;
}
for (size_t i = 0; i < tsize; ++i)
hhtable->table[i] = NULL;
hhtable->tsize = tsize;
hhtable->count = 0 ;
hhtable->lf = lf;
return hhtable;
}
NODE *insert_ht(HHTABLE hhtable, const char *key, const PERSON *value)
{
NODE *new_node;
size_t hash;
hash = hash_func(key, hhtable->tsize);
for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next)
if (!strcmp(key, node->key))
return NULL;
if ((hhtable->count / hhtable->tsize) >= hhtable->lf)
if (!resize_ht(hhtable, hhtable->tsize * 2))
return NULL;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
strcpy(new_node->key, key);
new_node->value = *value;
new_node->next = hhtable->table[hash];
hhtable->table[hash] = new_node;
++hhtable->count;
return new_node;
}
NODE *update_ht(HHTABLE hhtable, const char *key, const PERSON *value)
{
NODE *new_node;
size_t hash;
hash = hash_func(key, hhtable->tsize);
for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next)
if (!strcmp(key, node->key)) {
node->value = *value;
return node;
}
if ((hhtable->count / hhtable->tsize) >= hhtable->lf)
if (!resize_ht(hhtable, hhtable->tsize * 2))
return NULL;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
strcpy(new_node->key, key);
new_node->value = *value;
new_node->next = hhtable->table[hash];
hhtable->table[hash] = new_node;
++hhtable->count;
return new_node;
}
PERSON *find_ht(HHTABLE hhtable, const char *key)
{
size_t hash;
hash = hash_func(key, hhtable->tsize);
for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next)
if (!strcmp(key, node->key))
return &node->value;
return NULL;
}
bool remove_ht(HHTABLE hhtable, const char *key)
{
size_t hash;
NODE *node, *prev_node;
hash = hash_func(key, hhtable->tsize);
prev_node = NULL;
node = hhtable->table[hash];
while (node != NULL) {
if (!strcmp(key, node->key)) {
if (hhtable->table[hash] == node)
hhtable->table[hash] = node->next;
else
prev_node->next = node->next;
free(node);
--hhtable->count;
return true;
}
prev_node = node;
node = node->next;
}
return false;
}
bool resize_ht(HHTABLE hhtable, size_t new_size)
{
NODE **new_table;
NODE *node, *temp_node;
size_t hash;
if (new_size <= hhtable->tsize)
return false;
if ((new_table = (NODE **)malloc(new_size * sizeof(NODE *))) == NULL)
return false;
for (size_t i = 0; i < new_size; ++i)
new_table[i] = NULL;
for (size_t i = 0; i < hhtable->tsize; ++i) {
node = hhtable->table[i];
while (node != NULL) {
temp_node = node->next;
hash = hash_func(node->key, new_size);
node->next = new_table[hash];
new_table[hash] = node;
node = temp_node;
}
}
free(hhtable->table);
hhtable->table = new_table;
hhtable->tsize = new_size;
return true;
}
void clear_ht(HHTABLE hhtable)
{
NODE *node, *temp_node;
for (size_t i = 0; i < hhtable->tsize; ++i) {
node = hhtable->table[i];
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
hhtable->table[i] = NULL;
}
hhtable->count = 0;
}
void destroy_ht(HHTABLE hhtable)
{
NODE *node, *temp_node;
for (size_t i = 0; i < hhtable->tsize; ++i) {
node = hhtable->table[i];
while (node != NULL) {
temp_node = node->next;
free(node);
node = temp_node;
}
}
free(hhtable);
}
static size_t hash_func(const char *str, size_t tsize)
{
size_t hash = 0;
while (*str != '\0') {
hash = (13 * hash + *str) % tsize;
++str;
}
return hash;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "htable.h"
void get_random_record(char *key, PERSON *per);
int main(void)
{
HHTABLE hhtable;
char name[32];
PERSON per;
PERSON specific_per = {"Kaan Aslan", 34, 123456};
PERSON *retper;
srand((unsigned)time(NULL));
if ((hhtable = create_ht(10)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 1000; ++i) {
if (i == 500) {
if (insert_ht(hhtable, "Kaan Aslan", &specific_per) == NULL) {
fprintf(stderr, "cannot insert item!..\n");
exit(EXIT_FAILURE);
}
}
else {
get_random_record(name, &per);
if (insert_ht(hhtable, name, &per) == NULL) {
fprintf(stderr, "cannot insert item!..\n");
exit(EXIT_FAILURE);
}
}
}
if ((retper = find_ht(hhtable, "Kaan Aslan")) != NULL)
printf("Found: %s, %d, %d\n", retper->name, retper->city, retper->no);
else
printf("cannot find key!..\n");
strcpy(per.name, "Kaan Aslan");
per.city = 35;
per.no = 1000;
if (update_ht(hhtable, "Kaan Aslan", &per) == NULL)
printf("Update failed!..\n");
if ((retper = find_ht(hhtable, "Kaan Aslan")) != NULL)
printf("Found: %s, %d, %d\n", retper->name, retper->city, retper->no);
else
printf("cannot find key!..\n");
printf("count: %zd\n", count_ht(hhtable));
printf("tsize: %zd\n", tsize_ht(hhtable));
destroy_ht(hhtable);
return 0;
}
void get_random_record(char *key, PERSON *per)
{
int i;
for (i = 0; i < 31; ++i)
key[i] = per->name[i] = rand() % 26 + 'A';
per->name[i] = key[i] = '\0';
per->city = rand() % 81;
per->no = rand() % 1000000;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Anımsanacağı gibi hash tablolarının diğer bir gerçekleştirimi de "açık adresleme (open addressing)" denilen yöntem grubuydu. Açık adresleme
"yoklama (probing)" biçimine göre çeşitli alt yöntemlere ayrılıyordu. Açık adreslemenin en yaygın ve basit biçimi "doğrusal yoklama
(linear probing)" denilen biçimidir.
Doğrusal yoklama (linear probing) oldukça basit bir fikre dayanmaktadır. Bu yöntemde yine hash tablosu oluşturulur. Ancak hash tablosunda
bağlı listelerin adresleri tutulmaz bizzat değerlerin kendisi tutulur. Tabloya eleman ekleneceği zaman yine anahtardan bir hash değeri
elde edilir. Doğrudan değer tablonun hash ile elde edilen indeksine yerleştirilir. Başka bir anahtar aynı hash değerini verdiğinde (yani
çakışma durumu oluştuğunda) o indeksten itibaren boş yer bulunana kadar yan yana indekslere sırasıyla bakılır. Örneğin hash olarak 123
değerini elde etmiş olalım. Tablonun 123'üncü elemanın dolu olduğunu düşünelim. Bu durumda 124'üncü elemanına bakarız. O da doluysa 125'inci
elemanına bakarız. Ta ki boş bir indeks bulunana kadar. Değeri ilk boş indekse yerleştiririz. Tabii bu durumda nasıl başka bir değer
bizim indeksimize yerleşmişse biz de aslında başka bir değerin indeksine yerleşmiş oluruz. Ancak bizim yerleştiğimiz indeks için hash'e
sahip olan değer de bizim yaptığımız gibi ilk boş yer bulunana kadar ilerleyecektir. Bu yöntemde arama işlemi de benzer biçimde yapılmaktadır.
Yani aranacak elemanın hash değeri elde edilir. O indekse başvurulur. Değer o indekste değilse değer bulunana kadar ya da boş bir slot (bucket)
görülene kadar yan yana diğer indekslere bakılır.
Anahtara dayalı eleman silme de benzer biçimde yapılmaktadır. Ancak eleman silindiğinde ilgili slotun (bucket) boşaltılması arama
işlemlerinde sorunlara yol açabilecektir. Burada yöntemlerden biri silinen elemanın slotunu boş yapmayıp silinmenin özel bir değerler
belirtilmesidir. Örneğin her slot için bir status bayrağı tutulabilir. Bu status bayrağı ilgili slotun "dolu" olduğunu", "boş" olduğubnu
ya da "silinmiş" olduğunu belirtebilir. Böylece arama sırasında "silinmiş" slotlar görüldüğünde durulmaz. İlk boş slot görüldüğünde
durulur. Tabii silinmiş slotlara yeni elemanlar eklenebilir.
Aşağıda "linear probing" yöntemine bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* htable.h */
#ifndef HASHTABLE_
#define HASHTABLE_
#include <stddef.h>
#include <stdbool.h>
/* Symbolic Constants */
#define HT_DEF_LOAD_FACTOR 0.75
#define HT_STATUS_EMPTY 0
#define HT_STATUS_INUSE 1
#define HT_STATUS_DELETED 2
/* Type Declarations */
typedef struct tagPERSON {
char name[32];
int city;
int no;
} PERSON;
typedef struct tagBUCKET {
int status;
char key[32];
PERSON value;
} BUCKET;
typedef struct tagHTABLE {
BUCKET *ht;
size_t tsize;
size_t count;
double lf;
} HTABLE, *HHTABLE;
/* Function Prototypes */
HHTABLE create_lf_ht(size_t tsize, double lf);
bool insert_ht(HHTABLE hhtable, const char *key, const PERSON *value);
bool update_ht(HHTABLE hhtable, const char *key, const PERSON *value);
bool remove_ht(HHTABLE hhtable, const char *key);
void clear_ht(HHTABLE hhtable);
void destroy_ht(HHTABLE hhtable);
PERSON *find_ht(HHTABLE hhtable, const char *key);
bool resize_ht(HHTABLE hhtable, size_t new_size);
/* inline Function Definitions */
static inline size_t count_ht(HHTABLE hhtable)
{
return hhtable->count;
}
static inline size_t tsize_ht(HHTABLE hhtable)
{
return hhtable->tsize;
}
static inline HHTABLE create_ht(size_t tsize)
{
return create_lf_ht(tsize, HT_DEF_LOAD_FACTOR);
}
#endif
/* htable.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "hashtable.h"
static size_t hash_func(const char *str, size_t tsize);
HHTABLE create_lf_ht(size_t tsize, double lf)
{
HHTABLE hhtable;
if ((hhtable = (HHTABLE)malloc(sizeof(HTABLE))) == NULL)
return NULL;
if ((hhtable->ht = (BUCKET *)malloc(tsize * sizeof(BUCKET))) == NULL) {
free(hhtable);
return NULL;
}
for (size_t i = 0; i < tsize; ++i)
hhtable->ht[i].status = HT_STATUS_EMPTY;
hhtable->tsize = tsize;
hhtable->lf = lf;
hhtable->count = 0;
return hhtable;
}
bool insert_ht(HHTABLE hhtable, const char *key, const PERSON *value)
{
size_t index;
if (hhtable->count >= hhtable->tsize)
return false;
if (((double)hhtable->count / hhtable->tsize) >= hhtable->lf)
if (!resize_ht(hhtable, hhtable->tsize * 2))
return false;
index = hash_func(key, hhtable->tsize);
while (hhtable->ht[index].status != HT_STATUS_EMPTY) {
if (!strcmp(hhtable->ht[index].key, key))
return false;
index = (index + 1) % hhtable->tsize;
}
strcpy(hhtable->ht[index].key, key);
hhtable->ht[index].value = *value;
hhtable->ht[index].status = HT_STATUS_INUSE;
++hhtable->count;
return true;
}
bool update_ht(HHTABLE hhtable, const char *key, const PERSON *value)
{
size_t index;
if (hhtable->count >= hhtable->tsize)
return false;
if (((double)hhtable->count / hhtable->tsize) >= hhtable->lf)
if (!resize_ht(hhtable, hhtable->tsize * 2))
return false;
index = hash_func(key, hhtable->tsize);
while (hhtable->ht[index].status != HT_STATUS_EMPTY) {
if (!strcmp(hhtable->ht[index].key, key)) {
hhtable->ht[index].value = *value;
return true;
}
index = (index + 1) % hhtable->tsize;
}
strcpy(hhtable->ht[index].key, key);
hhtable->ht[index].value = *value;
hhtable->ht[index].status = HT_STATUS_INUSE;
++hhtable->count;
return true;
}
bool remove_ht(HHTABLE hhtable, const char *key)
{
size_t index, hash;
hash = index = hash_func(key, hhtable->tsize);
do {
if (hhtable->ht[index].status == HT_STATUS_EMPTY)
break;
if (hhtable->ht[index].status == HT_STATUS_INUSE && !strcmp(hhtable->ht[index].key, key)) {
hhtable->ht[index].status = HT_STATUS_DELETED;
--hhtable->count;
return true;
}
index = (index + 1) % hhtable->tsize;
} while (index != hash);
return false;
}
PERSON *find_ht(HHTABLE hhtable, const char *key)
{
size_t index, hash;
hash = index = hash_func(key, hhtable->tsize);
do {
if (hhtable->ht[index].status == HT_STATUS_EMPTY)
break;
if (hhtable->ht[index].status == HT_STATUS_INUSE && !strcmp(hhtable->ht[index].key, key))
return &hhtable->ht[index].value;
index = (index + 1) % hhtable->tsize;
} while (index != hash);
return NULL;
}
bool resize_ht(HHTABLE hhtable, size_t new_size)
{
BUCKET *new_ht;
size_t index;
if (new_size <= hhtable->tsize)
return false;
if ((new_ht = (BUCKET *)malloc(new_size * sizeof(BUCKET))) == NULL)
return false;
for (size_t i = 0; i < new_size; ++i)
new_ht[i].status = HT_STATUS_EMPTY;
for (size_t i = 0; i < hhtable->tsize; ++i) {
if (hhtable->ht[i].status == HT_STATUS_INUSE) {
index = hash_func(hhtable->ht[i].key, new_size);
while (new_ht[index].status != HT_STATUS_EMPTY)
index = (index + 1) % new_size;
new_ht[index] = hhtable->ht[i];
}
}
free(hhtable->ht);
hhtable->ht = new_ht;
hhtable->tsize = new_size;
return true;
}
void clear_ht(HHTABLE hhtable)
{
for (size_t i = 0; i < hhtable->tsize; ++i)
hhtable->ht[i].status = HT_STATUS_EMPTY;
hhtable->count = 0;
}
void destroy_ht(HHTABLE hhtable)
{
free(hhtable->ht);
free(hhtable);
}
static size_t hash_func(const char *str, size_t tsize)
{
size_t hash = 0;
while (*str != '\0') {
hash = (13 * hash + *str) % tsize;
++str;
}
return hash;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "hashtable.h"
void get_random_record(char *key, PERSON *per);
int main(void)
{
HHTABLE hhtable;
char name[32];
PERSON per, *pper;
PERSON specific_per = {"Kaan Aslan", 34, 123456};
srand((unsigned)time(NULL));
if ((hhtable = create_ht(10)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 1000; ++i) {
if (i == 500) {
if (!insert_ht(hhtable, "Kaan Aslan", &specific_per)) {
fprintf(stderr, "cannot insert item!..\n");
exit(EXIT_FAILURE);
}
}
else {
get_random_record(name, &per);
if (!insert_ht(hhtable, name, &per)) {
fprintf(stderr, "cannot insert item!..\n");
exit(EXIT_FAILURE);
}
}
}
if (remove_ht(hhtable, "Kaan Aslan"))
printf("Item removed...\n");
else
printf("cannot remove item!..\n");
if ((pper = find_ht(hhtable, "Kaan Aslan")) != NULL)
printf("Found: %s %d %d\n", pper->name, pper->city, pper->no);
else
printf("cannot find record!..\n");
destroy_ht(hhtable);
return 0;
}
void get_random_record(char *key, PERSON *per)
{
int i;
for (i = 0; i < 31; ++i)
key[i] = per->name[i] = rand() % 26 + 'A';
per->name[i] = key[i] = '\0';
per->city = rand() % 81;
per->no = rand() % 1000000;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında hash tablolarının oluşturulmasında kullanılan "açık adresleme (open addresing)" yönteminde "linear probing" dışında başka yoklama
yöntemleri de vardır. Örneğin "quadratic probing" yönteminde çakışma durumunda slotlara sırasıyla bakılmaz. İkinci derece bir polinom
yoluyla bakılır. Örneğin ekleme yapmaya çalışalım ve anahtara ilişkin hash değeri 20 olsun. 20 numaralı slot'tun dolu olduğunu düşünelim.
Biz bu yöntemde 20 + 1, 20 + 2, 20 + 9, 20 + 16 gibi karesel değerlerle yoklama yaparız.
Çift hash'leme (double hasing) yönteminde ise önce bir hash fonksiyonu ile indeks elde edilir. Eğer çakışma olursa atlanacak miktar
ikinci bir hash fonksiyonuna başvurularak belirlenir.
Yukarıdaki yöntemlerin en çok tercih edilenleri "ayrı zincir oluşturma "separate chaining" ve "açık adresleme linear probing" yöntemleridir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Biz tek bağlı listelerde bir önceki düğümün sonraki düğümü göstermesini sağladık. Çift bağlı listelerde de bir düğüm hem önceki hem de
sonraki düğümü gösteriyordu. Pekiyi bir düğüm birden fazla düğümü gösterirse bu nasıl veri yapısı olur? İşte düğümlerin birden fazla
düğümü göstermesi durumunda oluşan veri yapılarına "ağaç (tree)" ve "graf (graph)" denilmektedir. Biz önce ağaçları sonra grafları
inceleyeceğiz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Her ağacın bir kökü vardır. Kök kendisine gelinemeyen ancak kendisinden her düğüme erişilebilen özel bir düğümdir. Ağaçlarda kök düğümden
her düğüme gitmenin yalnızca tek bir yolu vardır. Eğer bir düğüme birden fazla yoldan gidilebiliyorsa bu tür veri yapılarına "ağaç" değil
"graf" denilmektedir. Ağaçlar konusunu iyi anlayabilmek için ağaç terminolojisini biliyor olmak gerekir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Ağaçlarda bir düğümden gidel sonraki düğümlere "o düğümün alt düğümleri (child nodes)" denilmektedir. Kök düğüm haricindeki her düğümün
tek bir "üst düğümü (parent node)" vardır. Zaten kök düğüm "üst düğümü olmayan dolayısıyla kendisine bir yerden gelinemeyen" tek düğümdür.
Ağacın en altında artık başka bir düğüme gidilemeyen düğümlere "yaprak (leaf)" düğümler denilmektedir. Bir ağaçta bir düğümlerin maksimum
sahip olabileceği alt düğüm sayısı belirlenmiş olabilir. Bunun n olduğunu varsayalım. Böyle ağaçlara "'li ağaç" denilmektedir. Örneğin
bir ağaçta her düğümün "en fazla düğümü" olabiliyorsa böyle ağaçlara "ikili ağaçlar (binary trees)", en fazla üç düğümü olabiliyorsa bunlara
"üçlü ağaçlar (ternary tree)", en fazla n düğümü olabiliyorsa bunlara "n'li ağaçlar (n'ary tree)" denilmektedir. Uygulamada en fazla
karşılaşılan ağaçlar ikili ağaçardır. İkili ağaçlarda bir düğümün sıfır tane alt düğümü olabilir. Zaten bu durumda bu düğüm bir yaprak
durumundadır. Bir tane alt düğümü olabilir. Ya da en fazla ki tane alt düğümü olabilir.
Ağaçlarda "her düğümün bir yükseliği (height)" vardır. Düğümün yüksekliği kök düğümden o düğüme gelirken kat edilen yol (edge) sayısı
ile belirlenir. Kök düğüm yüksekliği 0'dır. Ağacın en yüksek düğümünün yüksekliğine "ağacın yüksekliği" de denilmektedir.
Bir ağaçta bütün yaprak düğümlerin yükseklikleri arasındaki fark belli bir değerden büyük değilse (bu değer 1, 2, 3 gibi olabilir) bu
ağaçlara "dengelenmiş ağaçlar (balanced trees)" denilmektedir. Örneğin bu farklılık 1 olarak tespit edilmişse bu durum yaprakların
yükseklikleri arasında en fazla 1 fark olacağı anlamına gelmektedir.
Bir N'li ağaçta ağacın en alt kademe dışında tüm kademelerindeki düğümlerinin tam olarak N tane alt düğümü varsa ve ağacın tüm
yaprakları aynı yüksekliğe sahipse böyle ağaçlara "tam dolu olan ağaçlar (full trees)" denilmektedir.
Bir N'li ağaç en son kademe (en alt kademe) dışında tam dolu ise ve son kademedeki düğümler solda soldan sağa toplanmışsa böyle ağaçlara
"tam ağaçlar (complete tree)" denilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Uygulamada en fazla kullanılan ağaçlar ikili ağaçlardır. Yukarıda da belirttiğimiz gibi ikili ağaçlarda her düğümün en fazla iki alt
düğümü olabilmektedir. Bir ikili ağaca ilişkin düğüm aşağıdaki gibi bir yapıyla temsil edilebilir:
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *left;
struct tagNODE *right;
} NODE;
Burada val elemanı düğüme iliştirilen bilgiyi belirtmektedir. left ve right elemanları alt düğümlerin yerlerini tutmaktadır. Eğer düğümün
sol ya da sağ alt düğümü yoksa bu left ve right elemanları NULL adres içerebilir. Tabii ağacın kök düğümünün yerini de bir biçimde
tutmamız gerekir. Bu durumda ağacı temsil eden yapı da şöyle olabilir:
typedef struct tagBTREE {
NODE *root;
size_t count;
} BTREE, *HBTREE;
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Ağaçlar en çok "arama (search)" amaçlı kullanılmaktadır. Arama amacıyla kullanılan ağaçlara "arama ağaçları (search trees)" denilmektedir.
Arama ağaçları için en çok kullanılan ağaçlar ikili ağaçlardır. Bunlara "ikili arama ağaçları (binary search trees)" de denilmektedir.
Arama ağaçları oluşturulurken bilgiler düğümlerin içerisine yerleştirilir. Tabii düğümler belli bir kurala göre ağaca eklenir. Arama işlemi
bir anahtara göre yapılacağından arama ağaçlarındaki düğümlerin de "anahtar" ve "değer" çiftlerini tutaması gerekir. Örneğin:
typedef struct tagNODE {
KEY key;
VALUE value;
struct tagNODE *left;
struct tagNODE *right;
} NODE;
Burada key arama için kullanılan anahtarı value ise arama sonucunda elde edilecek olan değeri belirtmektedir. Örneğin anahtar bir kişinin
numarasını belirtiyor olabilir değer de o kişinin bilgilerini belirtiyor olabilir:
typedef struct tagNODE {
int key;
PERSON value;
struct tagNODE *left;
struct tagNODE *right;
} NODE;
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir ikili arama ağacında eleman eklenirken ekleme noktası "küçükler sola büyükler sağa" olacak biçimde yapılmaktadır. Yani kök düğümden
girilir. Eklenecek anahtar düğümdeki anahtarla karşılaştırılır. Eklenecek anahtar düğümdeki anahtardan küçükse sola doğru, büyükse sağa
doğru ilerlenir. Örneğin ağaca sırasıyla 40 60 25 16 52 8 değerleri eklenecek olsun. Ağaöç henüz boş olduğuna göre 40 değeri köke eklenir:
40
Sonra 60 değeri 40'dan büyük olduğu için 40'ın sağına eklenir:
40
60
Sonra 25 değeri 40'tan küçük olduğu için 40'ın soluna eklenir:
40
25 60
Sonra 16 değeri 40'tan küçük ve 25'ten küçük olduğu için 25'in soluna eklenir:
40
25 60
16
Sonra 52 değeri 40'tan büyük 60'tan küçük olduğuna göre 60'ın soluna eklenir:
40
25 60
16 52
8 değeri 40'tan küçük, 25'ten küçük ve 16'dan küçük olduğuna göre 16'nın soluna eklenir:
40
25 60
16 52
8
İkili ağaçlarda eleman arama benzer biçimde yapılmaktadır. Kök düğümden girilir aranan anahtar ile düğümdeki anahtar karşılaştırılır.
Eğer aranan anahtar düğümdeki anahtardan küçükse sola, büyükse sağa doğru ilerlenir. Yapraklara gelindiğinde yani daha fazla sola ya da
sağa gidilemediğinde arama sonlandırılır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
İkili arama ağacına eleman eklemenin en kötü durumdaki (worst case) karmaşıklığı O(N) biçimindedir. Dengelenmiş ağaçlarda ortalama
karmaşıklık O(log N) biçimindedir. Arama işlemleri de en kötü durumda O(N) dengelenmiş ağaçlarda O(log N) karmaşıklıktadır. Çünkü arama
sırasında aslında tıpkı "ikili aramada (binary search)" olduğu gibi sürekli iki bölme yaaparak ilerlenmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Ağaçların dolaşılması özyinelemeli bir biçimde yapılabilir. İkili ağaçlar dört biçimde dolaşılabilmektedir:
1) In-Order dolaşım
2) Pre-Order dolaşım
3) Post-Order dolaşım
4) Breadt-First Dolaşım
Buradaki "in", "pre" ve "post" sözcükleri alt düğümlere göre üst düğümün hangi sırada dolaşılacağını belirtmektedir. Bu dolaşımlar
soldan-sağa ya da sağdan sola yapılabilmektedir.
In-order soldan sağa dolaşım aşağıdaki temsili koddaki (pseudo code) gibi yapılmaktadır:
void inorder_walk_lr(Node *node)
{
if (node->left != NULL)
inorder_walk_lr(node->left);
printf(node->val);
if (node->right != NULL)
inorder_walk_lr(node->right);
}
In-order soldan sağa dolaşım ağacın anahtara göre küçükten büyüğe doğru dolaşımını sağlamaktadır. In-order soldan sağa dolaşımda önce
sol koldan ilerlenmeye çalışıldığına sonra ana düğümün ziyaret edildiğine, sonra da sağ koldan ilerlenmeye çalışıldığına dikkat ediniz.
In-order sağdan sola dolaşımda bunun tersi yapılmaktadır:
void inorder_walk_rl(Node *node)
{
if (node->right != NULL)
inorder_walk_rl(node->right);
printf(node->val);
if (node->left != NULL)
inorder_walk_rl(node->left);
}
In-order sağdan sola dolaşım anahtara göre büyükten küçüğe bir dolaşıma yol açmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Post-order dolaşımda önce alt düğümler sonra da ana düğün ziyaret edilmektedir. Bu dolaşım da soldan sağa ya da sağdan sola yapılabilmektedir.
Temsili kodu aşağıdaki gibidir:
void postorder_walk_lr(Node *node)
{
if (node->left != NULL)
postorder_walk_lr(node->left);
if (node->right != NULL)
postorder_walk_lr(node->right);
printf(node->val);
}
postoreder dolaşım sağdan sola da yapılabilir. Ancak genel olarak bu ikisi arasında programcıyı ilgilendirecek önemli bir farklılık yoktur.
Post-order dolaşım ikili ağaçtaki düğümleri free hale getirmek için gerekmektedir. Çünkü ikili ağaçtaki düğümleri free hale getirme işlemi
önce alt düğümlerin sonra ana düğümün free hale getirilmesiyle gerçekleştirilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pre-order dolaşımda ise önce ana düğüm ziyaret edilir. Sonra alt düğümler ziyaret edilir. Tabii bu da soldan sağa ya da sağdan sola
yapılabilmektedir. Ancak dolsdan sağa dolaşımla sağdan sola dolaşım arasında programcıyı ilgilendiren önemli bir farklılık yoktur.
Pre-order soldan sağa dolaşımın temsili kodu şöyledir:
void preorder_walk_lr(Node *node)
{
printf(node->val);
if (node->left != NULL)
preorder_walk_lr(node->left);
if (node->right != NULL)
preorder_walk_lr(node->right);
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
70. Ders 02/03/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Kademe Kademe breadth-first dolaşım denilmektedir. Örneğin:
1
2 3
4 5 6 7
8 9 10 11 12 13 14 15
Buradaki numaralar breadt-first dolaşımın nasıl yapıldığınııklamaktadır. Breadth-first dolaşım için bir kuyruk sistemine gereksinim
vardır. Algoritmada bir düğüm üzerinde işlem yapıldıktan sonra hemen onun alt düğümleri FIFO kuyruk sistemine atılır. İşlem yapılacak
düğümler kuyruk sisteminden çekilir. Dolaşımın temsili kodu şöyledir:
put(root);
while ((node = get()) != NULL) {
process(node);
if (node->left != NULL)
put(node->left);
if (node->right != NULL)
put(node->right);
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
İkili ağaçlarda en karmaşık işlemlerden biri belli bir düğümün silinmesidir. Bu işlemin karmaşık olmasının nedeni birden fazla özel duruma
sahip olmasındadır. İkili ağaçta düğüm silerken özel durumlar şunlar olabilir:
1) Silinecek düğüm bir yaprak düğüm ise tek yapılacak şey onun üst düğümünün ilgili göstericisini NULL yapmaktır.
2) Silinecek düğümün tek bir alt düğümü varsa işlem yine kolaydır. Silinecek düğümün var olan düğümü silinecek düğümün yerine taşınır.
3) Silincek düğümün iki alt düğümünün de olması durumunda işlem biraz karmaşık hale gelmektedir. Bu durumda silinecek düğüm yerine geçecek
düğüm "ya sol kolun en büyük düğümü" ya da "sağ kolun en küçük düğümü" olabilir. Fakat bu durumda ağaçta birkaç güncellemenin yapılması
gerekmektedir. Silinecek düğümünyerine geçecek olan düğümün süt düğümünün ve silinecek düğümün üst düğümünün gencellenmesi gerekmektedir.
4) Silinecek düğümün kök düğüm olması durumunda ağacın kökünü gösteren göstericinin de güncellenmesi gerekmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda ikili arama ağacının gerçekleştirilmesine ilişkin bir örneek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* binarytree. h */
#ifndef BINARYTREE_H_
#define BINARYTREE_H_
#include <stddef.h>
#include <stdbool.h>
/* Type Declararions */
typedef struct tagPERSON {
char name[32];
int city;
} PERSON;
typedef struct tagNODE {
int key;
PERSON value;
struct tagNODE *left;
struct tagNODE *right;
} NODE;
typedef struct tagQNODE {
struct tagQNODE *next;
NODE *node;
} QNODE;
typedef struct tagBTREE {
NODE *root;
size_t count;
QNODE *head;
QNODE *tail;
} BTREE, *HBTREE;
/* Function Prototypes */
HBTREE create_bt(void);
bool insert_item_bt(HBTREE hbtree, int key, const PERSON *value);
bool insert_item_alternative_bt(HBTREE hbtree, int key, const PERSON *value);
bool walk_inorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *));
bool walk_inorder_rl_bt(HBTREE hbtree, bool (*proc)(int, PERSON *));
bool walk_postorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *));
bool walk_preorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *));
bool walk_breadth_first_bt(HBTREE hbtree, bool (*proc)(int, PERSON *));
bool delete_bt(HBTREE hbtree, int key);
void clear_bt(HBTREE hbtree);
void destroy_bt(HBTREE hbtree);
/* inline Function Fefinitions */
static inline size_t count_bt(HBTREE hbtree)
{
return hbtree->count;
}
#endif
/* binarytree.c */
#include <stdio.h>
#include <stdlib.h>
#include "binarytree.h"
static bool walk_inorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *));
static bool walk_inorder_rl_recur(NODE *node, bool (*proc)(int, PERSON *));
static bool walk_postorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *));
static bool walk_preorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *));
static void clear_recur(NODE *node);
static void create_queue(HBTREE hbtree);
static NODE *put_queue(HBTREE hbtree, NODE *node);
static NODE *get_queue(HBTREE hbtree);
static void destroy_queue(HBTREE hbtree);
static void subtree_shift(HBTREE hbtree, NODE *node1, NODE *node2, NODE *node3);
HBTREE create_bt(void)
{
HBTREE hbtree;
if ((hbtree = (HBTREE)malloc(sizeof(BTREE))) == NULL)
return NULL;
hbtree->root = NULL;
hbtree->count = 0;
return hbtree;
}
bool insert_item_bt(HBTREE hbtree, int key, const PERSON *value)
{
NODE *new_node, *node, *parent_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return false;
new_node->key = key;
new_node->value = *value;
new_node->left = NULL;
new_node->right = NULL;
if (hbtree->root == NULL) {
hbtree->root = new_node;
++hbtree->count;
return true;
}
node = hbtree->root;
while (node != NULL) {
parent_node = node;
if (key < node->key)
node = node->left;
else if (key > node->key)
node = node->right;
else {
node->value = *value;
return true;
}
}
if (key < parent_node->key)
parent_node->left = new_node;
else
parent_node->right = new_node;
++hbtree->count;
return true;
}
bool insert_item_alternative_bt(HBTREE hbtree, int key, const PERSON *value)
{
NODE *new_node, *node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return false;
new_node->key = key;
new_node->value = *value;
new_node->left = NULL;
new_node->right = NULL;
if (hbtree->root == NULL) {
hbtree->root = new_node;
++hbtree->count;
return true;
}
node = hbtree->root;
while (node != NULL) {
if (key < node->key)
if (node->left == NULL) {
node->left = new_node;
break;
}
else
node = node->left;
else if (key > node->key)
if (node->right == NULL) {
node->right = new_node;
break;
}
else
node = node->right;
else {
node->value = *value;
return true;
}
}
++hbtree->count;
return true;
}
bool walk_inorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *))
{
if (hbtree->root != NULL)
return walk_inorder_lr_recur(hbtree->root, proc);
return true;
}
bool walk_inorder_rl_bt(HBTREE hbtree, bool (*proc)(int, PERSON *))
{
if (hbtree->root != NULL)
return walk_inorder_rl_recur(hbtree->root, proc);
return true;
}
bool walk_postorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *))
{
if (hbtree->root != NULL)
return walk_postorder_lr_recur(hbtree->root, proc);
return true;
}
bool walk_preorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *))
{
if (hbtree->root != NULL)
return walk_preorder_lr_recur(hbtree->root, proc);
return true;
}
void clear_bt(HBTREE hbtree)
{
if (hbtree->root != NULL) {
clear_recur(hbtree->root);
hbtree->root = NULL;
hbtree->count = 0;
}
}
void destroy_bt(HBTREE hbtree)
{
if (hbtree->root != NULL)
clear_recur(hbtree->root);
free(hbtree);
}
bool walk_breadth_first_bt(HBTREE hbtree, bool (*proc)(int, PERSON *))
{
NODE *node;
bool result = true;
create_queue(hbtree);
put_queue(hbtree, hbtree->root);
while ((node = get_queue(hbtree)) != NULL) {
if (!proc(node->key, &node->value)) {
result = false;
break;
}
if (node->left != NULL)
if (put_queue(hbtree, node->left) == NULL) {
fprintf(stderr, "put_queue cannot allocate memory!..\n");
result = false;
break;
}
if (node->right != NULL)
if (put_queue(hbtree, node->right) == NULL) {
result = false;
fprintf(stderr, "put_queue cannot allocate memory!..\n");
break;
}
}
destroy_queue(hbtree);
return result;
}
bool delete_bt(HBTREE hbtree, int key)
{
NODE *delete_node, *delete_parent_node, *replace_parent_node, *replace_node;
delete_node= hbtree->root;
delete_parent_node = NULL;
while (delete_node != NULL) {
if (delete_node->key == key)
break;
delete_parent_node = delete_node;
if (key < delete_node->key)
delete_node = delete_node->left;
else if (key > delete_node->key )
delete_node = delete_node->right;
}
if (delete_node == NULL)
return false;
if (delete_node->left == NULL)
subtree_shift(hbtree, delete_parent_node, delete_node, delete_node->right);
else if (delete_node->right == NULL)
subtree_shift(hbtree, delete_parent_node, delete_node, delete_node->left);
else {
replace_parent_node = delete_node;
replace_node = delete_node->right;
while (replace_node->left!= NULL) {
replace_parent_node = replace_node;
replace_node= replace_node->left;
}
if (replace_parent_node != delete_node) {
subtree_shift(hbtree, replace_parent_node, replace_node, replace_node->right);
replace_node->right= delete_node->right;
}
subtree_shift(hbtree, delete_parent_node, delete_node, replace_node);
replace_node->left = delete_node->left;
}
free(delete_node);
--hbtree->count;
return true;
}
static bool walk_inorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *))
{
if (node->left != NULL)
if (!walk_inorder_lr_recur(node->left, proc))
return false;
if(!proc(node->key, &node->value))
return false;
if (node->right != NULL)
if (!walk_inorder_lr_recur(node->right, proc))
return false;
return true;
}
static bool walk_inorder_rl_recur(NODE *node, bool (*proc)(int, PERSON *))
{
if (node->right != NULL)
if (!walk_inorder_rl_recur(node->right, proc))
return false;
if (!proc(node->key, &node->value))
return false;
if (node->left != NULL)
if (!walk_inorder_rl_recur(node->left, proc))
return false;
return true;
}
static bool walk_postorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *))
{
if (node->left != NULL)
if (!walk_postorder_lr_recur(node->left, proc))
return false;
if (node->right != NULL)
if (!walk_postorder_lr_recur(node->right, proc))
return false;
if (!proc(node->key, &node->value))
return false;
return true;
}
static bool walk_preorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *))
{
if (!proc(node->key, &node->value))
return false;
if (node->left != NULL)
if (!walk_preorder_lr_recur(node->left, proc))
return false;
if (node->right != NULL)
if (!walk_preorder_lr_recur(node->right, proc))
return false;
return true;
}
static void clear_recur(NODE *node)
{
if (node->left != NULL)
clear_recur(node->left);
if (node->right != NULL)
clear_recur(node->right);
free(node);
}
static void create_queue(HBTREE hbtree)
{
hbtree->head = NULL;
hbtree->tail = NULL;
}
static NODE *put_queue(HBTREE hbtree, NODE *node)
{
QNODE *new_qnode;
if ((new_qnode = (QNODE *)malloc(sizeof(QNODE))) == NULL)
return NULL;
new_qnode->node = node;
new_qnode->next = NULL;
if (hbtree->tail != NULL)
hbtree->tail->next = new_qnode;
else
hbtree->head = new_qnode;
hbtree->tail = new_qnode;
return node;
}
static NODE *get_queue(HBTREE hbtree)
{
QNODE *qnode;
NODE *rnode;
if (hbtree->head == NULL)
return NULL;
qnode = hbtree->head;
hbtree->head = qnode->next;
if (hbtree->head == NULL)
hbtree->tail = NULL;
rnode = qnode->node;
free(qnode);
return rnode;
}
static void destroy_queue(HBTREE hbtree)
{
QNODE *qnode, *temp_qnode;
qnode = hbtree->head;
while (qnode != NULL) {
temp_qnode = qnode->next;
free(qnode);
qnode = temp_qnode;
}
}
static void subtree_shift(HBTREE hbtree, NODE *node1, NODE *node2, NODE *node3)
{
if (node1 == NULL)
hbtree->root = node3;
else if (node1->left == node2)
node1->left = node3;
else
node1->right = node3;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "binarytree.h"
bool disp_node(int key, PERSON *value);
int main(void)
{
HBTREE hbtree;
PERSON per = {"Noname", 0};
int keys[] = {70, 50, 34, 56, 19, 80, 67, 43, 27, 76, 79, 105, 82, 65, 0};
if ((hbtree = create_bt()) == NULL) {
fprintf(stderr, "cannot create binary tree!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; keys[i] != 0; ++i)
if (!insert_item_alternative_bt(hbtree, keys[i], &per)) {
fprintf(stderr, "cannot insert item: %d\n", keys[i]);
exit(EXIT_FAILURE);
}
walk_inorder_lr_bt(hbtree, disp_node);
printf("\n------------------------------\n");
walk_inorder_rl_bt(hbtree, disp_node);
printf("\n------------------------------\n");
walk_postorder_lr_bt(hbtree, disp_node);
printf("\n------------------------------\n");
walk_inorder_lr_bt(hbtree, disp_node);
printf("\n------------------------------\n");
walk_preorder_lr_bt(hbtree, disp_node);
printf("\n------------------------------\n");
walk_breadth_first_bt(hbtree, disp_node);
printf("\n------------------------------\n");
if (!delete_bt(hbtree, 70)) {
fprintf(stderr, "cannot find item!...\n");
exit(EXIT_FAILURE);
}
walk_breadth_first_bt(hbtree, disp_node);
printf("\n------------------------------\n");
printf("total item: %jd\n", count_bt(hbtree));
destroy_bt(hbtree);
return 0;
}
bool disp_node(int key, PERSON *value)
{
printf("%d ", key);
fflush(stdout);
return true;;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Biz yukarıdaki örnekte "dengelenmemiş (unbalanced)" arama ağacı oluşturduk. Dengelenmemiş ağaçlar bir arama performansını düşürebilmektedir.
Dengelenmemiş ikili ağaçlarda eleman eklemenin ve aramanın "en kötü durum senaryosu (worst case)" O(N) karmaşıklığa kadar kötüleşmektedir.
Örneğin ağaca aşağıdaki gibi sıralı bilgilerin geldiğini düşünelim:
10, 20, 30, 40, 50, 60, 70, ...
Bu durumda ikili arama ağacının bağlı listeden bir farkı kalmamaktadır. Bu nedenle uygulamalarda "dengelenmiş (balanced)" ağaçlar tercih
edilmektedir. Dengelenmiş ağaçlarda ağaca her eleman eklendiğinde ve eleman silindiğinde ağacın dengede kalması sağlanmaktadır.
Pekiyi dengelenmiş ağaçlar nasıl oluşturulmaktadır? Dengeleme işlemi her eleman eklendiğinde ve silindiğinde O(1) karmaşıklıkta ya da
O(log N) karmaşıklıkta yapılan bir işlemdir. Böylece ağacın bir yandan uzamaıs engellenir. Ağacın tüm yapraklarının benzer yükseklikte
kalması sağlanır. Bunun sonucunda arama işlemi de en kütü durum senaryosunda O(log N) olacaktır. Dengeleme için çeşitli algoritmalar
önerilmiştir. En önemli iki dengeleme algoritmasına AVL (Adelson-Velsky-Landis) ve "Red Black Tree" algoritması denilmektedir.
Bu iki algoritma arasında özel durumlara göre birinin lehine farlılıklar oluşabilmektedir. Uyglamada en çok kullanılan dengeleme algoritması
AVL algoritmasıdır. Bu dengeleme algoritmasının kullanıldığı arama ağaçlarına "AVL ağacı (AVL tree)" da denilmektedir.
Biz kursumunda ikili ağaçların dengelenmesi üzerinde durmayacağız. Bu konu ve diğer ağaç algoritmaları "Sistem Programalama ve İleri C
Uygulamaları-II" kursunun konusu içerisindedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi 3'lü, 4'lü 5'li gibi arama ağaçları nasıl oluşturulmaktadır? Bu tür arama ağaçlarına "B-Tree" denilmektedir. 2'den daha fazla alt
düğüme sahip olan ağaçlar bellek üzerindeki aramalarda (internal search) tercih edilmemektedir. Bunlar tipik olarak veritabanı yönetim
sistemleri (DBMS) tarafından disk üzerindeki aramalarda (external search) kullanılmaktadır. Pekiyi örneğin 3'lü bir arama ağacı nasıl
organize edilmektedir. İşte bu tür durumlarda düğüm üzerinde tek değer değil birden fazla değer tutulur. Örneğin 3'lü bir amarama ağacında
göstericilerden biri a değerinden küçük olan düğümü, diğeri a ile b arasında olan düğümü diğeri de b'den büyük olan düğümü gösterir.
Böylece ağacın yükseklüği azaltılmış olur. Disk işlemlerinde her bir düğümden diğerine geçiş için bir disk okuması gerektiği için bu durum
ilgili kayda daha hızlı erişilmesine olanak sağlamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Çok kullanılan diğer bir ağaç türüne de "heap ağacı" ya da kısaca "heap" denilmektedir. Buradaki "heap" teriminin dinamik bellek yönetiminde
geçen "heap" terimi ile bir ilgisi yoktur. Heap ağaçları "öncelik kuyruklarını (priority queues)" oluşturmak için kullanılmaktadır.
Öncelik kuyruklarında kuyruğa yerleştirilen her elemanın bir öncelik derecesi (numarası) vardır. Kuyruktan eleman alınacağı zaman baştaki
ya da sondaki eleman değil öncelik numarası en yüksek olan eleman alınmaktadır. Örneğin kuyrukta şu elemanlar olsun:
4 10 7 2 14 6 5
Kuyruğun başının sol taraf olduğunu düşünelim. Yani kuyruğa son eklenen eleman 5, ilk eklenen eleman ise 4 olsun. Eğer bu kuyruk sistemi
FIFO olsaydı biz 4'ü alırdık. Kuyruk sistemi LIFO (stack) olsaydı biz kuyruktan 5'i alırdık. İşte eğer bir öncelik kuyruğu söz konusu ise
biz kuyruktan 14'ü alırız. Çünkü en öncelikli eleman 14'tür. Bundan sonra yeniden kuyruktan eleman almak istesek bu kez 10'u alırız.
Öncelik kuyruklarından eleman alma bu haliyle O(N) karmaşıklıkta bir işlemdir. İşte heap ağaçları bu işlemi O(log N) karmaşıklığa düşürmek
için kullanılmaktadır.
Heap ağaçları "heap sort" denilen sıraya dizme algortimesının gerçekleştirilmesinde de kullanılmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Heap ağaçları tipik olarak ikili ağaç biçiminde karşımıza çıkmaktadır. Her ne kadar heap ağaçları 2'den fazla düğüme sahip olabilirse de
uygulamada 2'li heap ağaçları tercih edilmektedir. İkili heap ağaçlarına İngilizce "binary heap" de denilmektedir. Biz bu bağlamda "heap
ağacı" ya da "heap" dediğimizde "ikili heap ağaçları" anlaşılmalıdır.
Bir heap ağacının iki önemli karaktersitiği vardır:
1) Heap ağaçları her zaman "tam ağaç (complete tree)" biçimindedir. Tam ağaçlarda yaprak düğümlerin dışında tüm düğümlerin 2 alt düğüme
sahip olduğunu yaprakların ise en alt kademede soldan sağa bulunduğunu anımsayınız.
2) Heap ağacında bir düğümün tüm alt düğümleri o düğümden düşük ya da yüksek değerleri sahip olmak zorundadır. Eğer bir düğüm alt
düğümlerinden daha yüksek değere sahipse böyle heap ağaçlarına "max-heap", eğer bir düğüm alt düğümlerinden daha düşük değere sahipse
böyle heap ağaçlarına "min-heap" denilmektedir. Örneğin:
100
50 70
30 40 60 35
17 24 32
Bu örnekteki ağaç yukarıdaki iki kuralı karşıladığı için bir "max-heap" ağacıdır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Heap ağacına eleman eklenirken eleman önce "tam ağacı (complete tree)" bozmayacak biçimde en alta en soldaki boş yere eklenir. Sonra
eklenen eleman kuralı bozabileceği için üst düğümle karşılaştırılarak yer değiştirilir. Bu işlem kural bozulmayana kadar devam ettirilir.
Bu işleme İngilizce "heapify" da denilmektedir. Örneğin yukarıdaki ağaca 75 eklemek isteyelim. Önce eklemeyi en altta en sol boş pozisyona
yaparız:
100
50 70
30 40 60 35
17 24 32 75
Sonra bu 75 değeri üst düğümle karşılatırılıp duruma yer göre yer değiştirilir:
100
50 70
30 75 60 35
17 24 32 40
Bu işlem devam ettirlir:
100
75 70
30 50 60 35
17 24 32 40
Artık kural korunmuştur ve ekleme süreci bitirilir. Heap ağacından eleman alınacağı zaman her zaman kökten eleman alınır. Örneğin yukarıdaki
ağaçtan eleman alacaksak 100'ü alırız. Bu durumda 100'ün yerine geçecek eleman onun alt düğümlerinin büyük olanıdır:
75
75 70
30 50 60 35
17 24 32 40
Tabii alt düğümün yerine onun alt düğümlerinden en büyük olan getirilir:
75
75 70
30 50 60 35
17 24 32 40
Böyle devam edilir:
75
50 70
30 75 60 35
17 24 32 40
Ta ki yapraklara iniline kadar:
75
50 70
30 40 60 35
17 24 32 75
Alınan değer en aşağı indiğinde işleme son verilir ve tam ağaç bozulmayacak biçimde o eleman silinir.
Heap ağacına eleman eklemek en kötü durumda O(log N) karmaşıklıktadır. Eleman almak da aynı biçimdedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında heap ağaçlarında gerçekte ağaç oluşturulmamaktadır. Çünkü tam ağaçlar her zaman diziye dönüştürülebilmektedir. Bir heap ağacını
diziye dönüştürürken dizinin 0'ıncı indisli elemanını kolaylık olsun diye boş bırakabiliriz. Diziye dönüştürme şu kurala göre yapımaktadır:
Her zaman üst düğümün alt düğümleri üzet düğümün indeksinin iki katı ve iki katından bir fazla yerde bulunur. Aşağıdaki heap ağacına
bakınız:
100
50 70
30 40 60 35
17 24 32 75
Bu heap ağacının diziye dönüştürülmesi şöyle yapılacaktır:
0 1 2 3 4 5 6 7 8 9 10 11
x 100 50 70 30 40 60 35 17 26 32 75
Burada örneğin 70 numaralı elemanın indeksi 3'tür. Bu durumda bu elemanın alt düğümleri 6 ve 7'inci indekstedir. İşte aslında heap
ağaçlarında hiç ağaç oluşturulmaz. Sanki ağaç oluşturulmuş gibi her şey dizi üzerinde yukarıdaki indeks kuralına uygun bir biçimde yapılır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda heap ağaçları ile öncelik kuyruklarının oluşturulmasına bir örnek verilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* hpqueue.h */
#ifndef PQUEUE_H_
#define PQUEUE_H_
#include <stddef.h>
#include <stdbool.h>
#define PQUEUE_DEF_CAPACITY 8
/* Type Declarartion */
typedef struct tagPERSON {
char name[32];
} PERSON;
typedef PERSON VALUETYPE;
typedef struct tagHEAP_ITEM {
int prio;
VALUETYPE value;
} HEAP_ITEM;
typedef struct PQUEUE {
HEAP_ITEM *hitems;
size_t capacity;
size_t count;
} PQUEUE, *HPQUEUE;
/* Function Prototypes */
HPQUEUE create_pq(void);
bool put_pq(HPQUEUE hpqueue, int prio, VALUETYPE *value);
bool get_pq(HPQUEUE hpqueue, int *prio, VALUETYPE *value);
bool resize_pq(HPQUEUE hpqueue, size_t new_capacity);
void destroy_pq(HPQUEUE hqueue);
/* inline Function Definitions */
static inline bool isempty_pq(HPQUEUE hpqueue)
{
return hpqueue->count == 0;
}
static inline void clear_pq(HPQUEUE hpqueue)
{
hpqueue->count = 0;
}
#endif
/* hpqueue.c */
#include <stdio.h>
#include <stdlib.h>
#include "pqueue.h"
HPQUEUE create_pq(void)
{
HPQUEUE hpqueue;
if ((hpqueue = (HPQUEUE)malloc(sizeof(PQUEUE))) == NULL)
return NULL;
if ((hpqueue->hitems = (HEAP_ITEM *)malloc(sizeof(HEAP_ITEM) * PQUEUE_DEF_CAPACITY)) == NULL) {
free(hpqueue);
return NULL;
}
hpqueue->capacity = PQUEUE_DEF_CAPACITY;
hpqueue->count = 0;
return hpqueue;
}
bool put_pq(HPQUEUE hpqueue, int prio, VALUETYPE *value)
{
size_t index;
if (hpqueue->count + 1 == hpqueue->capacity)
if (!resize_pq(hpqueue, hpqueue->capacity * 2))
return false;
index = hpqueue->count + 1;
while (index > 1 && prio > hpqueue->hitems[index / 2].prio) {
hpqueue->hitems[index] = hpqueue->hitems[index / 2];
index /= 2;
}
hpqueue->hitems[index].prio = prio;
hpqueue->hitems[index].value = *value;
++hpqueue->count;
return true;
}
bool get_pq(HPQUEUE hpqueue, int *prio, VALUETYPE *value)
{
size_t i, ci;
if (hpqueue->count == 0)
return false;
*prio = hpqueue->hitems[1].prio;
*value = hpqueue->hitems[1].value;
i = 1;
ci = 2;
while (ci <= hpqueue->count) {
if (ci + 1 < hpqueue->count && hpqueue->hitems[ci + 1].prio > hpqueue->hitems[ci].prio)
++ci;
if (hpqueue->hitems[hpqueue->count].prio > hpqueue->hitems[ci].prio)
break;
hpqueue->hitems[i] = hpqueue->hitems[ci];
i = ci;
ci *= 2;
}
hpqueue->hitems[i] = hpqueue->hitems[hpqueue->count];
--hpqueue->count;
return true;
}
bool resize_pq(HPQUEUE hpqueue, size_t new_capacity)
{
HEAP_ITEM *hitems;
if (new_capacity <= hpqueue->capacity)
return false;
if ((hitems = (HEAP_ITEM *)realloc(hpqueue->hitems, new_capacity * sizeof(HEAP_ITEM))) == NULL)
return false;
hpqueue->hitems = hitems;
hpqueue->capacity = new_capacity;
return true;
}
void destroy_pq(HPQUEUE hpqueue)
{
free(hpqueue->hitems);
free(hpqueue);
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "pqueue.h"
void disp_pq(HPQUEUE hpqueue)
{
for (size_t i = 1; i <= hpqueue->count; ++i)
printf("%d ", hpqueue->hitems[i].prio);
printf("\n");
}
int main(void)
{
HPQUEUE hpqueue;
int prios[] = {100, 85, 90, 60, 70, 10, 40, -1};
VALUETYPE values[] = {{"Ali Serce"}, {"Sacit Bulut"}, {"Ayse Er"}, {"Necati Ergin"}, {"Guray Sonmez"},
{"Ziya Taskent"}, {"Gencay Coskun"}};
int prio;
VALUETYPE value;
if ((hpqueue = create_pq()) == NULL) {
fprintf(stderr, "cannot create priority queue!...\n");
exit(EXIT_FAILURE);
}
for (int i = 0; prios[i] != -1; ++i)
if (!put_pq(hpqueue, prios[i], &values[i])) {
fprintf(stderr, "cannot add item!..\n");
exit(EXIT_FAILURE);
}
disp_pq(hpqueue);
while (!isempty_pq(hpqueue)) {
if (!get_pq(hpqueue, &prio, &value)) {
fprintf(stderr, "cannot get value!..\n");
exit(EXIT_FAILURE);
}
printf("prio: %d, value: %s\n", prio, value.name);
disp_pq(hpqueue);
printf("----------------------\n");
}
destroy_pq(hpqueue);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Şimdiye kadar yaptığımız örneklerde ilgili veri yapılarının türleri baştan belirlenmiş durumdaydı. Her ne kadar bazı örneklerde biz
veri yapısı içerisinde tutulacak dğerlerin türlerini DATATYPE, VALUETYPE gibisi isimlerle typedef ettiysek de bu durum bir genelleme
oluşturmamaktadır. Başka bir deyişle şimdiye kadar yaptığımız örneklerde veri yapılarımız toplamda tek bir türe ilişkin olabilmektedir.
Örneğin bağlı listeler için şu yapıları kullanmıştık:
typedef int DATATYPE;
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagLLIST {
NODE head;
NODE *tail;
size_t count;
} LLIST, *HLLIST;
Burada biz birden fazla bağlı liste oluşturabilecek bir mekanizma sağlamıştık. Ancak tüm bağlı listelerimiz DATATYPE türünü tutmaktadır.
DATATYPE türü int olarak typedef edilldiğine göre biz ne kadar bağlı liste yaratırsak yaratalım hepsi int değerleri tutan bağlı listeler
olacaktır. Örneğin İkili arama ağaçları için şu yapıları kullanmıştık:
typedef struct tagPERSON {
char name[32];
int city;
} PERSON;
typedef struct tagNODE {
int key;
PERSON value;
struct tagNODE *left;
struct tagNODE *right;
} NODE;
typedef struct tagBTREE {
NODE *root;
size_t count;
} BTREE, *HBTREE;
Burada da biz birden fazla bağlı liste oluşturabiliyorduk. Ancak bu bağlı listelerin hepsinin anahtarı int değerleri için PERSON türünden
olmak zorundadır. Özetle şimdiye oluşturduğumuz veri yapıları "genel" değil belli bir tür için oluşturuldu. Şimdi biz ""türden bağımsız"
yani her türle çalışabilecke veri yapılarının nasıl oluşturalacağı üzerinde duracağız. Böylelikle veri yapılarını her tür için çalışabilir
hale getirebileceğiz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Türden bağımsız veri yapılarının oluşturulabilmesi için iki teknik kullanılabilir:
1) void Gösterici Tekniği
2) Gömme Tekniği
void gösterici tekniği aslında etkin bir yöntem değildir. Bu yöntemin hem kullanımı zordur hem de bu yöntem diğerine göre yavaştır.
Bu nedenle programcılar gömme tekniğini tercih etmektedir. Örneğin Linux kaynak kodlarında veri yapılarında genellik sağlamak için hep
bu teknik kullanılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
void gösterici tekniğinde veri yapısının tutacağı değerlerin türleri yerine onların adresleri ve uzunlukları kullanılır. Eğer karşılaştırma
yapılmak istenirse callback fonksiyonlardan faydalanılmaktadır. Örneğin ikili arama ağacını bu teknikle gerçekleştirmek isteyelim. Biz
artık ağaç düğümlerinde tutulacak anahtar ve değerin türünü bilmek zorunda değiliz. Ancak onların uzunluklarını bilmek zorundayız.
Bunun için gereken yapı bildirimleri şöyle olabilir:
typedef struct tagNODE {
struct tagNODE *left;
struct tagNODE *right;
char key_value[];
} NODE;
typedef struct tagQNODE {
struct tagQNODE *next;
NODE *node;
} QNODE;
typedef struct tagBTREE {
NODE *root;
size_t count;
QNODE *head;
QNODE *tail;
size_t key_size;
size_t value_size;
bool (*compare)(const void *, const void *);
} BTREE, *HBTREE;
Burada tagNODE yapısının son elemanına dikkat ediniz. Burada C99 ile birlikte C'ye eklenmiş olan "flexible array member" özelliği
kullanılmıştır. Bu özelliğe göre bir yapının son elemanı (başka elemanlarında bu yapılamaz) uzunluğu belirtilmemiş bir dizi olabilir.
Derleyici bu dizi için bir yer ayırmamaktadır. Bu diziden amaç ilgili yapı nesnesinin bellekteki bitim adresinin kolay bir biçimde tespit
edilmesini sağlamaktır. Handle alanında (tagBTREE yapısı) anahtar ve değer olarak kullanılacak bilgilerin uzunluğunun ve bir karşılaştırma
fonksiyonunun adresinin bulundurulduğna dikkat ediniz. Biz ikili arama ağacını yaratırken bu bilgileri vermek zorundayız.
Aşağıda void göstericiler kullanılarak türden bağımsız ikili arama ağacına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* binarytree.h */
#ifndef BINARYTREE_H_
#define BINARYTREE_H_
#include <stddef.h>
#include <stdbool.h>
/* Type Declararions */
typedef struct tagNODE {
struct tagNODE *left;
struct tagNODE *right;
char key_value[];
} NODE;
typedef struct tagQNODE {
struct tagQNODE *next;
NODE *node;
} QNODE;
typedef struct tagBTREE {
NODE *root;
size_t count;
QNODE *head;
QNODE *tail;
size_t key_size;
size_t value_size;
int (*compare)(const void *, const void *);
} BTREE, *HBTREE;
/* Function Prototypes */
HBTREE create_bt(size_t key_size, size_t value_size, int (*compare)(const void *, const void *));
bool insert_item_bt(HBTREE hbtree, const void *key, const void *value);
bool walk_inorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *));
bool walk_inorder_rl_bt(HBTREE hbtree, bool (*proc)(const void *, const void *));
bool walk_postorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *));
bool walk_preorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *));
bool walk_breadth_first_bt(HBTREE hbtree, bool (*proc)(const void *, const void *));
bool delete_bt(HBTREE hbtree, const void *key);
void clear_bt(HBTREE hbtree);
void destroy_bt(HBTREE hbtree);
/* inline Function Fefinitions */
static inline size_t count_bt(HBTREE hbtree)
{
return hbtree->count;
}
#endif
/* binarytree.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "binarytree.h"
static bool walk_inorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *));
static bool walk_inorder_rl_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *));
static bool walk_postorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *));
static bool walk_preorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *));
static void create_queue(HBTREE hbtree);
static NODE *put_queue(HBTREE hbtree, NODE *node);
static NODE *get_queue(HBTREE hbtree);
static void destroy_queue(HBTREE hbtree);
static void subtree_shift(HBTREE hbtree, NODE *node1, NODE *node2, NODE *node3);
static void clear_recur(NODE *node);
HBTREE create_bt(size_t key_size, size_t value_size, int (*compare)(const void *, const void *))
{
HBTREE hbtree;
if ((hbtree = (HBTREE)malloc(sizeof(BTREE))) == NULL)
return NULL;
hbtree->root = NULL;
hbtree->count = 0;
hbtree->key_size = key_size;
hbtree->value_size = value_size;
hbtree->compare = compare;
return hbtree;
}
bool insert_item_bt(HBTREE hbtree, const void *key, const void *value)
{
NODE *new_node, *node, *parent_node = NULL;
int result;
if ((new_node = (NODE *)malloc(sizeof(NODE) + hbtree->key_size + hbtree->value_size)) == NULL)
return false;
memcpy(new_node->key_value, key, hbtree->key_size);
memcpy(new_node->key_value + hbtree->key_size, value, hbtree->value_size);
new_node->left = NULL;
new_node->right = NULL;
if (hbtree->root == NULL) {
hbtree->root = new_node;
++hbtree->count;
return true;
}
node = hbtree->root;
while (node != NULL) {
parent_node = node;
result = hbtree->compare(key, node->key_value);
if (result < 0)
node = node->left;
else if (result > 0)
node = node->right;
else {
memcpy(node->key_value + hbtree->key_size, value, hbtree->value_size);
return true;
}
}
if (hbtree->compare(key, parent_node->key_value) < 0)
parent_node->left = new_node;
else
parent_node->right = new_node;
++hbtree->count;
return true;
}
bool walk_inorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *))
{
if (hbtree->root != NULL)
return walk_inorder_lr_recur(hbtree->root, hbtree->key_size, proc);
return true;
}
bool walk_inorder_rl_bt(HBTREE hbtree, bool (*proc)(const void *, const void *))
{
if (hbtree->root != NULL)
return walk_inorder_rl_recur(hbtree->root, hbtree->key_size, proc);
return true;
}
bool walk_postorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *))
{
if (hbtree->root != NULL)
return walk_postorder_lr_recur(hbtree->root, hbtree->key_size, proc);
return true;
}
bool walk_preorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *))
{
if (hbtree->root != NULL)
return walk_preorder_lr_recur(hbtree->root, hbtree->key_size, proc);
return true;
}
static bool walk_preorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *))
{
if (!proc(node->key_value, node->key_value + key_size))
return false;
if (node->left != NULL)
if (!walk_preorder_lr_recur(node->left, key_size, proc))
return false;
if (node->right != NULL)
if (!walk_preorder_lr_recur(node->right, key_size, proc))
return false;
return true;
}
bool walk_breadth_first_bt(HBTREE hbtree, bool (*proc)(const void *, const void *))
{
NODE *node;
bool result = true;
create_queue(hbtree);
put_queue(hbtree, hbtree->root);
while ((node = get_queue(hbtree)) != NULL) {
if (!proc(node->key_value, node->key_value + hbtree->key_size)) {
result = false;
break;
}
if (node->left != NULL)
if (put_queue(hbtree, node->left) == NULL) {
fprintf(stderr, "put_queue cannot allocate memory!..\n");
result = false;
break;
}
if (node->right != NULL)
if (put_queue(hbtree, node->right) == NULL) {
result = false;
fprintf(stderr, "put_queue cannot allocate memory!..\n");
break;
}
}
destroy_queue(hbtree);
return result;
}
static void create_queue(HBTREE hbtree)
{
hbtree->head = NULL;
hbtree->tail = NULL;
}
static NODE *put_queue(HBTREE hbtree, NODE *node)
{
QNODE *new_qnode;
if ((new_qnode = (QNODE *)malloc(sizeof(QNODE))) == NULL)
return NULL;
new_qnode->node = node;
new_qnode->next = NULL;
if (hbtree->tail != NULL)
hbtree->tail->next = new_qnode;
else
hbtree->head = new_qnode;
hbtree->tail = new_qnode;
return node;
}
static NODE *get_queue(HBTREE hbtree)
{
QNODE *qnode;
NODE *rnode;
if (hbtree->head == NULL)
return NULL;
qnode = hbtree->head;
hbtree->head = qnode->next;
if (hbtree->head == NULL)
hbtree->tail = NULL;
rnode = qnode->node;
free(qnode);
return rnode;
}
bool delete_bt(HBTREE hbtree, const void *key)
{
NODE *delete_node, *delete_parent_node, *replace_parent_node, *replace_node;
int result;
delete_node = hbtree->root;
delete_parent_node = NULL;
while (delete_node != NULL) {
result = hbtree->compare(key, delete_node->key_value);
if (result == 0)
break;
delete_parent_node = delete_node;
if (result < 0)
delete_node = delete_node->left;
else if (result > 0)
delete_node = delete_node->right;
}
if (delete_node == NULL)
return false;
if (delete_node->left == NULL)
subtree_shift(hbtree, delete_parent_node, delete_node, delete_node->right);
else if (delete_node->right == NULL)
subtree_shift(hbtree, delete_parent_node, delete_node, delete_node->left);
else {
replace_parent_node = delete_node;
replace_node = delete_node->right;
while (replace_node->left != NULL) {
replace_parent_node = replace_node;
replace_node = replace_node->left;
}
if (replace_parent_node != delete_node) {
subtree_shift(hbtree, replace_parent_node, replace_node, replace_node->right);
replace_node->right = delete_node->right;
}
subtree_shift(hbtree, delete_parent_node, delete_node, replace_node);
replace_node->left = delete_node->left;
}
free(delete_node);
--hbtree->count;
return true;
}
void clear_bt(HBTREE hbtree)
{
if (hbtree->root != NULL) {
clear_recur(hbtree->root);
hbtree->root = NULL;
hbtree->count = 0;
}
}
void destroy_bt(HBTREE hbtree)
{
if (hbtree->root != NULL)
clear_recur(hbtree->root);
free(hbtree);
}
static bool walk_inorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *))
{
if (node->left != NULL)
if (!walk_inorder_lr_recur(node->left, key_size, proc))
return false;
if (!proc(node->key_value, node->key_value + key_size))
return false;
if (node->right != NULL)
if (!walk_inorder_lr_recur(node->right, key_size, proc))
return false;
return true;
}
static bool walk_inorder_rl_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *))
{
if (node->right != NULL)
if (!walk_inorder_rl_recur(node->right, key_size, proc))
return false;
if (!proc(node->key_value, node->key_value + key_size))
return false;
if (node->left != NULL)
if (!walk_inorder_rl_recur(node->left, key_size, proc))
return false;
return true;
}
static bool walk_postorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *))
{
if (node->left != NULL)
if (!walk_postorder_lr_recur(node->left, key_size, proc))
return false;
if (node->right != NULL)
if (!walk_postorder_lr_recur(node->right, key_size, proc))
return false;
if (!proc(node->key_value, node->key_value + key_size))
return false;
return true;
}
static void destroy_queue(HBTREE hbtree)
{
QNODE *qnode, *temp_qnode;
qnode = hbtree->head;
while (qnode != NULL) {
temp_qnode = qnode->next;
free(qnode);
qnode = temp_qnode;
}
}
static void subtree_shift(HBTREE hbtree, NODE *node1, NODE *node2, NODE *node3)
{
if (node1 == NULL)
hbtree->root = node3;
else if (node1->left == node2)
node1->left = node3;
else
node1->right = node3;
}
static void clear_recur(NODE *node)
{
if (node->left != NULL)
clear_recur(node->left);
if (node->right != NULL)
clear_recur(node->right);
free(node);
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "binarytree.h"
typedef struct tagPERSON {
char name[32];
int no;
} PERSON;
bool disp_node(const void *key, const void *value);
int comp_person(const void *key1, const void *key2);
int main(void)
{
HBTREE hbtree;
PERSON per = {"Noname", 0};
int keys[] = {70, 50, 34, 56, 19, 80, 67, 43, 27, 76, 79, 105, 82, 65, 0};
int del_key = 70;
if ((hbtree = create_bt(sizeof(int), sizeof(PERSON), comp_person)) == NULL) {
fprintf(stderr, "cannot create binary tree!..\n");
exit(EXIT_FAILURE);
}
for (int i = 0; keys[i] != 0; ++i)
if (!insert_item_bt(hbtree, &keys[i], &per)) {
fprintf(stderr, "cannot insert item: %d\n", keys[i]);
exit(EXIT_FAILURE);
}
walk_inorder_lr_bt(hbtree, disp_node);
printf("\n------------------------------\n");
walk_inorder_rl_bt(hbtree, disp_node);
printf("\n------------------------------\n");
walk_postorder_lr_bt(hbtree, disp_node);
printf("\n------------------------------\n");
walk_preorder_lr_bt(hbtree, disp_node);
printf("\n------------------------------\n");
walk_breadth_first_bt(hbtree, disp_node);
printf("\n------------------------------\n");
if (!delete_bt(hbtree, &del_key)) {
fprintf(stderr, "cannot find item!...\n");
exit(EXIT_FAILURE);
}
walk_breadth_first_bt(hbtree, disp_node);
destroy_bt(hbtree);
return 0;
}
bool disp_node(const void *key, const void *value)
{
const int *ikey = (const int *)key;
printf("%d ", *ikey);
fflush(stdout);
return true;
}
int comp_person(const void *key1, const void *key2)
{
const int *ikey1 = (const int *)key1;
const int *ikey2 = (const int *)key2;
if (*ikey1 > *ikey2)
return 1;
if (*ikey1 < *ikey2)
return -1;
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Genelleştirmede void gösterici tekniği yerine gömme tekniğinin tercih edildiğini belirtmiştik. Bu tekniği açıklamadna önce hazırlık
amacıyla C'ye ilişkin bir konunun üzerinde duracağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C'nin <stddef.h> içerisinde bildirilmiş olan offsetof makrosu yapının tür ismini ve yapıdaki bir eleman ismini parametre olarak alır,
o elemanın yapının başından itibaren kaçıncı offsette olduğunu verir. Örneğin:
struct SAMPLE {
int a;
int b;
char c[32];
char d;
int e;
};
size_t result;
result = offsetof(struct SAMPLE, e);
Burada yapının e elemanı yapı nesnesinin başından itibaren muhtemelen 44'üncü offset'indedir. Elemanların offset numaraları aşağıdaki
gibi olacaktır:
struct SAMPLE {
int a; /* 0 */
int b; /* 4 */
char c[32]; /* 8 */
char d; /* 40 */
int e; /* 44 */
};
İşte offsetof makrosu bir yapı elemanının yapın nesnesnin kaçıncı offsetinde olduğunu elde etmek için kullanılmaktadır. offsetof makrosu
size_t türündne bir değer vermektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stddef.h>
struct SAMPLE {
int a;
int b;
char c[32];
char d;
int e;
};
int main(void)
{
size_t result;
result = offsetof(struct SAMPLE, e);
printf("%zd\n", result); /* 44 */
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
offsetof makrosu tipik olarak aşağıdakine benzer bir biçimde yazılmaktadır:
#define myoffsetof(type, member) ((size_t)&((type *)0)->member)
Burada 0 adresi önce ilgili yapı türünden adrese dönüştürülmüştür. (0 değerini herhangi türden bir adrese dönüştürürsek bu NULL adres
anlamına gelmemektedir. 0 dğeri void * türüne dönüştürülürse bu NULL ares anlamına gelmektedir.) Sonra yapının elemanına eriilip onun adresi
alınmıştır. Bu durumda adresin sayısal bileşeni aslında ilgili eelemanın offset'i olacaktır. Nihayet bu değer son olarak size_t türüne
dönüştürülmüştür. Yukarıdaki 0 adresinden başlayan yapının elemanına erişip onun adresini alma işlemi C'de "tanımsız davranış" değildir.
Çünkü burada standartlara göre gerçek bir erişim yapılmamaktadır. Yalnızca oranın adresi elde edilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#define myoffsetof(type, member) ((size_t)&((type *)0)->member)
struct SAMPLE {
int a;
int b;
char c[32];
char d;
int e;
};
int main(void)
{
size_t result;
result = myoffsetof(struct SAMPLE, e);
printf("%zd\n", result); /* 44 */
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir yapı içerisindeki bir elemanın adresini, o elemanın ismini ve yapının türünü bildiğimizi varsayalım. Bu durumda elemanın adresine
ilişkin yapı nesnesinin başlangıç adresini veren container_of biçiminde bir makro yazabiliriz:
#define container_of(ptr, type, member) ((type *) ((char *) (ptr) - offsetof(type, member)))
Makronun birinci parametresi nesne içerisindeki elemanın adresi, ikinci parametresi yapının tür ismi ve üçüncü parametresi de elemanın
ismini almaktadır. C standartlarında offsetof bir makro bulunmakla birlikte bu işi yapabilecek bir makro bulunmamaktadır.
Aşağıdaki container_of makrosunn kullanımına ilişkin bir örnek verilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stddef.h>
#define container_of(ptr, type, member) ((type *) ((char *) (ptr) - offsetof(type, member)))
struct SAMPLE {
int a;
char b;
int c;
char d;
double e;
int f;
};
int main(void)
{
struct SAMPLE s = { 10, 'x', 20, 'y', 12.4, 100 };
double *pd = &s.e;
struct SAMPLE *ps;
ps = container_of(pd, struct SAMPLE, e);
printf("%d, %f\n", ps->a, ps->e);
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Gömme yöntemiyle veri yapıları genelleştirilirken aslında veri yapıları adeta kendi çekirdek kısımlarından oluşturulur. Örneğin bir bağlı
listenin türden bağımsız bir biçimde bu teknikle oluşturulmak istendiğini düşünelim. Bağlı liste belli türdne bilgileri değil NODE isimli
düğümleri birbirine bağlayacaktır. Ancak NODE isimli düğümler bağlı liste elemanlarının tutacağı nesneleri içermeyecektir. Örneğin:
typedef struct tagNODE {
struct tagNODE *next;
struct tagNODE *prev;
} NODE;
Görüldüğü gibi burada NODE yapısı düğümğn tuttuğu bilgiyi içermemektedir. İşte bağlı liste bu biçimdeki düğümleri birbirine bağlamaktadır.
Örneğin:
head_node <---> node <----> node <---> node <---> ....
Pekiyi bağlı liste düğümleri yalnızca link tuttuğuna göre böyle bir bağlı listeninin nasıl bir faydası olabilir? İşte bu düğümler aslında
başka bir yapının içerisinde bir eleman olarak tutulacaktır. Örneğin:
struct PERSON {
char name[32];
int no;
NODE link;
};
Görüldüğü gibi PERSON yapısının bir elemanı bağlı listenin düğümündne oluşmaktadır. Yani biz aslında düğümlerdne bağlı liste oluşturmaktayız
ancak bu düğümler de ilgili türden nesnelerin elemanı durumundadır. Nasıl olsa biz yapının bir elemanının adresini bildiğimizde o yapı
nesnesinin başlangıç adresini yukarıda gösterdiğimiz gibi container_of makrosu ile elde edebiliriz.
Bu yöntemde bir yapı nesnesi tek bir bağlı liste içerisinde bulunmak zorunda değildir. Örneğin Linux çekirdeğinde proses kontrol bloğu temsil
eden task_struct yapısı pek çok bağlı listede bulunmaktadır. Burada yapılacak şey yapı içerisinde birden fazla link elemanı tutmaktır.
Örneğin:
struct PERSON {
char name[32];
int no;
NODE link1;
NODE link2;
};
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte bir handle sistemi kullanılarak gömme tekniği ile türden bağımsız bağlı liste oluşturulmuştur. Burada yine bağlı listeler
create_llist gibi bir fonksiyonla yaratılmaktadır. Gömme tekniğinde handle sisteminin kullanılması aşağı seviyeli çekirdek kodları için
uygun bir yöntem olmayabilir. Ancak biz aşağıda önce handle sistemi ile gömme tekniğine örnek vereceğiz. Sonra handle sistemi olmadan
aynı tekniği kullanacağız. Yukarıda da belirttiğimiz gibi aslında örneğin Linux, BSD, CSD gibi işletim sistemi kodlarında bu handle sistemi
kullanılmamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* llist.h */
#ifndef LLIST_H_
#define LLIST_H_
#include <stddef.h>
#include <stdbool.h>
/* Type Declarations */
typedef struct tagNODE {
struct tagNODE *next;
struct tagNODE *prev;
} NODE;
typedef struct tagLLIST {
NODE head;
size_t count;
} LLIST, *HLLIST;
/* Function Prototypes */
HLLIST create_llist(void);
NODE *insert_next(HLLIST hllist, NODE *node, NODE *new_node);
NODE *insert_prev(HLLIST hllist, NODE *node, NODE *new_node);
NODE *add_tail(HLLIST hllist, NODE *new_node);
NODE *add_head(HLLIST hllist, NODE *new_node);
void remove_node(HLLIST hllist, NODE *node);
NODE *getp_item(HLLIST hllist, size_t index);
bool walk_llist(HLLIST hllist, bool (*proc)(NODE *));
bool walk_llist_rev(HLLIST hllist, bool (*proc)(NODE *));
void clear_llist(HLLIST hllist);
void destroy_llist(HLLIST hllist);
/* inline Function Definitions */
static inline size_t count_llist(HLLIST hllist)
{
return hllist->count;
}
static inline NODE *head_llist(HLLIST hllist)
{
return hllist->head.next;
}
static inline NODE *tail__llist(HLLIST hllist)
{
return hllist->head.prev;
}
/* Macro Definitions */
#define container_of(ptr, type, member) ((type *) ((char *) (ptr) - offsetof(type, member)))
#define FOR_EACH(hllist, node) for (node = (hllist)->head.next; node != &(hllist)->head; node = node->next)
#define FOR_EACH_REV(hllist, node) for (node = (hllist)->head.prev; node != &(hllist)->head; node = node->prev)
#define FREE_LLIST(hllist, node) for (NODE *tnode = (hllist)->head.next; node = tnode, tnode = tnode->next, node != &(hllist)->head; )
#endif
/* llist.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
/* Function Definitions */
HLLIST create_llist(void)
{
HLLIST hllist;
if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL)
return NULL;
hllist->head.next = &hllist->head;
hllist->head.prev = &hllist->head;
hllist->count = 0;
return hllist;
}
NODE *insert_next(HLLIST hllist, NODE *node, NODE *new_node)
{
node->next->prev = new_node;
new_node->next = node->next;
node->next = new_node;
new_node->prev = node;
++hllist->count;
return new_node;
}
NODE *insert_prev(HLLIST hllist, NODE *node, NODE *new_node)
{
node->prev->next = new_node;
new_node->next = node;
new_node->prev = node->prev;
node->prev = new_node;
++hllist->count;
return new_node;
}
NODE *add_tail(HLLIST hllist, NODE *new_node)
{
return insert_prev(hllist, &hllist->head, new_node);
}
NODE *add_head(HLLIST hllist, NODE *new_node)
{
return insert_next(hllist, &hllist->head, new_node);
}
void remove_node(HLLIST hllist, NODE *node)
{
node->prev->next = node->next;
node->next->prev = node->prev;
--hllist->count;
}
NODE *getp_item(HLLIST hllist, size_t index)
{
NODE *node;
if (index >= hllist->count)
return NULL;
node = hllist->head.next;
for (size_t i = 0; i < index; ++i)
node = node->next;
return node;
}
bool walk_llist(HLLIST hllist, bool (*proc)(NODE *))
{
for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next)
if (!proc(node))
return false;
return true;
}
bool walk_llist_rev(HLLIST hllist, bool (*proc)(NODE *))
{
for (NODE *node = hllist->head.prev; node != &hllist->head; node = node->prev)
if (!proc(node))
return false;
return true;
}
void clear_llist(HLLIST hllist)
{
hllist->head.next = &hllist->head;
hllist->head.prev = &hllist->head;
hllist->count = 0;
}
void destroy_llist(HLLIST hllist)
{
free(hllist);
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
typedef struct tagPERSON {
char name[32];
int no;
NODE link;
} PERSON;
bool disp_person(NODE *node);
int main(void)
{
HLLIST hllist;
PERSON *per;
NODE *node;
if ((hllist = create_llist()) == NULL) {
fprintf(stderr, "cannot create linked list...\n");
exit(EXIT_FAILURE);
}
for (int i = 0; i < 100; ++i) {
if ((per = (PERSON *)malloc(sizeof(PERSON))) == NULL) {
fprintf(stderr, "ccano tallocate memory!..\n");
exit(EXIT_FAILURE);
}
per->no = i;
per->name[0] = '\0';
add_tail(hllist, &per->link);
}
walk_llist(hllist, disp_person);
printf("\n--------------------\n");
FOR_EACH (hllist, node) {
per = container_of(node, PERSON, link);
printf("%d ", per->no);
fflush(stdout);
}
printf("\n--------------------\n");
FOR_EACH_REV (hllist, node) {
per = container_of(node, PERSON, link);
printf("%d ", per->no);
fflush(stdout);
}
FREE_LLIST(hllist, node) {
per = container_of(node, PERSON, link);
free(per);
}
return 0;
}
bool disp_person(NODE *node)
{
PERSON *per;
per = container_of(node, PERSON, link);
printf("%d ", per->no);
fflush(stdout);
return true;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
En sık karşılaşılan veri yapılaran biri de "graph" denilebn veri yapısıdır. Bir graph düğümlerden ve kenarlardan oluşur. Düğümlere
İngilizce "vertex" ya da "node" denilmektedir. Düğüm arasındaki bağlantıyı temsil eden kenarlara da İngilizce "edge" denilmektedir.
Aslında düğümlerden Ve kenarlardan oluşan en genel veri yapısı "graph" veri yapısıdır. Ağaçlar aslında bir çeşit graph'tır. Belli bir
düğüm kök yapıldığında her düğüme bu kök düğümden yalnızca tek bir yol ile gelinen özel graph'lara "ağaç (tree)" denilmektedir. Yani
aslında her ağaç bir graph'tır, ancak her graph bir ağaç değildir. Dolayısıyla genel olarak graph'larda bir düğüme biren fazla yerden
gelinebilmektedir. Graph veri yapılarının pratikte pek çok uygulama alanı vardır. Graplar üzerinde yüzden fazla uygulaması olan problem
tanımlanmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Graph'lar konusunda çalışmadan önce temel graph terminolojisini bilmek gerekir. Tmel graph terminolojisi şöyledir:
Yönlü ve Yönsüz Graflar (Directed and Undirected): Eğer düğümler arasındaki yollarda bir yön belirtiliyorsa bu tür graflara "yönlü graflar"
denilmektedir. Düğümler arasındaki kenarlarda bir yön bilgisi yoksa bu tür graflara da "yönsüz graflar" denilmektedir. Aslında yönsüz
graflar iki yönlü graflar gibi de düşünülebilir. Örneğin kara yolları yönlü bir grafla, sosyal ağlar (hepsi değil) yönsüz bir grafla temsil
edilebilir.
Döngüsel Olan (Cyclic) ve Döngüsel Olmayan (Acyclic) Graflar: Bir grafta bir düğümden başlanarak aynı düğüme gelebilmenin bir yolu varsa
bu tür graflara döngüsel graflar denilmektedir. Döngüsel graflarda her düğüm için kendine gelen bir yol olması gerekmez. Herhangi bir
düğümden kendisine gelen herhangi bir yol varsa bu grafı döngüsel graf yapmaktadır. Döngüsellik tipik olarak yönlü graflar için kullanılan
bir terimdir. İngilizce buna "directed acyclic grapg (DAG)" da denilmektedir.
Yol (Path): Bir grafta bir düğümden diğerine varabilmek için geçilen kenarların sırasıyla dizilimine "yol (path)" denilmektedir.
Tur (Cycle): Bir yolun ilk ve son düğümü aynıysa başka bir deyişle bir düğümden çıkılıp yeniden aynı düğüme gelen yola "tur" denilmektedir.
Tuların tüm düğümleri kapsaması gerekmez.
Bağlantılı Graflar (Connected Graphs): Her düğümle her düğüm arasında en az bir yolun olduğu yönsüz graflara "bağlantılı graflar (connected
graphs)" denilmektedir. Graflardaki düğümleri bilye, kenarları da bunları bağlayan ip olarak düşünürsek bağlantılı graflarda herhangi bir
bilyeyi tutup yukarı kaldırdığımızda tüm bilyelerin kalkması gerekir.
Öz döngülü (self-loop) Graflar: Eğer graflarda kendindne kendine bir kenar tanımlanabiliyorsa böylesi graflara öz döngülü graflar (self-loop
graphs) denilmektedir. Genellikle gragflar öz döngülü olmazlar.
Alt Graflar (Subgraphs): Bir grafın belli bir alt kümesine alt graph denilmektedir.
Tam Graflar (Complete Graphs): Bir yönsüz grafta her düğümden her düğüme bir kenar varsa bu tür graflara "tam graflar (complete graphs)"
denilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Graflarla çalışmak için öncelikle grafları veri yapısı olarak temsil etmek gerekir. Grafların temsil edilmesi için temel iki yöntem
kullanılmaktadır.
1) Komşuluk Matrisi (Adjacency-Matrix) Yöntemi
2) Komşuluk Listeleri (Adjacency-Lists) Yöntemi
Komşuluk matrisi yönteminde düğümlerden bir kare matris oluşturulur. Matrisin elemanları 0 ve 1'lerden oluşturulabilir. 0 ilgili iki
düğüm arasında bir kenar olmadığını 1 ise olduğunu gösterebilir. Örneğin:
A B C D
A 0 1 0 1
B 0 0 1 1
C 1 1 0 0
D 0 1 1 0
Burada örneğin C'den C'ye kenar yoktur, ancak C'den D'ye kenar vardır. Tabii eğer kenarlara bilgiler iliştirilecekse (örneğin bir yol uzunluğu
gibi) bu durumda matrisin elemanları birer gösterici olabilir. Kenar bilgileri bu göstericilerin gösterdiği yerdedir. İki düğüm arasında
yol yoksa matrisin ilgili elemanı NULL adres içerebilir. Yönsüz graflarda komşuluk matirisinin simetrik bir matris olması gerektiğine dikkat
ediniz.
Komşuluk listeleri yönteminde her düğümün bir listesi vardır. Bu liste o düğümden gidilebilecek düğümleri belirtir. Buradakli liste
dinamik büyütülen bir dizi olabilir ya da bağlı liste olabilir. Yukarıdaki graf'ın komşuluk listeleri yoluyla temsili şöyle olacaktırÇ:
A: B, C
B: C, D
C: A, B
D: B, C
BUrada eğer yollara bilgiler iliştirilecekse listenin elemanları birer gösterici olabilir ya da o bilgilerdne oluşan birer yapı da olabilir.
Pekiyi hangi temsil daha iyidir? Aslında iki yöntemin de avantajları ve dezavantajları bulunmaktadır. Komşul matrisi yönteminde kenara
iliştirilen bilgiye O(1) karmaşıklıkta erişilebilir. Komşuluk listeleri yönteminde sıralı arama gerekir. Eğer kenar sayısı çok fazla ise
bu yöntemde her düğüm için bir hash tablosu ya da ikili arama ağacı kullanılabilir. Uygulamada genellikle "komşuluk listeleri" yöntemi
tercih edilmektedir.
Bu temsillerde düğümlerin nasıl temsil edileceği de bir problemdir. Düğümlere isim verilirse ismin aranması uzun sürebilir. Bu durumda
en etkin yöntem düğümlere isim değil numara vermektir. Tabii numaradan isim elde edilebilir ya da isimden numara alde edilebilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Graflarlar üzerinde algoritmalar için değişik graf kütüphaneleri oluşturulmuştur. En yaygın kullanılan graf kütüphanesi C++'taki "Boost
Graf Kütüphanesi (Boost Graph Library (BGL))" denilen kütüphanedir. Bu kütüphanede düğümler ve kenarlar sınıflarla temsil edilmiştir. Boost
Graf Kütüphanesi pek çok graf kütüphanesine de ilham kaynağı olmuştur. Benzer biçimde Java için, C# için Python için çeşitli graf kütüphaneleri
geliştirilmiştir. C için yaygın kullanılan bir graf kütüphanesi bulunmamaktadır. Ancak incelemek için "igraph" kütüphanesi önerilebilir.
Kütüphanenin kaynak kodlarına aşağıdan erişebilirisiniz:
https://igraph.org/
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz gibi graf veri yapısı için iki yöntem kullanılmaktadır. Ancak ağırlıklı tercih "komşuluk listeleri" yöntemidir.
Bu yöntemin türden bağımsız uygulaması biraz zor olabilmektedir. Biz burada makul bir gerçekleştirim üzerinde duracağız.
Gerçekleştiririmizde DGRAPH nesnesi VERTEX nesnelerini, VERTEX nesneleri de EDGE nesnelerini tutacaktır. Komşuluk listeleri yönteminde
her vertex'in o vertex'ten gidilebilecek vertex'leri tuttuğunu belirtmiştik. Ancak biz burada bir vertex'ten gidilecek vertex'leri değil
gidilecek vertex'lere ilişkin kenar nesnelerini tutacağız. DGRAPH yapısı şöyleolabilir:
typedef struct tagDGRAPH{
VERTEX **vertices;
size_t count;
size_t capacity;
} DGRAPH;
Görüldüğü gibi burada graf veri yapısı vertex'lerin kendilerini değil adreslerini tutmaktadır. Bunun nedeni bu dizi büyütüldüğünde
VERTEX nesnelerinin adreslerinin değişmemesinin sağlanmasıdır. Buradaki count bu gösterici dizisinin dolu olan eleman sayısını, capacity
ise gösterici dizisi için ayrılan kapasiteyi belirtmektedir. VERTEX yapısı aşağıdakai gibi olabilir:
typedef struct tagVERTEX {
EDGE **edges;
size_t count;
size_t capacity;
/* VERTEX INFO */
char name[32];
} VERTEX;
Görüldüğü gibi VERTEX nesneleri EDGE nesnelerinin adreslerini tutmaktadır. Yine bu dizinin dolu olan eleman sayısı count elemanı ile
dizi için tahsis edilen toplam alan ise capacity elemanı ile tutulmaktadır. Burada biz vertex'e bir isim verdik. İsterseniz burada
vertex'lere başka bilgiler de iliştirebilirsiniz. EDGE yapısı da şöyle olabilir:
typedef struct tagEDGE {
VERTEX *v1;
VERTEX *v2;
/* EDGE INFO */
int length;
} EDGE;
Görüldüğü gibi bir EDGE nesnesi başlangıç ve bitiş VERTEX nesnelerinin adreslerini tutmaktadır. Burada EDGE nesnesine bir uzunluk bilgisi
iliştirilmiştir. Tabi başka bilgiler de iliştirilebilir. Bu tasarımda biz DGRAPH nesnesinin tuttuğu vertex'leri dinamik büyütülen bir
dizi ile, vertex'lerin tuttuğu kenarları da dinamik büyütülen bir dizi ile temsil ettik. Burada bağlı listeler ya da hash tabloları da
kullanılabilir.
Graf veri yapısını oluşturduktan sonra graflar üzerinde işlem yapan algoritmaların oluşturulması gerekir. Ancak çok çeşitli graf algoritmaları
vardır. Biz kursumuzda yalnızca birkaç algoritma üzerinde duracağız. Bunlardan biri belli bir düğümden başlanarak grafın gidilebilen
tüm düğümlerinin dolaşılmasıdır. Tabii bu da özyinelemeli bir biçimde yapılmalıdır. Yani bir düğümden gidilebilen tüm düğümler için
fonksiyon kendini çağırmalıdır. Ancak graflarda düğümlere birden fazla yolla gelinebildiği için döngüsel bir sonsuz döngüye girmemek
gerekir. Bunu engellemenin tipik yolu her ziyaret edilen düğümün ziyaret edildiğini bir yerde tutmak ve eğer düğüm daha önce ziyaret
edilmişse onu yeniden ziyaret etmemektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Graf veri yapısını kurduktan sonra graf üzerinde pek çok problemin çözümü yapılabilir. Literatürden yüzün üzerinde graf problemi
tanımlanmıştır. Graf problemlerinin en çok bilinenlerinden bazıları şunlardır:
- Grafın dolaşılması
- Grafta iki düğüm arasındaki tüm yolların (paths) elde edilmesi (shortest path problem)
- Grafta iki düğüm arasında en kısa yolun elde edilmesi (path finding problem)
- En küçük örten ağaç problemi (minimum spanning tree)
- Gezgin satıcı problemi (travelling salesperson problem)
- Maximum akış problemi (maximum flow problem)
- Graf boyama problemi (graph coloring problem)
- Graf çizdirme problemi (graph drawing problem)
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda graf veri yapısının oluşturulmasına ilişkin bir örnek verilmiştir. Buradaki veri yapılarını kullanarak yukarıdaki problemleri
çözecek algoritmaları deneyebilirsiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* graph.h */
#ifndef GRAPH_H_
#define GRAPH_H_
#include <stddef.h>
#include <stdbool.h>
/* Symbolic Constants */
#define DEF_VERTEX_CAPACITY 4
#define DEF_EDGE_CAPACITY 4
#define MAX_CSV_LINE_LENGTH 1024
/* Type Declarations */
typedef struct tagEDGE {
struct tagVERTEX *v1;
struct tagVERTEX *v2;
/* EDGE INFO */
double length;
} EDGE;
typedef struct tagVERTEX {
EDGE **edges;
size_t count;
size_t capacity;
size_t index;
/* VERTEX INFO */
char name[32];
} VERTEX;
typedef struct tagDGRAPH{
VERTEX **vertices;
size_t count;
size_t capacity;
size_t ecount;
} DGRAPH, *HDGRAPH;
/* Function Prototypes */
HDGRAPH create_dgraph(void);
VERTEX *add_vertex(HDGRAPH hdgraph, VERTEX *vertex);
VERTEX *add_vertex_info(HDGRAPH hdgraph, const char *name);
EDGE *add_edge(HDGRAPH hdgraph, VERTEX *v1, VERTEX *v2, double length);
EDGE *add_edge_info(HDGRAPH hdgraph, const char *name1, const char *name2, double length);
DGRAPH *build_graph(const char *path);
bool traverse_depth_first_vertex(HDGRAPH hdgraph, VERTEX *vertex, bool (*proc)(VERTEX *));
bool traverse_depth_first_name(HDGRAPH hdgraph, const char *name, bool (*proc)(VERTEX *));
void clear_dgraph(HDGRAPH hdgraph);
void destroy_dgraph(HDGRAPH hdgraph);
/* inline Funcrion Definitions */
static inline size_t vertex_count(HDGRAPH hdgraph)
{
return hdgraph->count;
}
#endif
/* graph.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "graph.h"
static VERTEX *find_vertex_name(HDGRAPH hdgraph, const char *name);
static EDGE *create_edge(VERTEX *v1, VERTEX *v2, double length);
static VERTEX *create_vertex(const char *name);
static bool traverse_depth_first_vertex_recur(VERTEX *vertex, int *visiteds, bool (*proc)(VERTEX *));
HDGRAPH create_dgraph(void)
{
HDGRAPH hdgraph;
if ((hdgraph = (HDGRAPH)malloc(sizeof(DGRAPH))) == NULL)
return NULL;
if ((hdgraph->vertices = (VERTEX **)malloc(DEF_VERTEX_CAPACITY * sizeof(VERTEX *))) == NULL) {
free(hdgraph);
return NULL;
}
hdgraph->count = 0;
hdgraph->capacity = DEF_VERTEX_CAPACITY;
hdgraph->ecount = 0;
return hdgraph;
}
VERTEX *add_vertex(HDGRAPH hdgraph, VERTEX *vertex)
{
VERTEX **new_vertices;
if (find_vertex_name(hdgraph, vertex->name) != NULL)
return NULL;
if (hdgraph->count == hdgraph->capacity) {
if ((new_vertices = realloc(hdgraph->vertices, hdgraph->capacity * sizeof(VERTEX *) * 2)) == NULL)
return NULL;
hdgraph->vertices = new_vertices;
hdgraph->capacity *= 2;
}
vertex->index = hdgraph->count;
hdgraph->vertices[hdgraph->count] = vertex;
++hdgraph->count;
return vertex;
}
VERTEX *add_vertex_info(HDGRAPH hdgraph, const char *name)
{
VERTEX *vertex;
VERTEX **new_vertices;
if (find_vertex_name(hdgraph, name) != NULL)
return NULL;
if ((vertex = create_vertex(name)) == NULL)
return NULL;
if (hdgraph->count == hdgraph->capacity) {
if ((new_vertices = realloc(hdgraph->vertices, hdgraph->capacity * sizeof(VERTEX *) * 2)) == NULL)
return NULL;
hdgraph->vertices = new_vertices;
hdgraph->capacity *= 2;
}
vertex->index = hdgraph->count;
hdgraph->vertices[hdgraph->count] = vertex;
++hdgraph->count;
return vertex;
}
EDGE *add_edge(HDGRAPH hdgraph, VERTEX *v1, VERTEX *v2, double length)
{
EDGE *edge;
EDGE **new_edges;
if ((edge = create_edge(v1, v2, length)) == NULL)
return NULL;
if (v1->count == v1->capacity) {
if ((new_edges = realloc(v1->edges, v1->capacity * 2 * sizeof(EDGE *))) == NULL) {
free(edge);
return NULL;
}
v1->capacity *= 2;
}
v1->edges[v1->count] = edge;
++v1->count;
++hdgraph->ecount;
return edge;
}
EDGE *add_edge_info(HDGRAPH hdgraph, const char *name1, const char *name2, double length)
{
VERTEX *v1, *v2;
EDGE *edge;
if ((v1 = find_vertex_name(hdgraph, name1)) == NULL) {
if ((v1 = create_vertex(name1)) == NULL)
return NULL;
if (add_vertex(hdgraph, v1) == NULL)
goto EXIT1;
}
if ((v2 = find_vertex_name(hdgraph, name2)) == NULL) {
if ((v2 = create_vertex(name2)) == NULL)
goto EXIT1;
if (add_vertex(hdgraph, v2) == NULL)
goto EXIT2;
}
if ((edge = add_edge(hdgraph, v1, v2, length)) == NULL)
goto EXIT2;
return edge;
EXIT2:
free(v2);
EXIT1:
free(v1);
return NULL;
}
DGRAPH *build_graph(const char *path)
{
FILE *f;
HDGRAPH hdgraph;
char buf[MAX_CSV_LINE_LENGTH + 2];
const char *name1, *name2, *length;
char *str;
if ((f = fopen(path, "r")) == NULL)
goto EXIT1;
if ((hdgraph = create_dgraph()) == NULL)
goto EXIT2;
while (fgets(buf, MAX_CSV_LINE_LENGTH + 2, f) != NULL) {
if ((name1 = strtok(buf, ",\n")) == NULL)
continue;
if ((name2 = strtok(NULL, ",\n")) == NULL)
goto EXIT3;
if ((length = strtok(NULL, ",\n")) == NULL)
goto EXIT3;
if (strtok(NULL, ",\n") != NULL)
goto EXIT3;
if (add_edge_info(hdgraph, name1, name2, atof(length)) == NULL)
goto EXIT3;
}
return hdgraph;
EXIT3:
destroy_dgraph(hdgraph);
EXIT2:
fclose(f);
EXIT1:
return NULL;
}
bool traverse_depth_first_vertex(HDGRAPH hdgraph, VERTEX *vertex, bool (*proc)(VERTEX *))
{
int *visiteds;
bool result;
if (!proc(vertex))
return false;
if ((visiteds = (int *)calloc(hdgraph->count, sizeof(int))) == NULL)
return false;
result = traverse_depth_first_vertex_recur(vertex, visiteds, proc);
free(visiteds);
return result;
}
bool traverse_depth_first_name(HDGRAPH hdgraph, const char *name, bool (*proc)(VERTEX *))
{
VERTEX *vertex;
if ((vertex = find_vertex_name(hdgraph, name)) == NULL)
return false;
return traverse_depth_first_vertex(hdgraph, vertex, proc);
}
void clear_dgraph(HDGRAPH hdgraph)
{
for (size_t v = 0; v < hdgraph->count; ++v) {
for (size_t e = 0; e < hdgraph->vertices[v]->count; ++e)
free(hdgraph->vertices[v]->edges[e]);
hdgraph->vertices[v]->count = 0;
free(hdgraph->vertices[v]);
}
hdgraph->count = 0;
}
void destroy_dgraph(HDGRAPH hdgraph)
{
for (size_t v = 0; v < hdgraph->count; ++v) {
for (size_t e = 0; e < hdgraph->vertices[v]->count; ++e)
free(hdgraph->vertices[v]->edges[e]);
free(hdgraph->vertices[v]->edges);
}
free(hdgraph->vertices);
free(hdgraph);
}
static VERTEX *create_vertex(const char *name)
{
VERTEX *vertex;
if ((vertex = (VERTEX *)malloc(sizeof(VERTEX))) == NULL)
return NULL;
if ((vertex->edges = (EDGE **)malloc(DEF_EDGE_CAPACITY * sizeof(EDGE *))) == NULL) {
free(vertex);
return NULL;
}
vertex->count = 0;
vertex->capacity = DEF_EDGE_CAPACITY;
strcpy(vertex->name, name);
return vertex;
}
static VERTEX *find_vertex_name(HDGRAPH hdgraph, const char *name)
{
for (size_t i = 0; i < hdgraph->count; ++i)
if (!strcmp(name, hdgraph->vertices[i]->name))
return hdgraph->vertices[i];
return NULL;
}
static EDGE *create_edge(VERTEX *v1, VERTEX *v2, double length)
{
EDGE *edge;
if ((edge = (EDGE *)malloc(sizeof(EDGE))) == NULL)
return NULL;
edge->v1 = v1;
edge->v2 = v2;
edge->length = length;
return edge;
}
static bool traverse_depth_first_vertex_recur(VERTEX *vertex, int *visiteds, bool (*proc)(VERTEX *))
{
VERTEX *v;
visiteds[vertex->index] = 1;
for (size_t e = 0; e < vertex->count; ++e) {
v = vertex->edges[e]->v2;
if (visiteds[v->index])
continue;
if (!proc(v))
return false;
if (!traverse_depth_first_vertex_recur(v, visiteds, proc))
return false;
}
return true;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "graph.h"
void err_exit(const char *msg);
bool disp_vertex(VERTEX *v);
int main(void)
{
HDGRAPH hdgraph;
if ((hdgraph = build_graph("graph.csv")) == NULL) {
fprintf(stderr, "cannot build graph!..\n");
exit(EXIT_FAILURE);
}
traverse_depth_first_name(hdgraph, "A", disp_vertex);
destroy_dgraph(hdgraph);
return 0;
}
void err_exit(const char *msg)
{
fprintf(stderr, "%s\n", msg);
exit(EXIT_FAILURE);
}
bool disp_vertex(VERTEX *v)
{
printf("%s\n", v->name);
return true;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Graph çizdirmek için en fazla kullanılan araç "graphviz" isimli programdır. Bu programda bir grafı çizdirmek için önce graf bir dosyada
ismine "dot dili (dot language)" bir dilde betimlenir. Sonra "dot" isimli programa bu dosya girdi olarak verilir. Bu program da bu girdi
dosyasından hareketle istenilen formatta bir çıktı dosyası üretir. Tabii bunları yapabilmek için önce graphviz programını bilgisayarımıza
kurmamız gerekir. Graphviz aşağıdaki bağlantıdan indirilebilir:
https://graphviz.org/download/
Graphviz'de bir graf çizmek için grafın türü belirtilerek blok oluşturulur. Sonra bloğun içerisinde grafın düğümleri ve kenarları belirtilir.
Örneğin:
digraph G {
size="30, 40!"
A -> B
B -> D
B -> C
C -> A
D -> C
D -> E
E -> D
E -> F
F -> C
C -> F
G -> F
C -> G
C -> E
D -> H
H -> I
I -> D
}
Buradaki size ibaresi inch cinsinden grafin genişlik ve yülksekliğiniş belirtmektedir. Satırların yanlarına köşeli parantez içerisinde
label eklenebilir. Bu durumda belirtilen yazılar kenarların yanlarında görüntülenir. Örneğin:
digraph G {
A -> B [label="10"]
B -> D [label="7"]
B -> C [label="15"]
C -> A [label="5"]
D -> C [label="20"]
D -> E [label="18"]
E -> D [label="18"]
E -> F [label="19"]
F -> C [label="16"]
C -> F [label="16"]
G -> F [label="11"]
C -> G [label="25"]
C -> E [label="12"]
D -> H [label="11"]
H -> I [label="9"]
I -> D [label="20"]
}
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
77. Ders 24/03/2024 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte Windows'ta CSV dosyasından hareketle bir ".dot" dosyasını programlama yoluyla oluşturulmuş sonra da "dot" programı
çalıştırılarak ".png" dosyası elde edilmiştir. Nihayet bu ".png" dosyası da yine programlama yoluyla görüntülenmiştir. Burada kullanılabielcek
örnek "graph.csv" dosyası şöyledir:
A,B,10
B,D,7
B,C,15
C,A,5
D,C,20
D,E,18
E,D,18
E,F,19
F,C,16
C,F,16
G,F,11
C,G,25
C,E,12
D,H,11
H,I,9
I,D,20
A,I,40
I,J,11
J,D,21
D,A,17
F,H,30
G,J,40
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* graphmake.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <windows.h>
#define MAX_LINE_LEN 4096
void ExitSys(LPCSTR lpszMsg);
bool csv2dot(const char *path_csv, const char *path_dot)
{
FILE *f_csv, *f_dot;
char buf[MAX_LINE_LEN];
char *name1, *name2, *length;
bool retval = false;
if ((f_csv = fopen(path_csv, "r")) == NULL)
return false;
if ((f_dot = fopen(path_dot, "w")) == NULL)
goto EXIT1;
if (fprintf(f_dot, "digraph G {\n") < 0)
goto EXIT2;
while (fgets(buf, MAX_LINE_LEN, f_csv) != NULL) {
if ((name1 = strtok(buf, ",\n")) == NULL)
continue;
if ((name2 = strtok(NULL, ",\n")) == NULL)
goto EXIT2;
if ((length = strtok(NULL, ",\n")) == NULL)
goto EXIT2;
if (strtok(NULL, ",\n") != NULL)
goto EXIT2;
if (fprintf(f_dot, "\t%s -> %s [label=\"%s\"]\n", name1, name2, length) < 0)
goto EXIT2;
}
if (fprintf(f_dot, "}\n") < 0)
goto EXIT2;
retval = true;
EXIT2:
fclose(f_dot);
EXIT1:
fclose(f_csv);
return retval;
}
bool dot2png(const char *path_dot, char *path_png)
{
char cmdline[4096];
STARTUPINFO si = {sizeof(si)};
PROCESS_INFORMATION pi;
bool result;
sprintf(cmdline, "dot -Tpng %s -o %s", path_dot, path_png);
result = CreateProcess(NULL, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return result;
}
int main(void)
{
HINSTANCE hResult;
if (!csv2dot("graph.csv", "test.dot")) {
fprintf(stderr, "cannot generate dot file!..\n");
exit(EXIT_FAILURE);
}
if (!dot2png("test.dot", "test.png")) {
fprintf(stderr, "cannot generate png file!..\n");
exit(EXIT_FAILURE);
}
hResult = ShellExecute(NULL, "open", "test.png", NULL, NULL, SW_NORMAL);
if ((INT_PTR)hResult < 32)
ExitSys("ShellExecute");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Kursumuzun bu bölümünde thread kavramını göreceğiz. Çok thread'li çalışma modeli ve thread senkronizasyonu konularını ele alacağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir prosesin bağımsız olarak çizelgelenen farklı akışlarına "thread" denilmektedir. (Thread sözcüğü İngilizce "iplik" anlamına gelmektedir.
Akışlar ipliğe benzetilerek bu sözcük uydurulmuştur.) Proses kavramı çalışmakta olan programın tüm bilgilerini temsil eden bir kavramdır.
Thread'ler proseslerin akışlarını temsil etmektedir. Yani thread'ler proses'lerin bir unsurudur. Bir proses tek bir akışa (yani thread'e)
sahip olabileceği gibi birden fazla akışa (yani thread'e) sahip olabilmektedir. Thread'ler 90'lı yılların ortalarında işletim sistemlerine
sokulmuştur. Bugün artık thread'ler programlamanın önemli konularından sayılmaktadır. Windows sistemi ilk kez Windows NT ile (1993) sonra da
Windows 95 ile thread'li çalışma modeline sahip olmuştur. Benzer biçimde UNIX/Linux sistemlerine de yine 90'lı yılların ortalarında thread'ler
eklenmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
İşletim sistemlerinde prosesler çalışmaya tek bir thread'le başlamaktadır. Bu thread proses yaratılırken işletim sistemi tarafından yaratılmaktadır.
Prosesin bu thread'ine "ana thread (main thread)" denilmektedir. Prosesin diğer thread'leri program çalışırken sistem fonksiyonlarıyla
ya da bunları çağıran kütüphane fonksiyonlarıyla yaratılmaktadır. Yani program tek bir thread'le çalışmaya başlamaktadır. Diğer thread'ler
programcı tarafından oluşturulmaktadır. Thread'ler tamamen işletim sisteminin kontrolü altında yaratılıp çalıştırılmaktadır. Dolayısıyla
Windows sistemlerinde thread'ler Windows API fonksiyonlaır ile, UNIX/Linux ve macOS sistemlerinde ise POSIX fonksiyonlaır ile yaratılıp
idare edilmektedir. Bazı programlama dillerinin standart kütüphanelerinde platform bağımzıs thread sınıfları ya da fonksiyonları bulunabilmektedir.
Tabii burada platform bağımsızlık kaynak kod temelindedir. Bu kütüphaneler aslında farklı sistemlerde o sistemlerin sistem fonksiyonlarını
çağırarak thread işlemlerinş yapmaktadır. Örneğin C++11 ile birlikte C++'a bir thread kütüphanesi eklenmiştir. Biz C++'ta threda işlemlerini
hep aynı biçimde yapabiliriz. Ancak buradaki fonksiyonlar ve sınıflar Windows sistemlerinde Windows API fonksiyonları, UNIX/Linux sistemleridne
POSIX fonksiyonları çağrılarak işlemlerini gerçekleştirmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi thread'lere neden gereksinim duyulmaktadır? Bunu birkaç maddeyle açıklayabiliriz:
1) Thread'ler arkaplan işlemlerin yapılması için iyi araç oluşturmaktadır. Örneğin biz bir yandan bir şeyler yaparken arka planda da periyodik
birtakım işlemleri yapmak isteyebiliriz. Thread'ler yokken bu tür işlemler çok zor yapılıyordu. Ancak thread'lerişletim sistemlerine
eklenenince bu işlemleri yapmak çok kolaylaştı. Tread'ler sayesinde biz normal işlemlerimizi yaparken bir thread oluşturup arkaplan işlemleri
o thread'e havale edebiliriz.
2) Thread'ler programları hızlandırmak amacıyla kullanılabilmektedir. Bir işi birden fazla kaışa yaptırmak hız kazancı sağlamaktadır.
Sistemimizde tek bir işlemci olsa bile bizim prosesimiz diğer proseslere göre daha fazla CPU zamanı kullanabilir hale gelebilmektedir.
3) Thread'ler blokeye yol açan durumlarda işlemlerin devam ettirilmesini sağlayabilmektedir. Çünkü işletim sistemleri thread temelinde
bloke uygulamaktadır. Örneğin prosesin bir thread'i bir kalvye okuması sırasında blokede beklerken diğer thread'leri çalışmaya devam
edecektir. Bu durum da prosesin başka işlemlere yanıt verebilmesini sağlamaktadır.
4) Thread'ler paralel programlama için mecburen kullanılması gereken unsurlardır. Paralel programlama "birden fazla CPU ya da çekirdeğin
olduğu durumda prosesin thread'lerinin farklı CPU ya da çekirdeklere atanarak aynı anda çalıştırıması" anlamına geşmektedir. Örneğin çok
büyük bir diziyi sıraya dizecek olalım. Makinemizde 10 tane çekirdek olsun. Biz bu işlemi tek bir thread'le yaparsak makinemizdeki 10
çekirdek olmasının avantajından faydalanamayız. Halbuki biz dizimizi 10 parçaya ayırıp 10 farklı thread'i farklı çekirdeklere atarsak
bunlar paralel bir biçimde dizinin çeşitli parçalarını aynı anda sort edecektir. Sonra bunları birleştirirsek toplamda zamanı çok kısaltmış
olabiliriz.
5) Thread'ler GUI programlama ortamlarında bir mesaj geldiğinde uzun süren işlemlerin yapılabilmesine olanak sağlamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
78. Ders 30/03/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir proses çalışmaya tek bir thread ile başlar. Buna prosesin ana thread'i (main thread) denilmektedir. Bu ana thread C programlarında
tipik olarak akışın main fonksiyonundan girdiği thread'tir. Diğer thread'ler işletim sistem fonksiyonlarıyla yaratılmaktadır. Tabii Windows'ta
bu sistem fonksiyonlarını çağıran API fonksiyonları UNIX/Linux sistemlerinde de POSIX fonksiyonları bulunmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Modern işletim sisemlerinin çizelgeleyici (scheduler) alt sistemleri thread'leri çizelgelemektedir. Yani işletim sistemi hangi prosese
ilişkin olursa olsun sıradaki thread'i CPU'ya atar, onu belli süre çalıştırır. Sonra çalışmasına ara vererek sonraki thread'i CPU'ya atar.
Modern çok thread'li (multithreaded) işletim sistemlerinde prosesler değil thread'ler çizelgelenmektedir. Yukarıda da belirttiğimiz gibi
prosesin bir thread'i bloke olduğunda diğerleri çalışmaya devam edebilmektedir. Bir thread'in çalışmasına ara verilerek diğer bir thread'in
kaldığı yerden çalışmasına devam ettirilmesi sürecine "bağlamsal geçiş (context switch)" denilmektedir. Bağlamsal geçiş sırasında önceki
thread ile sonraki thread aynı prosesin thread'leri olabildiği gibi farklı proseslerin thread'leri de olabilir. Çok işlemcili ya da çekirdekli
sistemlerde zaman paylaşımlı çalışma modeli değişmemektedir. Yalnızca servis veren birden fazla işlemci ya da çekirdek söz konusu olmaktadır.
Thread'lerin sıra beklediği kuyruk sistemine işletim sistemleri terminolojisinde "çalışma kuyruğu (run queue)" denilmektedir. Çok işlemcili
ya da çekirdekli sistemlerde çalışma kuyruğu bir tane olabilir ya da her işlemci ya da çekirdek için ayrı bir çalışma kuyruğu söz konusu
olabilir. Örneğin Linux bir ara O(1) çizelgelemesi adı altında toplamda bir tane çalışma kuyruğu oluşturuyordu. Hangi işlemci ya da çekirdekteki
thread'in işi biterse o çalışma kuyruğundan o işlemci ya da çekirdeğe atama yapıyordu. Ancak daha sonra bu çizelgeleme algoritması yine
değiştirildi. Bugünkü çizelgeleme algoritmasında her işlemci ya da çekirdeğin ayrı bir çalışma kuyruğu bulunmaktadır. Tabii işletim sistemi
bu tür durumlarda işlemci ya da çekirdeklerin çalışma kuyruklarını iş yükü bakımından dengelemeye çalışmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Thread'lerin stack'leri birbirinden ayrılmıştır. Yerel değişkenlerin "stack" denilen alanda yaratıldığını anımsayınız. Thread'lerin
stack'leri biribirindne ayrıldığı için bir thread akışı bir fonksiyon üzerinde ilerlerken o fonksiyonun yerel değişkenleri o stack üzerinde
yaratılmaktadır. Diğer bir thread de aynı fonksiyon üzerinde ilerliyorsa o yerel değişkenlee de o thread'in stack'inde yaratılacaktır. Böylece
iki thread aynı kod üzerinde ilerliyor olsa da aslında yerel değişkenlerin farklı kopyalarını kullanıyor olacaklardır. Başka bir deyişle
yerel değişkenlerin her thread için ayrı bir kopyası bulunmaktadır. Örneğin:
void foo(void))
{
int a;
a = 10;
...
++a;
...
++a;
...
}
İki farklı thread bu foo fonksiyonunda ilerliyor olsun. Burada aslında her thread'in ayrı bir a değişkeni vardır. Dolayısıyla thread'lerden
biri bu a değişkenini değiştirdiğinde kendi thread'inin stack'indeki a değişkenini değiştirmiş olur. Bu değişiklikten diğer thread'in a
değişkeni etkilenmeyecektir.
Ancak global değişkenler tüm thread'ler tarafından ortak biçimde kullanılmaktadır. Başka bir deyişle ".data" ve ".bss" bölümleri thread'e
özgü değil prosese özgüdür. Örneğin bir thread bir global değişkeni değiştirdiğinde diğer thread o global değişkeni değişmiş görmektedir.
Benzer biçimde heap alanı da prosese özgüdür. Yani thread'leri ayrı heap'leri yoktur. Toplamda bir tane heap vardır. O da prosesin heap
alanıdır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir işin birden fazla akışa yaptırılması gerektiği durumlarda thread'ler proseslere göre çok daha etkin bir çözüm sunmaktadır. Çünkü
yaratılması proseslerin yaratılmasından daha hızlı yapılmakta ve thread'ler proseslere göre daha az sistem kaynağı harcamaktadır. Prosesler
önceki konularda da gördüğümüz gibi proseslerarası haberleşme yöntemleri (IPC) denilen yöntemlerle haberleşmektedir. Oysa thread'ler
global nesneler yoluyla haberleşebilmektedir. Eskiden thread'ler yokken bir işin birden fazla akışa yaptırılması prosesler yoluyla
gerçekleştiriliyordu. Oysa thread'ler işletim sistemlerine girince artık bunun için thread'ler kullanılmaya başlanmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Artık günümüzde thread'ler ptogramlamanın temel unsurları durumuna gelmiştir. Pek çok programla dilinde thread'lerle kolay işlemler yapabilmek
için standart kütüpaheneler bulunmaktadır. Hatta bazı dillerde artık thread'ler dilin sentaksının da içine sokulmuştur. Eskiden işlemciler
mega hertz düzeyinde çalışıyordu. Zamanla bunların hızları 1000 kat civarında artırıldı. Ancak artırmanın fiziksel bir sınırına da yaklaşıldı.
Artık hızlandırma işlemciyi bireysel olarak hızlandırmak yerine birden fazla işlemci (ya da çekirdek) kullanarak sağlanmaktadır. İşletim
sistemleri de işlemcilere ya da çekirdeklere thread'leri atamaktadır. İşletim sistemlerinin çizelgeleyici alt sistemlerinin atama birimleri
thread'lerdir.
Eskiden board'lara birden fazla işlemci takılabiliyordu. Ancak zamanla teknoloji gelişince birden fazla işlemci tek bir chip'e yerleştirilmeye
başlandı. Tek bir chip içindeki farklı işlemcilere "çekirdek (core)" denilmeye başlandı.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz gibi thread'ler işletim sistemlerinin sistem fonksiyonlarıyla yaratılmaktadır. Windows'ta bu sistem fonksiyonlarını
çağıran API fonksiyonları UNIX/Linux be macOS sistemlerinde de POSIX fonksiyonları vardır. Yüksek seviyeli programlama dillerinin kütüphanelerinde
buluan thread fonksiyonları da aslında bu fonksiyonlar kullanılarak yazılmıştır. Örneğin biz C# ya da Java'da thread yarattığımızda aslında
bu dillerin kütüphaneleri yaratımı Windows sistemlerinde Windows API fonksiyonlarını çağırarak UNIX/Linux ve macOS sistemlerinde de POSIX
fonksiyonlarını çağırarak yapmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde thread'ler CreateThread isimli API fonksiyonuyla yaratılmaktadır. Fonksiyonun prototipi şöyledir:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
Fonksiyonun birinci parametresi thread kernel nesnesinin güvenlik bilgilerini belirtmektedir. Bu parametre NULL geçilebilir. İkinci parametre
yaratılacak thread'in stack miktarını byte olarak belirtmektedir. Bu parametre 0 geçilirse default stack uzunluğu çalıştırılabilir dosyada
(PE formatında) belirtilen uzunluk olarak alınır. Genel olarak default uzunluk 1MB'dir. Fonksiyonun üçüncü parametresi thread akıının
başlatılacağı fonksiyonun adresini almaktadır. Her thread bizim belirlediğimiz bir fonksiyondan çalışmaya başlar. LPTHREAD_START_ROUTINE
aşağıdaki gibi typedef edilmiştir:
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(LPVOID lpThreadParameter);
Görüldüğü gibi LPTHREAD_START_ROUTINE geri dönüş değeri DWORD, parametresi LPVOID (yani void *) türünden olan bir fonksiyon adresini
belirtmektedir. 32 bit Windows sistemlerinde "fonksiyon çağırma (calling convention)" biçimi __stdcall olmak zorundadır. Bu __stdcall
anahtar sözcüğü Microsoft'a özgü olan bir eklentidir (extension). __stdcall WINAPI olarak da define edişmiştir:
#define WINAPI __stdcall
Bu çağırma biçimi Microsoft derleyicilerinde fonksiyon isminin solunda bulundurulmak zorundadır. Örnek bir thread fonksiyonu şöyle olabilir:
DWORD WINAPI ThreadProc(LPVOID param)
{
...
}
64 bit Windows sistemlerinde "çağırma biçimi (calling convention)" kaldırılmıştır. Bu nedenle bus sistemlerde __stdcall önişlemci komutlarıyla
aşağıdaki gibi silinmiştir:
#define __stdcall
O halde Windows programımızı 32 bit ve 64 bit uyumlu yazmak istiyorsak bu çağırma biçimini fonksiyonun soluna yazabiliriz. Nasıl olsa
64 bit derlemede bu çağırma biçimi silinecektir.
CreateThread fonksiyonunun dördüncü parametresi thread fonksiyonuna geçirilecek olan parametreyi belirtmektedir. Thread'leryaratıldığında
akış başlatılacağı thread fonksiyonuna bu parametrede belirtilen değer aktarılmaktadır. Biz böyle bir parametre geçmek istemiyorsak
bu parametreyi NULL biçimde belirtebiliriz. Fonksiyonun beşinci parametresi tipik olarak 0 biçiminde ya da CREATE_SUSPENDED biçiminde
geçilir. Eğer bu parametre 0 geçilirse thread yaratılır yaratılmaz çalışmaya başlar. Eğer bu parametreye CREATE_SUSPENDED değeri geçilirse
threda yaratılır ancak henüz çalışmaya başlamaz. Thread'i çalıştırmak için ResumeThread API fonksiyonu kullanılmalıdır. Fonksiyonun son
parametresi thread'in ID değerinin yerleştirileceği DWORD nesnesinin adresini almaktadır. Bu ID değeri bazı durumlarda gerekebilmektedir.
Eğer thread'in ID değeri alınmayacaksa bu parametreye NULL geçilebilir. CreateThread fonksiyonu başarı durumunda yaratılan thread'in handle
değerine, başarısızlık durumunda NULL adres değerine geri dönmektedir. Buradan elde edilen handle değeri diğer thread işlemlerinde kullanılmaktadır.
Thread'in değeri thread işlemlerinde kullanılmaz. Handle değeri thread işlemlerinde kullanılmaktadır. Örneğin:
HANDLE hThread;
DWORD dwThreadId;
DWORD __stdcall ThreadProc(LPVOID param);
...
if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL)
ExitSys("CreateThread");
...
Thread fonksiyonu bittiğinde thread kaynaklarının önemli bir bölümü zaten boşaltılmaktadır. Ancak thread kernel nesnesinin tam olarak
boşaltımını sağlamak için CloseHandle fonksiyonu uygulanabilir.
Aşağıdaki örnekte bir thread yaratılmıştır. Hem ana threadhem de yaratılan thread çalışmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc(LPVOID param);
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
DWORD dwThreadId;
HANDLE hThread;
printf("main thread begins...\n");
if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL)
ExitSys("CreateThread");
for (int i = 0; i < 10; ++i) {
printf("Main thread: %d\n", i);
Sleep(1000);
}
CloseHandle(hThread);
return 0;
}
DWORD __stdcall ThreadProc(LPVOID param)
{
for (int i = 0; i < 10; ++i) {
printf("Other thread: %d\n", i);
Sleep(1000);
}
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde thread işlemleri başı "pthread_" ila başlatılan POSIX fonksiyonlarıyla yapılmaktadır. Thread işlemleri için
kullanılan bu fonksiyonların oluşturduüu topluluğa "pthread kütüphanesi" de denilmektedir. Yukarıda da belirttiğimiz gibi bu kütüpahendeki
tüm fonksiyonlar pthread_xxx biçiminde isimlendirilmiştir. Tüm thread fonksiyonlarının prototipleri <pthread.h> isimli başlık dosyasının
içerisindedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde thread yaratmak için pthread_create isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
#include <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'sinin yerleştirileceği pthread_t türünden nesnenin adresini almaktadır. Bu sistemlerde
thread'lerin yalnızca id'leri vardır. Thread işlemleri de id'lerle yapılmaktadır. pthread_t türü işletim sistemini yazanlar tarafından
herhangi bir olarak typedef edilebilmektedir. Linux sistemlerinde bu tür unsigned long olarak typedef edilmiştir. Ancak başka sistemlerde
bir yapı biçiminde de typedef edilmiş olabilir. Fonksiyonun ikinci parametresi yaratılacak thread'e ilişkin bazı özelliklerin belirtildiği
thread özellik nesnesinin adresinin almaktadır. Programcı thread özelliklerini bu nesne ile oluşturup bu nesnenin adresini fonksiyona
vermektedir. Ancak bu parametre NULL adres olarak da geçilebilir. Bu durumda thread default özelliklerle yaratılacaktır. Fonksiyonun üçüncü
parametresi thread akışının başlatılacağı fonksiynun adresini belirtmektedir. Thread fonksiyonlarının geri dönüş değerlerinin void * türünden
parametrelerinin de void * türünden olması gerekir. Örneğin:
void *thread_proc(void *param)
{
....
}
Fonksiyonun son parametresi thread fonksiyonuna geçirilecek olan argümanı belirtmektedir. Tabii eğer thread fonksiyonuna bir parametre
geçirilmek istenmiyorsa bu parametre için NULL adres kullanılabilir.
pthread_create fonksiyonu başarı durumunda 0 değerine geri dönmektedir. Fonksiyon başarısızlık durumunda errno değişkeninin set etmez.
Başarısızlığı belirten errno değeri ile geri döner. Biz de bu değeri strerror fonksiyonu ile yazıya dönüştürüp yazdırabiliriz. Örneğin:
pthread_t tid;
int result;
void *thread_proc(void *param);
...
if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(result));
exit(EXIT_FAILURE);
}
...
Tabii işlemleri kısaltmak için hata durumunu ele alan bir fonksiyon da yazabiliriz:
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
pthread_create fonksiyonuyla yaratılmış olan thread'ler hemen çalışmaya başlamaktadır. Aşağıda UNIX/Linux sistemlerinde thread yaratmaya
bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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);
}
CloseHandle(hThread);
return 0;
}
void *thread_proc(void *param)
{
for (int i = 0; i < 10; ++i) {
printf("other thread: %d\n", i);
sleep(1);
}
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Thread'ler çeşitli biçimlerde sonlanabilmektedir. Bir thread'in en doğal sonlanması thread fonksiyonunun bitmesi ile gerçekleşir. Hem
Windows sistemlerinde hem de UNIX/Linux ve macOS sistemlerinde thread fonksiyonu bittiğinde thread'ler de otomatik olarak sonlanmaktadır.
Yukarıdaki örneklerimizde sonlanma bu biçimde doğal yolla gerçekleşmiştir. Tavsiye edilen sonlanma biçimi böyledir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir thread herhangi bir noktada Windows'ta ExitThread API fonksiyonu ile UNIX/Linux ve macOS sistemlerinde pthread_exit fonksiyonu ile
sonlandırabilir. Bu fonksiyonları hangi thread akışı çağırırsa o thread sonlanmaktadır. exit fonksiyonunun tüm prosesi sonlandırdığına ancak
ExitThread ve pthread_exit fonksiyonlarının yalnızca tek bir thread'i sonlandırdığına dikkat ediniz.
Therad'lerin de tıpkı prosesler gibi exit kodları vardır. Windows sistemlerinde thread'lerin exit kodları DWORD değerle, UNIX/Linux ve macOS
sistemlerinde ise void * bir değerle temsil edilmektedir. Thread'in exit kodları thread sonlandığında ilgili prosesler tarafından alınıp
çeşitli amaçlarla kullanılabilmektedir. Ancak uygulamaların çoğunda bu exit kodunu kullanamaya gerek duyulmamaktadır.
Windows'taki ExitThread API fonksiyonunun prototipi şöyledir:
void ExitThread(
DWORD dwExitCode
);
Fonksiyon thread'in exit kodunu parametre olarak almaktadır.
Aşağıda Windows sistemlerinde ExitThread fonksiyonu ile thread'in sonlandırılmasına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc(LPVOID param);
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
DWORD dwThreadId;
HANDLE hThread;
printf("main thread begins...\n");
if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL)
ExitSys("CreateThread");
for (int i = 0; i < 10; ++i) {
printf("Main thread: %d\n", i);
Sleep(1000);
}
CloseHandle(hThread);
return 0;
}
DWORD __stdcall ThreadProc(LPVOID param)
{
for (int i = 0; i < 10; ++i) {
printf("Other thread: %d\n", i);
if (i == 5)
ExitThread(0);
Sleep(1000);
}
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux ve macOS sistemlerinde thread'i sonlandırmak için kullanılan pthread_exit fonksiyonunun prototipi şöyledir:
#include <pthread.h>
void pthread_exit(void *retval);
Fonksiyon yine thread'in exit kodunu parametre olarka olmaktadır. Yukarıda da belirttiğimiz gibi bu sistemlerde thread'in exit kodu void *
temsil edilmektedir.
Aşağıda UNIX/Linux ve macOS sistemlerinde thread'in pthread_exit fonksiyonu ile sonlandırılmasına 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);
}
CloseHandle(hThread);
return 0;
}
void *thread_proc(void *param)
{
for (int i = 0; i < 10; ++i) {
printf("other thread: %d\n", i);
if (i == 5)
pthread_exit(NULL);
sleep(1);
}
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir proses sonlandığında prosesin bütün thread'leri de sonlandırılmaktadır. Örneğin biz exit fonksiyonunu çağırdığımızda exit fonksiyonu
tüm prosesi sonlandıracağı için bütün thread'lerde sonlanacaktır. C' de main fonksiyonu bittiğinde exit fonksiyonu ile prosesin sonlandırıldığını
anımsayınız. Bu durumda main fonksiyonu biterse prosesin tüm thread'leri de sonlanacaktır. Thread'ler konusuna yeni başlayan kişiler
bu hatayı çok sık yapmaktadır. Aşağıdaki örnekte main fonksiyonunda bir thread yaratılmış ancak main fonksiyonu hemen sonlanmıştır. Bu
durumda yaratılmış olan thread de tüm program da sonlanacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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 bitince yaratılmış olan thread de sonlandırılacaktır */
return 0;
}
void *thread_proc(void *param)
{
for (int i = 0; i < 10; ++i) {
printf("other thread: %d\n", i);
if (i == 5)
pthread_exit(NULL);
sleep(1);
}
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Threadler arasında Windows sistemlerinde ve UNIX/Linux sistemlerinde üstlük/altlık (parent/child) ilişkisi yoktur. Yani bir thread'i hangi
thread'in yarattığının genel olarak bir önemi yoktur. (Bu konuda bazı ayrıntılar bulunmaktadır.) Bir thread başka bir thread'i Windows
sistemlerinde TerminateThread API fonksiyonuyla o anda zorla sonlandırabilir. Bu tür sonlandırmalar tavsiye edilmemektedir. Çünkü bir
thread'in belli bir noktada (örneğin printf fonksiyonunun içerisinde) zorla sonlandırılması programın çökmesine yol açabilmektedir.
TerminateThread API fonksiyonunun prototipi şöyledir:
BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);
Fonksiyonun birinci parametresi sonlandırılack thread'inb handle değerini almaktadır. İkinci parametre ise thread'in exit kodunu belirtmektedir.
Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir.
Aşağıdaki örnekte ana thread diğer thread'i zorla TerminateThread API fonksiyonuyla sonlandırmıştır. Burada tüm program bu zorla sonlandırmadan
olumsuz etkilenebilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc(LPVOID param);
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
DWORD dwThreadId;
HANDLE hThread;
printf("main thread begins...\n");
if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL)
ExitSys("CreateThread");
for (int i = 0; i < 10; ++i) {
if (i == 5)
if (!TerminateThread(hThread, 0))
ExitSys("TerminateThread");
printf("Main thread: %d\n", i);
Sleep(1000);
}
CloseHandle(hThread);
return 0;
}
DWORD __stdcall ThreadProc(LPVOID param)
{
for (int i = 0; i < 10; ++i) {
printf("Other thread: %d\n", i);
Sleep(1000);
}
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde de bir thread başka bir thread'i zorla pthread_cancel fonksiyonu ile sonlandırabilir. Ancak bu fonksiyon Windows
sistemlerindeki TerminateThread fonksiyonu gibi çalışmamaktadır. UNIX/Linux sistemlerinde bir thread'e pthread_cancel fonksiyonu uygulanırsa
thread akışı ancak bazı POSIX fonksiyonlarında sonlandırılmaktadır. Dolayısıyla bu sistemlerde pthread_cancel fonksiyonu TerminateThread
fonksiyonuna göre daha güvenlidir. pthread_cancel uygulandığında thread akışının sonlandırılabileceği POSIX fonksiyonlarına İngilizce
"cancellation points" denilmektedir. Bu fonksiyonarın listesi POSIX standartrlarında belirtilmiştir.
pthread_cancel fonksiyonunun prototipi şöyledir:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Fonksiyon parametre olarak sonlandırılacak thread'in id değerini almaktadır. Başarı durumunda 0 değerine başarısızlık durumunda errno
değerine geri dönmektedir.
Aşağıdaki örnekte ana thread'teki döngü 5 kez yibelendikten sonra diğer thread'i pthread_cancel fonksiyonu ile sonlandırmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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);
if (i == 5)
if ((result = pthread_cancel(tid)) != 0)
exit_sys_errno("pthread_cancel", result);
sleep(1);
}
return 0;
}
void *thread_proc(void *param)
{
for (int i = 0; i < 10; ++i) {
printf("other thread: %d\n", i);
if (i == 5)
pthread_exit(NULL);
sleep(1);
}
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki örnekte pthread_cancel fonksiyonu sonsuz döngüdeki thread'i sonlandıramayacaktır. Çünkü bu thread sonlandırma için gereken
POSIX fonksiyonlarına (cancellation points) girmemiştir. Thread'in sonlandırılıp sonlandırılmadığını başka bir terminalden ps komutunda -T
seçeneğini kullanarak görebilirsiniz. Komut şöyle uygulanabilir:
$ ps -t /dev/pts/0 -T
Burada /dev/pts/0 thread'li programın çalıştığı termşnali belirtmektedir. Bu terminal sizin denemenizde farklı olabilir. Bu terminali
tty komutu ile öğrenebilirisniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_proc(void *param);
void exit_errno(const char *msg, int result);
int main(int argc, char *argv[])
{
pthread_t tid;
int result;
int i;
if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0)
exit_errno("pthread_create", result);
for (i = 0; i < 10; ++i) {
if (i == 5)
if ((result = pthread_cancel(tid)) != 0)
exit_errno("pthread_cancel", result);
printf("main thread: %d\n", i);
sleep(1);
}
printf("press ENTER to exit..\n");
getchar();
return 0;
}
void *thread_proc(void *param)
{
for (;;)
;
return NULL;
}
void exit_errno(const char *msg, int result)
{
fprintf(stderr, "%s: %s\n", msg, strerror(result));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Thread'lerin stack'leri birbirlerinden ayrılmıştır. Bu nedenle farklı thread akışları aynı fonksiyon üzerinde ilerlerken o fonksiyondaki
yerel değişkenlerin ve parametre değişkenlerinin farklı kopyalarını kullanıyor durumda olurlar. Aşağıdaki Windows örneğinde ana thread
ve yaratılan thread aynı Foo fonksiyonunu çağırmıştır. Ancak Foo içerisindeki i nesnesi iki threda için de farklı i nesneleridir. Bu örneği
i yerel değişkenini global değişken yaparak da çalışırınız. İki çalıştırma arasındaki farkı gözlemleyiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc(LPVOID param);
void Foo(LPCSTR pszName);
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
DWORD dwThreadId;
HANDLE hThread;
printf("main thread begins...\n");
if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL)
ExitSys("CreateThread");
Foo("main thread");
return 0;
}
DWORD __stdcall ThreadProc(LPVOID param)
{
Foo("other thread");
return 0;
}
int i = 0;
void Foo(LPCSTR pszName)
{
while (i < 10) {
printf("%s: %d\n", pszName, i);
Sleep(1000);
++i;
}
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir thread'in sonlanmasının blokede beklenmesi sıkça gerekebilmektedir. Örneğin ana thread bir thread yaratıp belli bir noktada thread
sonlanana kadar beklemek isteyebilir.
Windows sistemlerinde thread sonlanana kadar bekleme yapmak için WaitForSingleObject ve WaitForMultipleObjects isimli API fonksiyonları
bulunmaktadır. Bu fonksiyonlar yalnızca thread'leri beklemek için değil diğer kernel senkronizasyon nesnelerini de beklemek için
kullanılmaktadır. Yani bu fonksiyonlar genel bir bekleme amacıyla tasarlanmıştır. Biz bu fonksiyonların kernel senkronizasyon nesneleriyle
nasıl kullanılacağını ilerleyen paragraflarda göreceğiz. Ancak burada yalnızca bu fonksiyonların thread'leri beklemek için nasıl kullanılacağı
üzerinde duracağız.WaitForSingleObject fonksiyonun prototipi şöyledir:
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
Yukarıda da belirttiğimiz gibi WaitForSingleObject fonksiyonu genel bir fonksiyondur. Bu fonksiyon bir senkronizasyon nesnesi kapalı
(nonsignaled) olduğu sürece bekleme yapar. Senktronizasyon nesnesi açık duruma (signaled) geçtiğinde bekleme sonlanır. Her senkronizasyon
nesnesinin hangi durumda kapalı hangi durumda açık olduğu ayrıca öğrenilmelidir. İşte thread'ler de bir senkronizasyon nesnesi gibi
kullanılabilmektedir. Thread senkronizasyon nesnesi thread devam ettiği sürece kapalı durumdadır. Thread sonlanınca açık duruma geçer.
O halde bu fonksiyon thread bitene kadar bekleme sağlamaktadır. Fonksiyonun ikinci parametresi milisaniye cinsinden "zaman aşımı (timeout)"
belirtmektedir. Eğer nesne burada belirtilen zaman aşımı dolana kadar açık hale gelmezse en fazla bu zaman aşıma kadar bekleme yapılmaktadır.
Bu parametre için INFINITE özel değeri girilirse zaman aşımı kullanılmaz. NEsne açık duruma geçene kadar bekleme yapılır.
WaitForSingleObject fonksiyonu başarısızlık durumunda WAIT_FAILED değeri ile geri dönmektedir. Bunun dışında diğer geri dönüş değerleri
şunlardan biri olabilir:
WAIT_OBJECT_0: Nesne açık duruma geçiği için fonksiyon sonlanmıştır. Bu en normal durumdur.
WAIT_TIMEOUT: Zaman aşımı nedenyiyle fonksiyon sonlanmıştır.
WAIT_ABONDONED: Mutex'in beklendiği durumda mutex'in sahipliğini alan thread'in sonlanması nedeniyle fonksiyon sonlanmıştır. Bu duruma
"abondoned mutex" denilmektedir.
WaitForSingleObject fonksyionu çağrıldığında zaten nesne açık durumdaysa (signaled) fonksiyon hiç bekleme yapmaz, WAIT_OBJECT_0 değeri ile
hemen geri döner.
Bu durumda bir thread sonlanana kadar bekleme yapmak şöyle sağlanabilir:
if (WaitForSingleObject(hThread, INFINITE) == WAIT_FAILED)
ExitSys("WaitForSingleObject")
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc(LPVOID param);
void Foo(LPCSTR pszName);
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
DWORD dwThreadId;
HANDLE hThread;
printf("main thread begins...\n");
if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL)
ExitSys("CreateThread");
printf("main thread waits at WaitForSingleObject...\n");
if (WaitForSingleObject(hThread, INFINITE) == WAIT_FAILED)
ExitSys("WaitForSingleObject");
CloseHandle(hThread);
printf("main thread continues...\n");
return 0;
}
DWORD __stdcall ThreadProc(LPVOID param)
{
for (int i = 0; i < 10; ++i) {
printf("Other thread: %d\n", i);
Sleep(1000);
}
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
WaitForMultipleObjects fonksiyonu birden fazla senkronizasyon nesnesini beklemek için kullanılmaktadır. Örneğin biz 10 thread yaratmış
olalım. Bunların hepsi sonlanana kadar bekleme yapmak isteyelim. Bunun bir yolu WaitForSingleObject fonksiyonunu 10 kez çağırmaktır.
Diğer bir yolu ise bir kez WaitForMultipleObject fonksiyonunu kullanmaktır.
WaitForMultipleObjects fonksiyonunun prototipi şöyledir:
DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE *lpHandles,
BOOL bWaitAll,
DWORD dwMilliseconds
);
Fonksiyonun birinci parametresi kaç senkronizasyon nesnesinin bekleneceğini belirtmektedir. (Yani bu parametre ikinci parametredeki dizinin
uzunluğunu belirtir.) İkinci parametre beklenecek senkronizasyon nesnelerinin hhandle değerlerinin bulundurğu dizinin adresini almaktadır.
Üçüncü parametre tek bir nesnenin mi yoksa bütün nesnelerin mi açık duruma geçince beklemenin sonlandırılacağını belirtir. Eğer bu parametre
TRUE geçilirse tüm nesneler açık duruma geçene kadar bekleme yapılır. Eğer bu parametre FALSE geçilirse en az bir nesne açık duruma geçene
kadar bekleme yapılır. Son parametre yine zaman aşımı belirtmektedir. Bu parametre INFINITE geçilebilir.
WaitForMultiplrObjects fonksiyonu başarısız olursa WAIT_FAILED değerine geri dönmektedir. Eğer fonksiyon zaman aşımı dolayısıyla sonlanmışsa
yine WAIT_TIMEOUT değerine geri döner. Eğer fonksiyonun üçüncü parametresi TRUE geçilirse tüm senkronizasyon nesneleri açıldığında fonksiyon
WAIT_OBJECT_0 değerinden WAIT_OBJECT_0 + nCunt değerine kadar herhangi bir değerle geri dönebilir. Eğer fonksiyonun üçüncü parametresi
FALSE geçilirse fonksiyon hangi senkronizasyon nesnesi açık duruma geçtiyse ona ilişkin WAIT_OBJET_0 + n değerine geri döner. Burada
n açık duruma geçen nesnenin dizideki indeksini belirtmektedir. Bu durumda birden fazla nesne açık duruma geçerse fonksiyon en düşük
indeksle geri dönmektedir.
Aşağıdaki örnekte 10 thread yaratılmış ve 10 thread'in sonlanması WaitForMultipleObjects fonksiyonu ile beklenmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define NTHREADS 10
DWORD __stdcall ThreadProc(LPVOID param);
void Foo(LPCSTR pszName);
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
DWORD dwThreadIds[NTHREADS];
HANDLE hThreads[NTHREADS];
char szName[64];
char *pszName;
printf("main thread begins...\n");
for (int i = 0; i < NTHREADS; ++i) {
sprintf(szName, "Thread %d", i);
if ((pszName = strdup(szName)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
if ((hThreads[i] = CreateThread(NULL, 0, ThreadProc, pszName, 0, &dwThreadIds[i])) == NULL)
ExitSys("CreateThread");
}
printf("main thread waits at WaitForSingleObject...\n");
if (WaitForMultipleObjects(NTHREADS, hThreads, TRUE, INFINITE) == WAIT_FAILED)
ExitSys("WaitForMultipkeObjects");
for (int i = 0; i < NTHREADS; ++i)
CloseHandle(hThreads[i]);
printf("main thread continues...\n");
return 0;
}
DWORD __stdcall ThreadProc(LPVOID param)
{
const char *pszName = (const char *)param;
for (int i = 0; i < 10; ++i) {
printf("%s: %d\n", pszName, i);
Sleep(1000);
}
free(pszName);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Thread'lerin de exit kodları vardır. Windows'ta thread'lerin exit kodları GetExitCodeThread fonksiyonu ile, UNIX/Linux sistemlerinde
sonraki paragrafta göreceğimiz pthread_join fonksiyonu ile elde edilmeketdir. Tabii sonlanmamış bir thread'in exit kodunun elde edilmeye
çalışılması anlamsızdır. Therad'lerin exit kodları thread fonksiyonlarının geri dönüş değerleridir. Windows'ta thread'lerin exit kodları
DWORD bir değerken UNIX/Linux sistemlerinde void * türünden bir değerdir. Anımsanacağı gibi thread'lerde üstlük-altlık (parent-child)
durumu yoktur. Bir thread'in exit kodu herhangi bir thread tarafındna alınabilir.
Windows'ta GetExitCodeThread fonksiyonunun protoipi şöyledir:
BOOL GetExitCodeThread(
HANDLE hThread,
LPDWORD lpExitCode
);
Foksiyonun birinci parametresi exit kodu elde edilecek thread'in HANDLE değerini belirtir. İkinci parametre exit kodunun yerleştirileceği
DWORD nesnenin adresini almaktadır. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri döner.
Aşağıdaki örnekte Windows'ta yaratılan bir thread'in exit kodu elde eidlmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc(LPVOID param);
void Foo(LPCSTR pszName);
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
DWORD dwThreadId;
HANDLE hThread;
DWORD dwExitCode;
printf("main thread begins...\n");
if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL)
ExitSys("CreateThread");
printf("main thread waits at WaitForSingleObject...\n");
if (WaitForSingleObject(hThread, INFINITE) == WAIT_FAILED)
ExitSys("WaitForSingleObject");
if (!GetExitCodeThread(hThread, &dwExitCode))
ExitSys("GetExitCodeThread");
CloseHandle(hThread);
printf("Exit Code: %u\n", dwExitCode);
return 0;
}
DWORD __stdcall ThreadProc(LPVOID param)
{
for (int i = 0; i < 10; ++i) {
printf("Other thread: %d\n", i);
Sleep(1000);
}
return 123;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde thread'lerin sonlanmasının beklenmesi ve exit kodlarının alınması pthread_join fonksiyonuyla sağlanmaktadır.
Yani fonksiyon hem bekleme yapıp hem de exit kodu almaktadır. Fonksyonunun prototipi şöyledir:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Fonksiyonun birinci parametresi beklemecek thread'in id değerini belirtmektedir. İkinci parametre exit kodunun yerleştirileceği void
göstericinin adresini belirtmektedir. Eğer ikinci parametre NULL geçilirse thread'in birmesi beklenir ancak exit kodu çağıran fonksiyona
iletilmez. Fonksiyon baları durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir.
Aşağıdaki örnekte main fonksiyonu içerisinde (yani ana thread'te) bir thread yaratılmış e thread'in sonlanması pthread_join fonksiyonuyla
beklenmiştir. Bu örnekte exit kodu bir tamsayı olduğu halde sanki bir adresmiş gibi oluşturulmuştur. Yine exit kod göstericinin içerisinden
alınarak int tüürne dönüştürülüp kullanılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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 *exit_code;
printf("main begins...\n");
if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0)
exit_sys_errno("pthread_create", result);
printf("main thread waits at pthread_join...\n");
if ((result = pthread_join(tid, &exit_code)) != 0)
exit_sys_errno("pthread_join", result);
printf("Exit code: %d\n", (int)exit_code);
return 0;
}
void *thread_proc(void *param)
{
for (int i = 0; i < 10; ++i) {
printf("other thread: %d\n", i);
sleep(1);
}
return (void *)123;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde "zombie proses" kavramı vardı. Pekiyi zombie thread kavramı da var mıdır? Aslında bu sistemlerde işletim sistemi
tıpkı proseslerde olduğu gibi therad'in ezit kodu alınmamışsa belli bir sistem kaynağını serbest bırakmadan bekletmektedir. Yani zombie
thread kavramı zombie proses kavramı gibi bu sistemlerde söz konsudur. Ancak zombie thread'ler zombie prosesler kadar probleme yol açma
potansiyelinde değildir. Fakat yine pthread_join fonksiyonuyla zombie thread'lerin oluşması engellenmelidir. Eğer programcı thread'in exit
kodu ile ilgilenmiyorsa thread biter bitmez kaynakalrın boşaltılmasını işletim sisteminden isteyebilir. Bunun için pthread_detach fonksiyonu
kullanılmaktadır. Fonksiyonun prototipi şöyledir:
#include <pthread.h>
int pthread_detach(pthread_t thread);
Fonksiyon parametre olarak thread'in id değerini alır, başarı drumunda sıfır değerine, başarısızlık durumunda errno değerine geri döner.
Tabii detach moda sokulmuş thread'ler artık pthread_join fonksiyonuyla beklenemezler. Eğer bunlar beklenmeye çaışılırsa ptherad_join
fonksiyonu başarısız olmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Thread'ler konusunun en önemli alt konusu thread senkronizasyonudur. Bir grup thread birlikte bir işi gerçekleştirirken kimi zaman
birbirlerini beklemesi, birbirleriyle koordineli bir biçimde çalışması gerekmektedir. İşte işletim sistemlerinde bunu sağlamaya yönelik
mekanizmalara ""thread senkronizasyonu" denilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Thread senkronizasyonunun önemi basit örnekle anlaşılabilir. Thread'lerin aynı global nesneleri kullandığını belirtmiştik. İki thread
aynı global değişkeni bir döngü içerisinde bir milyon kere artırıyor olsun. Bu global değişkenin değerinin iki milyon olması beklenir.
Ancak senkronizasyon problemi yüzünden muhtemelen iki milyon olamayacaktır. Aşağıda bu örnek Unix/Linux sistemleri için oluşturulmuştur.
Programın farklı çalıştırılmalarında elde edilen bazı değerler şunlardır:
$ ./sample
1140644
$ ./sample
1175870
$ ./sample
1900343
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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);
int g_count;
int main(void)
{
pthread_t tid1, tid2;
int result;
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_join(tid1, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_join(tid2, NULL)) != 0)
exit_sys_errno("pthread_join", result);
printf("%d\n", g_count);
return 0;
}
void *thread_proc1(void *param)
{
for (int i = 0; i < 1000000; ++i)
++g_count;
return NULL;
}
void *thread_proc2(void *param)
{
for (int i = 0; i < 1000000; ++i)
++g_count;
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıdaki program Windows sistemlerinde de aşağıdaki gibi yazılabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
void ExitSys(LPCSTR lpszMsg);
int g_count;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
printf("%d\n", g_count);
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
int i;
for (i = 0; i < 1000000; ++i)
++g_count;
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
int i;
for (i = 0; i < 1000000; ++i)
++g_count;
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Thread senkronizasyon problemleri tipik olarak birden fazla thread'in ortak bir kaynak üzerinde çalıştığı durumda ortaya çıkmaktadır.
Thread'lerden biri bu ortak kaynak üzerinde yazma (gemel olarka güncelleme) yaptığı bir durumda bu işlemler sırasında thread'ler arası
geçiş oluşursa ve başka bir thread de bu kararsız durumda kalmış kaynağı kullanırsa ya da o da bu kaynağa yazma yaparsa diğer thread
kaldığı yerden çalışmasına devam ettiğinde sorun oluşacaktır. Burada kaynak (resource) demekle ortak kullanılan bir nesne kastedilmektedir.
Bu nesne global bir değişken olabileceği gibi dış dünyadaki gerçek bir donanımsal aygıt olabilir.
Örneğin iki thread dış dünyadaki bir makineyi sırasıyla 1, 2, 3, 4, 5 konumlarına sokarak kullanıyor olsun. Bu kodu aşağıdaki gibi temsil
edelim:
...
<Makineyi 1'inci konuma sok>
<Makineyi 2'inci konuma sok>
<Makineyi 3'üncü konuma sok>
<Makineyi 4'üncü konuma sok>
<Makineyi 5'inci konuma sok>
...
Şimdi thread'lerden biri aşağıdaki noktada threadler arası geçiş (context switch) oluşup kesilmiş olsun:
...
<Makineyi 1'inci konuma sok>
<Makineyi 2'inci konuma sok>
<Makineyi 3'üncü konuma sok>
----> Bu noktada "context switch" olsun
<Makineyi 4'üncü konuma sok>
<Makineyi 5'inci konuma sok>
...
Başka bir thread aynı makineyi kullanmak istesin ve başından sonuna kadar kesilmeden makineyi konumlara sokarak kullansın. Makine şimdi
beşinci konumdadır. Şimdi daha önce kesilen thread kaldığı noktadn çalışmaya devam etsin. Bu thread makineyi 3'üncü konumda sanmaktadır.
Halbuki makine şu anda 5'inci konumdadır. Muhtemelen beklenmedik sorunlar oluşacaktır.
Şimdi de yukarıdaki sayaç örneğinin neden düzgün çalışmadığınııklayalım. Bu örnekte iki thread de ++g_count ile global değişkeni 1
artırmaktadır. Ancak derleyiciler bu ++g_count işlemini tipik olarak üç makine komutuyla yapmaktadır:
MOV reg, g_count
INC reg
MOV g_count, reg
Önce g_count CPU yazmacına çekilmiştir. Sonra bu yazmaç değer 1 artırılmıştır. Sonra da artırılmış değer yeniden g_count nesnesine
yerleştirilmiştir. Şimdi tam aşağıdaki noktada tesadüfen bir thread'ler arası geçişin oluştuğunu varsayalım:
MOV reg, g_count
----> Bu noktada "context switch" olsun
INC reg
MOV g_count, reg
Bu noktada g_cunt değerinin 1250305 olduğunu varsayalım. Şimdi diğer thread kesilmedne bir quanta çalışıp g_count değerini örneğin
1756340'a getirmiş olsun. Önceki thread kalan noktadan çalışmaya devam ettiğinde yazmaçta 1250305 değeri olacaktır. Bunu 1 artırdığında
1250306 elde edilir. Bu değeri yeniden g_count nesnesine yerleştrecek ve g_count'taki değeri bozacaktır.
Thread'ler arası geçiş bir makine komutu çalışırken oluşmaz, iki makine komutu arasında oluşabilir. Ancak hangi makine komutunda bu
geçişin oluşacağu "quanta" durumuna bağlıdır. Dolayısıyla bir thread herhangi bir makine komutunda kesilebilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Başından sonuna kadar tek bir thread akışı tarafından çalıştırılması gereken kod bloklarına "kritik kod blokları (critical sections)"
denilmektedir. Kritik kod bloklarına bir thread girdiğinde thread'ler arası geçiş (context switch) oluşabilir. Ancak diğer thread'ler
bu bloğa girmek istediğinde daha girmiş olan thread'in buradan çıkmasını beklerler. Böylece başından sonuna kadar tek bir threda akışı
tarafından kritik kodlar çalıştırılmış olur. Yukarıdaki örneklerimizde g_count nesnesinin artırılması bir kritik koddur. Örneğin:
...
MOV reg, g_count
INC reg
MOV g_count, reg
...
Bu üç makine komutu bir kritik kod oluşturmaktadır. Yani thread bu kodları çalıştırırken kesilebilir ancak başka bir thread diğeri
çıkana kadar bu kritik koda girmez. Tabii kritik kod oluşturmanın diğer bir yolu geçici süre thread'ler arası geçişi engellemek olabilir.
Ancak bu yöntemin user mode'tan uygulanması mümkün değildir. Yukarıdaki makine örneğimizde de makineyi kullanan kod kritik bir koddur:
...
<Makineyi 1'inci konuma sok>
<Makineyi 2'inci konuma sok>
<Makineyi 3'üncü konuma sok>
<Makineyi 4'üncü konuma sok>
<Makineyi 5'inci konuma sok>
...
Bir thread bu kritik koda girdiğinde arada kesilse bile başka thread bu thread kritik koddan çıkana kadar kritik koda girmemelidir.
Senkronizasyon bağlamında bir grup makine komutunun sanki tek bir makine komutuymuş gibi kesilmeden çalıştırılmasına "atomiklik (atomicity)"
denilmektedir. Yani "bir işlemin atomik bir biçimde yapılması" demek "kesilmeden başka bir deyişle thread'ler arası geçiş oluşmadan"
yapılması demektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Kritik kodların oluşturulabilmesi işletim sistemi tarafından sağlanan özel sistem fonksiyonları ya da bunları kullanan kütüphane
fonksiyonlarıyla sağlanabilmektedir. Kritik kodlar manuel biçimde işletim sisteminin desteği olmadan ya da özel birtakım yööntemler
kullanılmadan oluşturulamaz. Örneğin aşağıdaki gibi bir kritik kod oluşturma mümkün değildir:
int g_flag = 0;
...
while (g_flag == 1)
;
g_flag = 1;
......
...... <KRİTİK KOS>
......
g_falg = 0;
Buradaki kodun iki önemli problemi vardır:
1) Burada tam while döngüsünden çıkılmışken ancak g_flag = 1 işlemi yapılmadan thread'ler arası geçiş oluşabilir:
while (g_flag == 1)
;
----> Dikkat tan bu noktada "context switch" olabilir
g_flag = 1;
......
...... <KRİTİK KOS>
......
g_falg = 0;
Bu durumda g_flag 0 konumunda kalmıştır ancak thread kritik koda girmiştir. Yani bir thread de bu thread de kritik kodda ilerleyebilir.
2) Burada bekleme "meşgul bir döngüyle (busy loop)" yapılmaktadır. Yani bekleme yapılırken gereksiz biçimde CPU zamanı harcanmaktadır.
İşet bu tür kodlar özel birtakım mekanizmaalr olmadan oluşturulamamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Kritik kodlar tek bir blok biçiminde olmayabilir. Birden fazla yere yayılmış olarak bulunabilirler. Örneğin global bir bağlı listeye
thread'lerden biri ekleme yaparken diğer bir thread ekleme de silme de hatta dolaşma da yapmamı gerekir. Burada bağlı listeye ekleme
yapan, bağlı listeden silme yapan ve bağlı listeyi dolaşan kodlar kritik kodlardır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows'ta kritik kod oluşturmak için en yalın ve hızlı yöntem CRITICAL_SECTION isimli nesneyi kullanmaktır. Bu nesne ile kritik kodlar
şöyle oluşturulmaktadır:
1) Önce global bir değişken biçiminde CRITICAL_SECTION türünden bir nesne yaratılır. CRITICAL_SECTION typedef edilmiş bir yapı türünü
belirtmektedir. Ancak programcının bu yapının içeriğini bilmesine gerek yoktur. Örneğin:
CRITICAL_SECTION g_cs;
2) Yaratılan bu nesneye initializeCriticalSection API fonksiyonuyla ilkdeğerleri verilir. Fonksiyonun prototipi şöyledir:
void InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
Fonksiyon CRITICAL_SECTION nesneninin adresini parametre olarak almaktadır. Bu ilkdeğer verme işlemi henüz thread'ler yaratılmadan
yapılabilir. Örneğin:
InitializeCriticalSection(&g_cs);
3) Kritik kod EnterCriticalSection ve LeaveCriticalSection API fonksiyonları arasına alınır. Örneğin:
EnterCriticalSection(&g_cs);
...
... <KRİTİK KOD>
...
LeaveCriticalSection(&g_cs);
Fonksiyonların prototipleri şöyledir:
void EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
void LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
Her iki fonksiyon da CRITICAL_SECTION nesnesinin adresini almaktadır. Bir thread EnterCriticalSection fonksiyonundan girdiğinde
LeaveCriticalSection fonksiyonunu çağırana kaadar nesneyi kilitlemiş olur. Böylece başka bir thread EnterCriticalSection fonksiyonundan
geçiş yapma istediğinde bloke olur. Ta ki önceki thread LeaveCriticalSection fonksiyonunu çağırana kadar. CRITICAL_SECTION nesnesinin
bir kilit gibi davrandığına dikkat ediniz. Bir thread bu fonksiyondan geçtiğinde nesne kilitlenmekte başka thread'ler kritik koda girememektedir.
LeaveCriticalSection kritik kodun kilidini açmaktadır.
Bir thread EnterCriticalSection fonksiyonundan geçerek nesneyi kilitlemiş olsun. Bu sırada birden fazla thread nesne kilitli olduğu için
EnterCriticalSection fonksiyonunda bekliyor olsun. İlk thread kritik koddan çıktığında EnterCriticalSection fonskiyonunda bloke olmuş olan
hangi thread kritik koda girecektir? En adil durumun ilk gelen thread'in girmesi olduğunu düşünebilirsiniz. Ancak Windows sistemleri
çeşitli nedenlerden dolayı bunun garantisini vermemektedir.
4) Kullanım bittikten sonra CRITICAL_SECTION nesnesi DeleteCriticalSection fonksiyonu ile yok edilmeldir. Örneğin:
DeleteCriticalSection(&g_cs);
Fonksiyonun prototipi şöyledir:
VOID DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
Fonksiyon yine CRITICAL_SECTION nesnesinin adresini almaktradır.
Kritik kod bloğu birden fazla yere yayılmış olarak bulunabilir. Önemli olan Bu fonksiyonlarda kullanılan nesnedir. Aynı nesne aynı kilidi
temsil etmektedir. Örneğin:
void insert_item(...)
{
...
EnterCriticalSection(&g_s);
....
.... <bağlı listeye eleman insert ediyor>
....
LeaveCriticalSection(&g_s);
...
}
void delete_item(...)
{
...
EnterCriticalSection(&g_s);
....
.... <bağlı listeden eleman siliyor>
....
LeaveCriticalSection(&g_s);
...
}
Burada bir thread eleman insert ederken diğer thread elemanı silemeyecektir, bir thread eleman silerken diğer thread eleman insert
edemeyecektir. Çünkü bu iki kritik kod aynı nesneyi yani kilidi kullanmaktadır. Dolayısıyla aslında aynı kritik kodun değişik parçalarıdır.
Aşağıda daha önce yapmış olduğumuz sayaç artırma örneğini CRITICAL_SECTION nensnesi kullanarak düzeltiyoruz:
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
void ExitSys(LPCSTR lpszMsg);
int g_count;
CRITICAL_SECTION g_cs;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
InitializeCriticalSection(&g_cs);
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
printf("%d\n", g_count);
DeleteCriticalSection(&g_cs);
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
int i;
for (i = 0; i < 1000000; ++i) {
EnterCriticalSection(&g_cs);
++g_count;
LeaveCriticalSection(&g_cs);
}
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
int i;
for (i = 0; i < 1000000; ++i) {
EnterCriticalSection(&g_cs);
++g_count;
LeaveCriticalSection(&g_cs);
}
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aağıdaki örnekte iki thread aynı makineyi sırasıyla 1, 2, 3, 4 ve 5 numaralı konumlara sokmaktadır. Kritik kod bloğu sayesinde thread'lerden
biri kritik koda girdiğinde diğeri kiritk koda girmemektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
void UseMachine(LPCSTR pszName);
void ExitSys(LPCSTR lpszMsg);
CRITICAL_SECTION g_cs;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
InitializeCriticalSection(&g_cs);
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
DeleteCriticalSection(&g_cs);
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
int i;
for (i = 0; i < 10; ++i)
UseMachine("Therad-1");
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
int i;
for (i = 0; i < 10; ++i)
UseMachine("Thread-2");
return 0;
}
void UseMachine(LPCSTR pszName)
{
EnterCriticalSection(&g_cs);
printf("-------------------\n");
printf("%s: 1. Step\n", pszName);
Sleep(rand() % 500);
printf("%s: 2. Step\n", pszName);
Sleep(rand() % 500);
printf("%s: 3. Step\n", pszName);
Sleep(rand() % 500);
printf("%s: 4. Step\n", pszName);
Sleep(rand() % 500);
printf("%s: 5. Step\n", pszName);
Sleep(rand() % 500);
LeaveCriticalSection(&g_cs);
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Örneğin iki thread aynı global bağlı listeye ekleme yapacak olsun. Eğer işlemler senkronize edilmezse program çökebilir ya da tanımsız
davranışlar oluşabilir. Aşağıda buna bir örnek verilmiştir. Örneği kritk kodları kaldırarak da test ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* sample.c */
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include "llist.h"
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
void ExitSys(LPCSTR lpszMsg);
CRITICAL_SECTION g_cs;
HLLIST g_hllist;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
InitializeCriticalSection(&g_cs);
if ((g_hllist = create_llist()) == NULL) {
fprintf(stderr, "cannot create linked list!..\n");
exit(EXIT_FAILURE);
}
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
DeleteCriticalSection(&g_cs);
printf("%zd\n", count_llist(g_hllist));
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
for (int i = 0; i < 1000000; ++i) {
EnterCriticalSection(&g_cs);
if (add_tail(g_hllist, i) == NULL) {
fprintf(stderr, "cannot add item..\n");
exit(EXIT_FAILURE);
}
LeaveCriticalSection(&g_cs);
}
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
for (int i = 0; i < 1000000; ++i) {
EnterCriticalSection(&g_cs);
if (add_tail(g_hllist, i) == NULL) {
fprintf(stderr, "cannot add item..\n");
exit(EXIT_FAILURE);
}
LeaveCriticalSection(&g_cs);
}
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/* llist.h */
/* llist.h */
#ifndef LLIST_H_
#define LLIST_H_
#include <stddef.h>
#include <stdbool.h>
/* Type Declarations */
typedef int DATATYPE;
typedef struct tagNODE {
DATATYPE val;
struct tagNODE *next;
} NODE;
typedef struct tagLLIST {
NODE head;
NODE *tail;
size_t count;
} LLIST, *HLLIST;
/* Function Prototypes */
HLLIST create_llist(void);
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val);
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val);
NODE *add_tail(HLLIST hllist, DATATYPE val);
NODE *addp_tail(HLLIST hllist, const DATATYPE *val);
NODE *add_head(HLLIST hllist, DATATYPE val);
NODE *addp_head(HLLIST hllist, const DATATYPE *val);
void remove_next(HLLIST hllist, NODE *node);
void remove_head(HLLIST hllist);
DATATYPE *getp_item(HLLIST hllist, size_t index);
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *));
void clear_llist(HLLIST hllist);
void destroy_llist(HLLIST hllist);
/* inline Function Definitions */
static inline size_t count_llist(HLLIST hllist)
{
return hllist->count;
}
#endif
/* llist.c */
/* llist.c */
#include <stdio.h>
#include <stdlib.h>
#include "llist.h"
/* static Functions Prototypes */
static bool disp(DATATYPE *val);
/* Function Definitions */
HLLIST create_llist(void)
{
HLLIST hllist;
if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL)
return NULL;
hllist->head.next = &hllist->head;
hllist->tail = &hllist->head;
hllist->count = 0;
return hllist;
}
NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = val;
if (node == hllist->tail)
hllist->tail = new_node;
new_node->next = node->next;
node->next = new_node;
++hllist->count;
return new_node;
}
NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val)
{
NODE *new_node;
if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL)
return NULL;
new_node->val = *val;
if (node == hllist->tail)
hllist->tail = new_node;
new_node->next = node->next;
node->next = new_node;
++hllist->count;
return new_node;
}
NODE *add_tail(HLLIST hllist, DATATYPE val)
{
return insert_next(hllist, hllist->tail, val);
}
NODE *addp_tail(HLLIST hllist, const DATATYPE *val)
{
return insertp_next(hllist, hllist->tail, val);
}
NODE *add_head(HLLIST hllist, DATATYPE val)
{
return insert_next(hllist, &hllist->head, val);
}
NODE *addp_head(HLLIST hllist, const DATATYPE *val)
{
return insertp_next(hllist, &hllist->head, val);
}
void remove_next(HLLIST hllist, NODE *node)
{
NODE *next_node;
if (node == hllist->tail)
return;
if (node->next == hllist->tail)
hllist->tail = node;
next_node = node->next;
node->next = next_node->next;
--hllist->count;
free(next_node);
}
void remove_head(HLLIST hllist)
{
remove_next(hllist, &hllist->head);
}
DATATYPE *getp_item(HLLIST hllist, size_t index)
{
NODE *node;
if (index >= hllist->count)
return NULL;
node = hllist->head.next;
for (size_t i = 0; i < index; ++i)
node = node->next;
return &node->val;
}
bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *))
{
bool retval = true;
bool def_flag = false;
if (proc == NULL) {
proc = disp;
def_flag = true;
}
for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next)
if (!proc(&node->val)) {
retval = false;
break;
}
if (def_flag)
putchar('\n');
return retval;
}
void clear_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head.next;
while (node != &hllist->head) {
temp_node = node->next;
free(node);
node = temp_node;
}
hllist->head.next = &hllist->head;
hllist->tail = &hllist->head;
hllist->count = 0;
}
void destroy_llist(HLLIST hllist)
{
NODE *node, *temp_node;
node = hllist->head.next;
while (node != &hllist->head) {
temp_node = node->next;
free(node);
node = temp_node;
}
free(hllist);
}
static bool disp(DATATYPE *val)
{
printf("%d ", *val);
fflush(stdout);
return true;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda C++'ta birden fazla thread'in "vector" isimli dinamik diziye ekleme yapmasına ilişkin benzer bir örnek verilmiştir. Bu örneği de
krtik kodları kaldırarak ve muhafaza ederek ayrı ayrı test ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#include <vector>
void ExitSys(LPCSTR lpszMsg);
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
CRITICAL_SECTION g_cs;
std::vector<int> g_v;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
int i;
srand(time(NULL));
InitializeCriticalSection(&g_cs);
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
DeleteCriticalSection(&g_cs);
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
int i;
for (i = 0; i < 1000; ++i) {
EnterCriticalSection(&g_cs);
g_v.push_back(i);
LeaveCriticalSection(&g_cs);
}
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
int i;
for (i = 0; i < 1000; ++i) {
EnterCriticalSection(&g_cs);
g_v.push_back(i);
LeaveCriticalSection(&g_cs);
}
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Kritik kod oluşturmak için kullanılan diğer bir senkronizasyon mekanizması da "mutex (mutual exclusion)" denilen mekanizmadır. Mutex
nesneleri pek çok işletim sisteminde benzer bir biçimde bulunmaktadır. Örneğin CRITICAL_SECTION nesnesi Windows sistemlerine özgü olduğu
halde mutex nesneleri hem Windows, hem UNIX/Linux hem de macOS sistemlerinde benzer biçimde bulunmaktadır.
Mutex nesnelerinin thread temelinde bir "sahipliği (ownership)" vardır. Bir mutex nesnesinin sahipliğini bir thread almış ise o thread
o mutex nesnesini kilitlemiştir. Başka bir thread mutex nesnesinin sahipliğine almaya çalışırsa diğer thread sahipliği bırakana kadar
blokede bekler. Mutex nesnesinin sahipliğini nesnenin sahipliğin almış olan thread bırakabilmektedir. Eğer bir thread bir mutex nesnesinin
sahipliğini almışken onu bırakmadan sonlanırsa böyle mutex nesnelerine "terkedilmiş mutex nesneleri (abondened mutexes)" denilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows'ta mutex nesneleri hem aynı prosesin thread'leri arasında hem de farklı proseslerin thread'leri arasında senkronizasyon amacıyla
kullanılabilmektedir. Windows sistemlerinde aynı prosesin thread'leri arasında kritik kod oluşturmak için CRITICAL_SECTION nesneleri mutex
nesnelerinden daha hızlı çalışmaktadır.
Windows'ta mutex nesneleri şöyle kullanılmaktadır:
1) Önce mutex nesnesi CreateMutex isimli API fonksiyonuyla yaratılır. Tabii bu yaratım henüz thread'ler yaratılmadan önce yapılabilir.
CreateMutex fonksiyonun prototipi şöyledir:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
Fonksiyonun birinci parametresi mutex nesnesinin güvenlik bilgilerini belirlemek için kullanılmaktadır. Bu parametre NULL geçilebilir.
(Windows'ta mutex nesneleri birer kernel nesnesidir. Tüm kernel nesnelerinin SECURITY_ATTRIBUTES türünden bir güvenlik parametresi vardır.
Bu konu oldukça karmaşık bir konu olduğu için "Windows Sistem Programlama" kursunda ele alınmaktadır.) Fonksiyonun ikinci parametresi TRUE
geçilirse mutex nesnesini yaratan thread aynı zamanda sahipliğinde alır (yani aynı zamanda mutex nesnesini kilitler.) Bu parametre tipik
olarak FALSE biçiminde geçilmektedir. Fonksiyonun son parametresi mutex nesnesi farklı prosesler tarafından kullanılacaksa nesneyi temsil
eden ismi belirtmektedir. Bu isim programcı tarafından herhangi bir biçimde verilebilir. Eğer aynı prosesin thread'leri arasında senkronizasyon
yapılacaksa bu parametre NULL geçilmelidir. Fonksiyon başarı durumunda yaratılan mutex nesnesinin handle değerine başarısızlık durumunda
NULL adrese geri dönmektedir.
Eğer aynı prosesin thread'leri arasında senkronizasyon yapılacaksa bu durumda CreateMutex fonksiyonundan elde edilen handle değeri global
bir değişkene atanmalıdır. Böylece bu global değişken farklı thread'lerden kullanılabilecektir.
Örneğin:
HANDLE g_hMutex;
...
if ((g_hMutex = CreateMutex(NULL, FALSE, NULL)) == NULL)
ExitSys("CreateMutex");
2) Kritik kod aşağıdaki gibi WaitForSingleObject (ya da WaitForMultipleObjects) ve ReleseMutex API fonksiyonları arasına yerleştirilmektedir.
WaitForSingleObject(g_hMutex, INFINITE);
...
... KRİTİK KOD
...
ReleaseMutex(g_hMutex);
WaitForSingleObject fonksiyonunu daha önce görmüştük. Bu fonksiyon (ve WaitForMultipleObjects fonksiyonu) senkronizasyon nesnelerini
beklemek için kullanılan genel bir fonksiyondu. Eğer WaitForSingleObejct fonksiyonu ile bir mutex nesnesi bekleniyorsa fonksiyon nesnenin
sahipliğini başka bir thread almamışsa nesnenin sahipliğini alarak kritik koda girişi sağlar. Eğer nesnenin sahipliği başka bir thread
tarafından alınmışsa WaitForSingleObject fonksiyonu nesnenin sahipliğini almış olan thread bu sahipliği bırakana kadar blokede beklemektedir.
Böylece aynı anda tek bir thread'in kritik koda girişine izin verilmektedir. Eğer nesnenin sahipliğini almış olan thread sahipliğini
bırakmadan sonlanırsa bu durumda WaitForSingleObject fonksiyonu "kilitlenme (deadlock)" oluşmasını engellemek için WAIT_ABANDONED özel değeri
ile geri dönmektedir.
ReleaseMutex fonksiyonu mutex nesnesinin sahipliğini bırakmak için kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL ReleaseMutex(
HANDLE hMutex
);
Fonksiyon mutex nesnesinin handle değerini parametre olarak alır ve nesnenin sahipliğini bırakır. Başarı durumunda sıfır dışı bir değere,
başarısızlık durumunda sıfır değerine geri dönmektedir.
3) Mutex nesnesinin kullanımı bittikten sonra nesne CloseHandle fonksiyonu ile yok edilebilir. (Anımsanacağı gibi Windows'te ismine
"kernel nesneleri (kernel objects)" denilen tüm nesneler ortak biçimde CloseHandle fonksiyonuyla kapatılmaktadır.
Aşağıda Windows'ta mutex nesneleri ile kritik kod oluşturulmasına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
void UseMachine(LPCSTR pszName);
void ExitSys(LPCSTR lpszMsg);
HANDLE g_hMutex;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if ((g_hMutex = CreateMutex(NULL, FALSE, NULL)) == NULL)
ExitSys("CreateMutex");
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
CloseHandle(g_hMutex);
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
int i;
for (i = 0; i < 10; ++i)
UseMachine("Therad-1");
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
int i;
for (i = 0; i < 10; ++i)
UseMachine("Thread-2");
return 0;
}
void UseMachine(LPCSTR pszName)
{
WaitForSingleObject(g_hMutex, INFINITE);
printf("-------------------\n");
printf("%s: 1. Step\n", pszName);
Sleep(rand() % 500);
printf("%s: 2. Step\n", pszName);
Sleep(rand() % 500);
printf("%s: 3. Step\n", pszName);
Sleep(rand() % 500);
printf("%s: 4. Step\n", pszName);
Sleep(rand() % 500);
printf("%s: 5. Step\n", pszName);
Sleep(rand() % 500);
ReleaseMutex(g_hMutex);
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows'ta mutex nesneleri farklı proseslerin thread'lerini senkronize etmek için de kullanılabilmektedir. Bunun için CreateMutex
fonksiyonunun üçüncü (son) parametresine mutex nesnesi için bir isim girilir. İki farklı proses CreateMutex fonksiyonunda mutex nesnlerine
aynı isimleri verirse bu durumda iki farklı mutex yaratılmamakta aynı mutex nesnesi üzerinde çalışılmaktadır. Başka bir deyişle CreateMutex
fonksiyonu son parametresiyle belirtilen isimde daha önce bir mutex nesnesi yaratılmışsa yeni bir mutex nesnesi yaratmaz. Zaten yaratılmış olan
mutex nesnesi açmış olur. Böylece iki proses de aynı isimle CreateMutex fonksiyonunu çağırdığında yalnızca bunlardan biri mutex nesnesini
yaratacak diğer yaratılmış olan nesneyi açacaktır. Tabii mutex nesnesine verilen isme dikkat edilmelidir. Eğer sistemde o isimli bir mutex
nesnesi zaten başkaları tarafından yaratılmışsa böyle bir yaratım yapılmayacaktır.
Pekiyi farklı proseslerin thread'lerinin senkronize edilmesi neden gerekebilir? İşte prosesler kendi aralarında "paylaşılan bellek alanlarıyla"
ortak veriler üzerinde işlem yapıyor olabilirler. Bu durumda bu paylaşılan bellek alanındaki verilerin prosesler arası çalışan senkronizasyon
nesneleriyle senkronize edilmesi gerekebilmektedir.
Windows sistemlerinde mutex nesneleri "özyinelemeli (recursive)" davranışa sahiptir. Bir mutex nesnesinin özyinelemeli olması demek mutex
nesnesinin thread tarafından sahipliği alındığında aynı thread'in yeniden aynı mutex nesnesinin sahipliğini bloke olmadan alabilmesi demektir.
Örneğin:
void foo(void)
{
...
WaitForSingleObject(g_hMutex, INFINITE);
...
bar();
...
ReleaseMutex(g_hMutex);
...
}
void bar(void)
{
...
WaitForSingleObject(g_hMutex, INFINITE);
...
...
...
ReleaseMutex(g_hMutex);
...
}
Burada programcının foo fonksiyonunu çağırdığını varsayalım. foo fonksiyonu Mutex'in sahipliğini aldıktan sonra bar fonksiyonunu çağırdığında
aynı thread aynı mutex'in sahipliğini ikinci kez almaktadır. İşte Windows'ta bu durumda bir sorun oluşmamaktadır. Ancak thread mutex'in
sahipliğini ne kadar almışsa ReleaseMutex ile o kadar bırakmalıdır.
Aşağıdaki örnekte iki proses paylaşılan bellek alanı oluşturup oradaki bir sayacı belli miktar artırmaktadır. Senkronizasyon yapılmaığı
durumda bu sayaç değeri yanlış çıkacaktır. Senkronizasyon yapıldığında sayacın artırılması seri hale getirildiği için sorun oluşmayacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* prog1.c */
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#define SHARED_MEMORY_NAME "MySharedMemory"
#define MUTEX_NAME "MyMutexObject"
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFileMapping;
HANDLE hMutex;
long long *pCount;
if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, SHARED_MEMORY_NAME)) == NULL)
ExitSys("CreateFileMapping");
if ((pCount = (long long *) MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 4096)) == NULL)
ExitSys("MapViewOfFile");
if ((hMutex = CreateMutex(NULL, FALSE, MUTEX_NAME)) == NULL)
ExitSys("CreateMutex");
for (long long i = 0; i < 10000000; ++i) {
WaitForSingleObject(hMutex, INFINITE);
++*pCount;
ReleaseMutex(hMutex);
}
printf("Press ENTER to continue...\n");
getchar();
printf("%lld\n", *pCount);
UnmapViewOfFile(pCount);
CloseHandle(hFileMapping);
getchar();
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/* prog2.c */
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#define SHARED_MEMORY_NAME "MySharedMemory"
#define MUTEX_NAME "MyMutexObject"
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hFileMapping;
HANDLE hMutex;
long long *pCount;
if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, SHARED_MEMORY_NAME)) == NULL)
ExitSys("CreateFileMapping");
if ((pCount = (long long *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 4096)) == NULL)
ExitSys("MapViewOfFile");
if ((hMutex = CreateMutex(NULL, FALSE, MUTEX_NAME)) == NULL)
ExitSys("CreateMutex");
for (long long i = 0; i < 10000000; ++i) {
WaitForSingleObject(hMutex, INFINITE);
++*pCount;
ReleaseMutex(hMutex);
}
UnmapViewOfFile(pCount);
CloseHandle(hFileMapping);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirtitğimiz gibi UNIX/Linux sistemlerinde Windows'taki gibi bir CRITICAL_SECTION nesnesi yoktur. Kritik kod oluşturmak için
mutex nesneleri kullanılmaktadır. UNIX/Linux sistemlerinde mutex nesneleri aynı prosesin thread'leri arasındaki senkronizasyon için
tasarlandığından dolayı Windows'taki mutex nesnelerine göre daha hızlıdır. Ancak istenirse biraz zor olsa da UNIX/Linux sistemlerindeki
mutex nesneleri prosesler arasında da kullanılabilir.
UNIX/Linux sistemlerinde mutex nesneleri şu adımlardan geçilerek kullanılmaktadır:
1) Henüz thread'ler yaratılmadan pthread_mutex_init fonksiyonu ile mutex nesnesi yaraılır. Fonksiyonun prototiği şöyledir:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
Fonksiyonun birinci parametresi pthread_mutex_t türünden bir nesnenin adresini almaktadır. Fonksiyon bu nesneye bazı ilkdeğeri vermektedir.
Buna mutex nesnesi diyebiliriz. pthread_mutex_t bir yapı biçiminde typedef edilmiştir. Programcı aynı prosesin thread'leri arasında
senkronizasyon uygulamak için bu nesneyi global düzeyde ranımlamalıdır. Fonksiyonun ikinci parametresi yaparılacak mutex nesnesinin bazı
özelliklerini belirlemek için kullanılmaktadır. Bu parametre NULL geçilebilir. Bu durumda mutex nesnesi default özelliklerle yaratılacaktır.
Biz bu kursta mutex nesnelerinin özellikleri üzerinde durmayacağız. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda
errno değerine geri dönmektedir. Örneğin:
pthread_mutex_t g_mutex;
...
if ((result = pthread_mutex_init(&g_mutex, NULL)) != 0)
exit_sys_errno("pthread_mutex_init", result);
Mutex nesnelerine pthread_mutex_init yerine doğrudan PTHREAD_MUTEX_INITIALIZER makrosuyla da ilkdeğer verilebilmektedir. Örneğin:
pthread_mutex_t g_mutex = PTHERAD_MUTEX_INIALIZER;
2) Kritik kod pthread_muutex_lock ve pthread_mutex_unlock çağrıları arasında yerleştirilir:
pthread_mutex_lock(&g_mutex);
...
... KRİTİK KOD
...
pthread_mutex_unlock(&g_mutex);
Bir thread pthread_mutex_lock fonksiyonuna girdiğinde eğer mutex'in sahipliği başka bir thread tarafından alınmışsa o thread sahipliği
bırakana kadar fonksiyon blokede bekler. Eğer mutex nesnesinin sahipli alınmamışsa pthread_mutex_lock nensnenin sahipliğini alarak
kritik koda giriş yapar. Nesnenin sahipliği pthread_mutex_unlock fnksiyonuyla bırakılmaktadır. Fonksiyonların prototipleri şöyledir:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Fonksiyonlar mutex nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri
dönerler.
3) Mutex ile çalışma bittikten sonra mutex nesnesi pthread_mutex_destroy fonksiyonuyla yok edilir. Fonksiyonun prototipi şöyledir:
#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.
Aslında pek çok kütüphanede bu fonksiyon bir şey yapmamaktadır. Ancak başka gerçekleştirimlerde bu fonksiyon birtakım kaynakları boşaltıyor
olabilir.
Aşağıda UNIX/Linux sistemlerinde mutex kullanımına bir örnek verilmiştir. Yine örnekte global bir sayaç değişkeni alınmıştır. İki thread
de bu sayacı artırmaktadır. Ancak artırım sırasında mutex koruması uygulanmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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);
pthread_mutex_t g_mutex;
int g_count;
int main(void)
{
pthread_t tid1, tid2;
int result;
if ((result = pthread_mutex_init(&g_mutex, NULL)) != 0)
exit_sys_errno("pthread_mutex_init", result);
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_join(tid1, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_join(tid2, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_mutex_destroy(&g_muex)) != 0)
exit_sys_errno("pthread_mutex_destroy", result);
printf("%d\n", g_count);
return 0;
}
void *thread_proc1(void *param)
{
int result;
for (int i = 0; i < 1000000; ++i) {
if ((result = pthread_mutex_lock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
++g_count;
if ((result = pthread_mutex_unlock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
}
return NULL;
}
void *thread_proc2(void *param)
{
int result;
for (int i = 0; i < 1000000; ++i) {
if ((result = pthread_mutex_lock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
++g_count;
if ((result = pthread_mutex_unlock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
}
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda daha önce yapmış olduğumuz makine örneğininin UNIX/Linux sistemleriyle mutex nesneleriyle gerçekleştirimini veriyoruz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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);
void use_machine(const char *name);
pthread_mutex_t g_mutex;
int main(void)
{
pthread_t tid1, tid2;
int result;
srand(time(NULL));
if ((result = pthread_mutex_init(&g_mutex, NULL)) != 0)
exit_sys_errno("pthread_mutex_init", result);
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_join(tid1, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_join(tid2, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_mutex_destroy(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_destroy", result);
return 0;
}
void use_machine(const char *name)
{
int result;
if ((result = pthread_mutex_lock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
printf("-------------------\n");
printf("%s: 1. Step\n", name);
usleep(rand() % 300000);
printf("%s: 2. Step\n", name);
usleep(rand() % 300000);
printf("%s: 3. Step\n", name);
usleep(rand() % 300000);
printf("%s: 4. Step\n", name);
usleep(rand() % 300000);
printf("%s: 5. Step\n", name);
usleep(rand() % 300000);
if ((result = pthread_mutex_unlock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_unlock", result);
}
void *thread_proc1(void *param)
{
int i;
for (i = 0; i < 10; ++i)
use_machine("Thread-1");
return NULL;
}
void *thread_proc2(void *param)
{
int i;
for (i = 0; i < 10; ++i)
use_machine("Thread-2");
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde mutex nesneleri default durumda özyinelemeli değildir. Bu sistemlerde mutex nesnelerin özyinelemeli yapmak için
önce pthread_mutexattr_t türünden bir nesne oluşturulur. Sonra bu nesne pthread_mutexattr_init fonksiyonuyla ilkdeğerlenir. Sonra da
pthread_mutexattr_settype fonksiyonu ile PTHREAD_MUTEX_RECURSIVE parametresi kullanılarak mutex özelliği özyinelemeli olarak set edilir.
Nihayet bu attribute nesnesi pthread_mutex_create fonksiyonunda kullanılır. Sonunda da bu attribute nesnesi pthread_mutexattr_destroy
fonksiyonuyla yok edilir.Buradaki fonksiyonların prototipleri şöyledir:
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
Bu fonksiyonların birinci parametreleri mutex attribute nesnesinin adresini almaktadır. Fonksiyonlar başarı durumunda sıfır değerine
başarısızlık durumunda errno değerine geri dönmektedir.
Örneğin:
pthread_mutex_t g_mutex;
...
pthread_mutexattr_t mattr;
...
if ((result = pthread_mutexattr_init(&mattr)) != 0)
exit_sys_errno("pthread_mutexattr_init", result);
if ((result = pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE)) != 0)
exit_sys_errno("pthread_mutexattr_settype", result);
if ((result = pthread_mutex_init(&g_mutex, &mattr)) != 0)
exit_sys_errno("pthread_mutex_init", result);
if ((result = pthread_mutexattr_destroy(&mattr)) != 0)
exit_sys_errno("pthread_mutexattr_destroy", result);
Mutex attribute nesnesinin yalnızca mutex nesnesinin yaratılması sırasında kullanıldığına dikkat ediniz.
Tabii UNIX/Linux sistemlerinde de özyinelemeli mutex'lerin kilidini açmak için pthread_mutex_lock işlemi kadar pthread_mutex_unlock
işleminin yapılması gerekmektedir.
Aşağıda buna yönelik bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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);
void do_machine(const char *name);
void use_machine(const char *name);
pthread_mutex_t g_mutex;
int main(void)
{
pthread_t tid1, tid2;
int result;
pthread_mutexattr_t mattr;
srand(time(NULL));
if ((result = pthread_mutexattr_init(&mattr)) != 0)
exit_sys_errno("pthread_mutexattr_init", result);
if ((result = pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE)) != 0)
exit_sys_errno("pthread_mutexattr_settype", result);
if ((result = pthread_mutex_init(&g_mutex, &mattr)) != 0)
exit_sys_errno("pthread_mutex_init", result);
if ((result = pthread_mutexattr_destroy(&mattr)) != 0)
exit_sys_errno("pthread_mutexattr_destroy", result);
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_join(tid1, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_join(tid2, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_mutex_destroy(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_destroy", result);
return 0;
}
void do_machine(const char *name)
{
int result;
if ((result = pthread_mutex_lock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
use_machine(name);
if ((result = pthread_mutex_unlock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_unlock", result);
}
void use_machine(const char *name)
{
int result;
if ((result = pthread_mutex_lock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
printf("-------------------\n");
printf("%s: 1. Step\n", name);
usleep(rand() % 300000);
printf("%s: 2. Step\n", name);
usleep(rand() % 300000);
printf("%s: 3. Step\n", name);
usleep(rand() % 300000);
printf("%s: 4. Step\n", name);
usleep(rand() % 300000);
printf("%s: 5. Step\n", name);
usleep(rand() % 300000);
if ((result = pthread_mutex_unlock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_unlock", result);
}
void *thread_proc1(void *param)
{
int i;
for (i = 0; i < 10; ++i)
do_machine("Thread-1");
return NULL;
}
void *thread_proc2(void *param)
{
int i;
for (i = 0; i < 10; ++i)
do_machine("Thread-2");
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde mutex nesnesini prosesler arasında kullanabilmek için mutex nesnesinin paylaşılan bellek alanında yaratılması
gerekir. (Yani bizim pthread_mutex_t türünden nesnseyi paylaşılan bellek alanında yaratmamız gerekir.) Böylece iki proses de aynı mutex
nesnesini görecektir. Ancak ayrıca mutex'in prosesler arası kullanımını mümkün hale getirmek için mutex attribute nesnesinde
pthread_mutexattr_setpshared fonksiyonu ile belirleme yapmak gerekir. Bu fonksiyonun prototipi şöyledir:
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
Fonksiyonun birinci parametresi mutex attribute nesnesinin adresini almaktadır. İkinci parametre için PTHREAD_PROCESS_SHARED değeri
proseslerarası paylaşım yapılabileceğini, PTHREAD_PROCESS_PRIVATE değeri ise proseslerarası paylaşım yapılamayacağını belirtmektedir.
Fonksiyon başarı durumunda sıfır değerine başarısızlık durumunda errno değerine geri dönmektedir.
Aşağıdaki örnekte prog1 programı önce paylaşılan bellek alanını ve mutex nesnesini oluşturmuştur. prog2 ise paylaşılan bellek alanındaki
mutex nesnesini kullanmıştır. Paylaşılan bellek alanının başı aşağıdaki gibi bir yapı ile temsilk edilmiştir:
struct SHARED_OBJECT {
pthread_mutex_t mutex;
long long count;
};
Aşağıdaki örnekte önce prog1 programını çalıştırmalısınız. Çünkü bu örnekte paylaşılan bellek alanını ve mutex nesnesini prog1 programı
yaratmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* 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>
#include <pthread.h>
void exit_sys(const char* msg);
void exit_sys_errno(const char *msg, int eno);
struct SHARED_OBJECT {
pthread_mutex_t mutex;
long long count;
};
int main(void)
{
int fdshm;
int result;
void *shmaddr;
pthread_mutexattr_t mattr;
struct SHARED_OBJECT *so;
if ((fdshm = shm_open("/sample_shared_memory_name", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
exit_sys("shm_open");
if (ftruncate(fdshm, 4096) == -1)
exit_sys("ftruncate");
if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED)
exit_sys("mmap");
so = (struct SHARED_OBJECT*)shmaddr;
so->count = 0;
if ((result = pthread_mutexattr_init(&mattr)) != 0)
exit_sys_errno("pthread_mutexattr_init", result);
if ((result = pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED)) != 0)
exit_sys_errno("pthread_mutexattr_setpshared", result);
if ((result = pthread_mutex_init(&so->mutex, &mattr)) != 0)
exit_sys_errno("pthread_mutex_init", result);
if ((result = pthread_mutexattr_destroy(&mattr)) != 0)
exit_sys_errno("pthread_mutexattr_destroy", result);
for (long long int i = 0; i < 1000000000; ++i) {
if ((result = pthread_mutex_lock(&so->mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
++so->count;
if ((result = pthread_mutex_unlock(&so->mutex)) != 0)
exit_sys_errno("pthread_mutex_unlock", result);
}
printf("Press ENTER to continue...\n");
getchar();
printf("%lld\n", so->count);
if ((result = pthread_mutex_destroy(&so->mutex)) != 0)
exit_sys_errno("pthread_mutex_destroy", result);
if (munmap(shmaddr, 4096) == -1)
exit_sys("munmap");
close(fdshm);
if (shm_unlink("/sample_shared_memory_name") == -1)
exit_sys("shm_unlink");
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/* prog2.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>
#include <pthread.h>
void exit_sys(const char* msg);
void exit_sys_errno(const char *msg, int eno);
struct SHARED_OBJECT {
pthread_mutex_t mutex;
long long int count;
};
int main(void)
{
int fdshm;
int result;
void *shmaddr;
struct SHARED_OBJECT *so;
if ((fdshm = shm_open("/sample_shared_memory_name", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
exit_sys("shm_open");
if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED)
exit_sys("mmap");
so = (struct SHARED_OBJECT*)shmaddr;
for (long long int i = 0; i < 1000000000; ++i) {
if ((result = pthread_mutex_lock(&so->mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
++so->count;
if ((result = pthread_mutex_unlock(&so->mutex)) != 0)
exit_sys_errno("pthread_mutex_unlock", result);
}
if (munmap(shmaddr, 4096) == -1)
exit_sys("munmap");
close(fdshm);
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
83. Ders 27/04/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Diğer çok kullanılan bir senkronizasyon nesneleri de "semaphore" denilen nesnelerdir. (Semaphore trafikteki dur-geç lambalarına denilmektedir.
eskiden trafik ışıkları yokken onun yerine insanların manuel idare ettiği bayraklar kullanılıyormuş.) Semapore'lar sayaçlı senkronizasyon
nesneleridir. Bir kritk koda en fazla N tane thread'in (akışın) girmesini sağlamak için kullanılmaktadır. Daha önce görmüş olduğumuz mutex
nesneleri ve Windows'taki CRITCAL_SECTION nesneleri kritik koda yalnızca tek bir akışın girmesini sağlamaktadır. Semaphore'lar bilgisayar
bilimlerinin öncülerinden Edsger Dijkstra ("edsger daystra" gibi okunuyor) tarafından bulunmuştur.
Bir kritik koda birden fazla akışın girmesinin anlamı nedir? Normalde kritik koda giren iki akış bile ortak kullanılan kaynakları bozabilir.
İşte semaphore'lar özellikle "kaynak paylaşımını sağlamak" için kullanılmaktadır. Örneğin elimizde 3 makine olsun. Biz de bu 3 makineyi 10
thread'e paylaştırmak isteyelim. Yani thread'ler makineyi talep etsin, eğer bir makine boştaysa o makine ilgili thread'e atansın. Ancak tüm
makineler doluysa makine talep eden thread makinelerden biri boşaltılana kadar CPU zamanı harcamadan blokede beklesin. İşte bu tür problemler
tipik olarak semaphore nesneleriyle çözülmektedir.
Semaphore nesnelerinin bir sayacı vardır. Kritik koda her giren thread eğer sayaç 0'dan büyükse sayacı 1 eksiltir. Eğer sayaç 0 ise blokede
sayacın 0'dan büyük olmasını bekler. Örneğin başlangıçtaki semaphore sayacı 3 olsun. Bir thread kritik koda girdiğinde semaphore sayacı 2
olacaktır. Diğer bir thread kritik koda girdiğinde semaphore sayacı 1 olacaktır. Diğer bir thread kritik koda girdiğinde ise semaphore sayacı
0 olacaktır. Artık gelen thread'ler kritik koda giremeyip blokede bekleyecektir. Artık kritik kodun içerisinde 3 tane thread vardır. Şimdi
bir thread kritik koddan çıkıyor olsun. Thread kritik koddan çıkarken semaphore sayacı 1 artırılır. Böylece semaphore sayacı 1 olur. Kritik
kodda şu anda 2 thread vardır. İşte semaphore sayacı artık 0'dan büyük olduğu için bekleyen thread'lerden biri de kritik koda girgirer.
Böylece semaphore sayacı yeniden 0 olur. Şimdi kritik kod içerisinde yine 3 thread vardır. Başlangıçtaki semaphore sayacı kaç ise kritik
kodun içerisinde "en fazla" o kadar thread bulunacaktır.
Bşlangıçtaki semaphore sayacı 1 ise kritik koda en fazla 1 thread girebilir. Bu tür semaphore'lara "ikili semaphore'lar (binary semaphores)"
denilmektedir. İkili semaphore'lar mutex nesnelerine benzese de onlardan önemli bir farklılığa sahiptir. Mutex nesnelerinin sahipliğini
(yani kilidini) ancak sahipliğini almış olan thread bırakabilir. Ancak semaphore sayaçları başka thread'ler tarafından artırılabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde semaphore nesneleri şu adımlardan geçilerek kullanılmaktadır:
1) Önce semaphore nesnesi CreateSemaphore API fonksiyonuyla yaratılır. CreateSemaphore API fonksiyonunun prototipi şöyledir:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCSTR lpName
);
Fonksiyonun birinci parametresi semaphore nesnesinin güvenlik bilgilerini belirtir. Bu parametre NULL adres geçilebilir. İkinci parametre
semaphore sayacının başlangıçtaki sayaç değerini belirtir. Üçüncü parametre semaphore sayacının erişebileceği maksimum sayaç değeridir.
Genellikle ikinci ve üçüncü parametreye aynı değer girilmektedir. Son parametre semaphore nesnesinin proseslerarası kullanımdaki ismini
belirtmektedir. Eğer semaphore nesnesi aynı prosesin thread'leri arasında kullanılacaksa bu parametreye NULL adres geçilebilir. Fonksiyon
başarı durumunda semaphore nesnesinin handle değerine başarısızlık durumunda NULL adrese geri dönmektedir.
Semaphore nesnesi aynı prosesin thread'leri arasında kullanılacaksa CreateSemaphore fonksiyonunun geri dönüş değeri (yani nesnenin handle değeri)
global bir değişkende tutulmalıdır. Örneğin:
HANDLE g_hSemaphore;
...
if ((g_hSemaphore = CreateSemaphore(NULL, 1, 1, NULL)) == NULL)
ExitSys("CreateSemaphore");
2) Kritik kod aşağıdaki gibi oluşturulmaktadır:
WaitForSingleObject(g_hSemaphore, INFINITE);
...
... KRİTİK KOD
...
ReleaseSemaphore(g_hSemahore, 1, NULL);
Burada akış WaitForSingleObject fonksiyonuna geldiğinde eğer semaphore sayacı 0'dan büyükse bloke olunmadan kritik koda girilir. Ancak
semaphore sayacı atomik bir biçimde 1 eksiltilir. Eğer semaphore sayacı 0 ise WaitForSingleObject bloke oluşturarak thread'i bekletir.
ReleaseSemaphore fonksiyonu semaphore sayacını ikinci parametresinde belirtilen miktar kadar (genellikle 1) artırmaktadır.
ReleaseSemaphore fonksiyonun prototipi şöyledir:
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
Fonksiyonun birinci parametresi semaphore nesnesinin handle değerini almaktadır. İkinci parametre artırım değerini belirtmektedir. (Bu
değer hemen her zaman 1 olur) son parametre semaphore sayacının önceki değerinin yerleştirileceği nesnenin adresini almaktadır. Bu parametre
NULL adres biçiminde geçilirse önceki sayaç değeri yerleştirilmez. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda sıfır dışı
bir değere geri dönmektedir. Örneğin:
for (int i = 0; i < 1000000; ++i) {
WaitForSingleObject(g_hSemaphore, INFINITE);
...
...
...
ReleaseSemaphore(g_hSemaphore, 1, NULL);
}
3) Semaphore kullanımı bittiğinde semaphore nesnesi diğer kernel nesnelerinde oluduğu gibi CloseHandle fonksiyonu ile yok edilmelidir.
Örneğin:
CloseHandle(g_hSemaphore);
Aşağıdaki daha önce yaptığımız mutex örneği semaphore nesneleriyle gerçekleştirilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
void ExitSys(LPCSTR lpszMsg);
HANDLE g_hSemaphore;
int g_count;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if ((g_hSemaphore = CreateSemaphore(NULL, 1, 1, NULL)) == NULL)
ExitSys("CreateSemaphore");
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(g_hSemaphore);
CloseHandle(hThread1);
CloseHandle(hThread2);
printf("%d\n", g_count);
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
for (int i = 0; i < 1000000; ++i) {
WaitForSingleObject(g_hSemaphore, INFINITE);
++g_count;
ReleaseSemaphore(g_hSemaphore, 1, NULL);
}
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
for (int i = 0; i < 1000000; ++i) {
WaitForSingleObject(g_hSemaphore, INFINITE);
++g_count;
ReleaseSemaphore(g_hSemaphore, 1, NULL);
}
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde tıpkı paylaşılan bellek alanlarında olduğu gibi semaphore'lar için de iki ayrı arayüz fonksiyon grubu bulunmaktadır.
Eskiden beri var olan klasik semaphore fonksiyonlarına "Sistem 5 semaphore'ları" denilmektedir. Bu semaphore fonksiyonlarının kullanımı
oldukça zordur. 90'lı yılların ortalarında UNIX/Linux sistemlerinde "POSIX semaphore'ları" da denilen modern semaphore fonksiyonları
eklenmiştir. Artık programcılar genellikle bu POSIX semaphore fonksiyonlarını tercih etmektedir. Tabii her iki fonksiyon grubu da aslında
POSIX standartlarında yer almaktadır. Ancak "POSIX semaphore fonksiyonları" denildiğinde daha sonra tasarlanmış olan modern semaphore
fonksiyonları kastedilmektedir. Klasik Sistem 5 semaphore'ları proseslerarası kullanım için tasarlanmıştır. Halbuki modern POSIX semaphore'ları
hem aynı prosesin thread'leri arasında hem de farklı proseslerin thread'leri arasında kullanılabilmektedir.
Biz kursumuzda modern POSIX semaphore fonksiyonlarını göreceğiz. Eski tipi "Sistem 5 Semaphore" fonksiyonları "UNIX/Linux Sistem Programlama"
kurslarında ele alınmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
POSIX semaphore fonksiyonları aşağıdaki adımlardan geçilerek kullanılmaktadır:
1) POSIX semapore nesneleri sem_t türüyle temsil edilmektedir. sem_t bir yapıyı belirten typedef ismidir. Eğer semaphore nesnesi aynı
prosesin thread'leri arasında kullanılacaksa sem_t türünden global bir nesne tanımlanır ve bu nesneye sem_init fonksiyonu ile ilkdeğerleri
verilir. sem_init fonksiyonunun prototipi şöyledir:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
Fonksiyonun birinci parametresi sem_t türünden nesnenin adresini almaktadır. Fonksiyonun ikinci parametresi bu nesnenin prosesler arasında
paylaşılıp paylaşılmayacağını belirtmektedir. Eğer nesne prosesler arasında paylaşılacaksa bu parametreye sıfır dışı bir değer, paylaşılmayacaksa
sıfır değeri geçilmelidir. Ancak zaten proseslerarası kullanım için başka bir fonksiyon bulunduğundan genellikle bu parametre sıfır geçilir.
Fonlsiyonun üçüncü parametresi semaphore sayacının başlangıç değerini belirtmektedir. Yani bu değer kritik koda en fazla kaç akışın gireceğini
belirtir. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyon errno değerini set etmektedir.
(Diğer thread fonksiyonlarının errno değerini set etmediğini bizzat errno değerine geri döndüğünü anımsayınız.)
Eğer semaphore nesnesi prosesler arasında kullanılacaksa bu durumda sem_t nesnesi sem_open fonksiyonu ile elde edilmelidir. Bu fonksiyon
üzerinde daha sonra durulacaktır.
2) Kritik kod sem_wait ve sem_post çağrıları arasına yerleştirilir:
sem_wait(&g_sem);
...
... <KRİTİK KOD>
...
sem_post(&g_sem);
Akış sem_wait fonksiyonuna geldiğinde eğer semaphore sayacı 0 ise sem_wait blokeye yol açar ve semaphore sayacı 0'dan büyük olana kadar
bekler. Eğer semaphore sayacı 0'dan büyükse akış sem_wait fonksiyonundan geçer ancak semaphore sayacı 1 eksiltilir. sem_post fonksiyonu
semaphore sayacını 1 artırmaktadır. Bu fonksiyonların prototipleri şöyledir:
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
Fonksiyonlar semaphore nesnelerinin adresini parametre olarak almakta, başarı durumunda sıfır değerine başarısızlık durumunda -1 değerine
geri dönmektedirler. Yine başarısızlık durumunda errno değişkeni set edilmektedir.
3) Semaphore nesnesinin kullanımı bittikten sonra nesne sem_destroy fonksiyonu ile yok edilmelidir. Fonksiyonun prototipi şöyledir:
#include <semaphore.h>
int sem_destroy(sem_t *sem);
Fonksiyon semaphore nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner.
Başarısızlık durumunda errno değişkeni set edilmektedir. Eğer semaphore nesnesi proseslerarası kullanım için sem_open fonksiyonuyla
yaratılmışsa nesnenin yok edilmesi sem_close fonksiyonu ile yapılmalıdır.
Aşağıda daha önce yapmış olduğumuz global değişkeninin iki thread tarafından artırılması binary POSIX semaphore'larıyla sağlanmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.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;
int main(void)
{
pthread_t tid1, tid2;
int result;
if (sem_init(&g_sem, 0, 1) == -1)
exit_sys("sem_init");
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_join(tid1, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_join(tid2, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if (sem_destroy(&g_sem) == -1)
exit_sys("sem_destroy");
printf("%d\n", g_count);
return 0;
}
void *thread_proc1(void *param)
{
for (int i = 0; i < 1000000; ++i) {
if (sem_wait(&g_sem) == -1)
exit_sys("sem_wait");
++g_count;
if (sem_post(&g_sem) == -1)
exit_sys("sem_post");
}
return NULL;
}
void *thread_proc2(void *param)
{
for (int i = 0; i < 1000000; ++i) {
if (sem_wait(&g_sem) == -1)
exit_sys("sem_wait");
++g_count;
if (sem_post(&g_sem) == -1)
exit_sys("sem_post");
}
return NULL;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda daha önce yapmış olduğumuz makine konumlandırma örneği POSIX semaphore nesneleriyle gerçekleştirilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
void *thread_proc1(void* param);
void *thread_proc2(void* param);
void do_machine(const char* name);
void exit_sys(const char *msg);
void exit_sys_errno(const char *msg, int eno);
sem_t g_sem;
int main(void)
{
int result;
pthread_t tid1, tid2;
srand(time(NULL));
if (sem_init(&g_sem, 0, 1) == -1)
exit_sys("sem_init");
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_sys_errno("pthread_create", result);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&g_sem);
return 0;
}
void *thread_proc1(void *param)
{
int i;
for (i = 0; i < 10; ++i)
do_machine("thread-1");
return NULL;
}
void *thread_proc2(void *param)
{
int i;
for (i = 0; i < 10; ++i)
do_machine("thread-2");
return NULL;
}
void do_machine(const char *name)
{
if (sem_wait(&g_sem) == -1)
exit_sys("sem_wait");
printf("---------------\n");
printf("1) %s\n", name);
usleep(rand() % 300000);
printf("2) %s\n", name);
usleep(rand() % 300000);
printf("3) %s\n", name);
usleep(rand() % 300000);
printf("4) %s\n", name);
usleep(rand() % 300000);
printf("5) %s\n", name);
usleep(rand() % 300000);
if (sem_post(&g_sem) == -1)
exit_sys("sem_post");
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Üretici-Tüketici Problemi (Producer-Consumer Problem) gerçek hayatta en sık karşılaşılan senkronizasyon problemlerinden biridir. Bu problemde
thread'lerden biri (üretici thread) bir döngü içerisinde bir değer üretir ve onu paylaşılan bellek alanına yerleştirir. Diğer thread de
(tüketici thread) bir döngü içerisinde onu oradan alıp kullanır. Burada şöyle bir sorun vardır: Üretici thread henüz tüketici thread eski
değeri almdan paylaşılan alana yeni değeri yerleştirirse eski değer ezilir. Benzer biçimde tüketici thread de üretici thread paylaşılan alana
yeni bir değer koymadan paylaşılan alandan değeri alırsa eski değeri yeniden almış olur. O halde iki thread'in bu işi düzgün yapabilmesi için
uygun biçimde senkronize edilmesi gerekmektedir. Öyle ki üretici thread tüketici thread eski değeri almadan paylaşılan alana yeni değeri
yerleştirmemeli, tüketici thread de üretici thread paylaşılan alana yeni bir değer yerleştirmeden eski değeri yeniden almamalıdır.
Üretici-Tüketici probleminde aslında üretici thread'ler ve tüketici thread'ler birden fazla olabilmektedir. Örneğin üç thread üretici iki
thread tüketici olabilir. Ancak problemin en basit halinde tek üretici ve tek tüketici vardır.
Pekiyi üretici-tüketici probleminde neden üretici thread elde ettiği değeri kendisi işlemiyor da onu tüketici thread'e pas edip onun
işlemesini sağlıyor? İşte bunun en açık nedeni hız kazancı sağlamaktır. Tek bir thread bu işi yaptığında önce değeri elde edip işleyecek
ve sonra yeni değeri elde edip işleyecektir. Ancak thread'lerden biri değeri elde ederken diğeri onu işlerse işlemler toplamda daha hızlı
yürütülmüş olur.
Üretici-Tüketici probleminde paylaşılan alan tek bir değeri içerecek biçimde olmayabilir. Bu alan bir kuyruk sistemi biçiminde de olabilir.
Böylece üretici ve tüketici birbirlerini daha az bekler. Uygulamada genellikle paylaşılan alan bir kuyruk sistemi biçiminde olarak organize
edilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Üretici-Tüketici problemleri tipik olarak semaphore nesneleriyle çözülmektedir. Problemin çözümü için iki semaphore nesnesi yatatılır.
Semaphore'lardan biri üretici semaphore'u diğeri ise tüketici semaphore'u olur. Başlangıçta üretici semaphore'u 1'e tüketici semaphore'u
ise 0'a kurulur:
sem_t g_sem_producer;
sem_t g_sem_consumer;
...
sem_init(&g_sem_producer, 0, 1);
sem_init(&g_sem_consumer, 0, 0);
Üretici ve tüketici döngülerinin temsili biçimi şöyledir:
ÜRETİCİ
-------
for (;;) {
<değeri elde ediliyor>
sem_wait(&g_sem_producer);
<değer paylaşılan alana yerleştiriliyor>
sem_post(&g_sem_consumer);
}
TÜKETİCİ
--------
for (;;) {
sem_wait(&g_sem_consumer);
<değer paylaşılan alandan alınıyor>
sem_post(&g_sem_producer);
<değer kullanılıyor>
}
Burada üretici semaphore'unun başlangıçtaki sayaç değeri 1 olduğu için üretici sem_wait fonksiyonundan geçip elde ettiği değeri paylaşılan
alana bırakacaktır. Bu sırada tüketici semaphore'unun başlangıç değeri 0 olduğu için tüketici sem_wait fonksiyonunda bekleyecektir.
Üretici thread'in tüketicinin semaphore sayacını, tüketici thread'in ise üreticinin semaphore sayacını 1 artırdığına dikkat ediniz.
Böylece üretici tüketiciyi tüketici de üreticiyi sem_wait fonksiyonundan geçirmektedir. Bunu tbir tahteravalliye benzetebilirsiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aaşğıda uNIX/Linux sistemlerinde üretici-tüketici probleminin POSIX semaphore nesneleriyle çözümüne bir örnek verilmiştir. Bu örnekte
üretici thread 0'dan 100'a kadar sayıları rastgele beklemelerle paylaşılan alana yerleştirmekte tüketici thread de bunları rastgele
beklemelerle almaktadır. Örneği senkronizasyonu kaldırarak çalıştırıp nasıl bir durum oluştuğunu gözleyiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
void *thread_proc_producer(void* param);
void *thread_proc_consumer(void* param);
void exit_sys(const char *msg);
void exit_sys_errno(const char *msg, int eno);
sem_t g_sem_producer;
sem_t g_sem_consumer;
int g_shared;
int main(void)
{
int result;
pthread_t tid1, tid2;
srand(time(NULL));
if (sem_init(&g_sem_producer, 0, 1) == -1)
exit_sys("sem_init");
if (sem_init(&g_sem_consumer, 0, 0) == -1)
exit_sys("sem_init");
if ((result = pthread_create(&tid1, NULL, thread_proc_producer, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc_consumer, NULL)) != 0)
exit_sys_errno("pthread_create", result);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&g_sem_producer);
sem_destroy(&g_sem_consumer);
return 0;
}
void *thread_proc_producer(void *param)
{
int val;
val = 0;
for (;;) {
usleep(rand() % 300000);
if (sem_wait(&g_sem_producer) == -1)
exit_sys("sem_wait");
g_shared = val;
if (sem_post(&g_sem_consumer) == -1)
exit_sys("sem_post");
if (val == 99)
break;
++val;
}
return NULL;
}
void *thread_proc_consumer(void *param)
{
int val;
for (;;) {
if (sem_wait(&g_sem_consumer) == -1)
exit_sys("sem_wait");
val = g_shared;
if (sem_post(&g_sem_producer) == -1)
exit_sys("sem_post");
printf("%d ", val);
fflush(stdout);
usleep(rand() % 300000);
if (val == 99)
break;
}
putchar('\n');
return NULL;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde üretici-tüketici probleminin semaphore nesneleriyle çözümüne ilişkin bir örnek aşağıda verilmiştir. Bu örnek yukarıda
verdiğimiz UNIX/Linux örneğinin Windows eşdeğeri gibidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
DWORD __stdcall ThreadProducer(LPVOID lpvParam);
DWORD __stdcall ThreadConsumer(LPVOID lpvParam);
HANDLE g_hSemProducer;
HANDLE g_hSemConsumer;
int g_shared;
int main(void)
{
HANDLE hThreadProducer, hThreadConsumer;
DWORD dwThreadIDProducer, dwThreadIDConsumer;
srand(time(NULL));
if ((g_hSemProducer = CreateSemaphore(NULL, 1, 1, NULL)) == NULL)
ExitSys("CreateSemaphore");
if ((g_hSemConsumer = CreateSemaphore(NULL, 0, 1, NULL)) == NULL)
ExitSys("CreateSemaphore");
if ((hThreadProducer = CreateThread(NULL, 0, ThreadProducer, NULL, 0, &dwThreadIDProducer)) == NULL)
ExitSys("CreateThread");
if ((hThreadConsumer = CreateThread(NULL, 0, ThreadConsumer, NULL, 0, &dwThreadIDConsumer)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThreadProducer, INFINITE);
WaitForSingleObject(hThreadConsumer, INFINITE);
CloseHandle(hThreadProducer);
CloseHandle(hThreadConsumer);
CloseHandle(g_hSemProducer);
CloseHandle(g_hSemConsumer);
return 0;
}
DWORD __stdcall ThreadProducer(LPVOID lpvParam)
{
int val;
val = 0;
for (;;) {
Sleep(rand() % 300);
WaitForSingleObject(g_hSemProducer, INFINITE);
g_shared = val;
ReleaseSemaphore(g_hSemConsumer, 1, NULL);
if (val == 99)
break;
++val;
}
return 0;
}
DWORD __stdcall ThreadConsumer(LPVOID lpvParam)
{
int val;
for (;;) {
WaitForSingleObject(g_hSemConsumer, INFINITE);
val = g_shared;
ReleaseSemaphore(g_hSemProducer, 1, NULL);
printf("%d ", val);
fflush(stdout);
if (val == 99)
break;
Sleep(rand() % 300);
}
putchar('\n');
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğmiz gibi üretici-tüketici probleminde paylaşılan alan bir kuyruk sistemi olursa üretici ve tüketicinin birbirlerini
bekleme olasılıkları azaltılmış olur. Çünkü bu durumda üretici yalnızca "kuyruk tamamen doluyken", tüketici ise yalnızca "kuyruk tamamen
boşken" bekleyecektir.
Üretici-Tüketici probleminin kuyruklu versiyonunda üretici semaphore sayacının başlangıçta kuyruk uzunluğuna kurulması gerekmektedir.
(Yani tüketici hiç çalışmasa üretici tüm kuyruğu doldurup bekleyecektir.)
Aşağıda UNIX/Linux sistemlerinde üretici-tüketici probleminin kuyruklu versiyonuna bir örnek verilmiştir. Burada kuyruğun eşzamanlı
erişimler için senkronize edilmesine gerek yoktur. Çünkü aslında işleyiş dikkatle incelendiğinde iki thread'in aynı nesnelere eş zamanlı
erişmediği görülecektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define QUEUE_BUFFER_SIZE 10
void *thread_proc_producer(void* param);
void *thread_proc_consumer(void* param);
void exit_sys(const char *msg);
void exit_sys_errno(const char *msg, int eno);
sem_t g_sem_producer;
sem_t g_sem_consumer;
int g_qbuf[QUEUE_BUFFER_SIZE];
int g_head;
int g_tail;
int main(void)
{
int result;
pthread_t tid1, tid2;
srand(time(NULL));
if (sem_init(&g_sem_producer, 0, QUEUE_BUFFER_SIZE) == -1)
exit_sys("sem_init");
if (sem_init(&g_sem_consumer, 0, 0) == -1)
exit_sys("sem_init");
if ((result = pthread_create(&tid1, NULL, thread_proc_producer, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc_consumer, NULL)) != 0)
exit_sys_errno("pthread_create", result);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&g_sem_producer);
sem_destroy(&g_sem_consumer);
return 0;
}
void *thread_proc_producer(void *param)
{
int val;
val = 0;
for (;;) {
usleep(rand() % 300000);
if (sem_wait(&g_sem_producer) == -1)
exit_sys("sem_wait");
g_qbuf[g_tail++] = val;
g_tail = g_tail % QUEUE_BUFFER_SIZE; /* burası kritik kodun dışına alınabilir */
if (sem_post(&g_sem_consumer) == -1)
exit_sys("sem_post");
if (val == 99)
break;
++val;
}
return NULL;
}
void *thread_proc_consumer(void *param)
{
int val;
for (;;) {
if (sem_wait(&g_sem_consumer) == -1)
exit_sys("sem_wait");
val = g_qbuf[g_head++];
g_head = g_head % QUEUE_BUFFER_SIZE; /* burası kritik kodun dışına alınabilir */
if (sem_post(&g_sem_producer) == -1)
exit_sys("sem_post");
printf("%d ", val);
fflush(stdout);
usleep(rand() % 300000);
if (val == 99)
break;
}
putchar('\n');
return NULL;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda Windows sistemlerinde üretici-tüketici probleminin kuyruklu versiyonuna bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#define QUEUE_BUFFER_SIZE 10
void ExitSys(LPCSTR lpszMsg);
DWORD __stdcall ThreadProducer(LPVOID lpvParam);
DWORD __stdcall ThreadConsumer(LPVOID lpvParam);
HANDLE g_hSemProducer;
HANDLE g_hSemConsumer;
int g_queueBuf[QUEUE_BUFFER_SIZE];
size_t g_head;
size_t g_tail;
int main(void)
{
HANDLE hThreadProducer, hThreadConsumer;
DWORD dwThreadIDProducer, dwThreadIDConsumer;
srand(time(NULL));
if ((g_hSemProducer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, NULL)) == NULL)
ExitSys("CreateSemaphore");
if ((g_hSemConsumer = CreateSemaphore(NULL, 0, QUEUE_BUFFER_SIZE, NULL)) == NULL)
ExitSys("CreateSemaphore");
if ((hThreadProducer = CreateThread(NULL, 0, ThreadProducer, NULL, 0, &dwThreadIDProducer)) == NULL)
ExitSys("CreateThread");
if ((hThreadConsumer = CreateThread(NULL, 0, ThreadConsumer, NULL, 0, &dwThreadIDConsumer)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThreadProducer, INFINITE);
WaitForSingleObject(hThreadConsumer, INFINITE);
CloseHandle(hThreadProducer);
CloseHandle(hThreadConsumer);
CloseHandle(g_hSemProducer);
CloseHandle(g_hSemConsumer);
return 0;
}
DWORD __stdcall ThreadProducer(LPVOID lpvParam)
{
int val;
val = 0;
for (;;) {
Sleep(rand() % 300);
WaitForSingleObject(g_hSemProducer, INFINITE);
g_queueBuf[g_tail++] = val;
g_tail = g_tail % QUEUE_BUFFER_SIZE;
ReleaseSemaphore(g_hSemConsumer, 1, NULL);
if (val == 99)
break;
++val;
}
return 0;
}
DWORD __stdcall ThreadConsumer(LPVOID lpvParam)
{
int val;
for (;;) {
WaitForSingleObject(g_hSemConsumer, INFINITE);
val = g_queueBuf[g_head++];
g_head = g_head % QUEUE_BUFFER_SIZE;
ReleaseSemaphore(g_hSemProducer, 1, NULL);
printf("%d ", val);
fflush(stdout);
if (val == 99)
break;
Sleep(rand() % 300);
}
putchar('\n');
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
84. Ders 28/04/2024 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Konunun başında da belirttiğimiz gibi UNIX/Linux sistemlerinde semaphore nesneleri isim verilerek de prosesler arasında kullanılabilmektedir.
Bunun için sem_open fonksiyonu ile iki prosesin de ortak bir isimde anlaşarak semaphore nesnesini açması gerekmektedir. sem_open fonksiyonunun
prototipi şöyledir:
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ...);
Fonksiyon ya iki parametreyle ya da dört parametreyle kullanılmaktadır. Eğer fonksiyon dört parametreyle kullanılacaksa parametrik yapı
aşağıdaki gibi olmalıdır:
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
Fonksiyonun birinci parametresi prosesler arasında paylaşımı sağlamak için belirlenen ismi belirtmektedir. POSIX standartlarına göre bu
ismin "kök dizindeki bir dosya ismi gibi" oluşturulması gerekmektedir. (Burada bir dosya yaratılmamaktadır. Kök dizindeki dosya yalnızca
bir temsildir.) Fonksiyonun ikinci parametresi yaratım bayraklarını belirtmektedir. Bu ikinci parametre üç değerden biri olarak geçilmelidir:
O_CREAT
O_CREAT|O_EXCL
0
O_CREAT bayrağı yine "nesne yaratılmamışsa yarat, nesne yaratılmışsa yaratılanı kullan" anlamına gelmektedir. O_CREAT ile O_EXCL birlikte
kullanılırsa bu durumda "nesne zaten yaratılmış ise" fonksiyon baaşarısız olmaktadır. Bu parametreye 0 değeri geçilirse "yaratılmış olan
semaphore nesnesi" kullanılmak üzere açılmaktadır. Eğer fonksiyonun ikinci parametresinde O_CREAT kullanılmışsa ve nesne daha önce yaratılmamışsa
bu durumda fonksiyon üçünüc ve dördüncü parametreleri kullanmaktadır. Diğer durumlarda bu parametreleri kullanmamaktadır. Başka bir deyişle
üçüncü ve dördüncü parametreler nesne ilk kez yaratılırken kullanılmaktadır. Semaphore nesnelerinde açış bayraklarında O_RDONLY, O_WRONLY
ve O_RDWR kullanılması POSIX standartlarında "belirsiz (unspecified)" bırakılmıştır. Ancak Linux sistemlerinde bu bayrakların etkileri vardır.
Bu konu "UNIX Linux Sistem Programlama" kurslarında ele alınmaktadır.
sem_open fonksiyonu ile yaratılan isimli POSIX semaphore nesnesi yine paylaşılan bellek alanlarında olduğu gibi sistem reboot edilene
kadar yaşamaya devam etmektedir (kernel persistent). Bu nesneyi reboot etmeden silmek için sem_unlik fonksiyonu kullanılmaktadır.
Fonksiyonun prototipi şöyledir:
#include <semaphore.h>
int sem_unlink(const char *name);
Fonksiyon isimli semaphore nesnesinin ismini parametre olarak alır. Başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri
döner.
Bu fonksiyonları kullanırken "librt" ve "libpthread" kütüphanelerini link aşamasına dahil etmelisiniz. Derleme aşağıdaki gibi yapılmalıdr:
gcc -Wall -o sample sample.c -lrt -lpthread
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda UNIX/Linux sistemlerinde üretici-tüketici probleminin proseslerarası kullanımına bir örnek verilmiştir. Bu örnekte "producer" ve
"consumer" isimli iki program vardır. producer programı üretici, consumer programı ise tüketici programdır. Programlarda bir tane paylaşılan
bellek alanı iki tane de proseslerarası kullanılabilen isimli semaphore nesnesi oluşturulmuştur. Buradaki programların hangisinin önce
çalıştırıdığının bir önemi yoktur. Çünkü ilk çalıştırılan program nesneleri yaratmakta sonra çalıştırılan program yaratılmış olanları
açmaktadır. Her iki programda da nesneler yok edilmeye çalışılmıştır. Ancak bu nesneleri diğer program yok etmişse bu durum normal karşılanıp
bir hata rapor edilmemiştir. Örneğimizde kuyruk sistemi paylaşılan bellek alanında oluşturulmuştur. Programları aşağıdaki gibi derleyebilirsiniz:
$ gcc -o producer producer.c -lrt -lpthread
$ gcc -o consumer consumer.c -lrt -lpthread
Programları farklı terminallerden çalıştırmalısınız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* 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 <pthread.h>
#include <semaphore.h>
#define SHARED_MEM_NAME "/sample_shared_memory_name"
#define SEM_PRODUCER_NAME "/sample_mutex_producer_name"
#define SEM_CONSUMER_NAME "/sample_mutex_consumer_name"
#define QUEUE_BUFFER_SIZE 10
void exit_sys(const char* msg);
void exit_sys_errno(const char *msg, int eno);
struct SHARED_OBJECT {
int qbuf[QUEUE_BUFFER_SIZE];
size_t head;
size_t tail;
};
int main(void)
{
int fdshm;
sem_t *sem_producer;
sem_t *sem_consumer;
void *shmaddr;
struct SHARED_OBJECT *so;
int val;
srand(time(NULL));
if ((fdshm = shm_open(SHARED_MEM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
exit_sys("shm_open");
if (ftruncate(fdshm, 4096) == -1) {
perror("ftruncate");
goto EXIT1;
}
if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) {
perror("mmap");
goto EXIT2;
}
if ((sem_producer = sem_open(SEM_PRODUCER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, QUEUE_BUFFER_SIZE)) == NULL) {
perror("sem_open");
goto EXIT3;
}
if ((sem_consumer = sem_open(SEM_CONSUMER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, 0)) == NULL) {
perror("sem_open");
goto EXIT4;
}
so = (struct SHARED_OBJECT *)shmaddr;
so->head = 0;
so->tail = 0;
val = 0;
for (;;) {
usleep(rand() % 300000);
if (sem_wait(sem_producer) == -1) {
perror("sem_wait");
goto EXIT5;
}
so->qbuf[so->tail++] = val;
if (sem_post(sem_consumer) == -1) {
perror("sem_wait");
goto EXIT5;
}
so->tail = so->tail % QUEUE_BUFFER_SIZE;
if (val == 99)
break;
++val;
}
EXIT5:
sem_destroy(sem_consumer);
if (sem_unlink(SEM_CONSUMER_NAME) == -1 && errno != ENOENT)
exit_sys("sem_unlink");
EXIT4:
sem_destroy(sem_producer);
if (sem_unlink(SEM_PRODUCER_NAME) == -1 && errno != ENOENT)
exit_sys("sem_unlink");
EXIT3:
if (munmap(shmaddr, 4096) == -1)
exit_sys("munmap");
EXIT2:
close(fdshm);
EXIT1:
if (shm_unlink(SHARED_MEM_NAME) == -1 && errno != ENOENT)
exit_sys("shm_unlink");
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/* consumer.c */
#include <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 <pthread.h>
#include <semaphore.h>
#define SHARED_MEM_NAME "/sample_shared_memory_name"
#define SEM_PRODUCER_NAME "/sample_mutex_producer_name"
#define SEM_CONSUMER_NAME "/sample_mutex_consumer_name"
#define QUEUE_BUFFER_SIZE 10
void exit_sys(const char* msg);
void exit_sys_errno(const char *msg, int eno);
struct SHARED_OBJECT {
int qbuf[QUEUE_BUFFER_SIZE];
size_t head;
size_t tail;
};
int main(void)
{
int fdshm;
sem_t *sem_producer;
sem_t *sem_consumer;
void *shmaddr;
struct SHARED_OBJECT *so;
int val;
srand(time(NULL));
if ((fdshm = shm_open(SHARED_MEM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
exit_sys("shm_open");
if (ftruncate(fdshm, 4096) == -1) {
perror("ftruncate");
goto EXIT1;
}
if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) {
perror("mmap");
goto EXIT2;
}
if ((sem_producer = sem_open(SEM_PRODUCER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, QUEUE_BUFFER_SIZE)) == NULL) {
perror("sem_open");
goto EXIT3;
}
if ((sem_consumer = sem_open(SEM_CONSUMER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, 0)) == NULL) {
perror("sem_open");
goto EXIT4;
}
so = (struct SHARED_OBJECT *)shmaddr;
for (;;) {
if (sem_wait(sem_consumer) == -1) {
perror("sem_wait");
goto EXIT5;
}
val = so->qbuf[so->head++];
if (sem_post(sem_producer) == -1) {
perror("sem_wait");
goto EXIT5;
}
so->head = so->head % QUEUE_BUFFER_SIZE;
usleep(rand() % 300000);
printf("%d ", val);
fflush(stdout);
if (val == 99)
break;
}
putchar('\n');
EXIT5:
sem_destroy(sem_consumer);
if (sem_unlink(SEM_CONSUMER_NAME) == -1 && errno != ENOENT)
exit_sys("sem_unlink");
EXIT4:
sem_destroy(sem_producer);
if (sem_unlink(SEM_PRODUCER_NAME) == -1 && errno != ENOENT)
exit_sys("sem_unlink");
EXIT3:
if (munmap(shmaddr, 4096) == -1)
exit_sys("munmap");
EXIT2:
close(fdshm);
EXIT1:
if (shm_unlink(SHARED_MEM_NAME) == -1 && errno != ENOENT)
exit_sys("shm_unlink");
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde de proseslerarası üretici-tüketici problemi benzer biçimde gerçekleştirilmektedir. Aşağıda buna ilişkin bir örnek
verilmiştir. Bu örnekte de yine "producer" ve "consumer" isimli iki program vardır. Bu örneği yukarıuda verdiğimiz UNIX/Linux sistemlerindeki
örneğin Windows karşılığı olarak düşünebilirsiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* producer.c */
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#define FILE_MAPPING_NAME "ProducerConsumerSharedMemoryName"
#define PRODUCER_SEMAPHORE_NAME "ProducerSemaphoreName"
#define CONSUMER_SEMAPHORE_NAME "ConsumerSemaphoreName"
#define QUEUE_BUFFER_SIZE 10
void ExitSys(LPCSTR lpszMsg);
struct SHARED_OBJECT {
int qbuf[QUEUE_BUFFER_SIZE];
size_t head;
size_t tail;
};
int main(void)
{
HANDLE hFileMapping;
HANDLE hSemProducer;
HANDLE hSemConsumer;
int val;
struct SHARED_OBJECT *sharedObject;
if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL)
ExitSys("CreateFileMapping");
if ((sharedObject = (struct SHARED_OBJECT *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL)
ExitSys("MapViewOfFile");
if ((hSemProducer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, PRODUCER_SEMAPHORE_NAME)) == NULL)
ExitSys("CreateSemaphore");
if ((hSemConsumer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, CONSUMER_SEMAPHORE_NAME)) == NULL)
ExitSys("CreateSemaphore");
val = 0;
for (;;) {
Sleep(rand() % 300);
WaitForSingleObject(hSemProducer, INFINITE);
sharedObject->qbuf[sharedObject->tail++] = val;
ReleaseSemaphore(hSemConsumer, 1, NULL);
sharedObject->tail = sharedObject->tail % QUEUE_BUFFER_SIZE;
if (val == 99)
break;
++val;
}
CloseHandle(hSemProducer);
CloseHandle(hSemConsumer);
UnmapViewOfFile(sharedObject);
CloseHandle(hFileMapping);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/* Consumer.c */
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#define FILE_MAPPING_NAME "ProducerConsumerSharedMemoryName"
#define PRODUCER_SEMAPHORE_NAME "ProducerSemaphoreName"
#define CONSUMER_SEMAPHORE_NAME "ConsumerSemaphoreName"
#define QUEUE_BUFFER_SIZE 10
void ExitSys(LPCSTR lpszMsg);
struct SHARED_OBJECT {
int qbuf[QUEUE_BUFFER_SIZE];
size_t head;
size_t tail;
};
int main(void)
{
HANDLE hFileMapping;
HANDLE hSemProducer;
HANDLE hSemConsumer;
struct SHARED_OBJECT *sharedObject;
int val;
if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL)
ExitSys("CreateFileMapping");
if ((sharedObject = (struct SHARED_OBJECT *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL)
ExitSys("MapViewOfFile");
if ((hSemProducer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, PRODUCER_SEMAPHORE_NAME)) == NULL)
ExitSys("CreateSemaphore");
if ((hSemConsumer = CreateSemaphore(NULL, 0, QUEUE_BUFFER_SIZE, CONSUMER_SEMAPHORE_NAME)) == NULL)
ExitSys("CreateSemaphore");
for (;;) {
WaitForSingleObject(hSemConsumer, INFINITE);
val = sharedObject->qbuf[sharedObject->head++];
ReleaseSemaphore(hSemProducer, 1, NULL);
sharedObject->head = sharedObject->head % QUEUE_BUFFER_SIZE;
printf("%d ", val);
fflush(stdout);
if (val == 99)
break;
Sleep(rand() % 300);
}
putchar('\n');
CloseHandle(hSemProducer);
CloseHandle(hSemConsumer);
UnmapViewOfFile(sharedObject);
CloseHandle(hFileMapping);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastError = GetLastError();
LPSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde "event senkronizasyon nesnesi" denilen önemli bir senkronizasyon nesnesi daha vardır. Bu senkronizasyon nesnesinin
UNIX/Linux sistemlerinde tam bir karşılığı yoktur. Ancak UNIX/Linux sistemlerindeki "koşul değişkenleri (conditioned variable)" işlev olarak
Windows'taki event senkronizasyon nesnelerinin görevini de yapabilmektedir.
Windows'taki event senkronizasyon nesneleri belli bir thread akışının başka bir thread bir işlemi bitirene kadar bir noktada bekletilmesi
amacıyla kullanılmaktadır. Örneğin bir thread global bir diziyi sıraya dizecek olsun. Ancak bu dizi başka bir thread tarafından oluşturulacak
olsun. Thread'ler asenkron çalıştığına göre sıraya dizme işlemini yapacak thread sıraya dizmenin yapıldığı noktaya geldiğinide diğer
thread'in diziyi oluşturmuş olması gerekmektedir. Eğer diğer thread diziyi oluşturmamışsa sıraya dizmeyi yapacak thread o noktada beklemeli
diğer thread diziyi oluşturduktan sonra bu işe başlamalıdır.
Thread'in bir noktada bekletilmesi semaphore nesneleriyle de kısmen yapılabilir. Örneğin sayacı 0 olan bir semaphore blokeye yol açacağı
için thread'i bekletebilir. Diğer thread de semaphore sayacını artırarak onu oradan kurtarabilir. Ancak semaphore'lar event nesnelerinin
sağladığı bazı durumları sağlayamamaktadır. Örneğin diğer thread bekleyen thread'in çalışmasına devam etmesini istediğinde artık aynı
senkronizasyon nesnesinde thread'in beklememesi gerekebilir. Semaphore'lar bunu doğrudan sağlaymamaktadır. Ya da örneğin diğerini beklemekten
kurtaran thread bunu birden fazla kez yaptığında semaphore'lar sayaçlı olduğu için diğer thread'in bekletilmesini sağlayamayabilirler.
Windows sistemlerindeki event senkronizasyon nesnesi aşağıdaki adımlardan geçilerek kullanılmaktadır:
1) Event senkronizasyon nesnesi CreateEvent API fonksiyonuyla yaratılır. Fonksiyonun prototipi şöyledir:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCSTR lpName
);
Fonksiyonun birinci parametresi kernel nesnesinin güvenlik bilgilerini belirtmektedir. Bu parametre NULL geçilebilir. Fonksiyonun ikinci
parametresi event nesnesinin "manuel mi otomatik mi" olduğunu belirtmektedir. Eğer bu parametreye TRUE girilirse nesne manuel olur, FALSE
girilirse otomatik olur. Event nesnesi manuel ise SetEvent yapıldığında nesne açık (signaled) durumda kalır. Nesne otomatik ise SetEvent
yapıldığında geçiş sağlanınca nesne yeniden otomatik olarak kapalı duruma getirilir. Bunun anlamı izleyen paragraflarda daha iyi anlaşılacaktır.
Fonksiyonun üçüncü parametresi nesnenin başlangıçta açık mı (signaled) yoksa kapalı mı (nonsignaled) olacağını belirtmektedir. Bu parametre
TRUE girilirse nesne başlangıçta açık, FALSE girilirse kapalı durumda olur. Genellikle event nesnesi yaratılıken kapalı bir biçimde yaratılmaktadır.
Fonksiyonun son parametresi nesnenin proseslerarası kullanılması için gerekli olan ismini belirtmektedir. Eğer nesne aynı prosesin thread'leri
arasında kullanılacaksa bu parametre NULL adres biçiminde geçilebilir. Fonksiyon başarı durumunda event nesnesinin handle değerine, başarısızlık
durumunda NULL adrese geri dönemktedir. Örneğin:
HANDLE g_hEvent;
...
if ((g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL)) == NULL)
Exitrys("CreateEvent");
2) Event nesnesi yine diğer kernel senkronizasyon nesnelerinde olduğu gibi WaitForSingleObject ve WaitForMultipleObjects fonksiyonlarıyla
yapılmaktadır. WaitForSingleObject fonksiyonu eğer event nesnesi kapalıysa blokeye yol açar, eğer nesne açık durumdaysa geçiş yapar.
Yani akış WaitForSingleObject fonksiyonunda beklemeden hemen fonksiyondan çıkarak devam eder.
3) Event nesnesini açık duruma geçirmek için SetEvent API fonksiyonu kullanılır. SetEvent fonksiyonunun prototipi şöyledir:
BOOL SetEvent(
HANDLE hEvent
);
Fonksiyon event nesnesinin handle değerini parametre olarak alır ve nesneyi açık duruma geçirir. Artık nesne kapalı olduğundan dolayı
bekleyen thread WaitForSingleObject fonksiyonundan çıkar. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır
değerine geri dönmektedir. Örneğin:
SetEvent(g_hEvent);
Nesne SetEvent fonksiyonu ile açık duruma geçirildiktin sonra WaitForSingleObject fonksiyonunda bekleyen thread blokeden çıkar. İşte eğer
nesne otomatik ise bu durumda WaitForSİngleObject fonksiyonundan çıkılırken nesne otomatik olarak yeniden kapalı duruma geçmektedir.
Eğer nesne manuel durumdaysa WaitForSingleObject fonksiyonu sonlandığında nesne hala açık durumda olmaya devam eder. Event nesnelerini
kapalı duruma geçirmek için ResetEvent API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
BOOL ResetEvent(
HANDLE hEvent
);
Fonksiyon event nesnesinin handle değerini parametre olarak alır, başarı durumunda sıfır değerine başarısızlık durumunda sıfır dışı bir
değere geri döner.
WaitForSingleObject (ya da WaitForMultipleObjects) fonksiyonu ile birden fazla thread otomatik moddaki event nesnesini bekliyorsa, bu
event nesnesi açık duruma geçtiğinde yalnızca tek bir thread'in blokesi çözülür. Çünkü event nesnesi otomatik durumda olduğu için
WaitForSingleObject fonksiyonundan çıkılır çıkılmaz nesne atomik bir biçimde kapalı duruma geçirilecektir. Tabii eğer event nesnesi
manuel modda ise SetEvent yapıldığında event nesnesini bekleyen thread'lerin blokesi çözülecektir.
Aşağıda event senkronizasyon nesnesinin kullanımına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
void ExitSys(LPCSTR lpszMsg);
HANDLE g_hEvent;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if ((g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL)) == NULL)
ExitSys("CreateEvent");
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
printf("Press ENTER to set event...\n");
getchar();
SetEvent(g_hEvent);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
CloseHandle(hThread1);
CloseHandle(hThread2);
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
printf("Thread1 running...\n");
printf("waiting for the event object...\n");
WaitForSingleObject(g_hEvent, INFINITE);
printf("Ok, thread1 resumes...\n");
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
printf("Thread2 running...\n");
printf("waiting for the event object...\n");
WaitForSingleObject(g_hEvent, INFINITE);
printf("Ok, thread2 resumes...\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Sık karşılaşılan diğer bir senkronizasyon nesnesi de "okuma-yazma kilitleri (reder-writer lock)" denilen nesnelerdir. Önce bu nesnelere
neden gereksinim duyulduğunu açıklayalım. Bir kaynak üzerinde bir grup thread'in "okuma" bir grup thread'in de "yazma" eyleminde bulunduğunu
düşünelim. Burada "okuma" eylemi demekle "kaynakta değişiklik yaratmayan", yazma eylemi demekle de "kaynakta değişiklik yaratan" eylemleri
kastediyoruz. Örneğin global bir bağlı liste söz konusu olsun. Thread'lern bazıları bu bağlı liste üzerinde "arama" işlemi yaparken bazıları
da bu bağlı liste üzerinde insert işlemi yapıyor olsunlar. Burada arama işlemi okuma eylemini, insert işlemi ise yazma eylemini temsil
etmektedir. Birden fazla thread'in aynı anda bağlı listede arama yapmasının sakıncası yoktur. Ancak bir thread bağlı listede insert işlemi
yaparken başka thread'lerin o işlem bitene kadar arama işlemi ya da insert işlemi yapmaması gerekir. Benzer biçimde bir thread bağlı liste
üzerinde arama işlemi yaparken başka bir thread'in insert işlemi de yapmaması gerekir. İki thread için buradaki olası durumları şöyle ifade
edebiliriz:
Thread1 Thread2 Ne Yapılmalı?
-------------------------------------------------------
Okuma Okuma İzin Verilmeli
Okuma Yazma Senkronize Edilmeli
Yazma Okuma Senkronize Edilmeli
Yazma Yzma Senkronize Edilmeli
O halde buradan çıkan sonuç şudur:
1) Bir thread yazma yaparken yazma ve okuma yapacak thread'ler bu yazma olayının bitmesini beklemelidir.
2) Bir thread okuma yaparken yazma yapacak thread'ler bu okuma işlminin bitmesini beklemelidir.
3) Bir thread okuma yaparken, okuma yapacak diğer thread'ler eş zamanlı olarak bu işlemi yapabilirler.
Okuma-yazma kilitleri Windows sistemlerinde de UNIX/Linux sistemlerinde de var olan senkronizasyon nesnelerindendir. Biz burada önce Windows
sistemlerindeki okuma-yazma kilitlerini daha sonra UNIX/Linux sistemlerindeki okuma-yazma kilitlerini göreceğiz.
Pekiyi reader-writer lock yerine yukarıdaki işlem mutex nesneleri ile yapılamaz mı? Eğer yukarıdaki problem mutex nesneleriyle çözülmeye
çalışılırsa bu durumda mecburen okuma sırasında da kilitleme yapılır. Dolayısıyla gereksiz bir biçimde okuma yapmak isteyen birden fazla
thread birbirlerini beklemek zorunda kalır. Aşağıdaki temsili (pseudo) kodu inceleyiniz:
pthread_mutex_t g_mutex;
...
read()
{
pthread_mutex_lock(&g_mutex);
...
pthread_mutex_unlock(&g_mutex);
}
write()
{
pthread_mutex_lock(&g_mutex);
...
pthread_mutex_unlock(&g_mutex);
}
Burada görüldüğü gibi bireden fazla read işlemi birlikte yapılamamaktadır.
Reader-writer lock nesneleri aslında taban (base) senkronizasyon nesnelerinden değildir. Bunlar mutex ve koşul değişkenleri (condition
variable) kullanılarak gerçekleştirilebilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde okuma-yazma kilitleri şöyle kullanılmaktadır:
1) Okuma-yazma kilit nesneleri SRWLOCK türüyle temsil edilmektedir. Önce global düzeyde SRWLOCK türünden bir nesne yaratılır. Bu nesneye
InitializeSRWLock fonksiyonu ile ilkdeğerleri verilir. Fonksiyonun prototipi şöyledir:
void InitializeSRWLock(
PSRWLOCK SRWLock
);
Fonksiyon SRWLOCK nesnesinin başlangıç adresini parametre olarak almaktadır. Örneğin:
SWRLOCK g_srwLock;
...
InitializeSRWLock(&g_srwLock);
2) Okuma amaçlı kritik kod şöyle oluşturulmaktadır:
AcquireSRWLockShared(&g_srwLock);
...
... <KRİTİK KOD>
...
ReleaseSRWLockShared(&g_srwLock)
Burada okuma amaçlı kilidin alınması AcquireSRWLockShared fonksiyonu ile yapılmaktadır. Fonksiyon eğer kilit başka bir thread tarafından
yazma amaçlı olarak alındıysa blokeye yol açmaktadır. Ancak kilit başka bir thread tarafından okuma amaçlı alındıysa blokeye yol açmadan
kritik koda geçişi sağlamaktadır. Okuma amaçlı kritik koddan çıkılırken kilit ReleaseSRWLockShared fonksiyonu ile serbest bırakılmalıdır.
Yazma Amaçlı kritik kod da şöyle oluşturulmaktadır:
AcquireSRWLockExclusive(&g_srwLock);
...
... <KRİTİK KOD>
...
ReleaseSRWLockExclusive(g_srwLock)
Eğer kilit başka bir thread tarafından okuma amaçlı ya da yazma amaçlı olarak alınmışsa AcquireSRWLockExclusive fonksiyonu blokede bekler.
Eğer kilit okuma ya da yazma amaçlı hiçbir thread tarafından alınmadıysa kritik koda geçiş yapılır. Yazma amaçlı kritik koddan çıkılırken
kilit ReleaseSRWLockExclusive fonksiyonu ile serbest bırakılmalıdır.
Fonksiyonların prototipileri şöyledir:
void AcquireSRWLockShared(
PSRWLOCK SRWLock
);
void ReleaseSRWLockShared(
PSRWLOCK SRWLock
);
void AcquireSRWLockExclusive(
PSRWLOCK SRWLock
);
void ReleaseSRWLockExclusive(
PSRWLOCK SRWLock
);
Fonksiyonlar SRWLOCK nesnelerinin adreslerini parametre olarak almaktadır.
Windows'ta reader-writer lock nesnelerinin serbest bırakılması gibi bir durum söz konusu değildir. Bunlar zaten bir kaynak tutmamaktadır.
Aşağıda Windows sistemlerinde reader-write lock nesnelerinin kullanımına ilişkin bir örnek verilmiştir. Bu örnekte belli sayıda thread
yaratılıp bunların rastgele read-write işlemleri yapması sağlanmıştır. Ekrandaki çıktı incelendiğinde write işlemi başladığında bitene
kadar başka hiç read ya da write işleminin yapılmadığı görülecektir. Ancak read işlemleri birlikte yapılabilmektedir. Programın ekran
çıktısının bir kısmının örnek bir görüntüsü şöyledir.
...
Thread-1 thread begins writing...
Thread-1 thread ends writing...
Thread-3 thread begins reading...
Thread-3 thread ends reading...
Thread-6 thread begins writing...
Thread-6 thread ends writing...
Thread-2 thread begins reading...
Thread-7 thread begins reading...
Thread-7 thread ends reading...
Thread-2 thread ends reading...
...
Burada örneğin Thread-2 ve Thread-7'nin read işlemine birlikte girdiği görülmektedir. Ancak bir write işlemi başladığında o write işlemi
bitene kadar read ya da write yapılamamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <windows.h>
#define NTHREADS 10
DWORD __stdcall ThreadProc(LPVOID lpvParam);
void Read(const char *pszThreadName);
void Write(const char *pszThreadName);
void ExitSys(LPCSTR lpszMsg);
SRWLOCK g_srwLock;
int main(void)
{
HANDLE hThreads[NTHREADS];
DWORD dwThreadIds[NTHREADS];
char szThreadName[32];
char *pszThreadName;
srand(time(NULL));
InitializeSRWLock(&g_srwLock);
for (int i = 0; i < NTHREADS; ++i) {
sprintf(szThreadName, "Thread-%d", i + 1);
if ((pszThreadName = strdup(szThreadName)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
if ((hThreads[i] = CreateThread(NULL, 0, ThreadProc, pszThreadName, 0, &dwThreadIds[i])) == NULL)
ExitSys("CreateThread");
}
if (WaitForMultipleObjects(NTHREADS, hThreads, TRUE, INFINITE) == WAIT_FAILED)
ExitSys("CreateThread");
for (int i = 0; i < NTHREADS; ++i)
CloseHandle(hThreads[i]);
return 0;
}
DWORD __stdcall ThreadProc(LPVOID lpvParam)
{
const char *pszThreadName = (LPCTSTR)lpvParam;
for (int i = 0; i < 10; ++i) {
Sleep(rand() % 100);
if (rand() % 2 == 0)
Read(pszThreadName);
else
Write(pszThreadName);
}
free(lpvParam);
return 0;
}
void Read(const char *pszThreadName)
{
AcquireSRWLockShared(&g_srwLock);
printf("%s thread begins reading...\n", pszThreadName);
Sleep(rand() % 300);
printf("%s thread ends reading...\n", pszThreadName);
ReleaseSRWLockShared(&g_srwLock);
}
void Write(const char *pszThreadName)
{
AcquireSRWLockExclusive(&g_srwLock);
printf("%s thread begins writing...\n", pszThreadName);
Sleep(rand() % 300);
printf("%s thread ends writing...\n", pszThreadName);
ReleaseSRWLockExclusive(&g_srwLock);
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde de okuma-yazma kilitlerinin temel kullanım biçimi benzerdir. İşlemler şöyle yürütülmektedir:
1) Okuma yazma kilitleri pthread_rwlock_t türüyle temsil edilmektedir. Programcı bu türden global bir nesne tanımlar. Nesneye ilkdeğer
vermenin iki yolu vardır. Birincisi statik düzeyde PTHREAD_RWLOCK_INITIALIZER isimli maroyu kullanmaktadır. Örneğin:
#include <pthread.h>
pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER;
İkincisi ise pthread_rwlock_init fonksiyonunu kullanmaktır. Örneğin:
pthread_rwlock_t g_rwlock;
...
int pthread_rwlock_init(&g_rwlock);
pthread_rwlock_init fonksiyonunun prototipi şöyledir:
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
Fonksiyonun birinci parametresi ilkdeğer verilecek pthread_rwlock_t nesnesinin adresini almaktadır. İkinci parametre reader-writer lock
nesnesinin özelliklerinin belirtildiği pthread_rwlockattr_t türünden nesnenin adresini almaktadır. İkinci parametre NULL geçilebilir. Bu
durumda nesne default özelliklerle yaratılmaktadır. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri dönmektedir.
2) Okuma amaçlı kritik kod şöyle oluşturulmaktadır:
pthread_rwlock_rdlock(&g_rwlock);
...
... <KRİTİK KOD>
...
pthread_rwlock_unlock(&g_rwlock);
Yazma amaçlı kritik kod ise şöyle oluşturulmaktadır:
pthread_rwlock_wrlock(&g_rwlock);
...
... <KRİTİK KOD>
...
pthread_rwlock_unlock(&g_rwlock);
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 pthread_rwlock_t türünden nesnenin adresini almaktadır. Genel çalışma biçimi yukarıda Windows sistemlerinde açıkladığımız
gibidir. Burada Windows sistemlerinden farklı olarak unlock işlemi için tek fonksiyon bulunmaktadır. Yani kilit okuma amaçlı da alınsa,
yazma amaçlı da alınsa kilidin bırakılması pthread_rwlock_unlock fonksiyonu ile yapılmaktadır. Fonksiyonlar başarı durumunda 0 değerine
başarısızlık durumunda errno değerine geri dönmektedir.
3) Programcı reader-writer lock nesnesi ile işini bitirsikten sonra pthread_rwlock_destroy fonksiyonu ile nesneyi yok edilmelidir. Fonksiyonun
prototipi şöyledir:
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
Fonksiyon yine pthread_rwlock_t nesnesinin adresini parametre olarak alır, başarı durumunda sıfır değerine başrısızlık durumunda ise
errno değerine geri döner.
Aşağıda UNIX/Linux sistemlerinde reader-writer lock nesnelerinin kullanımına örnek verilmiştir. Bu örnek aslında yukarıda Windows
sistemeri için yaptiğimız örneğin UNIX/linux versiyonu gibidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#define NTHREADS 10
void *thread_proc(void *param);
void read_proc(const char *ptname);
void write_proc(const char *ptname);
void exit_sys_errno(const char *msg, int eno);
pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER;
int main(void)
{
pthread_t tids[NTHREADS];
char tname[32], *ptname;
int result;
srand(time(NULL));
for (int i = 0; i < NTHREADS; ++i) {
sprintf(tname, "Thread-%d", i + 1);
if ((ptname = strdup(tname)) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
if ((result = pthread_create(&tids[i], NULL, thread_proc, ptname)) != 0)
exit_sys_errno("pthread_create", result);
}
for (int i = 0; i < NTHREADS; ++i)
pthread_join(tids[i], NULL);
if ((result = pthread_rwlock_destroy(&g_rwlock)) != 0)
exit_sys_errno("pthread_rwlock_destroy", result);
return 0;
}
void *thread_proc(void *param)
{
const char *ptname = (const char *)param;
for (int i = 0; i < 10; ++i) {
usleep(rand() % 100000);
if (rand() % 2 == 0)
read_proc(ptname);
else
write_proc(ptname);
}
return NULL;
}
void read_proc(const char *ptname)
{
int result;
if ((result = pthread_rwlock_rdlock(&g_rwlock)) != 0)
exit_sys_errno("pthread_rwlock_rdlock", result);
printf("%s thread begins reading...\n", ptname);
usleep(rand() % 300000);
printf("%s thread ends reading...\n", ptname);
if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0)
exit_sys_errno("pthread_rwlock_unlock", result);
}
void write_proc(const char *ptname)
{
int result;
if ((result = pthread_rwlock_wrlock(&g_rwlock)) != 0)
exit_sys_errno("pthread_rwlock_wrlock", result);
printf("%s thread begins writing...\n", ptname);
usleep(rand() % 300000);
printf("%s thread ends writing...\n", ptname);
if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0)
exit_sys_errno("pthread_rwlock_unlock", result);
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde ismine "koşul değişkenleri (condition variables)" denilen önemli bir senkronizasyon nesnesi vardır. Koşul
değişkenleri eskiden Windows sistemlerinde yoktu. Ancak belli bir zamandan sonra Windows sistemlerine de sokuldu. Windows'ta event nesneleri
olduğu için koşul değişkenleri yerine bu nesneler onların görevlerini tam olmasa da yaklaşık olarak yerine getirebiliyordu Ancak yukarıda
da belirttiğimiz gibi daha sonraları Windows sistemlerine de koşul değişkenleri eklenmiştir.
Koşul değişkenlerinin amacı bir koşul sağlanmadığı sürece thread'i blokede bekletmek, koşul sağlanınca thread'in blokesini çözmektir.
Ancak koşul değişkenlerinin kullanılması ve çalışma biçiminin anlaşılması şimdiye kadar gördüğümüz senkronizasyon nesnelerine göre
daha zordur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde koşul değişkenleri tipik olarak aşağıdaki adımlardan geçilerek kullanılmaktadır:
1) UNIX/Linux sistemlerinde koşul değişkenleri tek başlarına kullanılmamaktadır. Mutex nesneleri ile birlikte kullanılmaktadır. Koşul
değişkenleri pthread_cond_t türüyle temsil edilmektedir. Programcı koşul değişkenlerini yine global düzeyde bir mutex ile birlikte
tanımlar. Koşul değişkenlerine ilkdeğer verilmesi statik düzeyde PTHREAD_COND_INITIALIZER makrosuyle yapılabilir. Örneğin:
pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
Kooşul değişkenlerine ilkdeğer vermek için pthread_cond_init fonksiyonu da kullanılabilmektedir. Fonksiyonun prototipi şöyledir:
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
Fonksiyonun birinci parametresi pthread_cond_t nesnesini adresini, ikinci parametresi koşul değişkeninin özelliklerin belirtildiği
pthread_condattr_t nesnesinin adresini almaktadır. Biz bu kursta koşul değişkenlerinin özelliklerini ele almayacağız. Bu ikinci parametreyi
NULL olarak geçebilirsiniz. Bu durumda koşul değişkenleri default özelliklerle yaratılmaktadır. Zaten PTHREAD_COND_INITIALIZER makrosu da
koşul değişkenlerini default özelliklerle yaratmaktadır. Fonksiyon başarı durumunda sıfır değerine, başarısızlık durumunda errno değerine
geri dönmektedir.
2) Koşul değişkenleri ile koşul sağlanana kadar bekleme yapmak için pthread_cond_wait isimli fonksiyon kullanılmaktadır. Fonksiyonun
prototipi şöyledir:
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
Fonksiyonun birinci parametresi koşul değişkeninin adresini, ikinci parametresi mutex nesnesinin adresini almaktadır. Fonksiyon başarı
durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin:
pthread_cond_wait(&g_cond, &g_mutex);
Ancak koşul sağlanana kadar bekleme böyle yapılmamaktadır. Belli bir kalıba uygun biçimde bu fonksiyon çağrılmalıdır. Koşul değişkenini
bekleme kalıbı aşağıdaki gibidir:
pthread_mutex_lock(&g_mutex);
while (koşul_sağlanmadığı_sürece)
pthread_cond_wait(&g_cond, &g_mutex);
pthread_mutex_unlock(&g_mutex);
Burada dikkat edilmesi gereken durumlar şunlardır: pthread_cond_wait bir döngü içerisinde çağrılmalıdır. Döngünün aşağıdaki gibi oluşturulduğuna
dikkat ediniz:
while (koşul_sağlanmadığı_sürece)
pthread_cond_wait(&g_cond, &g_mutex);
Burada döngünün içerisindeki koşul "koşulun sağlanmadığı sürece uykuda kalınmasına ilişkin" koşuldur. Yani bu koşul sağlanmadığı zaman
thread uyandırılacaktır. Örneğin:
while (g_flag == 0)
pthread_cond_wait(&g_cond, &g_mutex);
Burada g_flag değişkeni sıfır olduğu sürece thread blokede bekleyecektir. Yani blokenin çözülmesi için g_flag değişkeninin 0'dan farkllı
bir değerde olması gerekmektedir.
Yukarıdaki kalıptaki diğer önemli bir nokta da koşul döngüsüne girmeden mutex nesnesinin sahipliğinin alındığı ve çıkışta da sahipliğin
bırakıldığıdır. Yukarıdaki kalıbın ne anlama geldiği izleyen paragraflarda açıklanacaktır.
3) Koşul değişkenini bekleyen thread'i uyandırmak için iki POSIX fonksiyonu bulunmaktadır: pthread_cond_signal ve pthread_cond_broadcast.
pthread_cond_signal fonksiyonu koşul değişkeninde bekleyen tek bir thread'i uyandırma amacındadır. pthread_cond_broadcast ise koşul
değişkeninde bekleyen tüm thread'leri uyandırma amacındadır. pthread_cond_signal fonksiyonu aslında tek bir thread'i uyandırmak istemektedir.
Ancak işletim sistemlerinde bu durum her zaman mümkün olamadığı için istemeden birden fazla thread de uyandırılabilmektedir. Bu duruma
"yanlış uyanma (spurious wakeup)" denilmektedir. Bu iki uyandırma biçimini şu örneğe benzetebiliriz: Bir odada 10 kişi uyuyor olsun. Siz de
yalnızca Ahmet'i uyandırmak isteyin. Ancak yaptığınız gürültüden istemeden Mehmet ve Selami de uyanmış olabilir. İşte Mehmet ve Selami'nin
uyanması "yanlış uyanma (spurious wakeup)" durumudur. Ancak siz bir megafonla bağırarak odadaki herkesi de uyandırabilirsiniz. Bu işlem
pthread_cond_broadcast işlemine benzemektedir. Şimdi siz "yanlışlıkla uyandırma (spurious wakeup)" gibi bir sürecin nasıl olup da yüksek
teknoloji gereken işletim sistemlerinde söz konusu olabildiğini merak edebilirsiniz. İşte işletim sistemlerinin etkin tasarımında mecburen böylesi
durumlar oluşabilmektedir. pthread_cond_signal fonksiyonunun koşul değişkeninde bekleyen bir thread'i değil "en az bir thread'i" uyandırdığına
dikkat ediniz. Ayrıca yanlış uyandırmanın yalnızca pthread_cond_signal yoluyla değil başka nedenlerden dolayı da oluşabileceğini belirtmek
istiyoruz.
Koşul değişkeninde bekleyen bir thread'in pthread_cond_signal ya da pthread_cond_broadcast fonksiyonu ile uyandırılması koşulun sağlandığı
anlamına gelmemektedir. Uyanan thread'ler koşulu test etmeli, koşul sağlanmıyorsa yeniden uykuya dalacak biçimde hareket etmelidir.
O halde pthread_cond_signal ya da pthread_cond_broadcast uygulanmadan önce bu fonksiyonları uygulayan kitarafın koşulun sağlanmasına yol
açması gerekir. Aşağıdaki beklemeye dikkat ediniz:
pthread_mutex_lock(&g_mutex);
while (g_flag == 0)
pthread_cond_wait(&g_cond, &g_mutex);
pthread_mutex_unlock(&g_mutex);
Burada diğer bir thread g_flag değişkenini değiştrmeden pthread_cond_signal ya da pthread_cond_broadcast uygularsa thrad uyandırılıp
pthread_cond_wait fonksiyonundan çıksa bile koşul sağlanmadığından dolayı yeniden uykuya dalacaktır. Tipik olarak programcı pthread_cond_signal
ya da pthread_cond_broadcast uygulamadan önce mutex nesnesinin sahipliğini almalı koşulu kritik kod içerisinde oluşturmalı, bu fonksiyonları
çağırdıktan sonra mutex nesnesinin sahipliğini bırakmalıdır. Yani tipik uygulama şöyle olmalıdır:
pthread_mutex_lock(&g_mutex);
g_flag = 1;
pthread_cond_broadcast(&g_cond);
pthread_mutex_unlock(&g_mutex);
pthread_cond_signal ve pthread_cond_broadcast fonksiyonlarının prototipleri şöyledir:
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
Fonksiyonlar koşul değişken nesnesinin adresini paranetre olarak almakta, başarı durumunda 0 değerine, başarısızlık durumunda errno değerine
geri dönmektedirler.
4) Koşul değişkeninin ve mutex nesnesinin kullanımı bittiğinde bunlar pthread_cond_destroy ve pthread_mutex_destroy fonksiyonuyla
yok edilmelidir. pthread_cond_destroy fonksiyonunun prototipi şöyledir:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri dönmektedir. Aslında genel olarak pthread_cond_destroy
ve pthread_mutex_destroy fonksiyonları pek çok kütüphanede bir şey yapmamaktadır. Ancak yine de bu fonksiyonların uyumluluk bakımından
çağrılması gerekir.
Aşağıda koşul değişkenlerinin kullanımına tipik bir örnek verilmiştir. Örneğimizde iki thread g_flag değişkeninin sıfır dışı bir değer
olmasını beklemektedir. Ana thread'te g_flag değişkeni 1 değerine set edilip pthread_broadcast işlemi uygulandığında bekleyen iki thread
de uyanmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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);
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
int g_flag = 0;
int main(void)
{
pthread_t tid1, tid2;
int result;
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_sys_errno("pthread_create", result);
printf("press ENTER to continue...\n");
getchar();
if ((result = pthread_mutex_lock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
g_flag = 1;
if ((result = pthread_cond_broadcast(&g_cond)) != 0)
exit_sys_errno("pthread_cond_broadcast", result);
if ((result = pthread_mutex_unlock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lunock", result);
if ((result = pthread_join(tid1, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_join(tid2, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_cond_destroy(&g_cond)) != 0)
exit_sys_errno("pthread_cond_destroy", result);
if ((result = pthread_mutex_destroy(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_destroy", result);
return 0;
}
void *thread_proc1(void *param)
{
int result;
printf("thread-1 is waiting at the condition variable..\n");
if ((result = pthread_mutex_lock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
while (g_flag == 0)
if ((result = pthread_cond_wait(&g_cond, &g_mutex)) != 0)
exit_sys_errno("pthread_cond_wait", result);
if ((result = pthread_mutex_unlock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_unlock", result);
printf("ok, thread-1 resumes...\n");
return NULL;
}
void *thread_proc2(void *param)
{
int result;
printf("thread-2 is waiting at the condition variable..\n");
if ((result = pthread_mutex_lock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_lock", result);
while (g_flag == 0)
if ((result = pthread_cond_wait(&g_cond, &g_mutex)) != 0)
exit_sys_errno("pthread_cond_wait", result);
if ((result = pthread_mutex_unlock(&g_mutex)) != 0)
exit_sys_errno("pthread_mutex_unlock", result);
printf("ok, thread-2 resumes...\n");
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Şimdi de koşul değişkenlerini kullanmak için belirttiğimiz kalıpların ne anlam ifade ettiği üzerinde duracağız. Bekleme işleminin mutex
nesnesinin sahipliği alınarak yapılması gerektiğini belirtmiştik. Örneğin:
pthread_mutex_lock(&g_mutex);
while (g_flag == 0)
pthread_cond_wait(&g_cond, &g_mutex);
pthread_mutex_unlock(&g_mutex);
Burada önce mutex nesnesinin sahipliğinin alındığını görüyorsunuz. pthread_cond_wait fonksiyonu uykuya dalmadan önce atomik bir biçimde
mutex nesnesinin sahipliğini bırakmaktadır. Şimdi aşağıdaki koda bakalım:
pthread_mutex_lock(&g_mutex);
g_flag = 1;
pthread_mutex_unlock(&g_mutex);
pthread_cond_broadcast(&g_cond);
Burada pthread_cond_broadcast uygulandığında bu koşul değişkeninde bekleyen tüm thread'ler uyanacaktır. İşte pthread_cond_wait fonksiyonu
uykuya dalmadan önce mutex nesnesinin sahipliğini nasıl bırakıyorsa uyanırken de sahipliği alarak uyanmaktadır. Böylece thread uyandığında
koşul eğer sağlanıyorsa döngüden çıkıp yine mutex'i sahipliği bırakacaktır. Şimdi yukarıdaki kodda g_flag değerinin zaten 1 olduğunu
düşünelim. Bu durumda mutex nesnesinin sahipliği alınacak g_flag == 1 olduğu için de döngüye girilmeyecektir. Böylece mutex nesnesinin sahipliği
bırakılacaktır.
Pekiyi neden yukarıdaki kalıpta bir döngü kullanılmıştır. Neden döngü yerine if deyimi kullanılmamıştır? İşte bunun nedeni "yalancı uyanma
(spurious wakeup)" yüzündendir. Döngü yerine aşağıdaki gibi bir if deyiminin olduğunu varsayalım:
pthread_mutex_lock(&g_mutex);
if (g_flag == 0)
pthread_cond_wait(&g_cond, &g_mutex);
pthread_mutex_unlock(&g_mutex);
Burada yanlış uyandırma oluştuğunda eğer koşul sağlanmıyorsa thread'in yeniden uykuya dalması gerekir. Bunun için de bir döngünün kullanılması
gerekmektedir. Şimdi programcının koşul değişkeninde bekleyen thread'lerden yalnızca birini çözmek istediğini düşünelim. Aşağıdaki kalıp
bunu yapamayacaktır:
pthread_mutex_lock(&g_mutex);
while (g_flag == 0)
pthread_cond_wait(&g_cond, &g_mutex);
pthread_mutex_unlock(&g_mutex);
Burada diğer tarafın pthread_cond_signal uyguladığını kabul edelim. Bu durumda 2 thread'in de uyandırıldığını düşünelim. Bu iki thread de
mutex nesnesinin sahipliğini almaya çalışacak ancak yalnızca biri alacaktır. Dolayısıyla diğeri bu mutex nesnesini bekleyecektir. Ancak
mutex nesnesinin sahipliğini almış olan thread onu bıraktığında bu kez diğer thread mutex nesnesinin sahipliğini alarak koşul değişkeninden
çıkacaktır. O halde programcının mutex kontrolünde yeniden koşul değişkenini eski haline getirmesi gerekmektedir. Örneğin:
pthread_mutex_lock(&g_mutex);
while (g_flag == 0)
pthread_cond_wait(&g_cond, &g_mutex);
g_flag = 0;
pthread_mutex_unlock(&g_mutex);
pthread_cond_signal ya da pthread_cond_broadcast uygulayan tarafın koşul değişkenlerini aşağıdaki gibi mutex kontrolü içerisinde değiştirmesi
aslında mutlak anlamda gerekmemektedir:
pthread_mutex_lock(&g_mutex);
g_flag = 1;
pthread_cond_broadcast(&g_cond);
pthread_mutex_unlock(&g_mutex);
Ancak koşulların birden fazla ğeğişkene bağlı olduğu durumda önce mutex kontrolü ile kritik kod içerisinde koşulların ayarlanması
gerekebilmektedir. C'deki aslında basit bir atama işlemi bile çok thread'li ortamda senkronizasyon sorunlarına yol açabilmektedir.
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde durum değişkenleri daha çok aynı prosesin thread'leri arasında kullanılıyor olsa da aslında prosesler arasında
da kullanılabilmektedir. Tabii bu durumda koşul değişkeninin ve mutex nesnesinin paylaşılan bellek alanı üzerinde yaratılması ve yaratım
sırasında da attribute nesneleri ile prosesler arası kullanım durumunun set edilmesi gerekmektedir. Bu işlem mutex nesnelerindekine benzer
biçimde yapılmaktadır:
pthread_cond_t g_cond;
...
pthread_condattr_t attr;
pthread_condattr_init(&attr);
pthread_condattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_cond_init(&g_cond, &attr);
pthread_condattr_destroy(&attr);
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Daha önce de belirttiğimiz gibi belli bir zamandan sonra Windows sistemlerine de durum değişkenleri eklenmiştir. Windows sistemlerindeki
durum değişkenleri mutex nesneleriyle değil CRITICAL_SECTION ve Read/Write Lock nesneleriyle kullanılabilmektedir. Genel kullanım biçimi
UNIX/Linux sistemlerindekine çok benzemektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir thread'in çalışmasına ara verilmesi preemptive bir biçimde donanım kesmeleri yoluyla yapılmaktadır. İşlemci bir makine komutunu
çalıştırırken kesme oluşsa bile makine komutunun çalışması bitmeden kesme konumuna geçmez. Yani iki komut arasında kesmeleri işleme sokmaktadır.
Thread'ler dünyasında bir işlemin "atomik (atomic)" yapılması "kesilmeden yapılması" anlamına gelmektedir. O halde makine komutları atomiktir.
Aşağıdaki işleme bakınız:
++g_count;
Böyle bir kod genellikle derleyiciler tarafından tek bir makine komutuyla değil birkaç makine komutuyla yapılmaktadır. Örneğin Intel
işlemcilerinde bu işlem için derleyici aşağıdaki gibi üç makine komutu üretebilmektedir:
MOV EAX, g_count
INC EAX
MOV g_count, EAX
İşte komutlar arasında thread'ler arası geçiş (context switch) oluşursa daha önce ele aldığımız senkronizasyon sorunları oluşacaktır.
Aslında ++g_count işlemi bazı işlemcilerde tek bir makine komutuyla da yapılabilmektedir. Örneğin bu işlem Intel işlemcilerinde aşağıdaki
gibi tek bir makine komutuyla da yapılabilmektedir:
INC g_count
Makine komutları atomik olduğuna göre bu işlem çok thread'li ortamlarda tamamen güvenli midir? İşte makine komutları arasında thread'ler
arası geçiş oluşmamakla birlikte çok işlemcili ya da çok çekirdekli sistemlerde başka bir sorun da ortaya çıkmaktadır. İşlemcilerin
bazılarında birden fazla çekirdek aynı bellek adresine erişirken oradaki bilgiyi bozabilmektedir. Bunun donanımsal olarak nasıl gerçekleştiği
üzerinde burada durmayacağız. Böyle bir olasılık düşük olsa da maalesef yine de bulunmaktadır. Yine bir çekirdek bir bellek adresine
bir değer yazarken diğer bir çekirdek oradan değer okumak istediğinde okuyan taraf önceki ya da sonraki değeri değil bozuk bir değeri de
okuyabilmektedir. Bu durumda maalesef tek bir değişken bile iki thread arasında anlık kullanılacak olsa yine de bu işlemin senkronize
edilmesi gerekmektedir. Ancak bir tek değişkene bir değer atamak için mutex gibi bir senkronizasyon nesnesini kullanmak performansı
düşürmektedir. Aslında işlemcileri tasarlayanlar bunun çaresini de düşünmüşlerdir. Bir makine komutunun diğer çekirdekleri ya da işlemcileri
o komutluk durdurarak yalnızca o komutu çalıştıran işlemci ya da çekirdeğin belleğe erişmesi sağlanabilmektedir. Örneğin Intel işlemcilerinde
bir makine komutunun başına LOCK öneki getirilirse işlemci o komutu diğer işlemcileri durdurarak çalıştırmaktadır:
LOCK INC g_count
Bu işlem artık Intel işlemcilerinde çok çekirdekli sistemlerde de sorunu çözecektir. Tabii buradaki LOCK öneki performansı da düşürmektedir.
Derleyicinin her komutta bu öneki kullanması kodun yavaş çalışmasına yol açacaktır. Pekyi biz C programcısı olarak derleyicinin böyle
bir kod üretmesini nasıl sağlayabiliriz?
/*------------------------------------------------------------------------------------------------------------------------------------------
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde diğer işlemcileri bir komutluk durdurarak bellek işlemi yapabilmek için tasarlanmış başı Interlocked ile başlayan
InterlockedXXX biçiminde API fonksiyonları vardır. Tabii bu fonksiyonların içi sembolik makine dilinde yazılmıştır. Önemli birkaç Interlocked
fonksiyonlarının prototiplerini aşağıda veriyoruz:
LONG InterlockedIncrement(LONG volatile *Addend);
LONG InterlockedAdd(LONG volatile *Addend, LONG Value);
LONG InterlockedDecrement(LONG volatile *Addend);
LONG InterlockedExchange(LONG volatile *Target, LONG Value);
Aslında çok fazla Interlocked fonksiyon bulunmaktadır. Bunların listesi için aşağıdaki MSDN dokümanlarına başvurabilirsiniz:
https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-functions
Yukarıdaki fonksiyonlar long türden nesnelerin adreslerini parametre olarak almaktadır. Interlocked fonksiyonlarının genellikle tek bir
makine komutuyla yapılabilecek işlemler için bulundurulduğuna dikkat ediniz.
Aşağıdaki örnekte iki thread de aynı global değişkeni bir milyon kez artırmıştır. Ancak bu işlemi yukarıda görmüş olduğumuz InterllockedIncrement
fonksiyonuyla yapmıştır. Bu fonksiyon bir nesneyi atomik olarak tek bir makine komutuyla diğer işlemcileri durdurarak artırmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
long g_count;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
printf("%ld\n", g_count);
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
long i;
for (i = 0; i < 1000000; ++i)
InterlockedIncrement(&g_count);
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
long i;
for (i = 0; i < 1000000; ++i)
InterlockedIncrement(&g_count);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Derleyiciler tarafından doğrudan tanınan çağrılması için prototipe gereksinim duyulmayan ve genellikle derleyicilerin CALL makine komutu
yerine inline fonksiyon gibi açım yaptığı özel fonksiyonlara "built-in" ya da "intrinsic" fonksiyon denilmektedir. C derleyicilerinin bir
bölümü bazı standart C fonksiyonlarını ve kendilerine özgü bazı eklenti (extension) fonksiyonları bu biçimde ele almaktadır. Microsoft C
derleyicilerinde, gcc ve clang derleyicilerinde "built-in" ya da "intrinsic" fonksiyonlar bulunmaktadır. Microsoft C derleyicilerinin
"intrinsic" fonksiyon listesine aşağıdaki bağlantıdan ulaşabilirsiniz:
https://learn.microsoft.com/en-us/cpp/intrinsics/compiler-intrinsics?view=msvc-170
gcc Derleyicilerinin built-in fonksiyonlarına da aşağıdaki bağlantılardan ulaşabilirsiniz:
https://gcc.gnu.org/onlinedocs/gcc/Target-Builtins.html
https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
gcc'de bazı built-in fonksiyonlar atomik işlemler için bulundurulmuştur. Bunları Microsoft'un InterlockedXXX fonksiyonlarına benzetebilirsiniz.
Ancak gcc'de önceleri bu atomic built-in fonksiyonlar __sync_xxx biçiminde isimlendirilmişti. Sonra C++'a <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 iki thread aynı global değişkeni artırmaktadır. Biz daha önce bu örneği çeşitli senkronizasyon nesneleriyle zaten
yapmıştık. Burada hiç senkronizasyon nesnesi kullanmadan gcc'nin atomic built-in fonksiyonlarıyla aynı şeyi yapıyoruz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* 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);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C11 ile birlikte C'ye isteğe bağlı olarak _Atomic biçiminde bir tür belirleyicisi ve niteleyicisi de eklenmiştir. _Atomic ile tanımlanan
nesneler için derleyiciler atomik işlem yapacak biçimde kod üretmektedir. Tabii bu _Atomic belirleyicisi atomic yapılamayack işlemlerde
bir etki göstermemektedir. Bir C11 derleyicisinin _Atomic belirleyicisini destekleyip desteklemediği __STDC_NO_ATOMICS__ makrosuna
bakılarak belirlenebilir. Microsoft C derleyicileri henüz _Atomic belirleyicisini desteklememektedir. Ancak gcc derleyicileri belli bir
sürümden sonra bu belirleyiciyi desteklemektedir. Ayrıca C11 ile birlikte <atomic.h> başlık dosyasında aşağıdaki gibi tür makroları da
bulundurulmuştur:
atomic_bool _Atomic _Bool
atomic_char _Atomic char
atomic_schar _Atomic signed char
atomic_uchar _Atomic unsigned char
atomic_short _Atomic short
atomic_ushort _Atomic unsigned short
atomic_int _Atomic int
atomic_uint _Atomic unsigned int
atomic_long _Atomic long
atomic_ulong _Atomic unsigned long
atomic_llong _Atomic long long
...
Bunların tam listesi için standartlara başvurabilirsiniz. Yine C++11 ile birlikte atomik işlem yapan fonksiyonlar da isteğe bağlı olarak
standart hale getirilmiştir. Bunların bazılarını aşağıda veriyoruz:
atomic_store
atomic_store_explicit
atomic_load
atomic_load_explicit
atomic_exchange
atomic_exchange_explicit
atomic_compare_exchange_strong
atomic_compare_exchange_strong_explicit
atomic_compare_exchange_weak
atomic_compare_exchange_weak_explicit
atomic_fetch_add
atomic_fetch_add_explicit
atomic_fetch_sub
atomic_fetch_sub_explicit
Aşağıdaki örnekte g_count global değişkeni _Atomic ile tanımlandığı için zaten ++g_count işlemi derleyici tarafından atomik bir biçimde
yapılacaktır. Başka bir deyişle derleyici bu değişken işleme sokulurken onu tek bir makine komutuyla ve lock işlemi ile işleme sokmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#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);
_Atomic int g_count;
int main(void)
{
pthread_t tid1, tid2;
int result;
srand(time(NULL));
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_join(tid1, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_join(tid2, NULL)) != 0)
exit_sys_errno("pthread_join", result);
printf("%d\n", g_count);
return 0;
}
void *thread_proc1(void *param)
{
for (int i = 0; i < 1000000; ++i)
++g_count;
return NULL;
}
void *thread_proc2(void *param)
{
for (int i = 0; i < 1000000; ++i)
++g_count;
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C++'ta C'nin _Atomic belirleyicisi yoktur. (Genel olarak C++ Programlama Dili C Programlama Dilini kapsıyor olsa da bu kapsama mükemmel
düzeyde değildir. C++'ta atomik işlemler <atomic> başlık dosyasında bildirilen std::atomic<T> isimli sınıf şabşlonuyla yapılmaktadır.
Bu sınıfın operatör fonksiyonları gerçekten çok işlemcili sistemler için atomik işlemler yapmaktadır. Dolayısıyla yukarıdaki işlemlerin
C++'taki eşdeğerleri aşağıdaki gibi oluşturulabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <cstdio>
#include <cstdlib>
#include <atomic>
#include <windows.h>
using namespace std;
void ExitSys(LPCSTR lpszMsg);
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
atomic<int> g_count;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
printf("%ld\n", (int)g_count);
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
long i;
for (i = 0; i < 1000000; ++i)
++g_count;
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
long i;
for (i = 0; i < 1000000; ++i)
++g_count;
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir fonksiyon farklı thread'lerden aynı anda çağrıldığında herhangi bir sorun oluşmuyorsa o fonksiyon thread güvenlidir. Yani "thread
güvenlilik (thread safety)" birden fazla thread tarafından aynı anda çağrılan fonksiyonların sorun çıkartmaması anlamına gelmektedir.
Pekiyi thread güvenliliği bozan faktörler nelerdir? İşte eğer fonksiyon bir "statik data" kullanıyorsa (yani global bir nesneyi ya da
static yerel bir nesneyi kullanıyorsa) o fonksiyon thread güvenli olamaz. Çünkü global değişkenlerin ve static yerel değişkenlerin
toplamda tek bir kopyası vardır. Thread akışları aynı fonksiyonda ilerlerken aynı kopya üzerinde işlem yaptıklarından dolayı bir anomali
oluşabilecektir. Örneğin:
void foo(void)
{
static int a = 0;
++a;
...
++a
...
++a;
...
}
Burada bu foo fonksiyonu farklı thread'lerden aynı anda çağrıldığında bu farklı thread'ler static nesnenin aynı kopyasını kullanacağından
dolayı bir bozulma oluşacaktır. static yerel nesnelerin stack'te değil "data" ya da "bss" alanlarında yaratıldığını anımsayınız.
Aynı durum global nesne kullanan fonksiyonlar için de geçerlidir. Yukarıda da belirttiğimiz gibi "thread güvenlilik (thread safety)"
bir fonksiyonun farklı thread'ler tarafından aynı anda çağrıldığında sorun oluşmaması anlamına gelmektedir. Thread güvenliliği bozan
en faktör "static data" kullanımıdır. Yani programın static yerel ya da global nesneleri kullanmasıdır. Örneğin:
char *myitoa(int a)
{
static char buf[32];
sprintf(buf, "%d", a);
return buf;
}
Burada myitoa thread güvenli değildir. Çünkü bu fonksiyon birden fazla thread tarafından aynı anda (iç içe geçecek biçimde) çağrılırsa
sorun oluşacaktır. Çünkü iki çağrının kullandığı yerel nesne aynı nesnedir.
Pekiyi fonksiyonu "thread güvensiz" yapan şey nedir? Eğer bir fonksiyon statik veri kullanıyorsa (yani static yerel nesneler ya da global
nesneler) fonksiyon thread güvenli olmaz. Ortak kaynakları kullanan fonksiyonlar thread güvenli değildir. Mademki fonksiyonu thread güvenli
olmaktan çıkartan faktör fonksiyonun statik veri kullanmasıdır. O halde biz fonksiyonu static data kullanmaktan çıkartırsak thread güvenli
hale getirmiş oluruz. Örneğin:
char *myitoa_tsafe(char *buf, int a)
{
sprintf(str, "%d", a);
return buf;
}
Burada myitoa fonksiyonu artık static yerel bir dizinin adresiyle geri dönmemektedir. Ona geçirilen adresle geri dönmektedir. Örnek
bir kullanım şöyle olabilir:
char s[100];
myitoa_tsafe(s, 123456)
Burada myitoa_tsafe fonksiyonu artık farklı thread'ler tarafından aynı anda çağrılsa bile bir sorun oluşmayacaktır. (Tabii burada s dizisinin
yerel bir dizi olduğunu kabul ediyoruz.)
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C'nin de bazı standartc fonksiyonları static yerel nesne ya da dizi kullanmaktadır. Dolayısıyla bu fonksiyonlar thread güvenli değildir.
Örneğin localtime fonksiyonu thread güvenli değildir. local time fonksiyonunun parametrik yapısını anımsayınız:
struct tm *localtime(const time_t *timep);
Burada localtime fonksiyonu struct tm türünden static yerel bir nesnenin adresiyle geri dönmektedir.
struct tm *localtime(const time_t *timep)
{
static struct tm lt;
....
return &lt;
}
Görüldüğü gibi fonksiyon aslında bize bir adres vermektedir, ancak bu adres static yerel bir nesnenin adresidir. Dolayısıyla onun tek
bir kopyası vardır. Biz bu fonksiyonu farklı thread'lerden aynı anda çağırırsak aslında aynı nesne üzerinde işlem yapılacağı için bu
nesnenin içeriği bozulacaktır. Benzer biçimde ctime fonksiyonu da static yerel bir dizinin adresiyle dönmektedir. Bu fonksiyon da
thread güvenli değildir. Benzer biçimde rassal sayı üretmekte kullanılan rand fonksiyonu da global bir nesne (tohum nesnesi) kullanmaktadır.
Dolayısıyla bu fonksiyon da thread güvenli değildir. Aşağıda C'de thread güvenli olmama potansiyelinde olan standart C fonksiyonlarının
listesini veriyoruz:
strtok
strerror
ctime
gmtime
localtime
asctime
rand
srand
tmpnam
tempnam
setlocal
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi thread güvenli olmayan bir fonksiyonu nasıl thread güvenli hale getiririz? İlk yapılacak şey şüphesiz fonksiyonun static data
(statik yerel ya da global nesneleri kastediyoruz) kullanmasını engellemektir. Static data kullanan programlar thread güvenli olamazlar.
Bu durumda fonksiyonu thread güvenli hale getirmek için yapılacak şey onun static data kullanmasını engellemektir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi C'nin standart kütüphanesindeki bazı fonksiyonlar thread güvenli doğaya sahip değilse ne yapabiliriz? Bu fonksiyonlar izleyen
paragraflarda ele alacağımız gibi "thread'e özgü global alanlar" oluşturularak kütüphaleri yazanlar tarafından thread güvenli hale
getirilebilirler. Çalıştığınız C derleyicisinin standart kütüphanesinin thrad güvenli olup olmadığını öğrenmelisiniz.
C'nin 2011 versiyonuna kadar (C11) C standartlarında thread lafı edilmemişti. Ancak ilk kez C11'de thread kavramı standartlara sokuldu ve
isteğe bağlı (optional) bir thread kütüphanesi de standartla eklendir. Ancak bu standartlar da yukarıda belirttiğimiz fonksiyonların
thread güvenli olup olmadığı konusunda bir açıklama yapmamıştır. Yani özet olarak bir C derleyicisindeki yukarıdakine benzer standart
C fonksşyonları thread gğvenli olabilir ya da olmayabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Microsoft 2005 yılına kadar standart C kütüphanesinin thread güvenli olan ve thread güvenli olmayan iki farklı versiyonunu bulundurmaktaydı.
Ancak 2005'ten itibaren tek bir standart C kütüphanesi bulundurmaya başlamıştır. O da thread güvenli kütüphanedir.
POSIX sistemlerinde static data kullanan sorunlu standart C fonksiyonlarının hepsinin xxxxx_r isimli thread güvenli bir versiyonu da
bulundurulmuştur. Bu versiyonlar genel olarak static data kullanmak yerine ekstra bir parametre ile static data için kullanılacak alanı
fonksiyonu çağırandan istemeketdir. Örneğin localtime ve localtime_r fonksiyonlarının prototipleri şöyledir:
struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);
localtime_r fonksiyonu static yerel nesne kullanmamakta parametre olarak alınan struct tm nesnesine yerleştirme yapmaktadır. Tabii
fonksiyon parametresiyle aldığı nesnenin adresine geri dönmektedir. Örneğin rand ve rand_r fonksiyonlarının prototipleri şöyledir:
int rand(void);
int rand_r(unsigned int *seedp);
rand_r fonksiyonunun global tohum değişkeni kullanmadığına tohum değişkenini parametre olarak aldığına dikkat ediniz.
Aşağıdaki örnekte ctime fonksiyonun thread güvenli versiyonu olan ctime_r fonksiyonu kullanılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
void *thread_proc1(void* param);
void *thread_proc2(void* param);
void foo(void);
void exit_errno(const char* msg, int result);
int main(void)
{
int result;
pthread_t tid1, tid2;
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_errno("pthread_create", result);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
void foo(void)
{
time_t t;
char s[64];
t = time(NULL);
ctime_r(&t, s);
printf("%s", s);
}
void *thread_proc1(void *param)
{
foo();
return NULL;
}
void *thread_proc2(void *param)
{
foo();
return NULL;
}
void exit_errno(const char *msg, int result)
{
fprintf(stderr, "%s: %s\n", msg, strerror(result));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C'nin standart FILE stream işlemlerinin thread güvenli olup olmadığı hakkında da bir şey söylenmemiştir. Ancak hem Microsoft C kütüphanesi
hem de GNU C kütüphanesi FILE stream işlemlerini thread güveli biçimde yapmaktadır. Yani iki ayrı thread global bir stream nesnesi üzerinde
işlem yapıyorsa kullanılan tampon bu fonskiyonlar tarafından kritik kodlarla zaten korunmuş durumdadır. İç içe geçme durumu oluşmamaktadır.
Benzer biçimde C++'ın <iostream> kürüphanesi de aynı dosya üzerinde işlem yapılırken bile thread güvenlidir. Ancak standartlar bunu garanti
etmemktedir.
Aşağıda Windows sistemlerinde iki thread eş zamanlı olarak aynı dosyaya yazma yapaktadır. Dosya tamponunun her açılan dosya için ayrı
bir biçimde oluşturulduğunu anımsayınız. Eğer Windows'ta dosya nesneleri thread güvenli olmasaydı bu yazma işlemlerinde iç içe geçme
olabilirdi. Buradaki örnekte "test.txt" dosyasının içini inceleyiniz. İç içe geçmenin olmadığını göreceksiniz. Örneğin:
...
thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-2
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
...
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
FILE *g_f;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if ((g_f = fopen("test.txt", "w")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
int i;
for (i = 0; i < 1000; ++i)
fprintf(g_f, "Thread-1\n");
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
int i;
for (i = 0; i < 1000; ++i)
fprintf(g_f, "Thread-2\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz gibi GNU C Kütüphanesindeki stream işlemleri de thread güvenlidir. Yukarıda vermiş olduğumuz örneğin UNIX/Linux
karşılığı da aşağıdaki gibidir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <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);
FILE *g_f;
int main(void)
{
pthread_t tid1, tid2;
int result;
if ((g_f = fopen("test.txt", "w")) == NULL) {
fprintf(stderr, "cannot open file!..\n");
exit(EXIT_FAILURE);
}
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_join(tid1, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_join(tid2, NULL)) != 0)
exit_sys_errno("pthread_join", result);
return 0;
}
void *thread_proc1(void *param)
{
int i;
for (i = 0; i < 10000; ++i)
fprintf(g_f, "Thread-1\n");
return NULL;
}
void *thread_proc2(void *param)
{
int i;
for (i = 0; i < 10000; ++i)
fprintf(g_f, "Thread-2\n");
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
C++'ın iostream kütüphanesi de genel olarak Microsoft ve UNIX/Linux sistemlerinde thread güvenli yazılmıştır. Ancak standartlar bağlamında
bunun için bir garanti bulunmamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <iostream>
#include <string>
#include <fstream>
#include <thread>
using namespace std;
fstream g_f;
void thread_proc1()
{
for (int i = 0; i < 1000; ++i)
g_f << "thread-1 running...\n";
}
void thread_proc2()
{
for (int i = 0; i < 1000; ++i)
g_f << "Thread-2 running...\n";
}
int main(void)
{
g_f.open("test.txt", ios::out);
thread t1(thread_proc1);
thread t2(thread_proc2);
t1.join();
t2.join();
g_f.close();
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Daha önceden de belirttiğimiz gibi Windows sistemlerindeki ReadFile ve WriteFile API fonksiyonları, UNIX/Linux sistemlerindeki read ve
write POSIX fonksiyonları işletim sisteminin sistem fonksiyonlarını doğrudan çağırmaktadır. İşletim sisteminin sistem fonksiyonaları da
okuma ve yazma bağlamında sistem genelinde atomiktir. Bunun anlamı şudur: Biz iki farklı thread'ten ya da prosesten aynı dosyaya, dosyanın
aynı yerine aynı anda yazma ya da okuma yapsak bile iç içe geçme asla oluşmaz. Bu durum kernel tarafından senkronize edilmektedir. Örneğin
thread ya da proseslerden biri tam bir dosyanın belli bir offset'ine WriteFile ya da write fonksiyonuyla 100 byte yazıyor olsun. Tam o
sırada da aynı offset'ten başka bir thread ya da proses 100 byte okuyaacak olsun. Bu durumda okuyan taraf ya 100 yazılmadan önceki 100
byte'ı okur ya da diğerinin yazdığı 100 byte'ı okur. Ancak asla yarısı eski 100 byte'tan yarısı diğerinin yazdığı 100 byte'tan oluşan bir
100 byte okumaz. Benzer biçimde iki thread ya da proses WriteFile ya da write ile aynı dosyanın aynı offset'ine yazıyor olsalar bile
iç içe geçme oluşmamaktadır. Nihai durumda ya birinin ya da diğerinin tam yazdığı şeyler dosyada gözükür. Tabii birden fazla read ya da
write işlemi sırasında bu işlemler iç içe geçebilir. Örneğin:
thread-1
--------
write(...)
write(...)
thread-2
---------
write(...)
write(...)
Burada write çapırmaları atomşktir. Ancak thread!in iki write çağrısı arasına thread2'nin write çağrıları gerebilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
C++'da C++11 ile birlikte standart bir thread kütüphanesi oluşturulmuştur. Tabii aslında bu kütüphane Windows sistemlerinde Windows API
fonksiyonlarını, UNIX/Linux sistemlerinde POSIX'in pthread fonksiyonalrını kullanmaktadır. Yalnızca arayüz standart hale getirilmiştir.
Aşağıda C++'da thread yaratımına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <iostream>
#include <string>
#include <thread>
using namespace std;
class Sample {
public:
Sample(const char *name) : m_name(name)
{}
void operator()()
{
cout << m_name + "\n";
}
private:
string m_name;
};
void thread_proc1()
{
cout << "thread-1 running...\n";
}
void thread_proc2()
{
cout << "thread-2 running...\n";;
}
int main(void)
{
Sample s1("thread-1"), s2("thread-2");
thread t1(s1);
thread t2(s2);
thread t3(thread_proc1);
thread t4(thread_proc1);
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Genel olarak C++'ın sınıfları aynı nesne üzerinde okuma konusunda thread güvenli ancak yazma konusunda thread güvenli değildir. Fakat
farklı nesneler üzerinde okuma ve yazma işlemleri thread güvenlidir. Bunların standartlarda bu anlamda thread güvenli versionları yoktur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Thread'e özgü global değişkenler olabilir mi? Yani örneğin iki thread akışı bir global değişkeni kullanırken aslında bunlar farklı global
değişkenler olabilir mi? İşte işletim sistemleri uzun süredir bunu sağlamak için mekanizmalar bulundurmaktadır. Windows sistemlerinde
thread'e özgü global değişken oluşturma mekanizmasına "Thread Local Storage (TLS)", UNIX/Linux sistemlerinde ise "Thread Specific Data
(TSD)" denilmektedir. Hatta C11 ile birlikte C standartlarına _Thread_local isimli, C++11 ile C++ standartlarına thread_local isimli
yer belirleyici (storage class specifier) anahtar sözcükler sokulmuştur. Yani artık C ve C++'ta işletim sisteminin API fonksiyonlarını
ve POSIX fonksiyonlarını kullanmadan da thread'e özgü global dğeişkenler kullanılabilmektedir. Örneğin:
_Thread_local int g_tl;
Burada aslında bir g_tl nesnesi yoktur. Yaratılmış olan ve yaratılacak olan tüm thread'lerin birbirinden ayrı birer g_tl nesneleri vardır.
Aşağıdaki örnekte C11 ile C'e sokulan _Thread_local belirleyicisi kullanılarak bir global değişken oluşturulmuştur. Bu global değişkenin
her thread için ayrı bir kopyası bulunmaktadır. Aşağıdaki örnekte bu durum görülmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
void Foo(const char *str);
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
void ExitSys(LPCSTR lpszMsg);
_Thread_local int g_tl;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
Foo("Main Thread"); /* Main Thread: 0 */
return 0;
}
void Foo(const char *str)
{
printf("%s: %d\n", str, g_tl);
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
g_tl = 10;
Sleep(5000);
Foo("Thread1"); /* THread: 10 */
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
g_tl = 20;
Foo("Thread2"); /* Thread2: 20 */
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirttiğimiz gibi aslında thread'e özgü global alanlar işletim sisteminin desteğiyle oluşturulmaktadır. C ve C++ derleyicileri
de arka planda aslında izleyen paragraflarda ele alacağımız gibi işletim sisteminin bu mekanizmalarını kullanarak thread'e özgü global
değişkenler oluşturabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows'ta her thread için işletim sistemi TLS adı altında slotlardan oluşan bir dizi ayırmaktadır. Slotların indeksleri DWORD türüyle
temsil edilmektedir. Belli bir slot indeksi her thread'te ayrı bir alan belirtir.
Windows'ta TLS (Thread Local Storage) kullanımı şu adımlardan geçilerek sağlanmaktadır:
1) Önce henüz thread'ler yaratılmadan TlsAlloc API fonksiyonuyla bir TLS slotu yaratılır. TlsAlloc fonksiyonunun prototipi şöyledir:
DWORD TlsAlloc(void);
Fonksiyon parametre almamaktadır. Geri dönüş değeri olarak TLS alanında bir slot indeksi vermektedir. Bu slot indeksi yaratılmış olan ve
yaratılacak olan her thread için kullanılabilir ve farklı bir alan belirtmektedir. Programcı tipik olarak bu slot indeksini global
nesnede saklar. Fonksiyon başarısızlık durumunda TLS_OUT_OF_INDEXES özel değerine geri dönmektedir. Windows sistemlerinde toplam 1086
slot bulunmaktadır. Örneğin:
DWORD g_slot;
...
if ((g_slot = TlsAlloc()) == TLS_OUT_OF_INDEXES)
ExitSys("TlsAlloc");
2) Artık her thread aynı slot indeksini kullanarak slota bir adres yerleştirebilir. Slot indeksleri aynı olsa da aslında bu slotlar
farklı thread'lerde olduğu için her thread kendi slotunu kullanıyor olacaktır. Slota adres yerleştiren TlsSetValue fonksiyonunun prototipi
şöyledir:
BOOL TlsSetValue(
DWORD dwTlsIndex,
LPVOID lpTlsValue
);
Fonksiyonun birinci parametresi TLS slot indeksini, ikinci parametresi ise slota yerleştirilecek adres belirtmektedir. Fonksiyon başarı
durumunda sıfır dışı bir değer başarısızlık durumunda 0 değerine geri dönmektedir. Biz aslında malloc ile tahsisat yapıp slota tahsis
ettiğimiz alanın adresini yerleştirebiliriz. Böylece tek bir slot ile istediğimiz kadar thread'e özgü global nesne oluşturmuş oluruz.
Örneğin:
struct THREAD_LOCAL_DATA {
int a;
int b;
ibt c;
};
struct THREAD_LOCAL_DATA *tld;
...
if ((tld = (struct THREAD_LOCAL_DATA *)malloc(sizeof(struct THREAD_LOCAL_DATA))) == NULL) {
fprintf(stderr, "cannot allocate memory!...\n");
exit(EXIT_FAILURE);
}
tld->a = 10;
tld->b = 20;
tld->c = 30;
if (!TlsSetValue(g_slot, tld))
ExitSys("TlsSetValue");
3) Değer TLS slotundan geri almak için TLSGetValue fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
LPVOID TlsGetValue(
DWORD dwTlsIndex
);
Fonsiyon TLS slot indeksini parametre olarak alır ve oraya set edilmiş adresi geri dönüş değeri olarak verir. Fonksiyon başarısızlık
durumunda NULL adrese geri dönmektedir. Tabii yaratılmamış bir slot adresi geçmedikten sonra fonksiyonun başarısız olma olasılığı yoktur.
Dolayısyla fonksiyonun geri dönüş değerini kontrol etmeyebilirsiniz. Örneğin:
struct THREAD_LOCAL_DATA *tld;
...
if ((tld = (struct THREAD_LOCAL_DATA *)TlsGetValue(g_slot)) == NULL)
ExitSys("TlsGetValue");
Ancak gerçekten solota NULL adres de yerleştirilebilir. Bu durumda fonksiyon NULL adrese geri döndüğünde GetLastError değerine bakılmalıdır.
Eğer GetLastError ERROR_SUCESS değerindeyse işlem başarılıdır ve gerçekten slota yerleştirilen NULL adres geri alınmıştır. Ancak GetLastError
başka bir değer geri döndürürse işlemin başarısız olduğu sonucu çıkartılmalıdır.
4) Slot kullanımı bittikten sonra slotun TlsFree fonksiyonu ile serbest hale getirilmesi gerekir. TlsFree fonksiyonunun prototipi şöyledir:
BOOL TlsFree(
DWORD dwTlsIndex
);
Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir. Tabii her şey doğrıu yapılmışsa
fonksiyonun başarısız olma olsaılığı da yoktur.
Aşağıda bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
void Foo(const char *str);
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
void ExitSys(LPCSTR lpszMsg);
DWORD g_slot;
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if ((g_slot = TlsAlloc()) == TLS_OUT_OF_INDEXES)
ExitSys("TlsAlloc");
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
Foo("Main Thread");
TlsFree(g_slot);
return 0;
}
void Foo(const char *str)
{
int *pVal;
if ((pVal = (int *)TlsGetValue(g_slot)) == NULL && GetLastError() != ERROR_SUCCESS)
ExitSys("TlsGetValue");
printf("%d\n", (int)pVal);
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
if (!TlsSetValue(g_slot, (LPVOID)10)) /* g_tl = 10 */
ExitSys("TlsSetValue");
Sleep(5000);
Foo("Thread1");
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
if (!TlsSetValue(g_slot, (LPVOID)20)) /* g_tl = 20 */
ExitSys("TlsSetValue");
Foo("Thread2");
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
89. Ders 18/05/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Her ne kadar TlsSetValue ile slota yalnızca bir adres yerleştirebiliyorsak da aslında dinamik tahsisat yapıp bu alanın adresini slota
yerleştirebiliriz. Böylece istediğimiz kadar çok bilgiyi thread'e özgü biçimde kullanabiliriz. Örneğin:
struct THREAD_DATA {
int a;
int b;
int c;
};
...
struct THREAD_DATA *td;
td = (struct THREAD_DATA *)malloc(sizeof(struct THREAD_DATA));
td->a = 10;
td->b = 20;
td->c = 30;
TlsSetValue(g_slot, td);
Aşağıda bu yöntemle birden fazla bilginin slota yerleştirilmesine ilişkin bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
void Foo(const char *str);
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
void ExitSys(LPCSTR lpszMsg);
DWORD g_slot;
struct THREAD_DATA {
int a;
int b;
int c;
};
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if ((g_slot = TlsAlloc()) == TLS_OUT_OF_INDEXES)
ExitSys("TlsAlloc");
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
Foo("Main Thread");
TlsFree(g_slot);
return 0;
}
void Foo(const char *str)
{
struct THREAD_DATA* td;
if ((td = (struct THREAD_DATA *)TlsGetValue(g_slot)) == NULL && GetLastError() != ERROR_SUCCESS)
ExitSys("TlsGetValue");
printf("%s: %d, %d, %d\n", str, td->a, td->b, td->c);
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
struct THREAD_DATA* td;
if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
td->a = 10;
td->b = 20;
td->c = 30;
if (!TlsSetValue(g_slot, td))
ExitSys("TlsSetValue");
Sleep(5000);
Foo("Thread1");
free(td);
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
struct THREAD_DATA* td;
if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
td->a = 100;
td->b = 200;
td->c = 300;
if (!TlsSetValue(g_slot, td))
ExitSys("TlsSetValue");
Foo("Thread2");
free(td);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Standart C kütüphanesi fonksiyonların parametrik yapılarını değiştirmeden nasıl thread güvenli hale getirilir? İşte böyle bir faaliyette
bazı fonksiyonlar statik nesneleri kullanmak yerine TLS içerisindeki nesneleri kullanmalıdır. Aşağıdaki örnekte bu işlemin mantıksal olarak
nasıl yapılabileceği gösterilmiştir. Bu örnekte thread güvenli kütüphane için programın başında ve sonunda, her thread'in başında ve sonunda
init ve exit fonksiyonlarının çağrılması gerekmektedir. Aslında bu çağrılar programcıdan gizlenebilir. Şöyle ki: Eğer dinamik kütüphane söz
konusuysa dinamik kütüphanenin bazı fonksiyonları bu tür durumlarda otomatik olarak çağrılmaktadır. İşte programcı da bu kodları aslında
kendi kütüphanesinin içerine alabilmektedir. Ancak statik kütüphanelerde böyle bir callback mekanizması yoktur. Microsoft bunun için standart
C kütüphanesinin statik versiyonunu yazarken mecburen sarma thread fonksiyonları kullanmıştır. _beginthreadex fonksiyonu statik standart C
kütüphanesi kullanılacaksa thread yaratmak için tercih edilmelidir. Bu fonksiyon aslında arka planda CreateThread API fonksiyonunu zaten
çağırmaktadır. Ancak threda yaratılmadan önce ve thread sonlanırken aşağıdaki kodda bulunan init ve exit gibi fonksiyonlar bu sarma fonksiyon
tarafından çağrılır. Benzer biçimde eğer Microsoft sistemlerinde statik kütüphane kullanılıyorsa thread sonlanırken _endthreadex fonksiyonu
çağrılmalıdır. Dinamik kütüphanelerde bu sarma fonksiyonların kullanılmasına gerek yoktur. Ayrıca gcc derleyicilerinde standart C
fonksiyonlarının thread güvenli olmadığını bunların thread güvenli versiyonlarının farklı parametrik yapılarla statik nesne kullanmayack
biçimde xxxxx_r ismiyle yazıldığını anımsayınız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* cstdlib.h */
#ifndef CSTDLIB_H_
#define CSTDLIB_H_
#include <stddef.h>
/* Type Definitions */
typedef struct tagCSTDLIB_STATIC_DATA {
char *strtok_pos;
size_t rand_next;
} CSTDLIB_STATIC_DATA;
/* Function Prototypes */
int init_cstdlib(void);
void exit_cstdlib(void);
int init_csdlib_thread(void);
int exit_csdlib_thread(void);
char *csd_strtok(char *str, const char *delim);
void csd_srand(size_t seed);
int csd_rand(void);
#endif
/* cstdlib.c */
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include "cstdlib.h"
static DWORD g_cstdSlot;
int init_cstdlib(void)
{
CSTDLIB_STATIC_DATA *clib;
if ((g_cstdSlot = TlsAlloc()) == TLS_OUT_OF_INDEXES)
return 0;
return 1;
}
int init_csdlib_thread(void)
{
CSTDLIB_STATIC_DATA *clib;
if ((clib = (CSTDLIB_STATIC_DATA *)malloc(sizeof(CSTDLIB_STATIC_DATA))) == NULL) {
TlsFree(g_cstdSlot);
return 0;
}
clib->rand_next = 1;
if (!TlsSetValue(g_cstdSlot, clib))
return 0;
}
int exit_csdlib_thread(void)
{
CSTDLIB_STATIC_DATA *clib;
if ((clib = (CSTDLIB_STATIC_DATA *)malloc(sizeof(CSTDLIB_STATIC_DATA))) == NULL) {
TlsFree(g_cstdSlot);
return 0;
}
free(clib);
}
void exit_cstdlib(void)
{
TlsFree(g_cstdSlot);
}
char *csd_strtok(char *str, const char *delim)
{
CSTDLIB_STATIC_DATA *clib;
char *beg;
if ((clib = TlsGetValue(g_cstdSlot)) == NULL)
return NULL;
if (str != NULL)
clib->strtok_pos = str;
while (*clib->strtok_pos != '\0' && strchr(delim, *clib->strtok_pos) != NULL)
++clib->strtok_pos;
if (*clib->strtok_pos == '\0')
return NULL;
beg = clib->strtok_pos;
while (*clib->strtok_pos != '\0' && strchr(delim, *clib->strtok_pos) == NULL)
++clib->strtok_pos;
if (*clib->strtok_pos != '\0')
*clib->strtok_pos++ = '\0';
return beg;
}
void csd_srand(size_t seed)
{
CSTDLIB_STATIC_DATA *clib;
if ((clib = TlsGetValue(g_cstdSlot)) == NULL)
return NULL;
clib->rand_next = seed;
}
int csd_rand(void)
{
CSTDLIB_STATIC_DATA *clib;
if ((clib = TlsGetValue(g_cstdSlot)) == NULL)
return NULL;
clib->rand_next = clib->rand_next * 1103515245 + 12345;
return (unsigned int)(clib->rand_next / 65536) % 32768;
}
/* Sample.c */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#include "cstdlib.h"
void ExitSys(LPCSTR lpszMsg);
DWORD __stdcall ThreadProc1(LPVOID lpvParam);
DWORD __stdcall ThreadProc2(LPVOID lpvParam);
int main(void)
{
HANDLE hThread1, hThread2;
DWORD dwThreadID1, dwThreadID2;
if (!init_cstdlib()) {
fprintf(stderr, "cannot initialize CSD Standard C Library!..\n");
exit(EXIT_FAILURE);
}
if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL)
ExitSys("CreateThread");
if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL)
ExitSys("CreateThread");
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
exit_cstdlib();
return 0;
}
DWORD __stdcall ThreadProc1(LPVOID lpvParam)
{
char s[] = "ali, veli, selami, ayşe, fatma";
char *str;
int i, val;
if (!init_csdlib_thread()) {
fprintf(stderr, "CSDLib initialization failed!..\n");
exit(EXIT_FAILURE);
}
for (str = csd_strtok(s, ", "); str != NULL; str = csd_strtok(NULL, ", "))
printf("threadproc1 --> %s\n", str);
for (i = 0; i < 10; ++i) {
val = csd_rand();
printf("threadproc1_rand --> %d\n", val);
}
exit_csdlib_thread();
return 0;
}
DWORD __stdcall ThreadProc2(LPVOID lpvParam)
{
char s[] = "adana, izmir, balikesir, muğla";
char *str;
int i, val;
if (!init_csdlib_thread()) {
fprintf(stderr, "CSDLib initialization failed!..\n");
exit(EXIT_FAILURE);
}
for (str = csd_strtok(s, ", "); str != NULL; str = csd_strtok(NULL, ", "))
printf("threadproc2 --> %s\n", str);
for (i = 0; i < 10; ++i) {
val = csd_rand();
printf("threadproc2_rand --> %d\n", val);
}
exit_csdlib_thread();
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde thread'e özgü statik alanlara "Thread Specific Data (TSD)" denilmektedir. Genel kullanım biçimi Windows
sistemlerindekilere oldukça benzemektedir. Sırasıyla şunlar yapılmalıdır:
1) Önce pthread_key_create fonksiyonu ile TSD için bir slot yaratılır. pthread_key_create fonksiyonunu işlev olarak TlsAlloc fonksiyonuna
benzetebiliriz. Fonksiyonun prototipi şöyledir:
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void *));
Fonksiyonun birinci parametresi slot anahtarının yerleştirileceği pthread_key_t türünden nesnenin adresini almaktadır. Fonksiyon slot anahtar
bilgisini bu nesnenin içerisine yerleştirmektedir. Tipik olarak programcı bu türden global bir nesne tanımlar. Onun adresini fonksiyona geçirir.
Fonksiyonun ikinci parametresi thread sonlandığında çağrılacak callback fonksiyonun adresini almaktadır. Bu parametre NULL geçilebilir.
Bu durumda sonlanma sırasında fonksiyon çağrılmaz. Fonksiyon başarı durumunda sıfır değerine başarısızlık durumunda errno değerine geri dönmektedir.
2) TSD slotuna yerleştirme yapmak için pthread_setspecific slottan değer almak için ise pthread_getspecific fonksiyonları kullanılmaktadır.
Fonksiyonların prototipleri şöyledir:
#include <pthread.h>
int pthread_setspecific(pthread_key_t key, const void *value);
void *pthread_getspecific(pthread_key_t key);
pthread_setspecific fonksiyonunun birinci parametresi slotu belirten pthread_key_t türünden nesneyi ikinci parametresi ise o slota yerleştirilecek
adresi almaktadır. pthread_getspecific fonksiyonu da slota yerleştirilmiş olan adres değerini vermektedir.Bu fonksiyonları Windows'taki
TlsSetValue ve TlsGetValue fonksiyonlarına benzetebiliriz. pthread_setspecific fonksiyonu başarı durumunda 0 değerine başarısızlık durumunda
ise errno değerine geri dönmektedir. pthread_getspecific fonksiyonu başarısızlık durumunda NULL adrese geri dönmektedir.
3) Kullanım bittikten sonra elde edilen slot pthread_key_delete fonksiyonu ile isteme iade edilir. Fonksiyonun prototipi şöyledir:
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
Fonksiyon slota ilişkin pthread_key_t türünden nesneyi parametre olarak alır. Başarı durumunda sıfır değerine, başarısızlık durumunda
errno değerine geri döner.
pthread_key_create fonksiyonunun ikinci parametresine geçirilecek destructor fonksiyonunun parametrik yapısı şöyle olmalıdır:
void destructor(void *value);
Fonksiyona TSD alanına yerleştirilmiş olan slottaki adres parametre olarak geirilmektedir. Tipik olarak programcılar bu fonksiyonda
dinamik tahsis edilen alanları free hale getirirler.
Aşağıda UNIX/Linux sistemlerinde TSD kullanımına ilişkin bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void foo(const char *str);
void destructor(void *value);
void *thread_proc1(void *param);
void *thread_proc2(void *param);
void exit_sys_errno(const char *msg, int eno);
struct THREAD_DATA {
int a;
int b;
int c;
};
pthread_key_t g_tsdkey;
int main(void)
{
pthread_t tid1, tid2;
int result;
if ((result = pthread_key_create(&g_tsdkey, destructor)) != 0)
exit_sys_errno("pthread_key_create", result);
if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0)
exit_sys_errno("pthread_create", result);
if ((result = pthread_join(tid1, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_join(tid2, NULL)) != 0)
exit_sys_errno("pthread_join", result);
if ((result = pthread_key_delete(g_tsdkey)) != 0)
exit_sys_errno("pthread_key_delete", result);
return 0;
}
void foo(const char *str)
{
struct THREAD_DATA *td;
if ((td = (struct THREAD_DATA *)pthread_getspecific(g_tsdkey)) == NULL) {
fprintf(stderr, "cannot get key value!..\n");
exit(EXIT_FAILURE);
}
printf("%s: %d, %d, %d\n", str, td->a, td->b, td->c);
}
void destructor(void *value)
{
free(value);
}
void *thread_proc1(void *param)
{
struct THREAD_DATA* td;
int result;
if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
td->a = 10;
td->b = 20;
td->c = 30;
if ((result = pthread_setspecific(g_tsdkey, td)) != 0)
exit_sys_errno("pthread_setspecific", result);
sleep(5);
foo("Thread1");
return NULL;
}
void *thread_proc2(void *param)
{
struct THREAD_DATA* td;
int result;
if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) {
fprintf(stderr, "cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
td->a = 100;
td->b = 200;
td->c = 300;
if ((result = pthread_setspecific(g_tsdkey, td)) != 0)
exit_sys_errno("pthread_setspecific", result);
foo("Thread2");
return NULL;
}
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
90. Ders 25/05/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bugün işletim sistemlerinde en çok kullanılan çizelgeleme tekniği öncelik dereceleri dikkate alınarak uygulanan "döngüsel çizelgeleme
(round robin scheduling)" denilen tekniktir. Döngüsel çizelgelemede her thread sırasıyla çalıştırılmaktadır. Ancak bu sistemin işletim
sistemine özgü ayrıntıları vardır. Döngüsel çizelgelemede işletim sistemi thread'lere öncelik derecelerini de dikkate alarak quanta
süreleri atar. Quanta süresini dolduran thread'lerin quanta süreleri yeniden doldurulmadan önce çalışma kuyruğundaki tüm thread'lerin
quanta sürelerinin bitmesi beklenmektedir. Tabii bir thread çalışmaya başladığında bloke olabileceği için quanta süresini sonuna kadar
kullanamayabilmektedir. İşletim sistemi her thread'in quanta süresinden harcağı zamanı tutmaktadır. Örneğin işletim sisteminin her thread'e
60 ms. quanta süresi verdiğini düşünelim. O anda da çalışma kuyruğunda(run queue) aşağıdaki thread'ler bulunuyor olsun:
T1 (40 ms)
T2 (0 ms) ==> quanta süresi bitti
T3 (10 ms)
T4 (25 ms)
Burada T2 thread'inin ona atanan 60 ms'lik quanta süresinin dolduğunu varsayalım. İşletim sistemi T2 thread'ine hemen 60 ms quanta
doldurmayacaktır. Önce çalışma kuyruğundaki tüm thread'lerin kullandığı quanta sürelerinin 0'a düşmesini bekleyecek sonra hepsini
birlikte 60 ms ile dolduracaktır. Pekiyi bu durumda bloke olup bekleme kuyruğunda beklemekte olan thread'lerin quantda süreleri ne
olacaktır. Örneğin T5 thread'inin bloke beklediğini ve kalan quanta süresinin 30 ms. olduğunu varsayalım. Çalışma kuyruğundaki tüm
thread'lerin quanta süreleri sıfıra geldiğinde onlara 60 ms. quanta doldurulurken blokede bekleyen threa'2lere doldurma yapılacak mıdır?
İşte işletim sistemleri genellikle blokede bekleyen thread'lere de doldurma yapmaktadır. Çünkü onlar uyandıklarında diğerleriyle eşit hakka
sahip olacak biçimde daha fazla çalıştırılmalıdırlar. Ancak tabii blokede uzun süre bekleyen thread'lerin quanta'larının sürekli doldurulması
da anomali yaratabilmektedir. Bunun için işletim sistemleri blokede bekleyen thread'ler için maksimum bir üst sınır da belirleyebilmektedir.
Pekiyi çalışma kuyruğundaki tüm thread'lerin quanta süreleri 0'a düştüğünde tüm thread'ler aynı quanta değeri ile mi doldurulmaktadır? İşte
bu konuda işletim sistemleri arasında farklılıklar vardır. Windows'ta genel olarak aynı quanta süresi doldurulmaktadır. Ancak Linux sistemlerinde
SCHED_OTHER çizelgeleme politikasında her thread'e o threadin önceliği ile orantılı quanta süreleri doldurulmaktadır. Yani Linux sistemlerinde
bir thread'in önceliğini artırdığımızda onun diğerlerine göre daha fazla quanta süresi kullanmaısnı sağlayabiliriz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Thread'li işletim sistemlerinde thread'lerin zaman paylaşımlı biçimde çizelgelendiğini belirtmiştik. Ancak bu sistemlerde sisteme bağlı
olarak thread'lere öncelik dereceleri atanabilmektedir. Böylece yksek öncelikli thread'lerin CPU'dan daha fazla zaman alması sağlanabilmektedir.
Thread öncelikleri konusu yukarıda da belirttiğimiz gibi işletim sistemine özgü farklılıklar içermektedir. Biz burada önce Windows sistemlerindeki
durumu sonra da UNIX/Linux sistemlerindeki durumu kabaca ele alacağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinin kullandığı thread izelgeeleme algoritmasına "öncelik sınıflarıyla döngüsel çizelgeleme (priority class based round
robin scheduling )" denilmektedir. Windows'ta her thread'in [0, 31] arasında bir öncelik derecesi vardır. Şimdiye kadar yaratmış olduğumuz
thread'lerin default öncelik dereceleri 8'dir.
Windows'ta thread'ler şöyle çizelgelenmektedir: Önce en yüksek önceliğe sahip thread'lerden bir grup oluşturulur. Sanki diğer thread'ler
hiç yokmuş gibi yalnızca bu thread'ler zaman paylaşımlı biçimde çalıştırılır. Bu thread'ler sonlanırsa ya da bloke olursa bu kez daha
düşük öncelikli en yüksek grup aynı biçimde kendi aralarında izelgelenir. Bu yöntemde düşük öncelikli thread'lerin çalışabilmesi için
yüksek öncelikli thread'lerin sonlanması ya da bloke olması gerekmektedir. Yüksek öncelikli bir thread'in blokesi çözüldüğünde işletim
2024-06-09 22:42:45 +03:00
sistemi düşük öncelikli thread'in çalışmasına ara vererek yeniden bu yüksek öncelikli thread grubunu çizelgelemektedir. Örneğin sistemde
aşağıdaki öncelikte thread'ler bulunuyor olsun:
T1 -> 12
T2 -> 12
T3 -> 10
T4 -> 9
T5 -> 9
T6 -> 8
T7 -> 8
T8 -> 8
Burada sistem sanki diğer thread'ler yokmuş gibi T1 ve T2 thread'lerini kendi aralarında zaman paylaşımlı olarak çalıştırmaktadır.
Bu thread'ler sonlanırsa ya da bloke olursa T3 thread'i çalışma imlanı bulacaktır. T3 thread'i de sonlanırsa ya da bloke olursa bu durumda
T4 thread'i, o da sonlanırsa ya da bloke olursa T5, T6, T7 ve T8 thread'leri kendi aralarında zaman paylaşımlı olarak çizelgelenecektir.
Görüldüğü gibi bu sistemde düşük öncelikli bir thread'in çalışanilmesi için yüksek öncelikli thread'lerin bloke olması ya da sonlanması
gerekmektedir.
Windows'un çizelgeleme algoritması kabaca yukarıdaki gibi olsa da aslında oludukça ayrıntılar içermektedir. Çok işlemcili ya da çok çekirdekli
2024-06-09 22:42:45 +03:00
sistemlerde eeğer boşta işlemci ya da çekirdek varsa işletim sistemi daha düşük öncelikli sınıfları da bu işlemci ya da çekirdeklere
atayabilmektedir.
Windows'ta bir thread'in [0, 31] arasındaki öncelik derecesi iki değerin toplamıyla elde edilmektedir: Prosesin Öncelik Sınıfı + Thread'in
Göreli Öncelik Derecesi. Prosesin öncelik sınıfı bir taban değer belirtir. Thread'in göreli önceliği de bu taban değere toplanır. Prosesin
öncelik sınıflarının taban değerleri şöyledir:
NORMAL_PRIORITY_CLASS (8 default)
ABOVE_NORMAL_PRIORITY_CLASS (10)
BELOW_NORMAL_PRIORITY_CLASS (6)
HIGH_PRIORITY_CLASS (13)
REALTIME_PRIORITY_CLASS (24)
IDLE_PRIORITY_CLASS (4)
Thread'in göreli öncelik dereceleri de şöyledir:
THREAD_PRIORITY_NORMAL (0 default)
THREAD_PRIORITY_IDLE (Öncelik sınıfına göre değişmektedir)
THREAD_PRIORITY_LOWEST (-2)
THREAD_PRIORITY_BELOW_NORMAL (-1)
THREAD_PRIORITY_ABOVE_NORMAL (+1)
THREAD_PRIORITY_HIGHEST (+2)
THREAD_PRIORITY_TIME_CRITICAL (Öncelik sınıfına göre değişmektedir)
Herhangi bir öncelik oluşturmak için bu ikşi ayarlamadın da yapıması gerekir. Tabii aynı değeri veren birden fazla kombinasyon olabilir.
Default durumda prosesin öncelik sınıfı NORMAL_PRIORITY_CLASS, thread'in göreli önceliği THREAD_PRIORITY_NORMAL biçimdedir. Bu durumda
thread'in öncelik derecesi 8 + 0 = 8 biçimindedir. Örneğin thread önceliğini 15 yapmak isteyelim. Bunun birkaç yolu olabilir:
IDLE_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL
BELOW_NORMAL_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL
NORMAL_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL
ABOVE_NORMAL_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL
HIGH_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL
HIGH_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL
Thread'in önceliğini 31'e çekmek isteyelim. Bunun tek yolu şöyleidr:
2024-06-09 22:42:45 +03:00
REALTIME_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL
2024-06-09 22:42:45 +03:00
Thread önceliklerinin belirlenmesine ilişkin Microsoft dokğmanlarına aşağıdaki bağlantıdan erişebilirsiniz:
https://learn.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Prosesin öncelik sınıfı GetPriorityClass API fonksiyonuyla alınıp SetPriorityClass API fınksiyonuyla set edilebilir. Fonksiyonarın
prototipleri şöyledir:
DWORD GetPriorityClass(
HANDLE hProcess
);
BOOL SetPriorityClass(
HANDLE hProcess,
DWORD dwPriorityClass
);
Fonksiyonların birinci parametreleri öncelik sınıfı değiştirilecek prosesin HANDLE değerini belirtmektedir. GetPriorityClass API fonksiyonu
prosesin öncelik sınıfına geri dönmektedir. SetPriorityClass API fonksiyonu ise prosesin öncelik sınıfını ikinci parametresiyle belirtilen
sınıf haline getirmektedir. GetPriorityClass fonksiyonu başarısız olamamaktadır. SetPriorityClass fonksiyonu ise başarı durumunda sıfır dışı
bir değere başarsızlık durumunda sıfır değerine geri dönmektedir. Anımsanacağı gibi o anda çalışmakta olan prosesin handle değeri
GetCurrentProcess API fonksiyonuyla elde edilmektedir.
Biz bir prosesin öncelik sınıfını istediğimiz gibi değiştirebilir miyiz? Windows'ta karmaşık bir güvenlik mekanizması vardır. Bu kursta
bu konuya girmeyeceğiz. Ancak bu tür uygulamalarda programı "Run As Administrator" seçeneği ile çalıştırmalısınız.
2024-06-09 22:42:45 +03:00
Aşağıdaki örnekte prosesin öncelik değiştirilmiş ve yazdırılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
LPCSTR GetPriorityClassName(DWORD dwPriority);
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
DWORD dwPriorityClass;
if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0)
ExitSys("GetPriorityClass");
puts(GetPriorityClassName(dwPriorityClass));
2024-06-09 22:42:45 +03:00
if (!SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS))
ExitSys("SetPriorityClass");
if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0)
ExitSys("GetPriorityClass");
puts(GetPriorityClassName(dwPriorityClass));
return 0;
}
LPCSTR GetPriorityClassName(DWORD dwPriority)
{
2024-06-09 22:42:45 +03:00
const char *pszName = "NONE";
switch (dwPriority) {
2024-06-09 22:42:45 +03:00
case ABOVE_NORMAL_PRIORITY_CLASS:
pszName = "ABOVE_NORMAL_PRIORITY_CLASS";
break;
case BELOW_NORMAL_PRIORITY_CLASS:
pszName = "BELOW_NORMAL_PRIORITY_CLASS";
break;
case HIGH_PRIORITY_CLASS:
pszName = "HIGH_PRIORITY_CLASS";
break;
case IDLE_PRIORITY_CLASS:
pszName = "IDLE_PRIORITY_CLASS";
break;
case NORMAL_PRIORITY_CLASS:
pszName = "NORMAL_PRIORITY_CLASS";
break;
case REALTIME_PRIORITY_CLASS:
pszName = "REALTIME_PRIORITY_CLASS";
break;
}
2024-06-09 22:42:45 +03:00
return pszName;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Thread'in göreli önceliğini almak ve set etmek için GetThreadPriorty ve SetThreadPriority API fonksiyonları kullanılmaktadır. Fonksiyonların
prototipleri şöyledir:
int GetThreadPriority(
HANDLE hThread
);
BOOL SetThreadPriority(
HANDLE hThread,
int nPriority
);
Fonksiyonların birinci parametreleri göreli önceliği alınacak ya da dğeiştirilecek thread'in HANDLE değerini almaktadır. SetThreadPriority
fonksiyonunun ikinci parametresi thread'in göreli önceliğini belirtmektedir. GetThreadPriority fonksiyonu başarısız olamaz, thread'in
göreli önceliği ile geri dönmektedir. SetThreadPriority fonksiyonu başarı durumunda sıfır dışı bir değere başarısızlık durumunda sıfır
değerine geri dönmektedir. O anda çalışmakta olan thread'in HANDLE değeri GetCurrentThread API fonksiyonu ile elde edilebilmektedir.
Aşağıda thread'in göreli öncelik derecesi alınıp set edilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
LPCSTR GetThreadPriorityName(int threadPriority);
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
int threadPriority;
if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN)
ExitSys("GetPriorityClass");
puts(GetThreadPriorityName(threadPriority));
2024-06-09 22:42:45 +03:00
if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL))
ExitSys("SetThreadPriority");
if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN)
ExitSys("GetPriorityClass");
puts(GetThreadPriorityName(threadPriority));
return 0;
}
LPCSTR GetThreadPriorityName(int threadPriority)
{
2024-06-09 22:42:45 +03:00
const char *pszName = "NONE";
switch (threadPriority) {
case THREAD_PRIORITY_ABOVE_NORMAL:
2024-06-09 22:42:45 +03:00
pszName = "THREAD_PRIORITY_ABOVE_NORMAL";
break;
case THREAD_PRIORITY_BELOW_NORMAL:
2024-06-09 22:42:45 +03:00
pszName = "THREAD_PRIORITY_BELOW_NORMAL";
break;
case THREAD_PRIORITY_HIGHEST:
2024-06-09 22:42:45 +03:00
pszName = "THREAD_PRIORITY_HIGHEST";
break;
case THREAD_PRIORITY_IDLE:
2024-06-09 22:42:45 +03:00
pszName = "THREAD_PRIORITY_IDLE";
break;
case THREAD_PRIORITY_LOWEST:
2024-06-09 22:42:45 +03:00
pszName = "THREAD_PRIORITY_LOWEST";
break;
case THREAD_PRIORITY_NORMAL:
2024-06-09 22:42:45 +03:00
pszName = "THREAD_PRIORITY_NORMAL";
break;
case THREAD_PRIORITY_TIME_CRITICAL:
2024-06-09 22:42:45 +03:00
pszName = "THREAD_PRIORITY_TIME_CRITICAL";
break;
}
2024-06-09 22:42:45 +03:00
return pszName;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Aşağıcdaki örnekte proesin ana thread'i maksimum öncelik olan 31 önceliğe çekilmek istenmiştir. Programı "Arun As Administrator" ile
çalıştırınız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
2024-06-09 22:42:45 +03:00
LPCSTR GetPriorityClassName(DWORD dwPriority);
LPCSTR GetThreadPriorityName(int threadPriority);
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
2024-06-09 22:42:45 +03:00
DWORD dwPriorityClass;
int threadPriority;
if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0)
ExitSys("GetPriorityClass");
puts(GetPriorityClassName(dwPriorityClass));
if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN)
ExitSys("GetPriorityClass");
puts(GetThreadPriorityName(threadPriority));
printf("-----------------------\n");
if (!SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS))
ExitSys("SetPriorityClass");
if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL))
ExitSys("SetThreadPriority");
2024-06-09 22:42:45 +03:00
if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0)
ExitSys("GetPriorityClass");
2024-06-09 22:42:45 +03:00
puts(GetPriorityClassName(dwPriorityClass));
if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN)
ExitSys("GetPriorityClass");
puts(GetThreadPriorityName(threadPriority));
getchar();PCSTR GetPriorityClassName(DWORD dwPriority)
{
const char *pszName = "NONE";
switch (dwPriority) {
case ABOVE_NORMAL_PRIORITY_CLASS: pszName = "ABOVE_NORMAL_PRIORITY_CLASS";
brezName = "BELOW_NORMAL_PRIORITY_CLASS";
break;
case HIGH_PRIORITY_CLASS:
pszName = "HIGH_PRIORITY_CLASS";
break;
case IDLE_PRIORITY_CLASS:
pszName = "IDLE_PRIORITY_CLASS";
break;
case NORMAL_PRIORITY_CLASS:
pszName = "NORMAL_PRIORITY_CLASS";
break;
case REALTIME_PRIORITY_CLASS:
pszName = "REALTIME_PRIORITY_CLASS";
break;
}
return pszName;
}
LPCSTR GetThreadPriorityName(int threadPriority)
{
const char *pszName = "NONE";
switch (threadPriority) {
case THREAD_PRIORITY_ABOVE_NORMAL:
pszName = "THREAD_PRIORITY_ABOVE_NORMAL";
break;
case THREAD_PRIORITY_BELOW_NORMAL:
pszName = "THREAD_PRIORITY_BELOW_NORMAL";
break;
case THREAD_PRIORITY_HIGHEST:
pszName = "THREAD_PRIORITY_HIGHEST";
break;
case THREAD_PRIORITY_IDLE:
pszName = "THREAD_PRIORITY_IDLE";
break;
case THREAD_PRIORITY_LOWEST:
pszName = "THREAD_PRIORITY_LOWEST";
break;
case THREAD_PRIORITY_NORMAL:
pszName = "THREAD_PRIORITY_NORMAL";
break;
case THREAD_PRIORITY_TIME_CRITICAL:
pszName = "THREAD_PRIORITY_TIME_CRITICAL";
break;
}
return pszName;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
UNIX/Linux sistemlerinde her thread'in belirlenmiş olan bir "çizelgeleme politikası (scheduling policy)" vardır. POSIX standartlarına
göre şu çizelgeleme politikaları bulunmaktadır:
SCHED_FIFO
SCHED_RR
SCHED_OTHER
SCHED_SPORADIC
SCHED_SPORADIC politikasının desteklenesi isteğe bağlı bırakılmıştır. SCHED_FIFO ve SCHED_RR politikalarıba "gerçek zamanlı (real time)"
çizelgeleme politikalrı denilmektedir. SCHED_OTHER politikasının ayrıntılarına girilmemiş ve bu belirleme işletim sistemini yazanların
isteğine bırakılmıştır. Default çizelgeleme politikasının ne olacağı POSIX standartlarında açıkça belirtilmemiştir. O da işletim sistemini
yazanların isteğine bırakılmıştır. Linux ve pek çok UNIX türevi sistemdeki default çizelgeleme politikası SCHED_OTHER biçimindedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
POSIX standartlarına göre SCHED_OTHER thread'lerin bir dinamik öncelik derecesi vardır. Dinamik öncelik [0, 39] arasındadır. Default dinamik
öncelik 20'dir. Dinamik öncelikte yüksek değer düşük öncelik, düşük değer yüksek öncelik belirtmektedir. Linux sistemlerinde thread'lere
quanta doldurulurken her thread'e o thread'in dinamik önceliği ile orantılı bir quanta süresi atanmaktadır. Yani dinamik önceliği yüksek
olan thread'lere daha fazla quanta süresi, dinamik önceliği düşük thread'lere daha az quanta süresi atanmaktadır. Böylece programcı thread'in
diğer thread'lere göre daha fazla CPU zamanı almasını istiyorsa thread'inin dinamik önceliğini yükseltmektedir. Dinamik öncelikle quanta
süresi arasındaki ilişki sistemden sisteme hatta aynı sistemde versiyona bile değişiklik österebilmektedir. Örneğin eskiden Linux sistemlerinde
dinamik önceliğin etkisi nispeten azdı daha sonra bu etki yükseltilmiştir.
Eskiden UNIX/Linux sistemlerinde thread kavramı yoktu. O zamanlarda thread'ler yerine proseslerin dinamik önceliği vardı. Sonra thread'ler
sistemlere eklenince thread'lerin de dinamik öncelikleri oluşturuldu. Ancak eski POSIX fonksiyonları da muhafaze edildi. POSIX standartlarına
göre bir prosesin dinamik önceliğ değiştirildiğinde prosesin tüm thread'lerinin dinamik önceliği değiştirilmiş olmalıdır. Ancak Linux
sistemleri bu kurala uymamaktadır. Linux sistemlerinde prosesin dinamik önceliği değiştirildiğinde bundan yalnızca prosesin ana thread'i
etkilenmektedir. (Halbuki POSIX standartlarında o anda yaratılmış olan ve daha sonra yaratılacak olan tüm thread'lerin dinamik önceliğinin
değişmesi gerekmektedir.)
Dinamik önceliği değiştirmede en çok kullanılan fonksiyon nice isimli POSIX fonksiyonudur. nice fonksiyonu prosesin dinamik önceliğini
değiştirmektedir. (Linux sistemlerinde bu durum tüm thread2lerin değil ana thread'in dinamik önceliğinin değiştirilmesine yol açmaktadır.)
nice fonksiyonun protoripi şöyledir:
#include <unistd.h>
int nice(int inc);
nice fonksiyonu prosesin dinamik önceliğini o anki dinamik öncelikten parametresi belirtilen miktarda yükseltir ya da alçaltır. Ancak
parametrenin anlamı terstir. Yani pozitif değerler "düşürme", negatif değerler "yükseltme" anlamına gelmektedir. Fonksiyon parametre olarak
[-20, +19] arasında bir değer almaktadır. Böylelikle biz bu fonksiyona argüman olarak 19 değerini verirsek dinamik öncelik en düşük değeri
belirten 39 olur, -20 verirsek dinamik öncelik en yükske değeri belirten 0 olur. Fonksiyon başarı durumunda yeni nice değerine başarısızlık
durumunda -1 değerine geri dönmektedir. nice fonksiyonu ile sıradan prosesler önceliklerini düşürebilirler (pozitif parametre) ancak
yükseltemezler (negatif parametre). Yükseltme işlemi için prosesin uygun önceliğe sahip olması (örneğin root olması, yani sudo ile çalıştırılması)
gerekmektedir. Prosesin nice değeri getpriority POSIX fonksiyonyla elde edilebilmektedir.
nice fonksiyonun prosesin dinamik önceliğini değiştirdiğini belirtmiştik. Yalnızca belli bir thread'in dinamik önceliğini değiştirmek için
pthread_schedsetparam fonksiyonu kullanılmaktadır. Bu kodudaki ayrıntılar UNIX/Linux sistem programlama kurslarında ele alınmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıda da belirtildiği gibi SCHED_FIFO ve SCHED_RR çizelgeleme politikalarına "gerçek zamanlı çizelgeleme politikaları" denilmektedir.
UNIX/Linux sistemlerinde çalışma kuyruğunda (yani bloke olmamış) SCHED_FIFO ya da SCHED_RR thread'ler varsa hiçbir zaman SCHED_OTHER
thread çizelgelenmemektedir. Tabii çok işlemcili ya da çekirdekli sistemlerde SCHED_FIFO ya da SCHED_RR thread'ler CPU'ya atabndıktan sonra
çalışma kuyruğunda artık hiç SCHED_FIFO ya da SCHED_RR thread yoksa SCHED_OTHER thread'ler diğer işlemci ya da çekirdeklerde çizelgelenebilmektedir.
Yani SCHED_FIFO ve SCHED_RR thread'lerin SCHED_OTHER thread'lere tam bir üstünlüğü vardır. Pekiyi SCHED_FIFO ve SCHED_RR thread'lerin kendi
aralarındaki durum nasıldır?
SCHED_FIFO ve ve SCHED_RR thread'lerin de birer önceliği vardır. (Bu öncelik SCHED_OTHER thread'lerin dinamik önceliğinden farklıdır.)
Linux sistemlerinde SCHED_FIFO ve SCHED_RR thread'lerin öncelik derecesi 1 ile 99 arasındadır. Burada Yüksek değer yüksek öncelik belirtmektedir.
Ancak POSIX standartları bu 1 ve 99 değerlerinin sistemden sisteme değişebileceğini o sistemdeki değerlerin sched_get_priority_min ve
sched_get_priority_max fonksiyonlarıyla elde edilmesi gerektiğini belirtmektedir.
Çalışma kuyruğunda yalnızca SCHED_FIFO ve SCHED_RR thread'lerin olduğunu varsayalım. (Zaten SCHED_OTHER thread'ler olsa bile çizelgelenmeyecektir.)
Çizelgeleme algoritması şöyledir:
1) Çizelgeleyici kuyruktaki en yüksek önceliğe sahip olanlar arasında önde bulunan SCHED_FIFO ya da SCHED_RR thread'i CPU'ya atar. Eğer
atanan thread SCHED_FIFO ise bloke olana kadar sürekli çalıştrılır. Yani SCHED_FIFO bir thread bloke olmadıktan sonra CPU'yu bırakmamaktadır.
Ancak atanan CPU'ya atanan thread SCHED_RR ise bir quanta çalıştırılıp kuyruğun sonuna alınır.
2) SCHED_FIFO thread bloke olursa bu durumda kuyrukta en yüksek öncelikte ve önde olan ilk SCHED_FIFO ya da SCHED_RR thread CPU'ya atanır.
3) Bloke olmuş olan bir thread'in blokesi çözüldüğünde eğer o anda çalışmakta olan thread'ten daha yüksek öncelikliyse o anda çalışmakta
olan thread'in çalışması kesilir ve blokesi çözülen thred çalıştırılır. Tabii bu yeni thread SCHED_FIFO ise sürekli çalıştırılacak, SCHED_RR
ise bir quanta çalıştırılıp sona alınacaktır. Eğer blokesi çözülmüş olan thread o anda çalışmakta olan thread'le eşit öncelikli ya da ondan
daha düşük öncelikli ise kuyruğun sonuna alınmaktadır. Blokesi çözülmüş yüksek öncelikli thread tarafından kesilen thread eğer SCHED_FIFO
thread'se kuyruğun önüne yerleştirilir. SCHED_RR thread'se kuyruğun sonuna alınır.
Bu çizelgeleme sisteminde şu durumlara dikkat ediniz.
- Bu sistemde çalışma kuyruğundaki bütün geçek zamanlı zamanlı thread'lerin SCHED_RR olduğunu varsayalım. Bu durumdaki çizelgeleme Windows'taki
çizelgelemeye çok benzer durumdadır Yani en yüksek öncelikteki thread'ler kendi aralarında döngülsel çizelgelenecektir.
- CPU'ya atanmış olan SCHED_FIFO thread'in CPU'yu bırakması ancak daha yüksek öncelikli SCHED_FIFO ya da SCHED_RR thread'in uykudan
uyanmasıyla ya da yüksek öncelikli yeni bir thread'in yaratılmasıyla ya da o thread'ın bloke olmasıyla mümkündür. SCHED_FIFO politikası
bu sistemlerde "sürekli çalışan tüm CPU'yu tek başına kullanan thread'lerin oluşturulması" amacıyla bulundurulmuştur. Yüksek öncelikli
SCHED_FIFO thread'ler diğer thread'lerin çalışmasını engelleyebilmektedir.
Aşağıdaki örnekte gerçek zamanlı çizelgeleme politikalarına ilişkin öncelik derecelerinin en düşük ve en yüksek değeri sched_get_priority_min
ve sched_get_priority_max fonksiyonları ile elde edilip yazdırılmıştır. SCHED_OTHER politikası için bu fonksiyonlar 0 değerini geri
döndürmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
2024-06-09 22:42:45 +03:00
#include <time.h>
#include <pthread.h>
2024-06-09 22:42:45 +03:00
#include <unistd.h>
#include <semaphore.h>
2024-06-09 22:42:45 +03:00
#define QUEUE_SIZE 10
void *thread_producer(void *param);
void *thread_consumer(void *param);
void exit_sys(const char *msg);
void exit_sys_errno(const char *msg, int eno);
sem_t g_sem_producer;
sem_t g_sem_consumer;
int g_queue[QUEUE_SIZE];
size_t g_head;
size_t g_tail;
int main(void)
{
2024-06-09 22:42:45 +03:00
pthread_t tid1, tid2;
int result;
if (sem_init(&g_sem_producer, 0, QUEUE_SIZE) == -1)
exit_sys("sem_init");
if (sem_init(&g_sem_consumer, 0, 0) == -1)
exit_sys("sem_init");
if ((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0)
exit_sys_errno("pthread_create", result);
2024-06-09 22:42:45 +03:00
if ((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0)
exit_sys_errno("pthread_create", result);
2024-06-09 22:42:45 +03:00
if ((result = pthread_join(tid1, NULL)) != 0)
exit_sys_errno("pthread_join", result);
2024-06-09 22:42:45 +03:00
if ((result = pthread_join(tid2, NULL)) != 0)
exit_sys_errno("pthread_join", result);
2024-06-09 22:42:45 +03:00
if (sem_destroy(&g_sem_consumer) == -1)
exit_sys("sem_destroy");
2024-06-09 22:42:45 +03:00
if (sem_destroy(&g_sem_producer) == -1)
exit_sys("sem_destroy");
2024-06-09 22:42:45 +03:00
return 0;
}
2024-06-09 22:42:45 +03:00
void *thread_producer(void *param)
{
int val;
unsigned seed;
2024-06-09 22:42:45 +03:00
seed = time(NULL) + 123;
2024-06-09 22:42:45 +03:00
val = 0;
for (;;) {
usleep(rand_r(&seed) % 300000);
2024-06-09 22:42:45 +03:00
if (sem_wait(&g_sem_producer) == -1)
exit_sys("sem_wait");
2024-06-09 22:42:45 +03:00
g_queue[g_tail] = val;
g_tail = (g_tail + 1) % QUEUE_SIZE;
if (sem_post(&g_sem_consumer) == -1)
exit_sys("sem_post");
2024-06-09 22:42:45 +03:00
if (val == 99)
break;
++val;
}
2024-06-09 22:42:45 +03:00
return NULL;
}
2024-06-09 22:42:45 +03:00
void *thread_consumer(void *param)
{
int val;
unsigned seed;
seed = time(NULL) + 456;
for (;;) {
if (sem_wait(&g_sem_consumer) == -1)
exit_sys("sem_wait");
val = g_queue[g_head];
g_head = (g_head + 1) % QUEUE_SIZE;
if (sem_post(&g_sem_producer) == -1)
exit_sys("sem_post");
usleep(rand_r(&seed) % 300000);
printf("%d ", val);
fflush(stdout);
if (val == 99)
break;
}
printf("\n");
return NULL;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
void exit_sys_errno(const char *msg, int eno)
{
fprintf(stderr, "%s: %s\n", msg, strerror(eno));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
91. Ders 26/05/2024 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Pekiyi bir prosesin ya da thread'in çizelgeleme politikası ve gerçek zamanlı thread'lerin öncelikleri nasıl belirlenmektedir? Bir prosesin
çizelgeleme politikası sched_setscheduler fonksiyonuyla set edilip sched_getcheduler fonksiyonu ile alınabilmektedir. Bu POSIX fonksiyonları
Linux sistemlerinde doğrudan ilgili sistem fonksiyonlarını çağırmaktadır. Fonksiyonların prototipileri şöyledir:
#include <sched.h>
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);
int sched_getscheduler(pid_t pid);
Fonksiyonların birinci parametreleri prosesin id değerini belirtmektedir. Bu parametre 0 girilirse fonksiyonları çağıran proses için
işlem yapılmaktadır. sched_setscheduler fonksiyonun ikinci parametresi çizelgeleme politikasını üçüncü paarametresi ise gerçek zamanlı
çizelgeleme politikalarına ilişkin öncelik derecesini belirtmektedir. Vu üçüncü parametrede SCHED_OTHER için öncelik belirtilememektedir.
Bu parametre yalnızca SCHED_FIFO ve SCHED_RR için anlamlıdır. Fonksiyon başarı durumunda eski çizelgeleme politikasına başarısızlık
durumunda -1 değerine geri dönmektedir. sched_getscheduler fonksiyonu da başarı durumunda çizelgeleme politikasına, başarısızlık durumunda -1
değerine geri dönmektedir.
2024-06-09 22:42:45 +03:00
Prosesin çizelgeleme politikasını değiştirebilmek için sched_setscheduler fonksiyonunu çağıran prosesin uygun önceliğe sahip olması
gerekir.
Aşağıdaki programda proses kendi çizelgeleme politikasını SCHED_FIFO yapıp önceliğini de Linux'taki maksimum öncelik olan 99 yapmıştır.
Tabii programı sudo ile çalıştırmalısınız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void* thread_proc(void* param);
void exit_errno(const char* msg, int result);
int main(void)
{
int result;
pthread_t tid;;
if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0)
exit_errno("pthread_create", result);
pthread_join(tid, NULL);
return 0;
}
void* thread_proc(void* param)
{
struct sched_param sparam;
int result;
long i;
sparam.sched_priority = 99;
if ((result = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sparam)) != 0)
exit_errno("pthread_setschedparam", result);
for (i = 0; i < 1000000000; ++i)
;
return NULL;
}
void exit_errno(const char* msg, int result)
{
fprintf(stderr, "%s: %s\n", msg, strerror(result));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Yukarıda da belirttiğimiz gibi POSIX standartlarına göre aslında prosesin çizelgeleme politikası ya da öncelik dereceleri değiştirildiğinde
bundan prosesin tüm thread'leri etkilenmektedir. Ancak Linux'ta bundan yalnızca prosesin ana thread'i etkilenmektedir. Ancak biz belli bir
thread'imizin de diğerlerin bağımsız olarak çizelgeleme politikasını değiştirip elde edebiliriz. Bunlar için pthread_setschedparam ve
pthread_getschedparam fonksiyonları kullanılmaktadır:
#include <pthread.h>
2024-06-09 22:42:45 +03:00
int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param);
int pthread_getschedparam(pthread_t thread, int *policy, struct sched_param *param);
Fonksiyonlar sched_setscheduler ve sched_getscheduler fonksiyonları gibi çalışmaktadır. Yalnızca proses id yerine thread id değerini
parametre olarak almaktadır. Diğer thread fonksiyonlarında olduğu gibi bu fonksiyonlar da başarı durumunda 0 değerine, başarısızlık
durumunda errno değerine geri dönemektedir.
Aşağıdaki örnekte bir thread'in çizelgeleme politikası ve önceliği değiştirilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
2024-06-09 22:42:45 +03:00
#include <string.h>
#include <pthread.h>
2024-06-09 22:42:45 +03:00
void* thread_proc(void* param);
void exit_errno(const char* msg, int result);
int main(void)
{
int result;
2024-06-09 22:42:45 +03:00
pthread_t tid;;
2024-06-09 22:42:45 +03:00
if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0)
exit_errno("pthread_create", result);
2024-06-09 22:42:45 +03:00
pthread_join(tid, NULL);
return 0;
}
2024-06-09 22:42:45 +03:00
void* thread_proc(void* param)
{
2024-06-09 22:42:45 +03:00
struct sched_param sparam;
int result;
sparam.sched_priority = 99;
if ((result = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sparam)) != 0)
exit_errno("pthread_setschedparam", result);
for (long i = 0; i < 1000000000; ++i)
;
return NULL;
}
void exit_errno(const char* msg, int result)
{
fprintf(stderr, "%s: %s\n", msg, strerror(result));
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Kütüphane (library) kavramı genel olarak "kullanıma hazır halde bulunan fonksiyonlar ve sınıfların oluşturduğu" topluluklar için kullanılan
bir terimdir. Ancak aşağı seviyeli dünyada kütüphane ""içerisinde derlenmiş bir biçimde fonksiyonların (ve sınıfların) bulunduğu dosyalara
denilmektedir.
Kütüphaneler statik ve dinamik olmak üzere ikiye ayrılmaktadır. Statik kütüphanelerin Windows sistemlerinde uzantıları ".lib"", UNIX/Linux
ve macOS sistemlerindeki uzantıları ".a" biçimindedir. Dinamik kütüphanelerin ise Windows sistemlerindeki uzantıları ".dll", UNIX/Linux
ve macOS sistemlerindeki uzantıları ise ".so" biçimindedir. ".lib" uzantısı "library" sözcüğünde, ".a" uzantısı "archive" sözcüğünden,
"dll" uzantısı "dynamic link library" sözcüklerinden ve ".so" uzantısı da "shared object" sözüklerinden kısaltılmıştır.
Kütüphane konusunun aslında aşağı seviyeli oldukça ayrıntıları vardır. Biz bu kursumuzda temel düzeyde bu kavramlar üzerinde uygulamalı
bilgiler edineceğiz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Derleyicilerin ürettiği dosyalara "amaç dosyalar (object files)" ya da "yeniden yüklemebilen dosyalar (relocatable object files)" denilmektedir.
Bağlayıcılar bir grup amaç dosyayı (object files) girdi olarak alıp çeşitli kütüphanelere de başvurarak çalıştırılabilir doysalar
oluşturmaktadır. Bu sayede programlar farklı ekipler tarafından parça parça yazılıp bağlama aşamasında birleştirilebilmektedir. Örneğin
tipik bir C projesi şöyle derlenip oluşturulmaktadır:
2024-06-09 22:42:45 +03:00
a1.c ----> Derleyici ----> a1.obj ----> \
a2.c ----> Derleyici ----> a2.obj ----> \
a3.c ----> Derleyici ----> a3.obj ----> ===> Bağlayıcı ------> Çalıştırılabilir Dosya
a4.c ----> Derleyici ----> a4.obj ----> / /
a5.c ----> Derleyici ----> a5.obj ----> / /
Kütüphane Dosyaları
2024-06-09 22:42:45 +03:00
Amaç dosyaların Windows sistemlerindeki uzantıları ".obj" biçiminde UNIX/Linux ve nacOS sistemlerindeki uzantıları ise ".o" biçimindedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Pekiyi bağlayıcı tam olarak ne yapmaktadır? Farklı amaç dosyaları birleştirmek basit bir işlem değildir. Örneğin iki kaynak dosyadan oluşan
bir program söz konusu olsun:
2024-06-09 22:42:45 +03:00
/* a1.c */
2024-06-09 22:42:45 +03:00
extern int g_x;
void foo(void);
2024-06-09 22:42:45 +03:00
int main(void)
{
g_x = 10;
foo(); CALL adres
return 0;
}
/* a2.c */
int g_x;
void foo(void)
{
printf("foo\n");
}
Proje oluşturan bu "a1.c" ve "a2.c" dosyalarının birbirinden habersiz bağımsız bir biçimde derlendiğine dikkat ediniz. "a1.c" programı
derlendiğinde foo fonksiyonu o dosya yoktur. g_x global değişkeni de o dosyada yoktur. Pekiyi oradaki makine komutları nasıl üretilmektedir?
İşte derleyiciler bu yüt durumlarda makine kodlarının ilgili kısımlarını boş bırakıp bağlayıcının orayı doldurmasını istemektedir.
Bu işleme "relocation" denilmektedir. "a1.c" dosyası içerisindeki fonksiyon çağrısına dikkat ediniz:
foo();
Bir fonksiyonun çağrılabilmesi için CALL makine komutu kullanılmaktadır. CALL makine komutu da operand olarak çağrılacak fonksiyonu adresini
almaktadır. Örneğin:
CALL <adres>
Oysa derleyici derleme aşamasında foo fonksiyonunu görmediği için onun adresini bilememktedir. İşte derleyici bağlayıcıdan "foo fonksiyonun
yerini bulunup ilgili adresin düzeltilmesini" talep etmektedir. Bu relocation bilgileri amaç dosyanın içerisinde bulunmaktadır. Yani
bağlayıcı yalnızca amaç dosyaları birleştirmemekte aynı zamanda çeşitli makine komutlarını da düzeltmektedir. Pekiyi yukarıdaki örnekte
foo fonksiyonunu bağlayıcı nerelrde arayacaktır? Bağlayıcılar öncelikle fonksiyonları ve global nesneleri kendilerine girdi olarak verilmiş
olan kütğphane amaç dosyalarda aramaktadır. Onlar amaç dosyalarda bulunamazsa bağlayıcılar programcının belirlediği kütüphane dosyalarına
da bakmaktadır.
Standart C fonksiyonları, Windows API fonksiyonları, POSIX fonksiyonları çeşitli kütüphane dosyalarının içerisindedir. Bunlar bağlama
aşamasında bağlayıcı tarafından ele alınmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Statik kütüphane dosyaları "amaç dosyaları tutan kap" gibidir. Yani statik kütüphane dosyalarının içerisinde fonksiyonlar ya da sınıflar
değil amaç dosyalar bulunmaktadır. Yani örneğin biz bir grup fonksiyonu statik kütüphaneye yerleştirmek istesek önce onları bir C dosyasının
içerisine yazarız. Sonra dosyayı derleyip amaç dosya oluştururuz. Bu amaç dosyayı da statik kütüphane içerisine yerleştiririz. Microsoft
C derleyicilerinde dosyayı yalnızca derlemek için "/c" seçeneği, gcc ve clang derleyicilerinde ise "-c" seçeneği kullanılmaktadır. Örneğin
cl /c myutil.c
gcc -c myutil.c
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Microsoft Windows sistemlerinde statik kütüphane dosyaları özel bir formata sahiptir. Bunu oluşturmak için Microsoft "lib.exe" isimli
yardımcı bir program bulundurmuştur. "lib.exe" programı Microsoft C derleyicisi kurulduğunda (tabii Visual Studio IDE'si de kurulduğunda)
otomatik olarak kurulmaktadır.
lib.exe programının en önemli birkaç kullanımı şöyledir:
2024-06-09 22:42:45 +03:00
lib /OUT:myutil.lib a.obj b.obj
2024-06-09 22:42:45 +03:00
Burada "myutil.lib" dosyası yeniden yaratılır ve onun içerisine "a.obj" ve "b.obj" dosyaları eklenir. Eğer bu dosya varsa içerği sıfırlanarak
yeniden oluşturulmaktadır.
2024-06-09 22:42:45 +03:00
lib myutil.lib c.obj
2024-06-09 22:42:45 +03:00
Burada "myutil.lib" "dosyasının var olması gerekir. Onun içerisinde eğer "c.obj" dosyası yoksa eklenir varsa dosya değiştirilir.
2024-06-09 22:42:45 +03:00
lib /LIST myutil.lib
2024-06-09 22:42:45 +03:00
Burada "myutil.lib" içerisindeki amaç dosyalar görüntülenir.
2024-06-09 22:42:45 +03:00
lib /REMOVE:a.obj myutil.lib
2024-06-09 22:42:45 +03:00
Burada "myutil.lib" içerisinden "a.obj" dosyası silinir.
2024-06-09 22:42:45 +03:00
lib /EXTRACT:a.obj myutil.lib
2024-06-09 22:42:45 +03:00
Burada "myutil.lib" içerisindeki "a.obj" dosyası dışarıda "a.obj" "olarak save edilir. Fakat kütüphanenin içeriisinden silinmez.
"lib.exe" hakkında ayrıntılııklamalar için aşağıdaki MSDN bağlantısından faydalanabilirsiniz:
2024-06-09 22:42:45 +03:00
https://learn.microsoft.com/en-us/cpp/build/reference/overview-of-lib?view=msvc-170
Aşağıdaki örnekte "a.c" ve "b.c" dosyaları aşağıdaki gibi derlenip "myutil.lib" isimli bir statik kütüphane dosyası oluşturulmuştur:
2024-06-09 22:42:45 +03:00
cl /c a.c
cl /c b.c
lib /OUT:mmyutil.lib a.obj b.obj
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* a.c */
2024-06-09 22:42:45 +03:00
#include <stdio.h>
2024-06-09 22:42:45 +03:00
int g_x;
2024-06-09 22:42:45 +03:00
double add(double a, double b)
{
return a + b;
}
2024-06-09 22:42:45 +03:00
double sub(double a, double b)
{
2024-06-09 22:42:45 +03:00
return a - b;
}
2024-06-09 22:42:45 +03:00
double multiply(double a, double b)
{
return a * b;
}
2024-06-09 22:42:45 +03:00
double divide(double a, double b)
{
return a / b;
}
2024-06-09 22:42:45 +03:00
/* b.c */
#include <stdio.h>
2024-06-09 22:42:45 +03:00
int g_y;
2024-06-09 22:42:45 +03:00
void foo(void)
{
printf("foo\n");
}
2024-06-09 22:42:45 +03:00
void bar(void)
{
2024-06-09 22:42:45 +03:00
printf("bar\n");
}
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir kütüphanedeki fonksiyonlar çağrılabilmesi için çağrılan kaynak dosyada o fonksiyonların prototipilerinin bulundurulması gerekir. Benzer
biçimde kütüphane içerisindeki global değişkenlerin kullanılabilmesi için de o global değişkenlerin extern bildirimlerinin bulundurulması
gerekir. Her kütüphane için o kütüphanedeki bildirimleri barındıran bir başlık dosyasının bulundurulması iyi bir tekniktir. Örneğin:
2024-06-09 22:42:45 +03:00
#ifndef MYUTIL_H_
#define MYUTIL_H_
2024-06-09 22:42:45 +03:00
/* Function Prototypes */
2024-06-09 22:42:45 +03:00
double add(double a, double b);
double sub(double a, double b);
double multiply(double a, double b);
double divide(double a, double b);
void foo(void);
void bar(void);
2024-06-09 22:42:45 +03:00
/* extern Declarations */
2024-06-09 22:42:45 +03:00
extern int g_x;
extern int g_y;
2024-06-09 22:42:45 +03:00
#endif
2024-06-09 22:42:45 +03:00
Böylece bu kütüphaneyi kullanacak kişiler bu başlık dosyasını include ederek bütün kütüphane ile ilgili bildirimleri bulundurmuş olurlar.
Eğer kütüphane çok fazla öğeden oluşyorsa tek bir başlık dosyası önişlemci (preprocessor) zamanını uzatabilir. Bu durumda bir tane değil
birden çok başlık dosyası bulundurulabilir. Örneğin aslında standart C fonksiyonlarının hepsi tek bir kütüphane içerisindedir. Ancak
onlara ilişkin bildirimler çeşitli başlık dosyalarına yaydırılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bağlayıcılar onlara verilen amaç dosyalar dışında ayrıca temel bazı kütüphanelere otomatik olarak bakmaktadır. Microsoft'un bağlayıcı
programı olan "link.exe" hiç belirtilmese bile standart C fonksiyonlarının bulunduğu, Windows API fonksiyonlarının bulunduğu kütüphanelere
zaten bakmaktadır. Ancak programcı bağlayıcının kendi kütüphanelerine de bakmasını istiyorsa bunu bağlayıcıya komut satırından söylemelidir.
2024-06-09 22:42:45 +03:00
Anımsanacağı gibi Microsoft'un Cve C++ derleyicisi olan "cl.exe" programı "/c" seçeneği belirtilmezse derleme sonrasında "link.exe"
bağlayıcısını kendisi çalıştırmaktadır. İşte biz de "cl.exe" programının komut satırında derlenecek kaynak dosyalardan sonra uzantısı ".lib"
olan kütüphane dosyalarını belirtirsek "link.exe" bağlayıcısı o kütüphane dosyalarına da bağlama aşamasında bakmaktadır. Örneğin:
2024-06-09 22:42:45 +03:00
cl app.c myutil.lib
2024-06-09 22:42:45 +03:00
Burada "app.c" programı derlenerek "app.obj" dosyası oluşturulacak sonra bağlama aşamasında "myutil.lib" dosyasına da bakılacaktır.
Oluşturulacak çalıştırılabilir dosyanın ismi ilk kaynak dosyanın ismi olarak (örneğimizde "app.exe") alınacaktır. Tabii istenirse "cl.exe"
komut satırında /Fe:<dosya_ismi> argümanı ile çalıştırılabilir dosyaya arzu edilen bir isim de verilebilmektedir. Örneğin:
2024-06-09 22:42:45 +03:00
cl /Fe:project.exe app.c myutil.lib
2024-06-09 22:42:45 +03:00
Tabii istersek derleyici ve bağlayıcıyı ayrı ayrı da çalıştırabiliriz. Örneğin:
2024-06-09 22:42:45 +03:00
cl /c app.c
link app.obj myutil.lib
2024-06-09 22:42:45 +03:00
Burada yine default olarak "link.exe" programı ilk amaç dosyanın ismini çalıştırılabilir dosyaya vermektedir. Ancak çalıştırılabilen
dosyanın ismi /OUT:<isim> seçeneği ile de bağlayıcıya verilebilmektedir. Örneğin:
2024-06-09 22:42:45 +03:00
cl /c app.c
link /OUT:project.exe app.obj myutil.lib
2024-06-09 22:42:45 +03:00
Aşağıdak örnekte "a.c" ve "b.c" dosyalarından "myutil.lib" statik kütüphanesi oluiştuurulmuş ve bu statik kütüphane de "app.c" programından
kullanılmıştır. İşlemleri komut satırından aşağıdaki sırada yapabilirsiniz:
cl -c a.c
cl -c b.c
lib /OUT:myutil.lib a.obj b.obj
cl app.c myutil.lib
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* a.c */
#include <stdio.h>
int g_x;
double add(double a, double b)
{
2024-06-09 22:42:45 +03:00
return a + b;
}
2024-06-09 22:42:45 +03:00
double sub(double a, double b)
{
return a - b;
}
2024-06-09 22:42:45 +03:00
double multiply(double a, double b)
{
return a * b;
}
2024-06-09 22:42:45 +03:00
double divide(double a, double b)
{
return a / b;
}
2024-06-09 22:42:45 +03:00
/* b.c */
#include <stdio.h>
2024-06-09 22:42:45 +03:00
int g_y;
2024-06-09 22:42:45 +03:00
void foo(void)
{
printf("foo\n");
}
2024-06-09 22:42:45 +03:00
void bar(void)
{
2024-06-09 22:42:45 +03:00
printf("bar\n");
}
2024-06-09 22:42:45 +03:00
/* myutil.h */
2024-06-09 22:42:45 +03:00
#ifndef MYUTIL_H_
#define MYUTIL_H_
2024-06-09 22:42:45 +03:00
/* Function Prototypes */
2024-06-09 22:42:45 +03:00
double add(double a, double b);
double sub(double a, double b);
double multiply(double a, double b);
double divide(double a, double b);
void foo(void);
void bar(void);
2024-06-09 22:42:45 +03:00
/* extern declarations */
2024-06-09 22:42:45 +03:00
extern int g_x;
extern int g_y;
2024-06-09 22:42:45 +03:00
#endif
2024-06-09 22:42:45 +03:00
/* app.c*/
2024-06-09 22:42:45 +03:00
#include <stdio.h>
#include "myutil.h"
2024-06-09 22:42:45 +03:00
int main(void)
{
2024-06-09 22:42:45 +03:00
double result;
2024-06-09 22:42:45 +03:00
result = add(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = sub(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = multiply(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = divide(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
foo();
bar();
2024-06-09 22:42:45 +03:00
return 0;
}
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir statik kütüphaneden bir fonksiyon çağrıldığında bağlayıcı çağrılmış olan fonksiyonun statik kütüphane içerisindeki hangi amaç dosyada
olduğunu belirleri, o amaç dosyayı çalıştırılabilen dosyaya ekler. Böylece statik kütüphane kullanan bir program çalıştırılırken artık
o statik kütüphaneye gereksinim duyulmaz. Şu noktalara dikkat ediniz:
2024-06-09 22:42:45 +03:00
- Statik kütüphane içerisinde bir fonksiyon bile çağırsak bağlayıcı onun bulunduğu amaç dosyanın tamamını çalıştırılabilen dosyaya yazmaktadır.
Amaç dosya içerisindeki tek bir fonksiyonun çalıştırılabilen dosyaya yazılması mümkün değildir.
2024-06-09 22:42:45 +03:00
- Statik kütüphane kullanımında statik kütüphane içerisidnen çağrılan fonksiyonlar nihayetinde çalıştırılabilen dosyanın içerisine yazıldığı
için aynı fonksiyonları kullanan farklı programlar aynı fonksiyon kodlarını çalıştırılabilen dosya içerisinde barındırmış olacaklardır.
Bu da çalıştırılabilen dosyaların diskte daha fazla yer kaplamasına yol açmaktadır.
2024-06-09 22:42:45 +03:00
- Statik bağlanmış fonksiyonları kullanan farklı programlar birlikte çalıştırıldığında işletim sistemi mecburen onların ortak kullandığı
fonksiyonları tekrar belleğe yüklemektedir. Bu da yalnızca disk kullanımı bakımından değil ana bellek yönetini bakımından da etkin olmayan
bir durum oluşturmaktadır.
2024-06-09 22:42:45 +03:00
- Statik kütüphanelerin en önemli avantajı onları kullanan programların konuşlandırılmasının (deployment) kolay olmasıdır. Tek yapılacak
şey tek bir çalıştırılabilir dosyanın hedef makineye taşınmasıdır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
92. Ders 01/06/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında C ve C++ IDE'lerinde de hiç komut satırı işlemi yapmadan statik kütüphane oluşturulabilmektedir. Örneğin Visual Studio IDE'sinde
yeni boş bir bir proje oluşturulduktan sonra proje seçeneklerinden "Configuration Type" "Application" yerine "Static library" seçilirse
proje build edildiğinde statik kütüphane dosyası oluşturulmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Visual Studio derleyicisi default durumda standart C kütüphanesinin dinamik versiyonunu kullanmaktadır. Eğer projenizde standart C
kütüphanesinin statik versyionunu kullanmak istiyorsanız komut satırında "/MT" seçeneği eklenmelidir. Aynı işlem proje seçeneklerinde
"C-C++/Code Generation/Runtime Library" sekmesinden de ayarlanabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Visual Studio IDE'sinde (diğer IDE'lerde de benzer) bir projede başka bir statik kütüphanenin içerisindeki fonksiyonları çağırmak istediğimizi
düşünelim. Pekiyi bu statik kütüphaneye IDE içerisinden nasıl referans ederiz? İşte kullanılacak ek kütüphanelere bağlayıcının bakmasını
sağlamak için Visual Studio IDE'sinde proje seçeneklerinden "Linker/Input/Additional Dependencies" edit alanına kütüphanenin ismi eklenmelidir.
(Bu alandaki kütüphane isimleri ';' karakterleriyle birbirinden ayrılmaktadır.) Burada kütüphane yalnızca ismi girilebilir. Bir yol ifadesi
girilmez. Eğer kütüphane özel bazı dizinlerde ya da projenin çalışma dizininde değilse ayrıca kütüphanenin bulunduğu dizin "Linker/General
/Additional Library Directories" edit alanına girilmelidir. Burada dizinler mutlak ya da göreli biçimde girilebilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde statik kütüphane dosyalarını oluşturmak ve onlarla ilgili işlem yapmak için "ar" isimli program kullanılmaktadır.
UNIX/Linux sistemlerindeki "ar" programı aslında Microsoft sistemlerindeki "LIB.EXE" programının karşılığı olarak düşünülebilir. Bu "ar"
programının birkaç önemli komut satırı argümanı vardır. Bu program çok eskiden tasarlandığı için seçenekler '-' karakteri ile değil'-'
karakteri olmadan doğrudan belirtilmektedir. "r" komut satırı argümanı ("replace" sözcüğünden geliyor) amaç dosyaları kütüphaneye eklemek
için kullanılmaktadır. Bu seçenekte kütüphane dosyası yoksa aynı zamanda yaratılmaktadır. Kullan ım şöyledir:
2024-06-09 22:42:45 +03:00
ar r <kütüphane_dosyasının_ismi> <.o dosyaları>
2024-06-09 22:42:45 +03:00
Eğer eklenecek amaç dosya zaten kütüphanenin içerisinde varsa o değiştirilmektedir. Örneğin:
2024-06-09 22:42:45 +03:00
$ gcc -c a.c
$ gcc -c b.c
$ ar r libmyutil.a a.o
$ ar r libmyutil.a b.o
2024-06-09 22:42:45 +03:00
"t" seçeneği kütüphane içerisindeki amaç dosyaları görüntülemektedir. Kullanımı şöyledir:
2024-06-09 22:42:45 +03:00
ar t <kütüphane_dosyasının_ismi>
2024-06-09 22:42:45 +03:00
Örneğin:
2024-06-09 22:42:45 +03:00
$ ar t libmyutil.a
a.o
b.o
2024-06-09 22:42:45 +03:00
"d" seçeneği ("delete" sözcüğünden geliyor) kütüphane içerisindeki bir amaç dosyayı kütüphaneden silmek için kullanılmaktadır. Kullanımı
şöyledir:
2024-06-09 22:42:45 +03:00
ar d <kütüphane_dosyasnın_ismi> <.o dosyaları>
2024-06-09 22:42:45 +03:00
Örneğin:
2024-06-09 22:42:45 +03:00
$ ar t libmyutil.a
a.o
b.o
$ ar d libmyutil.a a.o
$ ar t libmyutil.a
b.o
2024-06-09 22:42:45 +03:00
"x" seçeneği ("extract" sözcüğünden geliyor) kütüphane içerisindeki bir amaç dosyayı kütüphaneden silmeden dışarıya save etmekte kullanılmaktadır.
Kullanımı şöyledir:
2024-06-09 22:42:45 +03:00
ar x <kütüphane_dosyasının_ismi> <.o dosyaları>
2024-06-09 22:42:45 +03:00
Örneğin:
2024-06-09 22:42:45 +03:00
$ ar x libmyutil.a a.o
2024-06-09 22:42:45 +03:00
UNIX/Linux sistemlerinde geleneksel olarak kütüphane dosyaları "lib" öneki başlatılarak isimlendirilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistelerinde bir statik kütüphaneye başvuru işlemi gcc komut satırında ".a" dosyasının belirtilmesi yoluyla yapılabilmektedir.
Örneğin:
2024-06-09 22:42:45 +03:00
$ gcc -o app app.c libmyutil.a
2024-06-09 22:42:45 +03:00
Burada gcc programı ".a" uzantılı dosyaları "ld" bağlayıcısını çalıştırırken ona gendermektedir. Tabii aynı işlem iki parça halinde şöyle
de yapılabilirdi:
2024-06-09 22:42:45 +03:00
$ gcc -c app.c
$ gcc -o app app.o libmyutil.a
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Daha önceden de belirttiğimiz gibi standart C fonksiyonları ve POSIX fonksiyonları "libc" isimli bir kütüphanede bulunmaktadır. Bu
kütüphanenin static ve dinamik biçimleri vardır. Default durumda bu kütüphanenin dinamik versiyonu kullanılmaktadır. Ancak komut satırında
"-static" seçeneği ile bu kütüphanenin statik versiyonunun kullanılması sağlanabilir. "-static" komut satırı seçeneği "tüm kütüphanelerin
statik versiyonlarının" kullanılacağı anlamına gelmektedir. (Yani biz "-static" seçeneğini beelirttikten sonra başka bir dinamik kütüphaneyi
de bağlama aşamasında kullanamayız.) Örneğin:
2024-06-09 22:42:45 +03:00
$ gcc -o app app.c -static
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde kütüphane dosyası başka bir dizindeyse bu durumda o dosyaya referans ederken yol ifadesi kullanılabilir.
Öneğin:
2024-06-09 22:42:45 +03:00
$ gcc -o app app.c xxx/libmyutil.a
2024-06-09 22:42:45 +03:00
Eğer kütüphanenin ismi "lib" öneki ile başlatılmışsa kütüphaneye referans etme (yani kütüphanenin bağlayıcı tarafından işleme sokulmasını sağlama)
"-l" seçeneği ile yapılabilmektedir. Ancak bu "-l" seçeneğinde kütüphanenin başındaki "lib" öneki ve ".a" ya da ".so" sonekleri kullanılmamaktadır.
Örneğin:
2024-06-09 22:42:45 +03:00
4 gcc -o app app.c -lmyutil
2024-06-09 22:42:45 +03:00
Burada bağlama işlemine "libmyutil.a" dosyası sokulacaktır. Ancak "-l" seçeneği ile kütüphane bu biçimde belirtildiğinde bu kütüphane "/lib" ve
"/usr/lib" dizinlerinde aranmaktadır. Yani bu durumda buradaki "libmyutil.a" dosyasının bu dizinlerden birine çekilmesi gerekir. Ancak "-l"
seçeneğine ek olarak "-L" seçeneği ile ek arama dizi belirtilebilmektedir. Örneğin:
2024-06-09 22:42:45 +03:00
$ gcc -o app app.c -lmyutil -L/home/kaan
2024-06-09 22:42:45 +03:00
Burada "libmyutil.a" dosaysı yukarıda belirttiğmiz dizinlerin yanı sıra "/home/kaan" dizininde de aranacaktır. Tabii biz "-L" seçeneğinde
o anda bulunulan dizini "." ile de belirtebiliriz. Örneğin:
2024-06-09 22:42:45 +03:00
$ gcc -o app app.c -lmyutil -L.
2024-06-09 22:42:45 +03:00
Örneğin daha önceden de kullandığımız thread kürüphanesi aslında "libpthread.a" ve "libpthread.so" isimli dosyalardır. Biz de bu ktüphaneiyi
bağlama aşamasında şöyle devreye sokuyorduk:
2024-06-09 22:42:45 +03:00
$ gcc -o app app.c -lpthread
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Dinamik kütüphaneler statik kütüphanelerden oldukça farklıdır. Windows'ta dinamik kütüphanelerin uzantısı ".dll", UNIX/Linux sistemlerinde
".so" biçimindedir. ".dll" uzantısı "dynamic link library" sözcüklerinden ".so" uzantısı ise "shared object" sözcüklerinden kısaltılmıştır.
Bir program dinamik kütüphaneden bir fonksiyon çağırdığında bağlayıcı o fonksiyonun kodunu dinamik kütüphaneden çekip çalıştırılabilen dosyaya
yazmaz. Bağlayıcı bunun yerine çalıştırılabilen dosyaya "falanca dinamik kütüphaneden filanca fonksiyonlar çağrıldı" biçiminde bir bilgi
yazmaktadır. Dinamik kütüphane kullanan bir program çalıştırılmak istendiğinde ise işletim sistemi bu programla birlikte bu programın kullandığı
dinamik kütüphaneleri prosesin sanal bellek alanına bütünsel bir biçimde yükler. Program çalışırken akış fonksiyonun çağrılma noktasına
geldiğinde bellekte dinamik kütüphanenin yüklü olduğu alana atlayarak fonksiyonu çalıştırır.
2024-06-09 22:42:45 +03:00
Dinamik kütüphane kullanımının şu avantajları vardır:
2024-06-09 22:42:45 +03:00
1) Dinamik kütüphane kullanan programlar diskte daha az yer kaplarlar.
2) Dinamik kütüphane kullanan programlarda dinamik kütüphane içerisindeki fonksiyonlarda değişiklikler yapıldığında çalıştırılabilen
programın yeniden derlenmesine ve link edilmesine gerek kalmaz.
2024-06-09 22:42:45 +03:00
3) Farklı proseslerin aynı dinamik kütüphaneyi kullanması durumunda bu dinamik kütüphane fiziksel RAM'e tekrar tekrar yüklenmemektedir.
Bu da aslında toplamda daha iyi bir RAM optimizasyonu sağlamaktadır.
2024-06-09 22:42:45 +03:00
Ancak dinamik kütüphane kullanan programlar başka bir makineye konuşlandırılırken (deploy edilirken) yalnızca çalıştırılabilen dosya değil
onun kullandığı bütün dinamik kütüphaneler de o makineye çekilmek zorundadır. Ayni artık program tek bir çalıştırılabilen dosyadan
oluşmamaktadır. O dosyanın kullandığı dinamik kütüphaneler de programın bir parçası durumundadır. Dinamik kütüphane kullanımının şu
dezavantajları söz konusu olabilmektedir:
2024-06-09 22:42:45 +03:00
1) Dinamik kütüphane kullanan programların hedef makineye konuşlandırılması daha zahmetlidir.
2024-06-09 22:42:45 +03:00
2) Dinamik kütüphane kullanan programların yüklenmesi daha uzun zaman alma eğilimindedir.
2024-06-09 22:42:45 +03:00
3) Dinamik kütüphane kullanan programlar prosesin sanal bellek alanında daha fazla yer kaplamaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Microsoft Windows sistemlerinde bir DLL oluşturmak için tek yapılacak şey link aşamasında "/DL"L seçeneğini kullanmaktır. Örneğin a.c ve
b.c dosyalarını derleyerek bunlardan DLL yapmak isteyelim. Bu işlemi parça paça şöyle gerçekleştirebiliriz:
2024-06-09 22:42:45 +03:00
cl /c a.c
cl /c b.c
link /DLL a.obj b.obj
2024-06-09 22:42:45 +03:00
Buradan ilk dosyanın ismine ilişkin "a.dll" dosyası elde edilecektir. Tabii "/DL"L seçeneği kullanılmasaydı default durumda bu dosyalardan
".exe" dosyası oluşturulmaya çalıştırılacaktı. Tabii bu durumda bir main fonksiyonun buluması gerekecekti. Hedef dosyanın ismi "/OUT" seçeneği
ile belirlenebilmektedir. Örneğin:
2024-06-09 22:42:45 +03:00
cl /c a.c
cl /c b.c
link /OUT:myutil.dll /DLL a.obj b.obj
2024-06-09 22:42:45 +03:00
Burada "myutil.dll" dosyası oluşturulacaktır. Yukarıdaki işlem aslında tek hamlede "cl" programının komut satırında "/LD" seçeneği kullanılarak
aşağıda gibi de yapılabilmektedir:
2024-06-09 22:42:45 +03:00
cl /LD a.c b.c
2024-06-09 22:42:45 +03:00
Burada ilk dosyanın ismine ilişkin DLL dosyası (yani "a.dll" dosyası) oluşturulacaktır. Ancak dinamik kütüphaneye istediğimiz ismi vermek
istiyorsak "cl" komut satırında "/Fe" kullanılması gerekir. "/OUT" seöeneği bir bağlayıcı seçeneğidir. "/Fe" seçeneği ise bir "cl" derleyicisinin
seçeneğidir. (Başka bir deyişle biz "cl" derleyicisini "/Fe" seçeneği ile çalıştırdığımızda aslında "cl" derleyicisi "link" isimli bağlayıcıyı
"/OUT" seçeneği ile çalıştırmaktadır.) Örneğin:
2024-06-09 22:42:45 +03:00
cl /Fe:myutil.dll /LD a.c b.c
2024-06-09 22:42:45 +03:00
Bura artık "myutil.dll" isimli dosya oluşturulacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Teknik anlamda Microsoft Windows sistemlerinde ".EXE" dosyası ile ".DLL" dosyası arasında format bakımından farklılık yoktur. He iki dosya
da "PE (Portable Executable)" dosya formaına ilişkindir. ".EXE" doaysının ".DLL" dosyasından tek farkı bir "entry point" yani main fonksiyonuna
sahip olmasıdır. Bir DLL içerisinde main fonksiyonunun olması o DLL dosyasını EXE dosya yapmaz. DLL dosyasının içerisindeki main fonksiyonu
sıradan bir fonksiyon gibi DLL'in içerisinde bulunur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows'ta bir DLL içerisindeki fonksiyonun ya da global nesnenin dışarıdan kullanılabilmesi için o fonksiyonun ya da global nesnenin adresinin
PE formatının "export tablosu" denilen bir yerinde bulunması gerekir. İşte DLL içerisindeki bir global nesnenin ya da fonksiyonun adresinin
export tablosuna yazılması için Micrososft derleyicilerine özgü __declspec(dllexport) belirleyicisinin kullanılması gerekmektedir. Aksi takdirde
o fonksiyonlar ve nesneler başka bir modülden (yani başka bir DLL ya da EXE programdan) kullanılamazlar. Aşağıdaki örnekte DLL içerisinde
foo ve g_a dışarıdan kullanılbilir ancak bar g_b dışarıdan kullanılmaz. Tabii bu bar ve g_b DLL'in kendi içeriisndeki fonksiyonlardan
kullanılabilir. Örneğin:
2024-06-09 22:42:45 +03:00
#include <stdio.h>
2024-06-09 22:42:45 +03:00
__declspec(dllexport) int g_y;
2024-06-09 22:42:45 +03:00
__declspec(dllexport) void foo(void)
{
printf("foo\n");
}
2024-06-09 22:42:45 +03:00
void bar(void)
{
printf("bar\n");
}
2024-06-09 22:42:45 +03:00
Buarada g_y global değişkeni ve foo fonksiyonu dışarıdan (örneğin bir exe dosya tarafından) kullanılabilir. Ancak bar fonksiyonu dışarıdan
kullanılamaz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
93. Ders 02/06/2024 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Aslında C++'taki sınıf (class) kavramı yapay ve mantıksal bir kavramdır. Aşağı seviyeli dünyada sınıf diye bir kavram yoktur. Aslında C++'ta
sınıflardaki üye fonksiyonlar global fonksiyonlar gibi derlenirler. Dolayısıyla C++'ta bir sınıfı DLL'e yerleştirmek için yine üye fonksiyonların
önünde hangi üye fonksiyonlar dışarıdan kullanılacaksa __declspec(dllexport) bildirimlerinin yapılması gerekir. Örneğin:
2024-06-09 22:42:45 +03:00
class Sample {
public:
__declspec(dllexport) Sample(int a);
__declspec(dllexport) void foo() const;
__declspec(dllexport) void disp() const;
void bar() const;
private:
int m_a;
};
2024-06-09 22:42:45 +03:00
Bu örnekte sınıfın "yapıcı fonksiyonu (constructor)" foo ve disp fonksiyonları dışarıdan kullanılabilir. Ancak bar fonksiyonu dışarıdan
kullanılamaz. __declspec(dllexport) belirleyicisi tanımlama sırasında kullanılmak zorunda değildir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
#include <iostream>
using namespace std;
class Sample {
public:
__declspec(dllexport) Sample(int a);
__declspec(dllexport) void foo() const;
__declspec(dllexport) void disp() const;
void bar() const;
private:
int m_a;
};
2024-06-09 22:42:45 +03:00
Sample::Sample(int a)
{
m_a = a;
}
2024-06-09 22:42:45 +03:00
void Sample::foo() const
{
2024-06-09 22:42:45 +03:00
cout << "foo" << endl;
}
2024-06-09 22:42:45 +03:00
void Sample::disp() const
{
cout << m_a << endl;
}
2024-06-09 22:42:45 +03:00
void Sample::bar() const
{
cout << "bar" << endl;
}
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir sınıfın bütün üye fonksiyonlarının dışarıdan kullanılması isteniyorsa Windows sistemlerinde __declspec(dllexport) belirleyicisi class
anahtar sözcüğü ile sınıf ismi arasına getirilebilir. Örneğin:
2024-06-09 22:42:45 +03:00
class __declspec(dllexport) Sample {
public:
Sample(int a);
void foo() const;
void disp() const;
void bar() const;
private:
int m_a;
};
2024-06-09 22:42:45 +03:00
Burada sınıfın bütün üye fonksiyonları dışarıdan kullanılabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
#include <iostream>
2024-06-09 22:42:45 +03:00
using namespace std;
2024-06-09 22:42:45 +03:00
class __declspec(dllexport) Sample {
public:
Sample(int a);
void foo() const;
void disp() const;
void bar() const;
private:
int m_a;
};
2024-06-09 22:42:45 +03:00
Sample::Sample(int a)
{
2024-06-09 22:42:45 +03:00
m_a = a;
}
2024-06-09 22:42:45 +03:00
void Sample::foo() const
{
cout << "foo" << endl;
}
2024-06-09 22:42:45 +03:00
void Sample::disp() const
{
cout << m_a << endl;
}
2024-06-09 22:42:45 +03:00
void Sample::bar() const
{
2024-06-09 22:42:45 +03:00
cout << "bar" << endl;
}
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows'ta bir DLL içerisindeki fonksiyonlar dışarıdan kullanılırken yalnızca prototip yazılması aslında yeterlidir. Benzer biçimde
bir DLL içerisindeki global nesneler dışarıdan kullanılırken yalnızca extern bildirimi yeterlidir. Ancak ilgili fonksiyonun ve global
nesnenin DLL içerisinde olduğu derleyiciye bildirilirse derleyici daha etkin kod üretebilmektedir. (Bu konudaki ayrıntılar "Windows Sistem
Programlama Kursu" içerisinde ele alınmaktadır.) İşte derleyiciye ilgili fonksiyonun ya da global nesnenin bir DLL içeirisinde olduğunu
anlatabilmek için fonksiyonun prototipinin önüne __declspec(dllimport) belirleyicisi getirilir. Örneğin:
2024-06-09 22:42:45 +03:00
#ifndef MYUTIL_H_
#define MYUTIL_H_
/* Function Prototypes */
__declspec(dllimport) double add(double a, double b);
__declspec(dllimport) double sub(double a, double b);
__declspec(dllimport) double multiply(double a, double b);
__declspec(dllimport) double divide(double a, double b);
__declspec(dllimport) void foo(void);
/* extern declarations */
__declspec(dllimport) extern int g_x;
__declspec(dllimport) extern int g_y;
#endif
Burada DLL içerisindeki fonksiyonların prototiplerinin başına __declspec(dllimport) belirleyicisi getirilmiştir.
Ancak DLL içerisinden export edilmiş fonksiyonlar ve global nesneler kullanılırken ayrıca bağlama aşamasında bağlayıcının hangi DLL'den
hangi sembollerin kullanıldığını bilmesi gerekir. İşte DLL kullanan programlar derlenirken bağlama aşamsında "DLL'in import kütüphanesi"
denilen bir kütüphanenin de bulundurulması gerekmektedir. DLL'in import kütüphanesi DLL yaratılırken bağlayıcı tarafından zaten
yaratılmaktadır. Örneğin:
cl /Fe:myutil.dll /LD a.c b.c
Burada biz "myutil.dll" isimli dinamik kütüphaneyi yaratmak istemekteyiz. İşte bu kütüphane yaratılırken aynı zamanda DLL'in import
kütüphanesi de yaratılmaktadır. DLL'in import kütüphanesinin uzantısı ".lib" biçimindedir. Yukarıdaki derleme işleminin sonucunda aynı
zamanda "myutil.lib" isimli DLL'in import kütüphanesi de oluşturulmaktadır. Her ne kadar import kütüphanelerinin uzantıları da ".lib"
biçiminde olsa da bu dosyalar statik kütüphane dosyaları değildir. Bu dosya içerisinde yalnızca "DLL içerisindeki fonksiyonlara ilişkin
bazı bilgiler" vardır. İşte bu dosya dinamik kütüphaneyi kullanan program bağlanırken bağlama aşamasında kulanılmalıdır. Örneğin:
cl app.c myutil.lib
Windows'ta bir DLL'i kullanabilmek için yalnızca DLL dosyası değil aynı zamanda o DLL'in import kütüphanesinin de elimizde bulunuyor olması
gerekir. DLL'in import kütüphanesi yalnızca bağlama aşamasında kullanılmaktadır. Programın hedef makineye konuşlandırılması sırasında import
kütüphanesinin hedef makineye kopyalanmasına gerek yoktur.
Aşağıdaki örnekte "a.c" ve "b.c" dosyalarından "myutil.dll" dosyası oluşturulmuştur. Sonra bu DLL'i kullanan "app.c" programı derlenerek
çalıştırılmıştır. Bu işlemleri komut satırında şöyle yapabilirsiniz:
cl /OUT:myutil.dll /LD a.c b.c
cl app.c myutil.lib
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* app.c */
#include <stdio.h>
__declspec(dllexport) int g_x;
__declspec(dllexport) double add(double a, double b)
{
return a + b;
}
2024-06-09 22:42:45 +03:00
__declspec(dllexport) double sub(double a, double b)
{
2024-06-09 22:42:45 +03:00
return a - b;
}
2024-06-09 22:42:45 +03:00
__declspec(dllexport) double multiply(double a, double b)
{
return a * b;
}
2024-06-09 22:42:45 +03:00
__declspec(dllexport) double divide(double a, double b)
{
return a / b;
}
/* b.c */
#include <stdio.h>
2024-06-09 22:42:45 +03:00
__declspec(dllexport) int g_y;
2024-06-09 22:42:45 +03:00
__declspec(dllexport) void foo(void)
{
printf("foo\n");
}
2024-06-09 22:42:45 +03:00
void bar(void)
{
2024-06-09 22:42:45 +03:00
printf("bar\n");
}
2024-06-09 22:42:45 +03:00
/* app.c */
2024-06-09 22:42:45 +03:00
#ifndef MYUTIL_H_
#define MYUTIL_H_
2024-06-09 22:42:45 +03:00
/* Function Prototypes */
2024-06-09 22:42:45 +03:00
__declspec(dllimport) double add(double a, double b);
__declspec(dllimport) double sub(double a, double b);
__declspec(dllimport) double multiply(double a, double b);
__declspec(dllimport) double divide(double a, double b);
__declspec(dllimport) void foo(void);
2024-06-09 22:42:45 +03:00
/* extern declarations */
2024-06-09 22:42:45 +03:00
__declspec(dllimport) extern int g_x;
__declspec(dllimport) extern int g_y;
2024-06-09 22:42:45 +03:00
#endif
2024-06-09 22:42:45 +03:00
/* app.c */
2024-06-09 22:42:45 +03:00
#include <stdio.h>
#include "myutil.h"
2024-06-09 22:42:45 +03:00
int main(void)
{
double result;
2024-06-09 22:42:45 +03:00
result = add(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = sub(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = multiply(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = divide(10, 20);
printf("%f\n", result);
foo();
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Kütüphaneler için birer başlık dosyası oluşturmanın iyi bir teknik olduğunu belirtmiştik. Bu başlık dosyası hem kütüphane derlenirken hem de
kütüphane kullanılırken include edilir. Pekiyi bu başlık dosyasının içerisinde fonksiyon prototiplerinin önünde __declspec(dllexport) belirleyicisi
mi yoksa __declspec(dllimport) belirleyicisi mi bulunmalıdır? İşte kütüphanenin kendisi derlenirken __declspec(dllexport) belirleyicisi
kütüphaneyi kullanan kodlar derlenirken __declspec(dllimport) belirleyicisi bulunmalıdır. Bu işlem basit bir sembolik sabite dayalı olarak
gerçekleştirilebilir. Örneğin:
#ifdef DLLBUILD
#define DLLSPEC __declspec(dllexport)
#else
#define DLLSPEC __declspec(dllimport)
#endif
Burada DLLBUILD isimli sembolik sabit define edilmişse DLLSPEC makrosu yerine __declspec(dllexport) belirleyicisi define edilmemişse
DLLSPEC makrosu yerine __declspec(dllimport) belirleyicisi yerleştirilecektir. Bu durumda fonksiyon prototipleri şöyle belirtilebilir:
DLLSPEC double add(double a, double b);
DLLSPEC double sub(double a, double b);
DLLSPEC double multiply(double a, double b);
DLLSPEC double divide(double a, double b);
DLLSPEC void foo(void);
İşte eğer DLL'in kendisi oluşturulacaksa bu başlık dosyasının include edildiği yerin başına DLLBUILD sembolik sabiti define edilir, eğer
DLL kullanılacaksa bu sembolik sabit define edilmez. Böylece tek bir başlık dosyası hem DLL'deki dosyalar tarafından hem de o DLL'i kullanan
dosyalar tarafından include edilebilir.
Aşağıda buna bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* myutil.h */
2024-06-09 22:42:45 +03:00
#ifndef MYUTIL_H_
#define MYUTIL_H_
2024-06-09 22:42:45 +03:00
#ifdef DLLBUILD
#define DLLSPEC __declspec(dllexport)
#else
#define DLLSPEC __declspec(dllimport)
#endif
2024-06-09 22:42:45 +03:00
/* Function Prototypes */
2024-06-09 22:42:45 +03:00
DLLSPEC double add(double a, double b);
DLLSPEC double sub(double a, double b);
DLLSPEC double multiply(double a, double b);
DLLSPEC double divide(double a, double b);
DLLSPEC void foo(void);
2024-06-09 22:42:45 +03:00
/* extern declarations */
2024-06-09 22:42:45 +03:00
DLLSPEC extern int g_x;
DLLSPEC extern int g_y;
2024-06-09 22:42:45 +03:00
#endif
2024-06-09 22:42:45 +03:00
/* a.c */
2024-06-09 22:42:45 +03:00
#define DLLBUILD
2024-06-09 22:42:45 +03:00
#include <stdio.h>
#include "myutil.h"
2024-06-09 22:42:45 +03:00
int g_x;
2024-06-09 22:42:45 +03:00
double add(double a, double b)
{
return a + b;
}
2024-06-09 22:42:45 +03:00
double sub(double a, double b)
{
2024-06-09 22:42:45 +03:00
return a - b;
}
2024-06-09 22:42:45 +03:00
double multiply(double a, double b)
{
return a * b;
}
2024-06-09 22:42:45 +03:00
double divide(double a, double b)
{
2024-06-09 22:42:45 +03:00
return a / b;
}
2024-06-09 22:42:45 +03:00
/* b.c */
2024-06-09 22:42:45 +03:00
#define DLLBUILD
2024-06-09 22:42:45 +03:00
#include <stdio.h>
#include "myutil.h"
int g_y;
void foo(void)
{
2024-06-09 22:42:45 +03:00
printf("foo\n");
}
2024-06-09 22:42:45 +03:00
void bar(void)
{
printf("bar\n");
}
2024-06-09 22:42:45 +03:00
/* app.c */
#include <stdio.h>
2024-06-09 22:42:45 +03:00
#include "myutil.h"
int main(void)
{
2024-06-09 22:42:45 +03:00
double result;
2024-06-09 22:42:45 +03:00
result = add(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = sub(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = multiply(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = divide(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
foo();
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir DLL'i kullanan bir program çalıştırılmak istendiğinde işletim sistemi o programın kullandığı DLL'i sırasıyla belirli bazı dizinlerde
aramaktadır. Çalıştırılabilen dosyanın içerisine kullanılan DLL'in yalnızca ismi yazılmaktadır. Onun bulunduğu yer yazılmamaktadır. Windows'ta
çalıştırılabilen dosyanın kullandığı DLL'ler sırasıyla şu dizinlerde aranmaktadır:
2024-06-09 22:42:45 +03:00
1) Çalıştırılabilen program dosyasının bulunduğu dizin
2) Windows\System32 dizini
3) Window\System dizini
4) Windows dizininin kendisi
5) Programı çalıştırmak isteyen prosesin (yani CreateProcess uygulayan prosesin) çalışma dizini
6) PATH çevre değişkeni ile belirtilen dizinlerde sırasıyla
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Dinamik kütüphaneler Windows sistemlerinde IDE'lerle daha kolay yaratılabilirler. Visual Studio IDE'sinde proje oluşturulurken proje türü
olarak "Dynamic Link Library" seçilirse bu proje build edildiğinde DLL dosyası elde edilecektir. Ancak bu proje seçeneği projeye içinde
yalnızca DllMain fonksiyonu bulunan bir C++ dosyası barındırmaktadır. Tabii boş olarak da bir DLL projesi oluşturulabilir. Bunun için
boş bir console projesi yaratılır. İçerisine kaynak dosyalar yerleştirilir. Sonra proje ayarlarından "General/Build Type" seçeneği "Application"
yerine "Dynamic Link Library" olarak seçilir. Artık proje build edildiğinde EXE dosyası yerine DLL dosyası dosyası oluşturulacaktır.
Tabii DLL'in import kütüphanesi de aynı dizinde yaratılmış olacaktır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde dinamik kütüphane oluşturmak için önce dinamik kütüphane oluşturulacak ".c" dosyaları "position indepenedent code"
tekniği ile derlenmelidir. "Position Independent Code" ilgili dinamik kütüphanenin yüklenme yerinden bağımsız çalışabilmesini sağşamaktadır.
Windows bu tekniği tercih etmemiştir. Windows'un yükleyicisi "relocation" işlemi ile DLL'leri farklı yerlere yükleyebilmektedir. Position
Independent Code tekniği ile derlenmiş olan dosyalar "-shared" bağlama seçeneği ile bağlanırsa dinamik kütüphane oluşturulabilmektedir.
UNIX/Linux sistemlerinde kütüphane içerisindeki global sembollerin hepsi otomatik olarak export edilmektedir. Dolayısıyla bu sistemlerde
Windows sistemlerinde olduğu gibi __declspec(dllexport) ve __declspec(dllimport) biçiminde bildirimler yoktur. Tabii bu konuda da bazı
ayrıntılar bulunmaktadır.
2024-06-09 22:42:45 +03:00
gcc ve clang derleyicilerinde bir dosyanın "konumdan bağımsız" biçimde derlenmesi için "-fPIC" seçeneğinin kullanılması gerekir. Örneğin:
2024-06-09 22:42:45 +03:00
$ gcc -c -fPIC a.C
$ gcc -c -fPIC b.C
2024-06-09 22:42:45 +03:00
Burada "a.o" ve "b.o" dosyaları oluşturulacktır. Ayrıca yukarıda da belirttiğimiz Bağlama işleminde "-shared" seçeneğinin kullanılması
gerekmektedir. Örneğin:
2024-06-09 22:42:45 +03:00
$ gcc -o libmyutil.so -shared a.o b.o
2024-06-09 22:42:45 +03:00
Aslında bu iki işlem tek hamlede de aşağıdaki gibi yapılabilir:
2024-06-09 22:42:45 +03:00
$ gcc -o libmyutil.so -shared -fPIC a.c b.c
2024-06-09 22:42:45 +03:00
Yine UNIX/Linux sistemlerinde dinamik kütüphane dosyaları başına "lib" öneki getirilerek isimlendirilmelidir.
Aşağıda dinamik kütüphaneyi oluşturan "a.c" ve "b.c" dosyaları verilmiştir. Dinamik kütüphaneyi yukarıda belirttiğimiz biçimde oluşturmayı
deneyiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* a.c */
#include <stdio.h>
int g_x;
double add(double a, double b)
{
return a + b;
}
2024-06-09 22:42:45 +03:00
double sub(double a, double b)
{
2024-06-09 22:42:45 +03:00
return a - b;
}
2024-06-09 22:42:45 +03:00
double multiply(double a, double b)
{
return a * b;
}
2024-06-09 22:42:45 +03:00
double divide(double a, double b)
{
return a / b;
}
2024-06-09 22:42:45 +03:00
/* b.c */
2024-06-09 22:42:45 +03:00
#include <stdio.h>
2024-06-09 22:42:45 +03:00
int g_y;
2024-06-09 22:42:45 +03:00
void foo(void)
{
printf("foo\n");
}
2024-06-09 22:42:45 +03:00
void bar(void)
{
printf("bar\n");
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
UNIX/Linux sistemlerinde "dinamik kütüphanelerin import kütüphanesi" denilen kütüphaneleri yoktur. Dinamik kütüphane kullanan programlar
doğrudan bu dinamik kütüphane dosyasının kendisini bağlama aşamasında kullanmaktadır. Örneğin "app.c" programı "libmyutil.so" dinamik
kütüphanesini kullanıyor olsun. Bu "app.c" programının derlenmesi şöyle yapılmaktadır:
gcc -o app app.c libmyutil.so
Aşağıdaki "app.c" programını bu biçimde derleyiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* myutil.h */
2024-06-09 22:42:45 +03:00
#ifndef MYUTIL_H_
#define MYUTIL_H_
2024-06-09 22:42:45 +03:00
/* Function Prototypes */
2024-06-09 22:42:45 +03:00
double add(double a, double b);
double sub(double a, double b);
double multiply(double a, double b);
double divide(double a, double b);
void foo(void);
void bar(void);
2024-06-09 22:42:45 +03:00
/* extern declarations */
2024-06-09 22:42:45 +03:00
extern int g_x;
extern int g_y;
2024-06-09 22:42:45 +03:00
#endif
2024-06-09 22:42:45 +03:00
/* app.c */
2024-06-09 22:42:45 +03:00
#include <stdio.h>
#include "myutil.h"
2024-06-09 22:42:45 +03:00
int main(void)
{
2024-06-09 22:42:45 +03:00
double result;
2024-06-09 22:42:45 +03:00
result = add(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = sub(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
result = multiply(10, 20);
printf("%f\n", result);
result = divide(10, 20);
printf("%f\n", result);
foo();
bar();
return 0;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
UNIX/Linux sistemlerinde yine çalıştırılabilen dosyanın içerisine yalnızca dinamik kullanılan dinamik kütüphanelerin isimleri yazılmaktadır.
Dinamik kütüphaneler yine bu sistemlerde de belli yerlerde aranmaktadır. Ancak bu sistemlerde dinamik kütüphaneler çalıştırılabilen dosyanın
bulunduğu dizinde ya da onu çalıştıran prosesin çalışma dizininde aranmamaktadır. Bu nedenle örneğin Linux sistemlerinde dinamik kütüphane
kullanan bir programla dinamik kütüphanenin kendisi aynı dizinde bulunuyor olsa bile program çalıştırıldığında dinamik kütüphane bulunamayacaktır.
UNIX/Linux sistemlerinde dinamik kütüphanelerin aranma prosedürlerine ilişkin bazıı ayrıntılar vardır. Biz burada bu ayrıntılar üzerinde
durmayacağız. Ancak kabaca arama için üç önemli adımı şöyle belirtebiliriz:
2024-06-09 22:42:45 +03:00
1) Programı çalıştıran (yani exec yapan) prosesin LD_LIBRARY_PATH çevre değişkeni ile belirtilen dizinlerinde tek tek arama yapılmaktadır.
Bu eçvre değişkeninin değeri ':' karakterleriyle ayrılan dizinler biçiminde olabilir. Örneğin:
2024-06-09 22:42:45 +03:00
$ export LD_LIBRARY_PATH=/home/kaan:/home/kaan/Study:.
2024-06-09 22:42:45 +03:00
Burada dinamik kütüphaneler sırasıyla "/home/kaan" dizininde, "/home/kaan/Study" dizininde ve o anda exec yapan prosesin çalışma dizininde
aranacaktır. Tabii kabuk üzerinde çalıtırmayı aşağıdaki gibi de yapabiliriz:
$ LD_LIBRARY_PATH=. ./app
2) Çalıştırılacak programın ".dynamic" bölümünde DT_RUNPATH özelliği varsa oradaki dizinde aranmaktadır.
3) "/lib" ve "/usr/lib" dizinlerine bakılır. Bazı 64 bit Linux dağıtımlarında "/lib64" ve "/usr/lib64" dizinlerine de bakılmaktadır.
Eğer bu sistemlerde oluşturduğumuz dinamik kütüphaneler başka programlar tarafından da kullanılıyorsa onun yerleştirilmesi gereken en doğal
"/usr/lib" dizinidir. "/lib" dizini daha aşağı seviyeli işletim sistemi tarafıdan kullanılan dinamik kütüphanelere ayrılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
94. Ders 08/06/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Dinamik kütüphaneler programın çalışma zamanı sırasında programcının istediği bir noktada programcı tarafından da yüklenebilmektedir.
Bu duruma "dinamik kütüphanelerin dinamik yüklenmesi" denilmektedir. Dinamik yükleme özelliği hem Windows sistemlerinde hem de UNIX/Linux
ve macOS sistemlerinde bulunmaktadır. Biz önce Windows sistemlerinde sonra da UNIX/linux sistemlerinde bunun yapılacağını göreceğiz. macOS
sistemlerinde işlemler tamamen UNIX/Linux sistemlerinde olduğu gibi yapılmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows sistemlerinde dinamik kütüphanelerin dinamaik biçimde yüklenerek kullanılması sırasıyla şu aşamalardan geçilerek yapılmaktadır:
2024-06-09 22:42:45 +03:00
1) Önce LoadLibrary API fonksiyonuyla DLLprosesin sanal bellek alanına yüklenir. LoadLibrary API fonksiyonunun prototipi şöyledir.
şöyledir:
2024-06-09 22:42:45 +03:00
HMODULE LoadLibraryA(
LPCSTR lpLibFileName
);
Fonksiyon yüklenecek olan DLL'in yol ifadesini parametre olaraj almaktadır. Eğer yol ifadesinde en az bir '\' karakteri varsa DLL yalnızca
o dizinde aranmaktadır. Eğer yol ifadesinde hiçbir '\' karakteri yoksa bu durumda DLL daha önce açıkladığımız gibi bazı dizinlerde sırasıyla
aranır. Bu fonksiyon dinamik kütüphaneyi yükleyerek yüklenen sanal bellek adresine geri dönmektedir. Geri dönüş değeri HMODULE türündendir.
Bu tür de aslında void * olarak typedef edilmiştir. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir. Örneğin:
2024-06-09 22:42:45 +03:00
HMODULE hModule;
2024-06-09 22:42:45 +03:00
if ((hModule = LoadLibrary("MyUtil.dll")) == NULL)
ExitSys("LoadLibrary");
2024-06-09 22:42:45 +03:00
2) DLL içerisinden çağrılacak fonksiyonun ya da kullanılacak global değişkenin adresi GetProcAddress fonksiyonuyla elde edilir. Fonksiyonun
prototipi şöyledir:
2024-06-09 22:42:45 +03:00
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
Fonksiyonun birinci parametresi LoadLibrary fonksiyonundan elde edilen modül yükleme adresini, ikinci parametresi ise adresi elde edlecek
fonksiyonun ya da global değişkenin ismini almaktadır. Fonksiyon başarı durumunda ilgili fonksiyonun ya da global değişkenin adresine
başarısızlık durumunda NULL adrese gerei dönmektedir. Fonksiyonun geri dönüş değeri olan FARPROC 32 bit Windows sistemlerinde aşağıdaki
typedef edilmiştir:
2024-06-09 22:42:45 +03:00
typedef int (WINAPI *FARPROC)();
2024-06-09 22:42:45 +03:00
64 bit Windows sistemlerinde ise şöyle typedef edilmiştir:
2024-06-09 22:42:45 +03:00
typedef INT_PTR (WINAPI *FARPROC)();
2024-06-09 22:42:45 +03:00
Bu geri dönüş değeri genel bir tür biçiminde bulundurulmuştur. Uygun fonksiyon türüne dönüştürrülmelidir. Örneğin:
2024-06-09 22:42:45 +03:00
typedef double (*PROCADDR)(double, double);
...
PROCADDR padd;
2024-06-09 22:42:45 +03:00
if ((padd = (PROCADDR)GetProcAddress(hModule, "add")) == NULL)
ExitSys("GetProcAddress");
2024-06-09 22:42:45 +03:00
GetProcAddress fonksiyonunda bir noktaya dikkat edilmesi gerekir. Derleyiciler C'deki değişken isimlerini amaç dosyaya dekore ederek
yazabilmektedir. Bizim GetProcAddress fonksiyonunda bu dekore edilmiş isimleri kullanmamız gerekmektedir. Microsoft C derleyicilerinde
cdecl çağırma biçiminde sembollerin başına '_' karakteri getirerek bir dekorasyon uygulamamaktadır. Fakat Microsoft bağlayıcıları sembolleri
DLL'in export tablosuna yazarken bu baştaki '_' karakterlerini atmaktadır. C++'ta farklı parametrik yapılara ilişkin aynı isimli fonksiyonlar
bulunabileceği için C++ derleyicilerinin hepsi meecburen bir isim dekorasyonu uygulamaktadır. İşte programcının export sembol isimlerin emin
olabilmesi için dumpbin programı ile DLL dosyasının export tablosuna bakabilir. Örneğin:
2024-06-09 22:42:45 +03:00
C:\Dropbox\Shared\Kurslar\SysProg-1\Src\Libraries\Dynamic>DUMPBIN /EXPORTS myutil.dll
Microsoft (R) COFF/PE Dumper Version 14.39.33523.0
Copyright (C) Microsoft Corporation. All rights reserved.
2024-06-09 22:42:45 +03:00
Dump of file myutil.dll
2024-06-09 22:42:45 +03:00
File Type: DLL
2024-06-09 22:42:45 +03:00
Section contains the following exports for myutil.dll
2024-06-09 22:42:45 +03:00
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
7 number of functions
7 number of names
2024-06-09 22:42:45 +03:00
ordinal hint RVA name
2024-06-09 22:42:45 +03:00
1 0 00001000 add
2 1 00001060 divide
3 2 00001080 foo
4 3 0001B39C g_x
5 4 0001B398 g_y
6 5 00001040 multiply
7 6 00001020 sub
2024-06-09 22:42:45 +03:00
Summary
2024-06-09 22:42:45 +03:00
2000 .data
7000 .rdata
1000 .reloc
12000 .text
2024-06-09 22:42:45 +03:00
Ancak eğer biz bir C++ dosyasını derleyip ondan dinmaik kütüphane yapmış olsaydık sembol isimleri oldukça farklılaşacaktı. Örneğin:
2024-06-09 22:42:45 +03:00
C:\Dropbox\Shared\Kurslar\SysProg-1\Src\Libraries\Dynamic>DUMPBIN /EXPORTS myutilcpp.dll
Microsoft (R) COFF/PE Dumper Version 14.39.33523.0
Copyright (C) Microsoft Corporation. All rights reserved.
2024-06-09 22:42:45 +03:00
Dump of file myutilcpp.dll
2024-06-09 22:42:45 +03:00
File Type: DLL
2024-06-09 22:42:45 +03:00
Section contains the following exports for myutilcpp.dll
2024-06-09 22:42:45 +03:00
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
7 number of functions
7 number of names
2024-06-09 22:42:45 +03:00
ordinal hint RVA name
2024-06-09 22:42:45 +03:00
1 0 00001000 ?add@@YANNN@Z
2 1 00001060 ?divide@@YANNN@Z
3 2 00001080 ?foo@@YAXXZ
4 3 0001A968 ?g_x@@3HA
5 4 0001A96C ?g_y@@3HA
6 5 00001040 ?multiply@@YANNN@Z
7 6 00001020 ?sub@@YANNN@Z
2024-06-09 22:42:45 +03:00
Summary
2024-06-09 22:42:45 +03:00
2000 .data
7000 .rdata
1000 .reloc
12000 .text
2024-06-09 22:42:45 +03:00
3) Artık adresi elde edilmiş olan fonksiyon çağrılabilir, global değişken kullanılabilir. Örneğin:
2024-06-09 22:42:45 +03:00
result = padd(10, 20);
2024-06-09 22:42:45 +03:00
4) Dinamik kütüphanenin kullanımı bittikten sonra kütüphane FreeLibrary API fonksiyonu ile prosesin adres alanından boşaltılabilir.
FreeLibrary fonksiyonunun prototipi şöyledir:
2024-06-09 22:42:45 +03:00
BOOL FreeLibrary(
HMODULE hLibModule
);
2024-06-09 22:42:45 +03:00
Fonksiyon modülün yüklenme adresini parametre olarak alır, başarı durumunda sıfır dışı bir değere başarısızlık durumunda sıfır değerine
geri döner. Tabii proses sonlaadığında bütün dinamik kütüphaneler zaten adres alanından boşaltılmaktadır.
Aşağıda Windows sistemlerinde dinamik kütüphanenin dinamik yüklenmesine ilişkin bir örnek verilmiştir. Bu örnekteki "myutil.dll" dosyasının
proje dizininde bulunduğundan emin olunuz. DLL'in derlenmesini komut satırından şöyle yapabilirsiniz.
cl /Fe:myutil.dll a.c b.c /LD
-------------------------------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void ExitSys(LPCSTR lpszMsg);
typedef double (*PROCADDR)(double, double);
int main(void)
{
2024-06-09 22:42:45 +03:00
HMODULE hModule;
PROCADDR padd, psub, pmultiply, pdivide;
double result;
if ((hModule = LoadLibrary("myutilcpp.dll")) == NULL)
ExitSys("LoadLibrary");
2024-06-09 22:42:45 +03:00
if ((padd = (PROCADDR)GetProcAddress(hModule, "?add@@YANNN@Z")) == NULL)
ExitSys("GetProcAddress");
result = padd(10, 20);
printf("%f\n", result);
if ((psub = (PROCADDR)GetProcAddress(hModule, "sub")) == NULL)
ExitSys("GetProcAddress");
result = psub(10, 20);
printf("%f\n", result);
if ((pmultiply = (PROCADDR)GetProcAddress(hModule, "multiply")) == NULL)
ExitSys("GetProcAddress");
result = pmultiply(10, 20);
printf("%f\n", result);
if ((pdivide = (PROCADDR)GetProcAddress(hModule, "divide")) == NULL)
ExitSys("GetProcAddress");
result = pdivide(10, 20);
printf("%f\n", result);
FreeLibrary(hModule);
return 0;
}
2024-06-09 22:42:45 +03:00
void ExitSys(LPCSTR lpszMsg)
{
2024-06-09 22:42:45 +03:00
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
2024-06-09 22:42:45 +03:00
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
2024-06-09 22:42:45 +03:00
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/* myutil.h */
2024-06-09 22:42:45 +03:00
#ifndef MYUTIL_H_
#define MYUTIL_H_
2024-06-09 22:42:45 +03:00
#ifdef DLLBUILD
#define DLLSPEC __declspec(dllexport)
#else
#define DLLSPEC __declspec(dllimport)
#endif
2024-06-09 22:42:45 +03:00
/* Function Prototypes */
DLLSPEC double add(double a, double b);
DLLSPEC double sub(double a, double b);
DLLSPEC double multiply(double a, double b);
DLLSPEC double divide(double a, double b);
/* extern declarations */
DLLSPEC extern int g_x;
#endif
/* a.c */
#define DLLBUILD
#include <stdio.h>
#include "myutil.h"
int g_x;
double add(double a, double b)
{
return a + b;
}
2024-06-09 22:42:45 +03:00
double sub(double a, double b)
{
2024-06-09 22:42:45 +03:00
return a - b;
}
2024-06-09 22:42:45 +03:00
double multiply(double a, double b)
{
return a * b;
}
2024-06-09 22:42:45 +03:00
double divide(double a, double b)
{
2024-06-09 22:42:45 +03:00
return a / b;
}
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Dinamik kütüphanelerin UNIX/Linux sistemlerinde dinamik yüklenmesi aslında Windows sistemlerindekine benzemektedir. İşlemler sırasıyla
şöyle yürütülür:
1) Önce dinamik kütüphane ldopen fonksiyonuyla adres alanına yüklenir. Bu fonksiyonu Windows sistemlerindeki LoadLibrary fonksiyonuna
benzetebilirsiniz. Bu fonksiyon yine dinamik kütüphanenin bellekteki yükleme adresine geri dönmektedir. Fonksiyonun prototipi şöyledir:
2024-06-09 22:42:45 +03:00
#include <dlfcn.h>
2024-06-09 22:42:45 +03:00
void *dlopen(const char *filename, int flags);
Fonksiyonun birinci parametresi yüklenecek dinamik kütüphane dosyasının yol ifadesini belirtmektedir. Burada yol ifadesinde hiçbir '/'
karakteri kullanılmazsa dosya yukarıda belirtilen dizinlerde sırasıyla aranmaktadır. Ancak buradaki yol ifadesinde en az bir '/' kullanılırsa
dosya yalnızca o yol ifadesinde belirtilen dizinde aranır. İkinci parametre sembol çözümlemesinin ne zaman yapılacağına ilişkin bayraklardan
oluşmaktadır. Bu parametre RTLD_LAZY ya da RTLD_NOW değerlerinden biri biçiminde girilebilir. RTLD_LAZY sembol çözümlemesinin başvuru (yani
fonksiyonun çağrılması ya da global değişkenin kullanılması sırasında) sırasında yapılacağını RTLD_NOW ise yükleme sırasında yapılacağını
belirtmektedir. Diğer bayraklar için dokümanalara başvurulabilir. Fonksiyon başarı durumunda yükleme adresine, başarısızlık durumunda NULL
adrese geri döner. Başarısızlık nedeni için errno değişkeni set edilmez. Başarısızlık nedeni yazısal olarak dlerror fonksiyonuyla elde
edilmelidir. dlerror fonksiyonun prototipi şöyledir:
char *dlerror(void);
Fonksiyon statik bir biçimde tahsis edilmiş olan hata yazısının adresine geri dönmektedir. Örneğin:
void *soaddr;
...
if ((soaddr = dlopen("./libmyutil.so", RTLD_LAZY)) == NULL) {
fprintf(stderr, "dlopen: %s\n", dlerror());
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
Tıpkı Windows sistemlerinde olduğu gibi dlopen fonksiyonu dinamik kütüphanenin yol ifadesi belirtilirken eğer hiç '/' karakteri kullanılmazsa
bu durumda dosya Linux sistemlerindeki yukarıda belirttiğimiz dizinlerde aranmaktadır. (Linux sistemlerinde arama sırasında çalışma dizinine
bakılmadığını anımsayınız). Eğer yol ifadesinde en az bir tane '/' karakteri kullanılmışsa dosya belirtilen yol ifadesinde aranmaktadır.
2) dlsym fonksiyonuyla dinamik kütüpahane içerisindeki herhangi bir fonksiyon ya da nesnenin adresi elde edilebilmektedir. dlsym fonksiyonun
prototipi şöyledir:
#include <dlfcn.h>
void *dlsym(void *handle, const char *symbol);
Fonksiyonun birinci parametresi dlopen fonksiyonundan elde edilen adresi, ikinci parametresi ise adresi elde edilecek fonksiyon ya da global
nesnenin ismini belirtmektedir. Fonksiyon başarı dıurumunda ilgili nesnenin adresine başarısızlık durumunda NULL adrese geri dönmektedir.
Yine başarısızlık nedeni dlerror fonksiyonuyla elde edilmelidir. C'de data adreslerinden fonksiyon adreslerine, fonksiyon adreslerinden data
adreslerine tür dönüştürme operatör ile bile dönüştürmenin geçerli olmadığına dikkat ediniz. (Ancak derleyicilerin çoğu buna izin vermektedir.)
Anımsayanacağınız gibi void * türü bir data adresi kabul edilmektedir. Örneğin:
typedef double (*PROCADDR)(double);
...
PROCADDR padd;
...
if ((*(void **)&padd = dlsym(soaddr, "add")) == NULL) {
fprintf(stderr, "dlsym: %s\n", dlerror());
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
dlsym fonksiyonunu Windows sistemlerindeki GeProcAddress fonksiyonuna benzetebilirsiniz.
2024-06-09 22:42:45 +03:00
3) Artık adresi elde edilmiş olan fonksiyon çağrılabilir, global değişken kullanılabilir.
4) Dinamik kütüphanenin kullanımı bittiğinde kütüphane dlclose fonksiyonuyla boşaltılır. Fonksiyonun prototipi şöyledir:
#include <dlfcn.h>
int dlclose(void *handle);
Fonksiyon parametre olarak dinamik kütüphanenin yükleme adres,n, alıp onu adres alanından yok etmektedir. Fonksiyon başarı durumunda sıfır
değrine başarısızlık durumunda sıfır dışı bir değere geri dönemektedir. Yine başarısızlığın nedeni dlerror fonksiyonuyla yazdırılabilir.
Örneğin:
dlclose(soaddr);
Bütün bu fonksiyonlar "libdl.so" kütüphanesi içerisinde bulunmaktadır. Bu nedenle derleme yaparken komut satırında "-ldl" argümanını
bulundurunuz.
Örnek bir kullanım aşağıda verilmiştir. Derlemeleri aşağıdaki gibi yapabilirsiniz:
$ gcc -fPIC -o libmyutil.so libmyutil.c -shared
$ gcc -o app app.c -ldl
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
void exit_sys(const char *msg);
typedef double (*PROCADDR)(double, double);
int main(void)
{
2024-06-09 22:42:45 +03:00
void *soaddr;
PROCADDR padd, psub, pmultiply, pdivide;
double result;
2024-06-09 22:42:45 +03:00
if ((soaddr = dlopen("./libmyutil.so", RTLD_LAZY)) == NULL) {
fprintf(stderr, "dlopen: %s\n", dlerror());
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
if ((*(void **)&padd = dlsym(soaddr, "add")) == NULL) {
fprintf(stderr, "dlsym: %s\n", dlerror());
exit(EXIT_FAILURE);
}
result = padd(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
if ((*(void **)&psub = dlsym(soaddr, "sub")) == NULL) {
fprintf(stderr, "dlsym: %s\n", dlerror());
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
result = psub(10, 20);
printf("%f\n", result);
if ((*(void **)&pmultiply = dlsym(soaddr, "multiply")) == NULL) {
fprintf(stderr, "dlsym: %s\n", dlerror());
exit(EXIT_FAILURE);
}
result = pmultiply(10, 20);
printf("%f\n", result);
if ((*(void **)&pdivide = dlsym(soaddr, "divide")) == NULL) {
fprintf(stderr, "dlsym: %s\n", dlerror());
exit(EXIT_FAILURE);
}
result = pdivide(10, 20);
printf("%f\n", result);
2024-06-09 22:42:45 +03:00
return 0;
}
2024-06-09 22:42:45 +03:00
void exit_sys(const char *msg)
{
2024-06-09 22:42:45 +03:00
perror(msg);
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/* libmyutil.c */
#include <stdio.h>
double add(double a, double b)
{
2024-06-09 22:42:45 +03:00
return a + b;
}
2024-06-09 22:42:45 +03:00
double sub(double a, double b)
{
return a - b;
}
2024-06-09 22:42:45 +03:00
double multiply(double a, double b)
{
return a * b;
}
2024-06-09 22:42:45 +03:00
double divide(double a, double b)
{
return a / b;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Kursumuzun bu bölümünde IP prokol ailesi ile ağa bağlı birimler arasında haberleşmelerin nasıl yapıldığı üzerinde duracağız. Bu bağlamda
TCP ve UDP protokollerini inceleyeceğiz ve bu protokolleri kullanarak soket arayüzü ile temel programların nasıl yazıldığnııklayacağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Farklı makinelerin prosesleri arasında haberleşme (yani bir ağ içerisinde haberleşme), aynı makinenin prosesleri arasındaki haberleşmeye
göre daha karmaşık unsurlar içermektedir. Çünkü burada ilgili işletim sisteminin dışında pek çok belirlemelerin önceden yapılmış olması
gerekir. İşte ağ haberleşmesinde önceden belirlenmiş kurallar topluluğuna "protokol" denilmektedir. Ağ haberleşmesi için tarihsel süreç
içerisinde pek çok protokol ailesi gerçekletirilmiştir. Bunların bazıları büyük şirketlerin kontrolü altındadır ve hala kullanılmaktadır.
Ancak açık bir protokol ailesi olan "IP protokol ailesi" günümüzde farklı makinelerin prosesleri arasındaki haberleşmede hemen her zaman
tercih edilen protokol ailesidir.
Protokol ailesi (protocol family) denildiğinde birbirleriyle ilişkili bir grup protokol anlaşılmaktadır. Bir protokol ailesinin pek çok
protokolü başka protokollerin üzerine konumlandırılmış olabilmektedir. Böylece protokol aileleri katmanlı (layered) bir yapıya sahip
olmuştur. Üst seviye bir protokol alt seviye protokolün "zaten var olduğu fikriyle" o alt seviye protokol kullanılarak oluşturulmaktadır.
Bu katmanlı yapıyı prosedürel programlama tekniğinde "zaten var olan bir fonksiyonu kullanarak daha yüksek seviyeli bir fonksiyon yazmaya"
benzetebiliriz.
Ağ haberleşmesi için katmanlı bir protokol yapısının kavramsal olarak nasıl oluşturulması gerektiğine yönelik ISO tarafından 80'li yılların
başlarında "OSI Model (Open System Interconnection Model)" isimli bir referans dokümanı oluşturulmuştur. OSI model bir gerçekleştirim değildir.
Kavramsal bir referans dokümanıdır. Ancak bu referans dokümanı pek çok çalışma için bir zemin oluşturmuştur. OSI referans modeline göre bir
protokol ailesinde tipik olarak 7 katman bulunmalıdır. Bu katmanlar aşağıdaki gibi birbirlerini üzerine oturtulmuştur:
Uygulama Katmanı (Application Layer)
Sunum Katmanı (Presentation Layer)
Oturum Katmanı (Session Layer)
Aktarım Katmanı (Transort Layer)
Network Katmanı (Network Layer)
Veri Bağlantı Katmanı (Data Link Layer)
Fiziksel Katman (Physical Layer)
- En aşağı seviyeli elektriksel tanımlamaların yapıldığı katmana "fiziksel katman (physical layer)" denilmektedir. (Örneğin kabloların,
konnektörlerin özellikleri, akım, gerilim belirlemeleri vs. gibi.) Yani bu katman iletişim için gereken fiziksel ortamı betimlemektedir.
- Veri bağlantı katmanı (data link layer) artık bilgisayarlar arasında fiziksel bir adreslemenin yapıldığı ve bilgilerin paketlere ayrılarak
gönderilip alındığı bir ortam tanımlarlar. Yani bu katmanda bilgilerin gönderildiği ortam değil, gönderilme biçimi ve fiziksel adresleme
tanımlanmaktadır. Ağ üzerinde her birimin donanımsal olarak tanınabilen fiziksel bir adresinin olması gerekir. Örneğin bugün kullandığımız
Ethernet kartları "Ethernet Protocolü (IEEE 802.11)" denilen bir protokole uygun tasarlanmıştır. Bu ethernet protokolü OSI'nin fiziksel
ve veri bağlantı katmanına karşılık gelmektedir. Ethernet protokolünde yerel ağa bağlı olan her birimin ismine "MAC adresi" denilen 6
byte'lık fiziksel bir adresi vardır. Ethernet protokolünde MAC adresini bildiğimiz ağa bağlı bir birime bilgi gönderebiliriz. Bilgiler
"paket anahtarlaması packet switching)" denilen teknikle gönderilip alınmaktadır. Bu teknikte byte'lar bir paket adı altında bir araya
getirilir sonra ilgili fiziksel katmanla seri bir biçimde gönderilir. Bugün kullandığımız yerel ağlarda aslında bilgi bir birimden diğerine
değil hub'lar yoluyla ağa bağlı olan tüm birimleregönderilmektedir. Ancak bunlardan yalnızca biri gelen bilgiyi sahiplenmektedir. Bugün
kablosuz haberleşmede kullanılan "IEEE 802.11" protokolü de tıpkı Ethernet protokolü gibi hem bir fiziksel katman hem de veri bağlantı
katmanı tanımlamaktadır.
Fiziksel katman ve veri katmanı oluşturulduğunda artık biz yerel ağda bir birimden diğerine paket adı altında bir grup byte'ı gönderip
alabilir duruma gelmekteyiz.
- Ağ Katmanı (network layer) artık "internetworking" yapmak için gerekli kuralları tanımlamaktadır. "Internetworking" terimi "network'lerden
oluşan network'ler" anlamına gelir. Aynı fiziksel ortamda bulunan ağlara "Yerel Ağlar (Local Area Networks)" denilmektedir. Bu yerel
ağlar "router" denilen aygıtlarla birbirlerine bağlanmaktadır. Böylece "internetworking" ortamı oluşturulmaktadır. Tabii böyle bir ortamda
artık ağa bağlı birimler için fiziksel adresler kullanılamaz. Bu ortamlarda ağa bağlı birimlere mantıksal bir adreslerin atanması gerekmektedir.
İşte "network katmanı" internetworking ortamı içerisinde bir birimden diğerine bir paket bilginin gönderilmesi için gereken tanımlamaları içermektedir.
Ağ katmanı bu nedenle en önemli katmandır. Ağ katmanında artık fiziksel adresleme değil, mantıksal adresleme sistemi kullanılmaktadır. Ayrıca
bilgilerin paketlere ayrılarak router'lardan dolaşıp hedefe varması için rotalama mekanizması da bu katmanda tanımlanmaktadır. Yani elimizde
yalnızca ağ katmanı ve onun aşağısındaki katmanlar varsa biz artık "internetworking" ortamında belli bir kaynaktan belli bir hedefe paketler
yollayıp alabiliriz.
- Aktarım katmanı (transport layer) network katmanının üzerindedir. Aktarım katmanında artık kaynak ile hedef arasında mantıksal bir bağlantı
oluşturulabilmekte ve veri aktarımı daha güvenli olarak yapılabilmektedir. Aynı zamanda aktarım katmanı "multiplex" bir kaynak-hedef yapısı
da oluşturmaktadır. Bu sayede bilgiler hedefteki spesifik bir programa gönderilebilmektedir. Bu işleme "port numaralandırması" da denilmektedir.
Bu durumda aktarım katmanında tipik şu işlemlere yönelik belirlemeler bulunmaktadır:
- Bağlantnın nasıl yapılacağına ilişkin belirlemeler
- Ağ katmanından gelen paketlerin stream tabanlı organizasyonuna ilşkin belirlemeler
- Veri aktarımını güvenli hale getirmek için akış kontrolüne ilişkin belirlemeler
- Gönderilen bilgilerin hedefte ayrıştıtılmasını sağlayan protokol port numaralandırmasına ilişkin belirlemeler
- Oturum katmanı (session) katmanı pek çok protokol ailesinde yoktur. Görevi oturum açma kapama gibi yüksek seviyeli bazı belirlemeleri
yapmaktır. Örneğin bu katmanda bir grup kullanıcıyı bir araya getiren oturumların nasıl açılacağına ve nasıl kapatılacağına ilişkin
belirlemeler bulunmaktadır. IP protokol ailesinde OSI'de belirtilen biçimde bir oturum katmanı yoktur.
- Sunum katmanı (presentation layer) verilerin sıkıştırılması, şifrelenmesi gibi tanımlamalar içermektedir. Yine bu katman IP protokol ailesinde
OSI'de belirtildiği biçimde bulunmamaktadır.
- Nihayet protokol ailesini kullanarak yazılmış olan tüm kullanan bütün programlar aslında uygulama katmanını oluşturmaktadır. Yani ağ
ortamında haberleşen her program zaten kendi içerisinde açık ya da gizli bir protokol oluşturmuş durumdadır. Örneğin IP protokol ailesindeki
somut işleri yapmakta kullanılan Telnet, SSH, HTTP, POP3, FTP gibi protokoller uygulama katmanı protokolleridir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bugün farklı makinelerin prosesleri arasında en çok kullanılan protokol ailesi IP (Internet Protocol) denilen protokol ailesidir. IP
protokol ailesi temel ve yardımcı pek çok protokolden oluşmaktadır. Aileye ismini veren ailenin ağ katmanı (network layer) protokolü olan
IP protoküdür. Pekiyi pekiyi IP ailesi neden bu kadar popüler olmuştur? Bunun en büyük nedeni 1983 yılında hepimizin katıldığı Internet'in
(I'nin büyük yazıldığına dikkat ediniz) bu aileyi kullanmaya başlamasıdır. Böylece IP ailesini kullanarak yazdığımız programlar hem aynı
bilgisayarda hem yerel ağımızdaki bilgisayarlarda hem de Internet'te çalışabilmektedir. Aynı zamanda IP ailesinin açık bir (yani bir şirketin
malı değil) protokol olması da cazibeyi çok artırmıştır.
IP ailesi 70'li yıllarda Vint Cerf ve Bob Kahn tarafından geliştirilmiştir. IP ismi Internet Protocol'den gelmektedir. Burada internet
"internetworking" anlamında kullanılmıştır. Cerf ve Kahn 1974 yılında önce TCP protokolü üzerinde sonra da IP protokolü üzerinde çalışmışlar
ve bu protokollerin ilk versiyonlarını oluşturmuşlardır.
Bugün hepimizin bağlandığı büyük ağa "Internet" denilmektedir. Bu ağ ilk kez 1969 yılında Amerika'da Amerikan Savunma Bakanlığı'nın
bir soğuk savaş projesi biçiminde başlatıldı. O zamana kadar yalnızca kısıtlı ölçüde yerel ağlar vardı. 1969 yılında ilk kez bir
"WAN (Wide Area Network)" oluşturuldu. Bu proje Amerikan Savunma Bakanlığı'nın DARPA isimli araştırma kurumu tarafından başlatılmıştır
ve projeye "ARPA.NET" ismi verilmiştir. Daha sonra bu ağa Amerika'daki çeşitli devlet kurumları ve üniversiteler katıldı. Sonra
ağ Avrupa'ya sıçradı. 1983 yılında bu ağ NCP protokolünden IP protokol ailesine geçiş yaptı. Bundan sonra artık APRA.NET ismi
yerine "Internet" ismi kullanılmaya başlandı. (Internet sözcüğü I herfi küçük harfle yazılırsa "internetworking" anlamında büyük
harfle yazılırsa bugün katıldığımız dev ağ anlamında kullanılmaktadır.) Biz de IP ailesini kullanarak kendi "internetworking" ortamımızı
oluşturabiliriz. Örneğin bir şirket hiç Internet'e bağlanmadan kendi internet'ini oluşturabilir. Buna eskiden "intranet" denirdi. IP
protokol ailesi herkesin kendi internet'ini oluşturabilmesi için bütün gerekli protokolleri barındırmaktadır. Tabii sinerji bakımından
herkes zaten var olan ve "Internet" denilen bu dev ağa bağlanmayı tercih etmektedir.
IP protokol ailesi 4 katmanlı bir ailedir. Bu ailede "fiziksel ve veri bağlantı katmanı" bir arada düşünülebilir. Bugün bunlar
Ethernet ve Wireless protokolleri biçiminde kullanılmaktadır. IP ailesinin ağ katmanı aileye ismini veren IP protokolünden oluşmaktadır.
Aktarım katmanı ise TCP ve UDP protokollerinden oluşur. Nihayet TCP üzerine oturtulmuş olan HTTP, TELNET, SSH, POP3, IMAP gibi pek
çok protokol ailenin uygulama katmanını oluşturmaktadır. Tabii IP protokol ailesinde bu hiyerarşik yapıyla ilgili olmayan irili ufaklı pek
çok protokol de bulunmaktadır.
+---------------------+-------------------------------+
| Application Layer | HTTP, SSH, POP3, IMAP, ... |
+---------------------+---------------+---------------+
| Transport Layer | TCP | UDP |
+---------------------+---------------+---------------+
| Network Layer | IP |
+---------------------+-------------------------------+
| Physical/Data Link | Ethernet |
| Layer | Wireless |
+---------------------+-------------------------------+
IP protokolü tek başına kullanılırsa ancak ağa bağlı bir birimden diğerine bir paket gönderip alma işini yapar. Bu nedenle bu protokolün
tek başına kullanılması çok seyrektir. Uygulamada genellikle "aktarım (transport) katmanına" ilişkin TCP ve UDP ptotokolleri kullanılmaktadır.
IP ailesinin uygulama katmanındaki HTTP, SSH, POP3, IMAP, FTP gibi önemli protokollerinin hepsi TCP protokolü üzerine oturtulmuştur.
Ailede genellikle TCP protokolü kullanıldığı için buna kısaca "TCP/IP" de denilmektedir.
IP protokolü ailenin en önemli ve taban protokolüdür. IP protokolünde ağa bağlı olan ve kendisine IP adresiyle erişilebilen her birime
"host" denilmektedir. IP protokolü bir host'tan diğerine bir paket (buna IP paketi denilmektedir) bilginin gönderimine ilişkin tanımlamaları
içermektedir. IP protokolünde her host'un ismine "IP adresi" denilen mantıksal bir adresi vardır. Paketler belli bir IP adresinden diğerine
gönderilmektedir. IP protokolünün iki önemli versiyonu vardır: IPv4 ve IPv6. Bugün her iki versiyon da aynı anda kullanılmaktadır. IPv4'te
IP adresleri 4 byte uzunluktadır. (Protokolün tasarlandığı 70'li yıllarda 4 byte adres alanı çok geniş sanılmaktaydı). IPv6'da ise IP
adresleri 16 byte uzunluğundadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
TCP bağlantılı (connection-oriented), UDP bağlantısız (connectionless) bir protokoldür. Buradaki bağlantı IP paketleriyle yapılan
mantıksal bir bağlantıdır. Bağlantı sırasında gönderici ve alıcı birbirlerini tanır ve haberleşme boyunca haberleşmenin güvenliği için
birbirleriyle konuşabilirler. Bağlantılı protokol "client-server" tarzı bir haberleşmeyi akla getirmektedir. Bu nedenle TCP/IP denildiğinde
akla "client-server" haberleşme gelmektedir. TCP modelinde client önce server'a bağlanır. Sonra iletişim güvenli bir biçimde karşılıklı
konuşmalarla sürdürürlür. Tabii TCP bunu yaparken IP paketlerini yani IP protokolünü kullanmaktadır. UDP protokolü bağlantısızdır. Yani
UDP protokolünde bizim bir host'a UDP paketi gönderebilmemiz için bir bağlantı kurmamıza gerek kalmaz. Örneğin biz televizyon yayını UDP
modeline benzemektedir. Verici görüntüyü yollar ancak alıcının alıp almadığıyla ilgilenmez. Vericinin görüntüyü yollaması için alıcıyla
bağlantı kurması gerekmemektedir.
TCP "stream tabanlı", UDP ise "datagram (paket) tabanlı" bir protokoldür. Stream tabanlı protokol demek tamamen boru haberleşmesinde
olduğu gibi gönderen tarafın bilgilerinin bir kuyruk sistemi eşliğinde oluşturulması ve alıcının istediği kadar byte'ı parça parça
okuyabilmesi demektir. Datagram tabanlı haberleşme demek ise tamamen mesaj kuyruklarında olduğu gibi bilginin paket paket iletilmesi
demektir. Yani datagram haberleşmede alıcı taraf gönderen tarafın tüm paketini tek hamlede almak zorundadır. Stream tabanlı haberleşmenin
oluşturulabilmesi için IP paketlerine bir numara verilmesi ve bunların hedefte birleştirilmesi gerekmektedir. Örneğin biz bir host'tan
diğerine 10K'lık bir bilgi gönderelim. TCP'de bu bilgi IP paketlerine ayrılıp numaralandırılır. Bunlar hedefte birleştirilir ve sanki
10000 byte'lık ardışıl bir bilgiymiş gibi gösterilir. Halbuki UDP'de paketler birbirinden bağımsızdır. Dolayısıyla bunların hedefte
birleştirilmesi zorunlu değildir. IP protokolünde bir host birtakım paketleri diğer host'a gönderdiğinde alıcı taraf bunları aynı sırada
almayabilir. Bu özelliğinden dolayı TCP, ailenin en çok kullanılan aktarım (transport) katmanı protokolüdür.
TCP güvenilir (reliable), UDP güvenilir olmayan (unreliable) bir protokoldür. TCP'de mantıksal bir bağlantı oluşturulduğu için yolda kaybolan
paketlerin telafi edilmesi mümkündür. Alıcı taraf gönderenin bilgilerini eksiksiz ve bozulmadan aldığını bilir. Aynı zamanda TCP'de "bir akış
kontrolü (flow control)" de uygulanmaktadır. Akış kontrolü sayesinde alıcı taraf tampon taşması durumuna karşı gönderici tarafı durdurabilmektedir.
Halbuki UDP'de böyle bir mekanizma yoktur. Gönderen taraf alıcının bilgiyi alıp almadığını bilmez.
Tüm bunlar eşliğinde IP ailesinin en çok kullanılan aktarım (transport) katmanının neden TCP olduğunu anlayabilirsiniz. Uygulama katmanındaki
protokoller hep TCP kullanmaktadır.
Yukarıda da belirttiğimiz gibi IP protokol ailesinde ağa bağlı olan birimlere "host" denilmektedir. Host bir bilgisayar olmak zorunda değildir.
İşte bu protokol ailesinde her host'un mantıksal bir adresi vardır. Bu adrese IP adresi denilmektedir. IP adresi IPv4'te 4 byte uzunlukta,
IPv6'da 16 byte uzunluktadır. Ancak bir host'ta farklı programlar farklı host'larla haberleşiyor olabilir. İşte aynı host'a gönderilen IP
paketlerinin o host'ta ayrıştırılması için "protokol port numarası" diye isimlendirilen içsel bir numara uydurulmuştur. Port numarası bir
şirketin içerisinde çalışanların dahili numarası gibi düşünülebilir. Port numaraları IPv4'te ve IPv6'da 2 byte'la ifade edilmektedir. İlk
1024 port numarası IP ailesinin uygulama katmanındaki protokoller için ayrılmıştır. Bunlara İngilizce "well known ports" denilmektedir. Bu
nedenle programcıların port numaralarını 1024'ten büyük olacak biçimde seçmeleri gerekir. Bir host TCP ya da UDP kullanarak bir bilgi
gönderecekse bilginin gönderileceği host'un IP numarasını ve bilginin orada kime gönderileceğini anlatan port numarasını belirtmek zorundadır.
IP numarası ve port numarası çiftine "IP End Point" de denilmektedir. Bilgiyi almak isteyen program kendisinin hangi portla ilgilendiğini de
belirtmek durumundadır. Örneğin biz bir host'ta çalışacak bir TCP/IP ya da UDP/IP program yazmak istiyorsak o host'un belli bir port numarasına
gelen bilgilerle ilgileniriz. Port numarası kavramının IP protokolünde olmadığına TCP ve UDP protokollerinde bulunduğuna dikkat ediniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
TCP ve UDP protokollerinin IP protokolü üzerine oturdulduğunu belirtmiştik. Bu ne anlama gelmektedir? Biz TCP ile belli bir IP numarası
ve port numarası (end point) belirterek bir grup byte'ı göndermiş olalım. Aslında bu byte topluluğu bir TCP paketi oluşturularak bir IP
paketi biçiminde yola çıkarılmaktadır. Şöyle ki: IP paketlerinin yapısı şöyledir:
+-------------------------+
| IP Header |
+-------------------------+
| IP Data |
+-------------------------+
Burada IP Header'da söz konusu IP paketinin hedefe ulaştırılabilmesi için gerekli bilgiler bulunur. Gönderilecek asıl bilgi bu paketin
"IP Data" kısmındadır. İşte bir TCP paketi aslında bir IP paketi olarak IP paketinin "IP Data" kısmına gömülerek gönderilmektedir.
Bu durumda TCP paketinin genel görünümü şöyledir:
+-------------------------+
| IP Header |
+-------------------------+ <---+
| TCP Header | |
+-------------------------+ IP Data
| TCP Data | |
+-------------------------+ <---+
TCP paketinin de bir header ve data alanı olduğuna ancak paketin tamamının IP paketinin data alanında yolculuk ettirildiğine dikkat ediniz.
Yani TCP paketinin header ve data kısmı aslında IP paketinin data kısmı gibi oluşturulmaktadır. Böylece yolculuk eden paket aslında bir
TCP paketi değil IP paketidir. TCP bilgileri bu IP paketinin data kısmında bulunmaktadır.
IPv4 başlık uzunluğu 20 byte'dır. IPv4 paket başlık alanları aşağıdaki verilmiştir.
<------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 ------->
+-----------+-----------+----------------------+-----------------------------------------------+ ^
| Version | IHL | Type of Service | Total Length | (4 bytes) |
| (4 bits) | (4 bits) | (8 bits) | (16 bits) | |
+-----------+-----------+----------------------+-----------+-----------------------------------+ |
| Identification | Flags | Fragment Offset | (4 bytes) |
| (16 bits) | (3 bits) | (13 bits) | |
+-----------------------+----------------------+-----------+-----------------------------------+ |
| Time to Live (TTL) | Protocol | Header Checksum | (4 bytes) | 20 bytes
| (8 bits) | (8 bits) | (16 bits) | |
+-----------------------+----------------------+-----------------------------------------------+ |
| Source IP Address (32 bits) | (4 bytes) |
+----------------------------------------------------------------------------------------------+ |
| Destination IP Address (32 bits) | (4 bytes) |
+----------------------------------------------------------------------------------------------+ v
| Segment (L4 protocol (TCP/UDP) + Data) |
+----------------------------------------------------------------------------------------------+
TCP header'ı 20 byte'tan oluşmaktadır ve yapısı aşağıdaki gibidir.
<------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 ------->
+----------------------------------------------+-----------------------------------------------+ ^
| Source Port | Destination Port | (4 bytes) |
| (16 bits) | (16 bits) | |
+----------------------------------------------+-----------------------------------------------+ |
| Sequence Number | (4 bytes) |
| (32 bits) | |
+----------------------------------------------------------------------------------------------+ |
| Acknowledgement Number | (4 bytes) |
| (32 bits) | | 20 bytes
+-----------+----------------+-----------------+-----------------------------------------------+ |
|Header Len.| Reserved | Control Bits | Window Size | (4 bytes) |
| (4 bits) | (6 bits) | (6 bits) | (16 bits) | |
+-----------+----------------+-----------------+-----------------------------------------------+ |
| Checksum | Urgent | (4 bytes) |
| (16 bits) | (16 bits) | |
+----------------------------------------------+-----------------------------------------------+ v
| Options |
| (0 or 32 bits) |
+----------------------------------------------------------------------------------------------+
| Application Layer Data |
| (Size Varies) |
+----------------------------------------------------------------------------------------------+
UDP header'ı 8 byte'tan oluşmaktadır ve yapısı aşağıdaki gibidir.
<------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 ------->
+----------------------------------------------+-----------------------------------------------+ ^
| Source Port | Destination Port | (4 bytes) |
| (16 bits) | (16 bits) | |
+----------------------------------------------+-----------------------------------------------+ | 8 bytes
| Header Length | Checksum | (4 bytes) |
| (16 bits) | (16 bits) | |
+----------------------------------------------+-----------------------------------------------+ v
| Application Layer Data |
| (Size Varies) |
+----------------------------------------------------------------------------------------------+
/*------------------------------------------------------------------------------------------------------------------------------------------
IP haberleşmesi (yani paketlerin, oluşturulması, gönderilmesi alınması vs.) işletim sistemlerinin çekirdekleri tarafından yapılmaktadır.
Tabii User mode programlar için sistem çağrılarını yapan API fonksiyonlarına ve kütüphanelerine gereksinim vardır. İşte bunların en yaygın
kullanılanı "soket kütüphanesi" denilen kütüphanedir. Bu kütüphane ilk kez 1983 yılında BSD 4.2'de gerçekleştirilmiştir ve pek çok UNIX
türevi sistem bu kütüphaneyi aynı biçimde benimsemiştir. Sonra bu kütüphane POSIX standartlarına da dahil edilmiştir. Microsoft Windows
sistemleri için kendi soket kütüphanesini oluşturmuştur. Buna "Windows Socket API (WSA)" denilmektedir. Ancak Microsoft aynı zamanda klasik
BSD soket arayüzünü de desteklemektedir. Yani biz Windows sistemlerinde hem başı WSAXXX ile başlayan Windows'a özgü soket fonksiyonlarını
hem de klasik Berkeley soket fonksiyonlarını kullanabilmekteyiz. Böylece UNIX/Linux sistemlerinde yazdığımız soket programlarını küçük
değişikliklerle Windows sistemlerine taşıyabilmekteyiz.
Berkeley soket kütüphanesi yalnızca IP protokol ailesi için tasarlanmış bir kütüphane değildir. Bütün protokollerin ortak kütüphanesidir. Bu
nedenle kütüphanedeki fonksiyonlar daha genel biçimde tasarlanmıştır.
Biz soket fonksiyonlarını kullanırken aslında arka planda işlemler TCP/IP ve UDP/IP protokollerine uygun bir biçimde gerçekleştirilmektedir.
Örneğin biz send soket fonksiyonu ile bir bilgiyi göndermek istediğimizde aslında bu fonksiyon arka planda bir TCP paketi dolayısıyla da bir
IP paketi oluşturarak protokole uygun bir biçimde bu bilgiyi göndermektedir. Soket kütüphanesinin yalnızca bir API arayüzü olduğuna dikkat
ediniz.
Yukarıda da belirttiğimiz gibi Berkeley soket kütüphanesi POSIX tarafından desteklenmektedir. Yani burada göreceğimiz soket fonksiyonları
aynı zamanda birer POSIX fonksiyonudur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-07-15 13:23:34 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
96. Ders 16/06/2024 - Pazar
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir TCP/IP uygulamasında iki ayrı program yazılır: "TCP Server Program" ve "TCP Client Program". Biz önce TCP server programın
daha sonra da TCP client programın yazımı üzerinde duracağız. Tabii TCP server programın üzerinde dururken zaten bazı ortak
soket fonksiyonlarını da göreceğiz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir TCP server program tipik olarak aşağıdaki soket API'lerinin sırayla çağrılmasıyla gerçekleştirilmektedir:
(Windows'ta WSAStartup --->) socket ---> bind ---> listen ---> accept ---> send/recv (ya da UNIC/Linux'ta read/write)
---> shutdown ---> close (Windows'ta closesocket) (---> Windows'ta WSACleanup)
Buradan da gördüğünüz gibi her ne kadar Windows UNIX tarzı Bekeley soket kütüphanesini destekliyorsa da şu küçük
farklılıklara sahiptir:
1) Windows'ta soket sistemini başlatmak ve sonlandırmak için WSAStartup ve WSACleanup API fonksiyonları kullanılmaktadır.
2) Windows'ta soket nesnesini yok etmek için close yerine closesocket API fonksiyonu bulunmaktadır.
3) Windows'ta bazı istisnalar dışında bir soket fonksiyonu başarısız olduğunda başarısızlığın nedeni GetLastError fonksiyonuyla değil
WSAGetLastError fonksiyonuyla elde edilmektedir. Bu nedenle Windows örneğimizde hataları rapor etmek için ExitSys fonksiyonunu aşağıdaki
biçimde tanımlayacağız:
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
4) Windows'ta soket kütüphanesi API fonksiyonlarının bulunduğu kütüphaneler içerisinde değildir. Bu nedenle link amasında soket API'lerinin
bulunduğu dinamik kütüphanenin import kütüphanesi ("Ws2_32.lib") linker ayarlarında belirtilmelidir.
5) Windows sistemlerinde soket fonksiyonlarının protoipleri <winsock2.h> dosyası içerisindedir. Halbuki UNIX/Linux sistemlerinde fonksiyonların
prototipleri farklı başlık dosyalarında bulunabilmektedir. Eğer <windows.h> dosyası da include edilecekse önce <winsock2.h> dosyası sonra
<windows.h> dosyası include edilmelidir.
Windows'ta soket sistemini başlatmak için WSAStartup fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
#include <winsock2.h>
int WSAStartup(
WORD wVersionRequired,
LPWSADATA lpWSAData
);
Fonksiyonun birinci parametresi talep edilen soket kütüphanesinin versiyonunu belirtmektedir. Buradaki WORD değer iki ayrı byte'ın birleşiminden
oluşmaktadır. MAKEWORD(high, low) makrosu ile bu parametre için argüman oluşturulabilir. Windows soket kütüphanesinin son versiyonu 2.2 versiyonudur.
Dolayısıyla bu parametre için argümanı MAKEWORD(2, 2) biçiminde geçebiliriz. Eğer talep edilen versiyon yüksekse bu durum hataya yol açmamakta
talep edilenden küçük olan en yüksek versiyon kullanıma hazır hale getirilmektedir. Fonksiyonun ikinci parametresi WSADATA isimli bir yapı
nesnesinin adresini almaktadır. Fonksiyon bu yapıya bazı bilgiler yerleştirmektedir. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda
hata kodunun kendisine geri dönmektedir. (Yani bu fonksiyon için WSAGetLastError çağrısına gerek yoktur.) Fonksiyon tipik olarak şöyle kullanılır:
WSADATA wsaData;
int result;
...
if ((result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", result);
Windows'ta WSACleaanup fonksiyonun prototipi ise şöyledir:
#include <winsock2.h>
int WSACleanup(void);
Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda SOCKET_ERROR özel değerine (bu değer genellikle -1 olarak define edilmektedir)
geri dönmektedir. Hatanın nedeni için WSAGetLastError fonksiyonuna başvurulmalıdır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Haberleşme için öncelikle bir soket nesnesinin yaratılması gerekmektedir. Bu işlem socket isimli fonksiyonla yapılmaktadır. socket fonksiyonu
bir soket nesnesi (handle alanı) yaratır ve bize handle değeri verir. Windows sistemlerinde socket fonksiyonunun geri döndürdüğü handle
değeri SOCKET isimli bir türdendir. Ancak UNIX/Linux sistemlerinde bir dosya betimleyicisi biçimindedir. Yani UNIX/Linux sistemlerinde
socket nesneleri tamamen bir dosya gibi kullanılmaktadır. Windows sistemlerinde socket fonksiyonunun prototipi şöyledir:
#include <winsock2.h>
SOCKET socket(
int af,
int type,
int protocol
);
UNIX/Linux sistemlerindeki prototipi ise şöyledir:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
Fonksiyonların parametreleri her iki sistemde de aynıdır. Yukarıda da gördüğünüz gibi tek fark Windows sistemlerinde soketin handle değerinin
SOCKET türüyle UNIX/Linux sistemlerinde ise int türüyle temsil edilmesidir.
socket fonksiyonunun birinci parametresi kullanılacak protokol ailesini belirtir. Bu parametre AF_XXX (Address Family) biçimindeki sembolik
sabitlerden biri olarak girilir. IPv4 için bu parametreye AF_INET, IPv6 için AF_INET6 girilmelidir. UNIX domain soketler için bu parametre
AF_UNIX olarak girilmelidir.
Fonksiyonun ikinci parametresi kullanılacak protokolün stream tabanlı mı yoksa datagram tabanlı mı olacağını belirtmektedir. Stream soketler
için SOCK_STREAM, datagram soketler için SOCK_DGRAM kullanılmalıdır. Ancak başka soket türleri de vardır. TCP protokolünde bu parametre
SOCK_STREAM biçiminde UDP protokülünde ise bu parametre SOCK_DGRAM biçiminde girilmelidir.
Fonksiyonun üçüncü parametresi aktarım (transport) katmanındaki protokolü belirtmektedir. Ancak zaten ikinci parametreden aktarım protokolü
anlaşılıyorsa üçüncü parametre 0 olarak geçilebilmektedir. Örneğin IP protokol ailesinde üçüncü parametreye gerek duyulmamaktadır. Çünkü ikinci
parametredeki SOCK_STREAM zaten TCP'yi, SOCK_DGRAM ise zaten UDP'yi anlatmaktadır. Fakat yine de bu parametreye istenirse IP ailesi için
IPPROTO_TCP ya da IPPROTO_UDP girilebilir. (Bu sembolik sabitler UNIX/Linux sistemlerinde <netinet/in.h> içerisindedir.)
socket fonksiyonu başarı durumunda Windows'ta soket handle değerine UNIX/Linux sistemlerinde ise soket betimeleyicisine geri dönemktedir.
Fonksiyon başarısızlık durumunda Windows'ta SOCKT_ERROR değerine UNIX/Linux sistemlerinde ise -1 değerine geri dönmektedir. Windows sistemlerindeki
SOCKET_ERROR sembolik sabiti de zaten -1 biçiminde define edilmiştir. Ancak Windows sistemlerinde SOCKET_ERROR sembolik sabitinin kullanılması
gerekir.
Örneğin Windows sistemlerinde socket nesnesi şöyle yaratılabilir:
SOCKET serverSock;
...
if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR)
ExitSys("socket", WSAGetLastError());
Aynı işlem UNIX/Linux sistemlerinde şöyle yapılabilir:
int server_sock;
...
if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
exit_sys("socket");
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Server program soketi yarattıktan sonra onu bağlamalıdır (bind etmelidir). bind işlemi sırasında server'ın hangi portu dinleyeceği ve
hangi network arayüzünden (kartından) gelen bağlantı isteklerini kabul edeceği belirlenir. Ancak bind fonksiyonu dinleme işlemini başlatmaz.
Yalnızca soket nesnesine bu bilgileri yerleştirir. Fonksiyonun Windows sistemlerindeki prototipi şöyledir:
#include <winsock2.h>
int bind(
SOCKET s,
const sockaddr *addr,
int namelen
);
UNIX/Linux sistemlerindeki prototipi ise şöyledir:
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *addr, socklen_t addrlen);
Görüldüğü gibi fonksiyonların iki sistemde de prototipi aynıdır. Ancak Windows sistemlerinde soket nesnesi SOCKET türüyle temsil edilmektedir.
Biz kursumuzda anlatımı kolaylaştırmak için her iki sistemde de socket handle değerine soket betimelyicisi diyeceğiz.
bind fonksiyonunun birinci parametresi yaratılmış olan soket betimleyicisini belirtir. İkinci parametre her ne kadar sockaddr isimli bir yapı
türünden gösterici ise de de aslında her protokol için ayrı bir yapı nesnesinin adresini almaktadır. Yani sockaddr yapısı burada genelliği
(void gösterici gibi) temsil etmek için kullanılmıştır. IPv4 için kullanılacak yapı sockaddr_in, IPv6 için sockaddr_in6 ve örneğin UNIX
domain soketler için ise sockaddr_un biçiminde olmalıdır. Üçüncü parametre, ikinci parametredeki yapının uzunluğu olarak
girilmelidir.
sockaddr_in yapısı UNIX/Linux sistemlerinde <netinet/in.h> dosyası içerisindedir. Windows sistemlerinde bu yapı şöyle bildirilmiştir:
#include <winsock2.h>
typedef struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN, *LPSOCKADDR_IN;
UNIX/Linux sistemlerinde ise bu yapı şöyle bildirilmiştir:
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
Yapı elemanlarının türleri için iki sistemde farklı typedef isimleri kullanılmış olasa da elemanlar aynı anlamdadır. Yapının sin_family
elemanına protokol ailesini belirten AF_XXX değeri girilmelidir. Bu eleman tipik olarak short biçimde bildirilmiştir. Yapının sin_port
elemanı her iki sistemde de unsigned short türdendir. Server programın hangi portu dinleyeceği bu elemanla belirlenmektedir. Yapının
sin_addr elemanı IP numarası belirten bir elemandır. Bu eleman in_addr isimli bir yapı türündendir. Bu yapı Windows sistemlerinde şöyle
bildirilmiştir:
struct in_addr {
union {
struct {
u_char s_b1;
u_char s_b2;
u_char s_b3;
u_char s_b4;
} S_un_b;
struct {
u_short s_w1;
u_short s_w2;
} S_un_w;
u_long S_addr;
} S_un;
};
#define s_addr S_un.S_addr
UNIX/Linux sistemlerinde ise in_addr yapısı <netint/in.h> dosyası içerisinde şöyle bildirilmiştir:
#include <netinet/in.h>
struct in_addr {
in_addr_t s_addr;
};
Aslında her iki sistemde de yapının s_addr elemanı IPV4 için 4 byte'lık işaretsiz tamsayı türü belirtmektedir. İşte bu 4 byte'lık işaretsiz
tamsayı türü IPV4 için IP adresini belirtmektedir. Eğer buradaki IP adresi INADDR_ANY biçiminde geçilirse bu durum "herhangi bir network
kartından gelen bağlantı isteklerinin kabul edileceği" anlamına gelmektedir.
Yukarıda da belirttiğimiz gibi IP ailesinde tüm sayısal değerler "big endian" formatıyla belirtilmek zorundadır. Bu ailede
"network byte ordering" denildiğinde "big endian" format anlaşılır. Oysa makinelerin belli bir bölümü (örneğin Intel ve default ARM)
"little endian" kullanmaktadır. İşte elimizdeki makinenin endian'lığı ne olursa olsun onu big endian formata dönüştüren htons
(host to network byte ordering short) ve htonl (host to network byte ordering long) isimli iki fonksiyon vardır. Bu işlemlerin tersini
yapan da ntohs (network byte ordering to host short) ve ntohl (network byte ordering to host long) fonksiyonları da bulunmaktadır.
Fonksiyonların Windows sistemlerindeki prototipleri şöyledir:
#include <winsock2.h>
u_short htons(
u_short hostshort
);
u_long htonl(
u_long hostlong
);
u_short ntohs(
u_short netshort
);
u_long ntohl(
u_long netlong
);
Fonksiyonların UNIX/Linux sistemlerindeki prototipleri şöyledir:
#include <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);
Bu durumda sockaddr_in yapısı her iki sistemde de tipik olarak şöyle doldurulabilir:
struct sockaddr_in sinaddr;
sinaddr.sin_family = AF_INET;
sinaddr.sin_port = htons(SERVER_PORT);
sinaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind fonksiyonu başarı durumunda sıfır değerine, başarısızlık durumunda Windows sistemlerinde SOCKET_ERROR değerine, UNIX/Linux sistemlerinde
ise -1 değerine geri dönmektedir. Örneğin Windows sistemlerinde fonksiyon şöyle çağrılabilir:
if (bind(serverSock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
UNIX/Linux sistemlerinde de çağrı şöyle olabilir:
if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1)
exit_sys("bind");
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
server program bind işleminden sonra soketi aktif dinleme konumuna sokmak için listen fonkiyonunu çağırmalıdır. Fonksiyonun Windows
sistemlerindeki prototipi şöyledir:
#include <windows.h>
int listen(
SOCKET s,
int backlog
);
UNIX/Linux sistemlerindeki prototipi ise şöyledir:
#include <sys/socket.h>
int listen(int socket, int backlog);
Fonksiyonun birinci parametresi soket betimleyicisini, ikinci parametresi kuyruk uzunluğunu belirtir. listen işlemi blokeye yol açmamaktadır.
İşletim sistemi listen işleminden sonra ilgili porta gelen bağlantı isteklerini uygulama için oluşturduğu bir bağlantı kuyruğuna yerleştirmektedir.
Kuyruk uzunluğunu yüksek tutmak meşgul server'larda bağlantı isteklerinin kaçırılmamasını sağlayabilir. Linux'ta default durumda verilebilecek
en yüksek değer 128'dir. Ancak "/proc/sys/net/core/somaxconn" dosyasındaki değer değiştirilerek bu default uzunluk artırılabilir. Fonksiyon
başarı durumunda 0 değerine, başarısızlık durumunda Windows sistelerinde SOCKET_ERROR değerine, UNIX/Linux sistemlerinde ise -1 değerine
geri dönmektedir. Örneğin Windows sistemlerinde fonksiyon şöyle çağrılabilir:
if (listen(serverSock, 8) == SOCKET_ERROR)
ExitSys("listen", WSAGetLastError());
UNIX/Linux sistemlerinde de çağrı şöyle yapılabilir:
if (listen(server_sock, 8) == -1)
exit_sys("listen");
Bu fonksiyon işletim sistemlerinin "firewall mekanizması" tarafından denetlenebilmektedir. Eğer çalıştığınız sistemde söz konusu port
firewall tarafından kapatılmışsa bunu açmanız gerekir. (Windows sistemlerinde listen fonksiyonu bir pop pencere çıkartarak uyarı mesajı
görüntülemektedir.)
Yukarıda da belirttiğimiz gibi listen fonksiyonu herhangi bir blokeye yol açmaz. Bu fonksiyon çağrıldıktan sonra işletim sistemi ilgili
porta gelecek bağlantı isteklerini prosese özgü bir bağlantı kuyruğuna yerleştirmektedir. Bu bağlantı kuyruğundan bağlantı isteklerini
alarak bağlantısıyı sağlayan asıl fonksiyon accept fonksiyonudur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
97. Ders 22/06/2024 - Cumartesi
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
2024-07-15 13:23:34 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
accept fonksiyonu bağlantı kuyruğuna bakar. Eğer kuyrukta bir bağlantı isteği varsa onu alır ve hemen geri döner. Eğer orada bir bağlantı
isteği yoksa default durumda blokede bekler. Ancak accept fonksiyonu blokesiz modda da kullanılabilmektedir. Fonksiyonun Windows
sistemlerindeki prototipi şöyledir:
#include <winsock2.h>
SOCKET accept(
SOCKET s,
struct sockaddr *addr,
int *addrlen
);
UNIX/Linux sistemlerindeki prototipi ise şöyledir:
#include <sys/socket.h>
int accept(int socket, struct sockaddr *address, socklen_t *address_len);
Fonksiyonların birinci parametreleri dinleme soketinin dosya betimleyicisini almaktadır. İkinci parametre bağlanılan client'a ilişkin bilgilerin
yerleştirileceği sockaddr_in yapısının adresini almaktadır. Bu parametre yine genel bir sockaddr yapısı türünden gösterici ile temsil edilmiştir.
Bizim bu parametre için IPv4'te sockaddr_in türünden, IPv6'da sockaddr_in6 türünden bir yapı nesnesinin adresini argüman olarak vermemiz gerekir.
sockaddr_in yapısının üç elemanı olduğunu anımsayınız. Biz bu parametre sayesinde bağlanan client programın IP adresini ve client makinedeki
port numarasını elde edebilmekteyiz. Client program server programa bağlanırken bir IP adresi ve port numarası belirtir. Ancak kendisinin de bir
IP adresi ve port numarası vardır. Client'ın port numarası kendi makinesindeki (host'undaki) port numarasıdır. Client'ın IP adresine ve oradaki
port numarasına "remote end point" de denilmektedir. Örneğin 178.231.152.127 IP adresinden bir client programın 52310 port'u ile server'ın bulunduğu
176.234.135.196 adresi ve 55555 numaralı portuna bağlandığını varsayalım. Burada remote endpoint "178.231.152.127:52310" biçiminde ifade edilmektedir.
İşte biz accept fonksiyonunun ikinci parametresinden client hakkında bu bilgileri almaktayız.
Client (178.231.152.127:52310) ---> Server (176.234.135.196:55555)
accept fonksiyonunun üçüncü parametresi yine ikinci parametredeki yapının (yani sockaddr_in yapısının) byte uzunluğunu belirtmektedir. Ancak bu
parametre bir adres olarak alınmaktadır. Yani programcı Windows'ta int türünden UNIX(Linux sistemlerinde socklen_t türünden bir nesne tanımlamalı,
bu nesneye bu sizeof değerini yerleştirmeli ve nesnenin adresini de fonksiyonun üçüncü parametresine geçirmelidir. Fonksiyon bağlanılan client'a
ilişkin soket bilgilerinin byte uzunluğunu yine bu adrese yerleştirmektedir. Tabii IP protokol ailesinde her iki taraf da aynı yapıyı kullanıyorsa
fonksiyon çıkışında bu sizeof değerinde bir değişiklik olmayacaktır. Ancak tasarım genel yapıldığı için böyle bir yola gidilmiştir.
accept fonksiyonu başarı durumunda bağlanılan client'a ilişkin yeni bir soket betimleyicisine geri dönmektedir. Artık bağlanılan client ile bu
soket yoluyla konuşulacaktır. accept fonksiyonu başarısızlık durumunda Windows sistemlerinde SOCKET_ERROR değerine UNIC/Linux sistemlerinde ise -1
değeri ile geri dönmektedir. Örneğin Windows sistemlerine accpet fonksiyonu şöyle kullanılabilir:
struct sockaddr_in sin_client;
int sin_len;
SOCKET serverSock;
...
sin_len = sizeof(sin_client);
if ((clientSock = accept(serverSock, (struct sockaddr *)&sin_client, &sin_len)) == SOCKET_ERROR)
ExitSys("accept", WSAGetLastError());
UNIX/Linux sistemlerinde de fonksiyon şöyle kullanılabilir:
struct sockaddr_in sin_client;
socklen_t sin_len;
int client_sock;
...
sin_len = sizeof(sin_client);
if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1)
exit_sys("accept");
Server tarafta temelde iki soket bulunmaktadır. Birincisi bind, listen, accept işlemini yapmakta kullanılan sokettir. Bu sokete TCP/IP
terminolojisinde ""pasif soket (passive socket)" ya da "dinleme soketi (listening socket)" denilmektedir. İkinci soket ise client ile
konuşmakta kullanılan accept fonksiyonunun geri döndürdüğü sokettir. Buna da "aktif soket (active socket)" denilmektedir. Tabii server
program birden fazla client ile konuşacaksa accept fonksiyonunu bir kez değil, çok kez uygulamalıdır. Her accept o anda bağlanılan client
ile konuşmakta kullanılabilecek yeni bir soket vermektedir. bind, listen işlemleri bir kez yapılmaktadır. Halbuki accept işlemi her client
bağlantısı için ayrıca uygulanmalıdır.
accept fonksiyonu default durumda blokeli modda çalışmaktadır. Eğer accept çağrıldığında o anda bağlantı kuyruğunda hiç bir client isteği
yoksa accept fonksiyonu blokeye yol açmaktadır. Eğer accept çağrıldığında zaten bağlantı kuyruğunda bağlantı için bekleyen client varsa
accept bloke olmadan bağlantıyı gerçekleştirir ve geri döner.
accept fonksiyonu ile elde edilen client bilgilerindeki IP adresini ve port numaraları "big endian" formatında yani "network byte ordering"
formatındadır. Bunları sayısal olarak görüntülemek için ntohl ve ntohs fonksiyonlarının kullanılması gerekir. Tabii izleyen paragrafta ele
alacağımız gibi aslında IP adresleri genellikle "noktalı desimal format (dotted decimal format)" denilen bir format ile yazı biçiminde
görüntülenmektedir.
IPV4 adresini alarak noktalı desimal formata dönüştüren inet_ntoa isimli bir fonksiyon vardır. Fonksiyonun prototipi her iki sistemde de
şöyledir:
#include <winsock2.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
Fonksiyon parametre olarak sockaddr_in yapısının içerisindeki IP adresinin bulunduğu in_addr yapısını (adresini değil) parametre olarak
alır ve noktalı desimal format yazısının adresiyle geri döner. Bu durumda accept işlemindne elde edilen client bilgileri aşağıdaki gibi
ekrana yazdırabilir:
printf("waiting for client...\n");
sin_len = sizeof(sin_client);
if ((clientSock = accept(serverSock, (struct sockaddr *)&sin_client, &sin_len)) == SOCKET_ERROR)
ExitSys("accept", WSAGetLastError());
printf("Connected %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port));
inet_nto fonksiyonunun tersini yapan inet_aton isimli bir fonksiyon da bulunmaktadır. Bu fonksiyon noktalı desimal formattaki ip adresini
in_addr yapısınn içerisine yerleştirmektedir. Bu fonksiyon Windows sistemlerinde yoktur. Yalonızca UNIX/Linux sistemlerinde bulunmaktadır:
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
Fonksiyonun birinci parametresi noktalı desimal formattaki ip adresini almaktadır. İkinci paranetresi ise ip adresinin yerleştirileceği
in_addr yapısının adresini almaktadır. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda ise -1 değerine geri dönmektedir.
inet_ntoa ve inet_aton fonksiyonlarının inet_ntop ve inet_pton isimli versiyonları da vardır. Aslında artık inet_ntoa ve inet_aton fonksiyonları
"deprecated" yapılmıştır. Ancak inet_ntop ve inet_pton fonksiyonlarının kullanımı biraz zordur. Biz kursumuzda "deprecated" olmasına karşın
kolaylığı nedeniyle inet_ntoa ve inet_aton fonksiyonlarını kullanacağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Aşağıda Windows ve UNIX/Linux sistemlerinde geldiğimiz noktaya kadar server programların kodları bir bütün olarak verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* Windows ""server.c" */
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#include <windows.h>
#define PORT_NO 55555
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
int main(void)
{
WSADATA wsaData;
int result;
SOCKET serverSock, clientSock;
struct sockaddr_in sinServer, sinClient;
int sin_len;
if ((result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", result);
if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR)
ExitSys("socket", WSAGetLastError());
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(PORT_NO);
sinServer.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(serverSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
if (listen(serverSock, 8) == SOCKET_ERROR)
ExitSys("listen", WSAGetLastError());
printf("waiting for client...\n");
sin_len = sizeof(sinClient);
if ((clientSock = accept(serverSock, (struct sockaddr *)&sinClient, &sin_len)) == SOCKET_ERROR)
ExitSys("accept", WSAGetLastError());
printf("Connected %s:%d\n", inet_ntoa(sinClient.sin_addr), ntohs(sinClient.sin_port));
WSACleanup();
printf("Ok\n");
return 0;
}
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/* UNIX/Linux "server.c" */
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT_NO 55555
void exit_sys(const char* msg);
int main(void)
{
int server_sock, client_sock;
struct sockaddr_in sin_server, sin_client;
socklen_t sin_len;
if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
exit_sys("socket");
sin_server.sin_family = AF_INET;
sin_server.sin_port = htons(PORT_NO);
sin_server.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1)
exit_sys("bind");
if (listen(server_sock, 8) == -1)
exit_sys("listen");
printf("waiting for client..\n");
sin_len = sizeof(sin_client);
if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1)
exit_sys("accept");
printf("Client connected %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port));
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
TCP client program, server programa bağlanabilmek için tipik bazı adımları uygulamak zorundadır. Bu adımlar sırasında çağrılacak fonksiyonlar
şunlardır:
(Windows'ta WSAStartup) ---> socket ---> bind (isteğe bağlı) ---> gethostbyname (isteğe bağlı) ---> connect ---> send/recv (ya da read/write)
---> shutdown ---> close (Windows'ta closesocket) (---> Windows'ta WSACleanup)
Client taraf önce yine socket fonksiyonuyla bir soket yaratır. Soketin bind edilmesi gerekmez. Zaten genellikle client taraf soketi bind etmez.
Eğer client taraf belli bir port'tan bağlanmak istiyorsa bu durumda bind işlemini uygulayabilir. Eğer client bind işlemi yapmazsa zaten işletim
sistemi connect işlemi sırasında sokete boş bir port numarasını atamaktadır. İşletim sisteminin bind edilmemiş client programa connect işlemi
sırasında atadığı bu port numarasına İngilizce "ephemeral port (ömrü kısa olan port)"" denilmektedir. Seyrek olarak bazı server programlar
client için belli bir remote port numarası talep edebilmektedir. Bu durumda client'ın bu remote port'a sahip olabilmesi için bind işlemini
uygulaması gerekir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Client bağlantı için server'ın IP adresini ve port numarasını bilmek zorundadır. IP adreslerinin akılda tutulması zordur. Bu nedenle IP
adresleri ile eşleşen "host isimleri" oluşturulmuştur. Ancak IP protokol ailesi host isimleriyle değil, IP numaralarıyla çalışmaktadır.
İşte host isimleriyle IP numaralarını eşleştiren ismine DNS (Domain Name Server) denilen özel server'lar bulunmaktadır. Bu server'lar
IP protokol ailesindeki DNS isimli bir protokol ile çalışmaktadır. Dolayısıyla client programın elinde IP adresi yerine host ismi varsa
DNS işlemi yaparak o host ismine karşı gelen IP numarasını elde etmesi gerekir. DNS server'lar dağıtık biçimde bulunmaktadır. Bir kayıt
bir DNS server'da yoksa başka bir DNS server'a referans edilmektedir.
DNS server'larda host isimleriyle IP numaraları bire bir karşılık gelmemektedir. Belli bir host ismine birden fazla IP numarası eşleştirilmiş
olabileceği gibi belli bir IP numarasına da birden fazla host ismi eşleştirilmiş olabilmektedir.
DNS işlemleri yapan iki geleneksel fonksiyon vardır: gethostbyname ve gethostbyaddr. Bu fonksiyonların kullanımları kolaydır. Ancak bu
fonksiyonlar artık "deprecated" yapılmış ve POSIX standartlarından da silinmiştir. Bunların yerine getnameinfo ve getaddrinfo fonksiyonları
oluşturulmuştur. Bu fonksiyonlar POSIX standartlarında bulunmaktadır. Biz kursumuzda artık "deprecated" hale getirilmiş "gethostbyname"
ve "gethostbyaddress" kullanmayacağız. Bunun yerine "getaddrinfo" fonksiyonunu açıklayıp onu kullanacağız.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
getaddrinfo isimli fonksiyon inet_addr ve gethosybyname fonksiyonlarının IPv6'yı da içerecek biçimde genişletilmiş bir biçimidir. Yani
getaddrinfo hem noktalı desimal formatı nümerik adrese dönüştürür hem de eğer geçersiz bir noktalı desimal format söz konusuysa (bu durumda
server isimsel olarak girilmiş olabilir) DNS işlemi yaparak ilgili host'un IP adresini elde eder. Maalesef fonksiyon biraz karışık tasarlanmıştır.
Fonksiyonun Windows sistemlerindeki prototipi şöyledir:
#include <ws2tcpip.h>
INT getaddrinfo(
PCSTR pNodeName,
PCSTR pServiceName,
const ADDRINFOA *pHints,
PADDRINFOA *ppResult
);
UNIX/Linux sistemlerindeki prototipi ise şöyledir:
#include <netdb.h>
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
Aslında her iki sistemde de typedef isimleri farklı olmasına karşın fonksiyon aynı biçimde kullanılmaktadır. Fonksiyonun birinci parametresi
"noktalı desimal formatlı IP adresi" ya da "host ismini" belirtmektedir. İkinci parametre NULL geçilebilir ya da buraya port numarası girilebilir.
Ancak bu parametreye port numarası girilecekse yazısal biçimde girilmelidir. Fonksiyon bu port numarasını htons yaparak "big endian" formata
dönüştürüp bize verecektir. Bu parametreye aynı zamanda IP ailesinin uygulama katmanına ilişkin spesifik bir protokolün ismi de girilebilmektedir
(Örneğin "http" gibi, "ftp" gibi). Bu durumda bu protokollerin port numaraları bilindiği için sanki o port numaraları girilmiş gibi işlem
yapılır. Eğer bu parametreye NULL girilirse bize port olarak 0 verilecektir. Port numarasını biz yerleştiriyorsak bu parametreye NULL girebiliriz.
Fonksiyonun üçüncü parametresi nasıl bir adres istediğimizi anlatan filtreleme seçeneklerini belirtir. Bu parametre addrinfo isimli bir yapı
türündendir. Bu yapının yalnızca ilk dört elemanı programcı tarafından girilebilmektedir. Ancak POSIX standartları bu yapının elemanlarının
sıfırlanmasını öngörmektedir (buradaki sıfırlanmak terimi normal türdeki elemanlar için 0 değerini, göstericiler için NULL adres değerini
belirtmektedir). addrinfo yapısı şöyledir:
struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next;
};
Yapının ai_flags elemanı pek çok bayrak değeri alabilmektedir. Bu değer 0 olarak da geçilebilir. Yapının ai_family elemanı AF_INET girilirse
host'a ilişkin IPv4 adresleri, AF_INET6 girilirse host'a ilişkin IPv6 adresleri, AF_UNSPEC girilirse hem IPv4 hem de IPv6 adresleri elde edilir.
Yapının ai_socktype elemanı 0 girilebilir ya da SOCK_STREAM veya SOCK_DGRAM girilebilir. Fonksiyonun ayrıntılııklaması için dokümanlara başvurunuz.
Bu parametre NULL adres de girilebilir. Bu durumda ilgili host'a ilişkin tüm adresler elde edilir.
getaddrinfo fonksiyonunun son parametresine bir bağlı listenin ilk elemanını gösteren adres yerleştirilmektedir. Buradaki bağlı listenin bağ
elemanı addrinfo yapısının ai_next elemanıdır. Bu bağlı listenin boşaltımı freeaddrinfo fonksiyonu tarafından yapılmaktadır.
getaddrinfo fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda doğrudan hata koduna geri döner. Bu hata kodları Windows şistemlerinde
WSAGetLastError fonksiyonuyla elde ettiğimiz hata kodlarıdır. Ancak UNIX/Linux sistemlerinde bu hata kodları errno kodları değildir. Bu hata kodlarının
UNIX/Linux sistemlerinde gai_strerror fonksiyonuyla yazıya dönüştürülmesi gerekir.
Bağlı listenin düğümlerini free hale getirmek için freeaddrinfo fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
#include <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.
Bu fonksiyon bize connect için gereken sockaddr_in ya da sockadd_in6 yapı nesnelerini kendisi oluşturup sockaddr türünden bir adres gibi vermektedir.
Örneğin biz "microsoft.com" host isminin bütün IPV4 AIP adreslerini aşağıdaki gibi elde edebiliriz:
struct addrinfo *ainfoHead, *ainfo;
struct sockaddr_in *sinHost;
struct addrinfo hints = {0, AF_INET, SOCK_STREAM};
...
if ((result = getaddrinfo(HOST_NAME, "55555", &hints, &ainfoHead)) != 0)
ExitSys("bind", result);
for (ainfo = ainfoHead; ainfo != NULL; ainfo = ainfo->ai_next) {
sinHost = (struct sockaddr_in *)ainfo->ai_addr;
printf("%s\n", inet_ntoa(sinHost->sin_addr));
}
freeaddrinfo(ainfoHead);
Programcı host isminden elde ettiği tüm IP numaralarını bağlantı için deneyebilir.
getaddrinfo fonksiyonunun tersini yapan getnameinfo isminde bir fonksiyon da sonraları soket kütüphanesine eklenmiştir. Fonksiyon temel
olarak ilgili host'un IP adresini alıp bize aynı biçimde host isimlerini vermektedir. Biz burada bu fonksiyonu açıklamayacağız.
Eğer elimizde zaten server'ın IP adresi noktalı desimal formatta varsa biz getaddrinfo fonksiyonunu kullanmak yerine inet_addr ile de bu
noktalı desimal formatı IP adresine dönüştürebiliriz. Ancak genel olarak getaadrinfo fonksiyonu daha genel ve daha yeteneklidir. Bu
fonksiyonun kullanılması tavsiye edilebilir. inet_addr fonksiyonunun Windows sistemlerindeki prototipi şöyledir:
#include <winsock2.h>
unsigned long inet_addr(const char *cp);
UNIX/Linux sistemlerindeki prototipi ise şöyledir:
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
Fonksiyonlar noktasıl desimal formattaki IP adresini 4 byte'lık IPV4 adresine dönüştürmektedir. Fonksiyon başarısızlık durumunda Windows
sistemlerinde INADDR_NONE değerine, UNIX/Linux sistemlrinde ise -1 değerine geri dönmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------------------------------------------------------------------------------------------------
Artık client program connect fonksiyonuyla TCP bağlantısını sağlayabilir. connect fonksiyonunun Windows sistemlerindeki prototipi şöyledir:
#include <winsock2.h>
int WSAAPI connect(
SOCKET s,
const sockaddr *name,
int namelen
);
UNIX/Linux sistemlerindeki prototipi ise şö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 örnektlerde bağlantı için gereken minimum client program verilmiştir. Burada henüz görmediğimiz işlemleri hiç uygulamadık.
Client programda bind işlemini yorum satırı içerisine aldık. Yukarıda da belirttiğimiz gibi eğer client program bind işlemi yapmazsa
(ki genellikle yapmaz) bu durumda işletim sistemi client program için o programın çalıştığı makinede boş bir port numarası atamaktadır.
Ayrıca biz bu örneklerde inet_addr fonksiyonunun nasıl kullanılacağını da yorum satırları içerisinde gösterdik.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* Windows "client.c" */
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#define PORT_NO "55555"
#define HOST_NAME "127.0.0.1"
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
int main(void)
{
WSADATA wsaData;
int result;
SOCKET clientSock;
struct addrinfo *ainfoHead, *ainfo;
struct addrinfo hints = {0, AF_INET, SOCK_STREAM};
if ((result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", result);
if ((clientSock = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR)
ExitSys("socket", WSAGetLastError());
/*
{
#define CLIENT_PORT_NO 50000
struct sockaddr_in sinClient;
sinClient.sin_family = AF_INET;
sinClient.sin_port = htons(CLIENT_PORT_NO);
sinClient.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(clientSock, (struct sockaddr *)&sinClient, sizeof(sinClient)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
}
*/
if ((result = getaddrinfo(HOST_NAME, PORT_NO, &hints, &ainfoHead)) != 0)
ExitSys("getaddrinfo", result);
for (ainfo = ainfoHead; ainfo != NULL; ainfo = ainfo->ai_next)
if (connect(clientSock, ainfo->ai_addr, sizeof(struct sockaddr_in)) == 0)
break;
if (ainfo == NULL)
ExitSys("connect", WSAGetLastError());
freeaddrinfo(ainfoHead);
/*
{
struct sockaddr_in sinServer;
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(55555);
if ((sinServer.sin_addr.s_addr = inet_addr(HOST_NAME)) == INADDR_NONE)
ExitSys("inet_addr", WSAGetLastError());
if (connect(clientSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("connect", WSAGetLastError());
}
*/
printf("connected...\n");
WSACleanup();
return 0;
}
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/* Unix/Linux "client.c" */
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#define PORT_NO "55555"
#define HOST_NAME "127.0.0.1"
void exit_sys(const char* msg);
int main(void)
{
int client_sock;
struct addrinfo *ainfo_head, *ainfo;
struct addrinfo hints = {0, AF_INET, SOCK_STREAM};
int result;
if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
exit_sys("socket");
/*
{
#define CLIENT_PORT_NO 50000
struct sockaddr_in sin_client;
sin_client.sin_family = AF_INET;
sin_client.sin_port = htons(CLIENT_PORT_NO);
sin_client.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1)
exit_sys("bind");
}
*/
if ((result = getaddrinfo(HOST_NAME, PORT_NO, &hints, &ainfo_head)) != 0)
exit_sys("getaddrinfo");
for (ainfo = ainfo_head; ainfo != NULL; ainfo = ainfo->ai_next)
if (connect(client_sock, ainfo->ai_addr, sizeof(struct sockaddr_in)) == 0)
break;
if (ainfo == NULL)
exit_sys("connect");
freeaddrinfo(ainfo_head);
/*
{
struct sockaddr_in sin_server;
sin_server.sin_family = AF_INET;
sin_server.sin_port = htons(55555);
if ((sin_server.sin_addr.s_addr = inet_addr(HOST_NAME)) == -1)
exit_sys("inet_addr");
if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1)
exit_sys("connect");
}
*/
printf("connected...\n");
return 0;
}
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*-------------------------------------------------------------------------------------------------------------------------------------------
Bağlantı sağlandıktan sonra artık gönderme ve alma işlemleri yapılabilir. Soketten karşı tarafa byte göndermek için send karşı taraftan
byte okumak için recv fonksiyonları kullanılmaktadır. UNIX/Linux sistemlerinde soketler de dosya gibi betimleyici oldukları için bu
sistemlerde send yerine write, recv yerine read POSIX fonksiyonları da kullanılabilir. TCP full duplex bir haberleşme sunmaktadır. Yani
client ve server programlar aynı soket ile hem bilgi gönderip hem de bilgi alabilmektedir.
recv fonksiyonunun Windows sistemlerindeki prototipi şöyledir:
#include <winsock2.h>
int recv(
SOCKET s,
char *buf,
int len,
int flags
);
UNIX/linux sistemlerindeki prototipi ise şöyledir:
#include <sys/socket.h>
ssize_t recv(int socket, void *buffer, size_t length, int flags);
Fonksiyonların birinci parametresi aktif soketin betimleyicisini belirtmektedir. İkinci parametre alınacak bilginin yerleştirileceği dizinin
adresini almaktadır. Üçüncü parametre ise okunmak istenen byte sayısını belirtmektedir. Fonksiyonun son parametresi aşağıdaki üç sembolik
sabitin bit OR işlemine sokulmasıyla oluşturulabilir:
MSG_PEEK
MSG_OOB
MSG_WAITALL
Biz şimdilik bu değerlerin anlamlarınııklamayacağız. Ancak MSG_PEEK değeri bilginin network tamponundan alındıktan sonra oradan atılmayacağını
belirtmektedir. Bu parametre 0 da geçilebilir. Zaten recv fonksiyonunun read fonksiyonundan tek farkı bu son parametredir. Bu son parametrenin
0 geçilmesiyle read kullanılması arasında hiçbir farklılık yoktur.
recv fonksiyonu blokeli modda (default durum blokeli moddur) tıpkı borularda olduğu gibi eğer hazırda en az 1 byte varsa okuyabildiği kadar
bilgiyi okur ve okuyabildiği byte sayısına geri döner. Eğer o anda network tamponunda hiç byte yoksa recv fonksiyonu en az 1 byte okuyana
kadar blokede bekler. (Yani başka bir deyişle recv tıpkı borularda olduğu gibi eğer okunacak bir şey yoksa blokede bekler, ancak okunacak
en az 1 byte varsa okuyabildiğini okur ve beklemeden geri döner.)
recv fonksiyonu başarı durumunda okunabilen byte sayısına, başarısızlık durumunda Windows sistemlerinde SOCKET_ERROR, UNIX/Linux sistemlerinde
-1 değerine geri dönmektedir. Eğer karşı taraf soketi (peer socket) kapatmışsa bu durumda tıpkı borularda olduğu gibi recv fonksiyonu 0
ile geri dönmektedir. Soketlerle boruların kullanımlarının birbirlerine çok benzediğine dikkat ediniz.
Soketten bilgi göndermek için send ya da UNIX/Linux sistemleribde write fonksiyonu kullanılmaktadır. send fonksiyonunun Windows sistemlerindeki
prototipi şöyledir:
#include <winsock2.h>
int send(
SOCKET s,
const char *buf,
int len,
int flags
);
UNIX/Linux sistemlerindeki prototipi ise şöyledir:
#include <sys/socket.h>
ssize_t send(int socket, const void *buffer, size_t length, int flags);
Fonksiyoların birinci parametresi aktif soketin betimleyicisini belirtmektedir. İkinci parametre gönderilecek bilgilerin bulunduğu dizinin adresini
belirtir. Üçüncü parametre ise gönderilecek byte miktarını belirtmektedir. Son parametre aşağıdaki sembolik sabitlerin bit düzeyinde OR işlemine
sokulmasıyla oluşturulabilir:
MSG_EOR
MSG_OOB
MSG_NOSIGNAL
Bu parametre 0 da geçilebilir. Biz şimdilik bu bayraklar üzerinde durmayacağız.
send fonksiyonu bilgileri karşı tarafa o anda göndermez. Onu önce network tamponuna yerleştirir. İşletim sistemi o tampondan TCP (dolayısıyla
IP) paketleri oluşturarak mesajı göndermektedir. Yani send fonksiyonu geri döndüğünde bilgiler network tamponuna yazılmıştır, ancak henüz
karşı tarafa gönderilmemiş olabilir. Pekiyi o anda network tamponu doluysa ne olacaktır? İşte UNIX/Linux sistemlerinde send fonksiyonu,
gönderilecek bilginin tamamı network tamponuna aktarılana kadar blokede beklemektedir. Ancak bu konuda işletim sistemleri arasında farklılıklar
olabilmektedir. Örneğin Windows sistemlerinde send fonksiyonu eğer network tamponununda gönderilmek istenen kadar yer yoksa ancak en az
bir byte'lık boş bir yer varsa tampona yazabildiği kadar byte'ı yazıp hemen geri dönmektedir. Diğer UNIX/Linux sistemleri arasında da
send fonksiyonunun davranışı bakımından bu yönde farklılıklar olabilmektedir. Ancak POSIX standartları blokeli modda tüm bilginin network
tamponuna yazılana kadar send fonksiyonunun bloke olacağını belirtmektedir. Linux çekirdeği de buna uygun biçimde çalışmaktadır.
send fonksiyonu network tamponuna yazılan byte sayısı ile geri dönmektedir. Blokeli modda bu değer UNIX/Linux sistemlerinde yazılmak istenen
değerle aynı olur. Ancak Windows sistemlerinde daha az olabilmektedir. send fonksiyonu Windows'ta başarısızlık durumunda SOCKET_ERROR değeri
ile UNIX/Linux sistemlerinde ise -1 değeri ile geri döner.Tıpkı borularda olduğu gibi UNIX/Linux sistemlerinde send fonksiyonunda da eğer
karşı taraf soketi kapatmışsa send fonksiyonu default durumda SIGPIPE sinyalinin oluşmasına yol açmaktadır. Eğer bu sinyalin oluşturulması
istenmiyorsa bu durumda send fonksiyonunun son parametresi (flags) MSG_NOSIGNAL olarak geçilmelidir. Bu durumda karşı taraf soketi kapatmışsa
send fonksiyonu başarısız olur ve errno değeri EPIPE olarak set edilir. send fonksiyonunun soketlerdeki davranışının borulardaki davranışa
çok benzediğine dikkat ediniz.
send fonksiyonunun son parametresi 0 geçildiğinde bu fonksiyonun davranışı tamamen write fonksiyonunda olduğu gibidir.
------------------------------------------------------------------------------------------------------------------------------------------*/
<BURADA KALDIK>
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Aşağıdaki iskelet bir TCP/IP client-server uygulama verilmiştir. Burada client klavyeden birtakım yazılar girer. Bunu server'a gönderir.
Server da bu yazının tersini client'a geri yollamaktadır. Client "exit" yazdığında her iki taraf da işlemini sonlandırmaktadır.
2024-07-15 13:23:34 +03:00
------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* Server.c */
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define SERVER_PORTNO 55000
2024-06-09 22:42:45 +03:00
#define BUFFER_SIZE 1024
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
int main(void)
{
WSADATA wsaData;
DWORD dwResult;
2024-06-09 22:42:45 +03:00
SOCKET serverSock, clientSock;
struct sockaddr_in sinServer, sinClient;
int addrLen;
char buf[BUFFER_SIZE];
2024-06-09 22:42:45 +03:00
char *str;
int result;
if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", dwResult);
2024-06-09 22:42:45 +03:00
if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) /* third parameter IPPROTO_TCP */
ExitSys("socket", WSAGetLastError());
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(SERVER_PORTNO);
2024-06-09 22:42:45 +03:00
sinServer.sin_addr.s_addr = htonl(INADDR_ANY);
2024-06-09 22:42:45 +03:00
if (bind(serverSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
2024-06-09 22:42:45 +03:00
if (listen(serverSock, 8) == SOCKET_ERROR)
ExitSys("listen", WSAGetLastError());
2024-06-09 22:42:45 +03:00
printf("Waiting for client connection....\n");
2024-06-09 22:42:45 +03:00
addrLen = sizeof(sinClient);
if ((clientSock = accept(serverSock, (struct sockaddr *)&sinClient, &addrLen)) == INVALID_SOCKET)
ExitSys("accept", WSAGetLastError());
2024-06-09 22:42:45 +03:00
printf("Connected: %s:%d\n", inet_ntoa(sinClient.sin_addr), ntohs(sinClient.sin_port));
for (;;) {
2024-06-09 22:42:45 +03:00
if ((result = recv(clientSock, buf, BUFFER_SIZE - 1, 0)) == SOCKET_ERROR)
ExitSys("recv", WSAGetLastError());
if (result == 0)
break;
2024-06-09 22:42:45 +03:00
buf[result] = '\0';
if (!strcmp(buf, "exit"))
break;
2024-06-09 22:42:45 +03:00
printf("%d bytes received: %s\n", result, buf);
_strrev(buf);
if (send(clientSock, buf, strlen(buf), 0) == SOCKET_ERROR)
ExitSys("send", WSAGetLastError());
}
2024-06-09 22:42:45 +03:00
shutdown(clientSock, SD_BOTH);
closesocket(clientSock);
2024-06-09 22:42:45 +03:00
closesocket(serverSock);
WSACleanup();
return 0;
}
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/* Client.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define CLIENT_PORTNO 62000
#define BUFFER_SIZE 1024
#define SERVER_NAME "127.0.0.1"
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
int main(void)
{
WSADATA wsaData;
DWORD dwResult;
SOCKET clientSock;
struct sockaddr_in sinServer;
struct hostent *hostEnt;
2024-06-09 22:42:45 +03:00
char buf[BUFFER_SIZE];
char *str;
2024-06-09 22:42:45 +03:00
int result;
if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", dwResult);
if ((clientSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
ExitSys("socket", WSAGetLastError());
/*
{
struct sockaddr_in sinClient;
sinClient.sin_family = AF_INET;
sinClient.sin_port = htons(CLIENT_PORTNO);
sinClient.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(clientSock, (struct sockaddr *)&sinClient, sizeof(sinClient)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
}
*/
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(SERVER_PORTNO);
sinServer.sin_addr.s_addr = inet_addr(SERVER_NAME);
if (sinServer.sin_addr.s_addr == INADDR_NONE) {
2024-06-09 22:42:45 +03:00
if ((hostEnt = gethostbyname(SERVER_NAME)) == NULL)
ExitSys("gethostbyname", WSAGetLastError());
memcpy(&sinServer.sin_addr.s_addr, hostEnt->h_addr_list[0], hostEnt->h_length);
}
if (connect(clientSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("connect", WSAGetLastError());
2024-06-09 22:42:45 +03:00
printf("Connected...\n");
for (;;) {
2024-06-09 22:42:45 +03:00
printf("Text:");
fgets(buf, BUFFER_SIZE, stdin);
if ((str = strchr(buf, '\n')) != NULL)
*str = '\0';
2024-06-09 22:42:45 +03:00
if (send(clientSock, buf, strlen(buf), 0) == SOCKET_ERROR)
ExitSys("send", WSAGetLastError());
2024-06-09 22:42:45 +03:00
if (!strcmp(buf, "exit"))
break;
2024-06-09 22:42:45 +03:00
if ((result = recv(clientSock, buf, BUFFER_SIZE - 1, 0)) == SOCKET_ERROR)
ExitSys("recv", WSAGetLastError());
if (result == 0)
break;
buf[result] = '\0';
2024-06-09 22:42:45 +03:00
printf("%d bytes received: %s\n", result, buf);
}
2024-06-09 22:42:45 +03:00
shutdown(clientSock, SD_BOTH);
closesocket(clientSock);
WSACleanup();
return 0;
}
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Yukarıdaki iskelet client/server programın UNIX/Linux sistemlerindeki eşdeğeri aşağıdaki verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* 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 SERVER_PORTNO 55000
#define BUFFER_SIZE 1024
void exit_sys(const char *msg);
2024-06-09 22:42:45 +03:00
void reverse_str(char *str);
int main(void)
{
2024-06-09 22:42:45 +03:00
int server_sock, client_sock;
struct sockaddr_in sin_server, sin_client;
socklen_t addrlen;
2024-06-09 22:42:45 +03:00
char buf[BUFFER_SIZE];
char *str;
int result;
2024-06-09 22:42:45 +03:00
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_PORTNO);
sin_server.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1)
exit_sys("bind");
2024-06-09 22:42:45 +03:00
if (listen(server_sock, 8) == -1)
exit_sys("listen");
printf("Waiting for client connection....\n");
addrlen = sizeof(sin_client);
if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &addrlen)) == -1)
exit_sys("accept");
printf("Connected: %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port));
for (;;) {
2024-06-09 22:42:45 +03:00
if ((result = recv(client_sock, buf, BUFFER_SIZE - 1, 0)) == -1)
exit_sys("recv");
if (result == 0)
break;
buf[result] = '\0';
2024-06-09 22:42:45 +03:00
if (!strcmp(buf, "exit"))
break;
printf("%d bytes received: %s\n", result, buf);
reverse_str(buf);
if (send(client_sock, buf, strlen(buf), 0) == -1)
exit_sys("recv");
}
2024-06-09 22:42:45 +03:00
shutdown(client_sock, SHUT_RDWR);
close(client_sock);
close(server_sock);
2024-06-09 22:42:45 +03:00
return 0;
}
void reverse_str(char *str)
{
size_t len, i;
char temp;
for (len = 0; str[len] != '\0'; ++len)
;
for (i = 0; i < len / 2; ++i) {
temp = str[i];
str[i] = str[len - i - 1];
str[len - i - 1] = temp;
}
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/* 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_PORTNO 55000
2024-06-09 22:42:45 +03:00
#define CLIENT_PORTNO 62000
#define BUFFER_SIZE 1024
#define SERVER_NAME "127.0.0.1"
void exit_sys(const char *msg);
int main(void)
{
2024-06-09 22:42:45 +03:00
int client_sock;
struct sockaddr_in sin_server;
struct hostent *hostent;
char *str;
char buf[BUFFER_SIZE];
int result;
2024-06-09 22:42:45 +03:00
if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
exit_sys("socket");
2024-06-09 22:42:45 +03:00
/*
struct sockaddr_in sin_client;
2024-06-09 22:42:45 +03:00
sin_client.sin_family = AF_INET;
sin_client.sin_port = htons(CLIENT_PORTNO);
sin_client.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1)
exit_sys("socket");
*/
sin_server.sin_family = AF_INET;
sin_server.sin_port = htons(SERVER_PORTNO);
sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME);
if (sin_server.sin_addr.s_addr == INADDR_NONE) {
if ((hostent = gethostbyname(SERVER_NAME)) == NULL)
exit_sys("gethostbyname");
memcpy(&sin_server.sin_addr.s_addr, hostent->h_addr_list[0], hostent->h_length);
}
2024-06-09 22:42:45 +03:00
if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1)
exit_sys("gethostbyname");
printf("Connected...\n");
2024-06-09 22:42:45 +03:00
for (;;) {
printf("Text:");
fgets(buf, BUFFER_SIZE, stdin);
if ((str = strchr(buf, '\n')) != NULL)
*str = '\0';
2024-06-09 22:42:45 +03:00
if (send(client_sock, buf, strlen(buf), 0) == -1)
exit_sys("send");
if (!strcmp(buf, "exit"))
break;
2024-06-09 22:42:45 +03:00
if ((result = recv(client_sock, buf, BUFFER_SIZE - 1, 0)) == -1)
exit_sys("recv");
if (result == 0)
break;
buf[result] = '\0';
printf("%d bytes received: %s\n", result, buf);
}
2024-06-09 22:42:45 +03:00
shutdown(client_sock, SHUT_RDWR);
close(client_sock);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Windows'ta çok client'lı server uygulaması. Bu örnekte thread modeli kullanılmıştır. Server her client bağlantısında bir thread yaratmış ve
o thread'in bağlanılan client konuşmasını sağlamıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* Server.c */
2024-06-09 22:42:45 +03:00
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define BUFFER_SIZE 1024
2024-06-09 22:42:45 +03:00
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
DWORD __stdcall ClientThreadProc(LPVOID lpParam);
2024-06-09 22:42:45 +03:00
struct CLIENT_INFO {
SOCKET sock;
char addr[16];
int port;
};
2024-06-09 22:42:45 +03:00
int main(void)
{
WSADATA wsaData;
DWORD dwResult;
SOCKET serverSock, clientSock;
struct sockaddr_in sinServer, sinClient;
int addrLen;
HANDLE hThread;
DWORD dwThreadId;
struct CLIENT_INFO *ci;
2024-06-09 22:42:45 +03:00
if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", dwResult);
2024-06-09 22:42:45 +03:00
if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) /* third parameter IPPROTO_TCP */
ExitSys("socket", WSAGetLastError());
2024-06-09 22:42:45 +03:00
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(SERVER_PORTNO);
sinServer.sin_addr.s_addr = htonl(INADDR_ANY);
2024-06-09 22:42:45 +03:00
if (bind(serverSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
2024-06-09 22:42:45 +03:00
if (listen(serverSock, 8) == SOCKET_ERROR)
ExitSys("listen", WSAGetLastError());
2024-06-09 22:42:45 +03:00
printf("Waiting for client connection....\n");
for (;;) {
addrLen = sizeof(sinClient);
if ((clientSock = accept(serverSock, (struct sockaddr *)&sinClient, &addrLen)) == INVALID_SOCKET)
ExitSys("accept", WSAGetLastError());
2024-06-09 22:42:45 +03:00
if ((ci = (struct CLIENT_INFO *)malloc(sizeof(struct CLIENT_INFO))) == NULL) {
fprintf(stderr, "Cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
ci->sock = clientSock;
strcpy(ci->addr, inet_ntoa(sinClient.sin_addr));
ci->port = ntohs(sinClient.sin_port);
2024-06-09 22:42:45 +03:00
printf("Connected: %s:%d\n", ci->addr, ci->port);
2024-06-09 22:42:45 +03:00
if ((hThread = CreateThread(NULL, 0, ClientThreadProc, ci, 0, &dwThreadId)) == NULL)
ExitSys("CreateThread", WSAGetLastError());
2024-06-09 22:42:45 +03:00
CloseHandle(hThread);
}
closesocket(serverSock);
2024-06-09 22:42:45 +03:00
WSACleanup();
2024-06-09 22:42:45 +03:00
return 0;
}
2024-06-09 22:42:45 +03:00
DWORD __stdcall ClientThreadProc(LPVOID lpParam)
{
2024-06-09 22:42:45 +03:00
struct CLIENT_INFO *ci = (struct CLIENT_INFO *)lpParam;
int result;
char buf[BUFFER_SIZE];
for (;;) {
if ((result = recv(ci->sock, buf, BUFFER_SIZE - 1, 0)) == SOCKET_ERROR)
goto FAILED;
if (result == 0)
break;
2024-06-09 22:42:45 +03:00
buf[result] = '\0';
if (!strcmp(buf, "exit"))
break;
printf("%d bytes received from %s:%d => \"%s\"\n", result, ci->addr, ci->port, buf);
_strrev(buf);
if (send(ci->sock, buf, strlen(buf), 0) == SOCKET_ERROR)
goto FAILED;
}
FAILED:
printf("Client %s:%d exits...\n", ci->addr, ci->port);
2024-06-09 22:42:45 +03:00
shutdown(ci->sock, SD_BOTH);
closesocket(ci->sock);
free(ci);
return 0;
}
2024-06-09 22:42:45 +03:00
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
2024-06-09 22:42:45 +03:00
LPTSTR lpszErr;
2024-06-09 22:42:45 +03:00
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
2024-06-09 22:42:45 +03:00
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/* Client.c */
2024-06-09 22:42:45 +03:00
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define CLIENT_PORTNO 62000
#define BUFFER_SIZE 1024
#define SERVER_NAME "127.0.0.1"
2024-06-09 22:42:45 +03:00
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
2024-06-09 22:42:45 +03:00
int main(void)
{
WSADATA wsaData;
DWORD dwResult;
SOCKET clientSock;
struct sockaddr_in sinServer;
struct hostent *hostEnt;
char buf[BUFFER_SIZE];
char *str;
int result;
2024-06-09 22:42:45 +03:00
if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", dwResult);
2024-06-09 22:42:45 +03:00
if ((clientSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
ExitSys("socket", WSAGetLastError());
/*
{
struct sockaddr_in sinClient;
2024-06-09 22:42:45 +03:00
sinClient.sin_family = AF_INET;
sinClient.sin_port = htons(CLIENT_PORTNO);
sinClient.sin_addr.s_addr = htonl(INADDR_ANY);
2024-06-09 22:42:45 +03:00
if (bind(clientSock, (struct sockaddr *)&sinClient, sizeof(sinClient)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
}
*/
2024-06-09 22:42:45 +03:00
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(SERVER_PORTNO);
2024-06-09 22:42:45 +03:00
sinServer.sin_addr.s_addr = inet_addr(SERVER_NAME);
if (sinServer.sin_addr.s_addr == INADDR_NONE) {
if ((hostEnt = gethostbyname(SERVER_NAME)) == NULL)
ExitSys("gethostbyname", WSAGetLastError());
2024-06-09 22:42:45 +03:00
memcpy(&sinServer.sin_addr.s_addr, hostEnt->h_addr_list[0], hostEnt->h_length);
}
if (connect(clientSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("connect", WSAGetLastError());
printf("Connected...\n");
for (;;) {
printf("Text:");
fgets(buf, BUFFER_SIZE, stdin);
if ((str = strchr(buf, '\n')) != NULL)
*str = '\0';
if (send(clientSock, buf, strlen(buf), 0) == SOCKET_ERROR)
ExitSys("send", WSAGetLastError());
2024-06-09 22:42:45 +03:00
if (!strcmp(buf, "exit"))
break;
2024-06-09 22:42:45 +03:00
if ((result = recv(clientSock, buf, BUFFER_SIZE - 1, 0)) == SOCKET_ERROR)
ExitSys("recv", WSAGetLastError());
2024-06-09 22:42:45 +03:00
if (result == 0)
break;
2024-06-09 22:42:45 +03:00
buf[result] = '\0';
2024-06-09 22:42:45 +03:00
printf("%d bytes received: %s\n", result, buf);
}
2024-06-09 22:42:45 +03:00
shutdown(clientSock, SD_BOTH);
closesocket(clientSock);
2024-06-09 22:42:45 +03:00
WSACleanup();
2024-06-09 22:42:45 +03:00
return 0;
}
2024-06-09 22:42:45 +03:00
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
LPTSTR lpszErr;
2024-06-09 22:42:45 +03:00
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
2024-06-09 22:42:45 +03:00
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Yukarıdaki programın UNIX/Linux sistemlerindeki karşılığı
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* server.c */
2024-06-09 22:42:45 +03:00
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define BUFFER_SIZE 1024
2024-06-09 22:42:45 +03:00
struct CLIENT_INFO {
int sock;
char addr[16];
int port;
};
2024-06-09 22:42:45 +03:00
void exit_sys(const char *msg);
void *client_thread_proc(void *param);
void reverse_str(char *str);
2024-06-09 22:42:45 +03:00
int main(void)
{
int server_sock, client_sock;
struct sockaddr_in sin_server, sin_client;
socklen_t addrlen;
struct CLIENT_INFO *ci;
pthread_t tid;
int result;
if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
exit_sys("socket");
2024-06-09 22:42:45 +03:00
sin_server.sin_family = AF_INET;
sin_server.sin_port = htons(SERVER_PORTNO);
sin_server.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1)
exit_sys("bind");
2024-06-09 22:42:45 +03:00
if (listen(server_sock, 8) == -1)
exit_sys("listen");
2024-06-09 22:42:45 +03:00
printf("Waiting for client connection....\n");
2024-06-09 22:42:45 +03:00
for (;;) {
addrlen = sizeof(sin_client);
if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &addrlen)) == -1)
exit_sys("accept");
2024-06-09 22:42:45 +03:00
if ((ci = (struct CLIENT_INFO *)malloc(sizeof(struct CLIENT_INFO))) == NULL) {
fprintf(stderr, "Cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
ci->sock = client_sock;
strcpy(ci->addr, inet_ntoa(sin_client.sin_addr));
ci->port = ntohs(sin_client.sin_port);
2024-06-09 22:42:45 +03:00
printf("Connected: %s:%d\n", ci->addr, ci->port);
2024-06-09 22:42:45 +03:00
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_create: %s\n", strerror(result));
exit(EXIT_FAILURE);
}
}
shutdown(server_sock, SHUT_RDWR);
close(server_sock);
2024-06-09 22:42:45 +03:00
return 0;
}
2024-06-09 22:42:45 +03:00
void *client_thread_proc(void *param)
{
struct CLIENT_INFO *ci = (struct CLIENT_INFO *)param;
int result;
char buf[BUFFER_SIZE];
for (;;) {
if ((result = recv(ci->sock, buf, BUFFER_SIZE - 1, 0)) == -1)
goto FAILED;
if (result == 0)
break;
2024-06-09 22:42:45 +03:00
buf[result] = '\0';
if (!strcmp(buf, "exit"))
break;
printf("%d bytes received from %s:%d => \"%s\"\n", result, ci->addr, ci->port, buf);
reverse_str(buf);
if (send(ci->sock, buf, strlen(buf), 0) == -1)
goto FAILED;
}
FAILED:
printf("Client %s:%d exits...\n", ci->addr, ci->port);
2024-06-09 22:42:45 +03:00
shutdown(ci->sock, SHUT_RDWR);
close(ci->sock);
free(ci);
2024-06-09 22:42:45 +03:00
return NULL;
}
2024-06-09 22:42:45 +03:00
void reverse_str(char *str)
{
size_t len, i;
char temp;
2024-06-09 22:42:45 +03:00
for (len = 0; str[len] != '\0'; ++len)
;
2024-06-09 22:42:45 +03:00
for (i = 0; i < len / 2; ++i) {
temp = str[i];
str[i] = str[len - i - 1];
str[len - i - 1] = temp;
}
}
2024-06-09 22:42:45 +03:00
void exit_sys(const char *msg)
{
perror(msg);
2024-06-09 22:42:45 +03:00
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/* client.c */
2024-06-09 22:42:45 +03:00
#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>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define CLIENT_PORTNO 62000
#define BUFFER_SIZE 1024
#define SERVER_NAME "127.0.0.1"
2024-06-09 22:42:45 +03:00
void exit_sys(const char *msg);
2024-06-09 22:42:45 +03:00
int main(void)
{
int client_sock;
struct sockaddr_in sin_server;
struct hostent *hostent;
char *str;
char buf[BUFFER_SIZE];
int result;
2024-06-09 22:42:45 +03:00
if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
exit_sys("socket");
2024-06-09 22:42:45 +03:00
/*
struct sockaddr_in sin_client;
2024-06-09 22:42:45 +03:00
sin_client.sin_family = AF_INET;
sin_client.sin_port = htons(CLIENT_PORTNO);
sin_client.sin_addr.s_addr = htonl(INADDR_ANY);
2024-06-09 22:42:45 +03:00
if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1)
exit_sys("socket");
*/
2024-06-09 22:42:45 +03:00
sin_server.sin_family = AF_INET;
sin_server.sin_port = htons(SERVER_PORTNO);
sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME);
if (sin_server.sin_addr.s_addr == INADDR_NONE) {
if ((hostent = gethostbyname(SERVER_NAME)) == NULL)
exit_sys("gethostbyname");
2024-06-09 22:42:45 +03:00
memcpy(&sin_server.sin_addr.s_addr, hostent->h_addr_list[0], hostent->h_length);
}
2024-06-09 22:42:45 +03:00
if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1)
exit_sys("gethostbyname");
2024-06-09 22:42:45 +03:00
printf("Connected...\n");
2024-06-09 22:42:45 +03:00
for (;;) {
printf("Text:");
fgets(buf, BUFFER_SIZE, stdin);
if ((str = strchr(buf, '\n')) != NULL)
*str = '\0';
if (send(client_sock, buf, strlen(buf), 0) == -1)
exit_sys("send");
2024-06-09 22:42:45 +03:00
if (!strcmp(buf, "exit"))
break;
2024-06-09 22:42:45 +03:00
if ((result = recv(client_sock, buf, BUFFER_SIZE - 1, 0)) == -1)
exit_sys("recv");
if (result == 0)
break;
2024-06-09 22:42:45 +03:00
buf[result] = '\0';
2024-06-09 22:42:45 +03:00
printf("%d bytes received: %s\n", result, buf);
}
2024-06-09 22:42:45 +03:00
shutdown(client_sock, SHUT_RDWR);
close(client_sock);
return 0;
}
2024-06-09 22:42:45 +03:00
void exit_sys(const char *msg)
{
2024-06-09 22:42:45 +03:00
perror(msg);
2024-06-09 22:42:45 +03:00
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Aslında UNIX/Linux sistemlerinde her client bağlantısında o client ile yeni bir proses yaratılıp o prosesin konuşması da sağlanabilir. Yani server
client ile bağlantı yaptığında fork yapar. Sonra alt proseste client ile konuşur. Bu tür uygulamalarda hortlak (zombie) oluşumun engellenmesi gerekir.
Aşağıdaki kodda sigaction fonksiyonuyla hortlak proses oluşması engellenmiştir. Tabii fork modeli aslında thread modeline göre daha kötü bir modeldir.
Ancak daha pratiktir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* fork-server.c */
2024-06-09 22:42:45 +03:00
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define BUFFER_SIZE 1024
2024-06-09 22:42:45 +03:00
void exit_sys(const char *msg);
void client_proc(int sock, const char *addr, int port);
void reverse_str(char *str);
int main(void)
{
2024-06-09 22:42:45 +03:00
int server_sock, client_sock;
struct sockaddr_in sin_server, sin_client;
socklen_t addrlen;
pid_t pid;
char *addr;
int port;
struct sigaction act;
act.sa_handler = SIG_IGN;
act.sa_flags = SA_NOCLDWAIT;
if (sigaction(SIGCHLD, &act, 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_PORTNO);
sin_server.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1)
exit_sys("bind");
if (listen(server_sock, 8) == -1)
exit_sys("listen");
printf("Waiting for client connection....\n");
for (;;) {
addrlen = sizeof(sin_client);
if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &addrlen)) == -1)
exit_sys("accept");
addr = inet_ntoa(sin_client.sin_addr);
port = ntohs(sin_client.sin_port);
printf("Connected: %s:%d\n", addr, port);
if ((pid = fork()) == -1)
exit_sys("fork");
if (pid == 0) {
client_proc(client_sock, addr, port);
exit(EXIT_SUCCESS);
}
}
shutdown(server_sock, SHUT_RDWR);
close(server_sock);
return 0;
}
2024-06-09 22:42:45 +03:00
void client_proc(int sock, const char *addr, int port)
{
2024-06-09 22:42:45 +03:00
int result;
char buf[BUFFER_SIZE];
for (;;) {
if ((result = recv(sock, buf, BUFFER_SIZE - 1, 0)) == -1)
goto FAILED;
if (result == 0)
break;
buf[result] = '\0';
if (!strcmp(buf, "exit"))
break;
printf("%d bytes received from %s:%d => \"%s\"\n", result, addr, port, buf);
reverse_str(buf);
if (send(sock, buf, strlen(buf), 0) == -1)
goto FAILED;
}
FAILED:
printf("Client %s:%d exits...\n", addr, port);
shutdown(sock, SHUT_RDWR);
close(sock);
}
2024-06-09 22:42:45 +03:00
void reverse_str(char *str)
{
2024-06-09 22:42:45 +03:00
size_t len, i;
char temp;
for (len = 0; str[len] != '\0'; ++len)
;
for (i = 0; i < len / 2; ++i) {
temp = str[i];
str[i] = str[len - i - 1];
str[len - i - 1] = temp;
}
}
2024-06-09 22:42:45 +03:00
void exit_sys(const char *msg)
{
2024-06-09 22:42:45 +03:00
perror(msg);
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/* client.c */
#include <stdio.h>
2024-06-09 22:42:45 +03:00
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define CLIENT_PORTNO 62000
#define BUFFER_SIZE 1024
#define SERVER_NAME "127.0.0.1"
void exit_sys(const char *msg);
int main(void)
{
2024-06-09 22:42:45 +03:00
int client_sock;
struct sockaddr_in sin_server;
struct hostent *hostent;
char *str;
char buf[BUFFER_SIZE];
int result;
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(CLIENT_PORTNO);
sin_client.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1)
exit_sys("socket");
*/
sin_server.sin_family = AF_INET;
sin_server.sin_port = htons(SERVER_PORTNO);
sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME);
if (sin_server.sin_addr.s_addr == INADDR_NONE) {
if ((hostent = gethostbyname(SERVER_NAME)) == NULL)
exit_sys("gethostbyname");
memcpy(&sin_server.sin_addr.s_addr, hostent->h_addr_list[0], hostent->h_length);
}
if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1)
exit_sys("gethostbyname");
printf("Connected...\n");
for (;;) {
printf("Text:");
fgets(buf, BUFFER_SIZE, stdin);
if ((str = strchr(buf, '\n')) != NULL)
*str = '\0';
if (send(client_sock, buf, strlen(buf), 0) == -1)
exit_sys("send");
if (!strcmp(buf, "exit"))
break;
if ((result = recv(client_sock, buf, BUFFER_SIZE - 1, 0)) == -1)
exit_sys("recv");
if (result == 0)
break;
2024-06-09 22:42:45 +03:00
buf[result] = '\0';
printf("%d bytes received: %s\n", result, buf);
}
shutdown(client_sock, SHUT_RDWR);
close(client_sock);
return 0;
}
2024-06-09 22:42:45 +03:00
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Şüphesiz TCP/IP uygulamalarında client ve server programların hangi dille ve hangi işletim sisteminde yazıldığının bir önemi yoktur. Önemli olan
bizim programımızda uyguladığımız "uygulama katmanı protokolüne" uygunluktur.
2024-06-09 22:42:45 +03:00
Aşağıdaki örnekte yukarıdaki programın client kısmının C# karşılığı verilmiştir. Bu tür dillerde kendi içerisindkei sınıflar yoluyla pek çok işlem
arka planda otomatik yapılabilmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
using System;
using System.Net.Sockets;
using System.Text;
2024-06-09 22:42:45 +03:00
namespace CSD
{
class App
{
public const int SERVER_PORTNO = 55000;
public static void Main()
{
try
{
TcpClient tcpClient = new TcpClient("127.0.0.1", SERVER_PORTNO);
string text;
byte[] buf;
int result;
2024-06-09 22:42:45 +03:00
for (; ; )
{
Console.Write("Text:");
text = Console.ReadLine();
if (text == "exit")
break;
buf = Encoding.UTF8.GetBytes(text);
tcpClient.Client.Send(buf);
2024-06-09 22:42:45 +03:00
buf = new byte[1024];
result = tcpClient.Client.Receive(buf);
text = Encoding.UTF8.GetString(buf, 0, result);
Console.WriteLine(text);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
}
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Bir soketten garantili biçimde n byte okumak ve sokete garantili biçimde n byte yamakiçin aşağıdaki gibi iki fonksiyon kullanılabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
int ReadSocket(SOCKET sock, char *buf, int size)
{
int left, count;
int result;
2024-06-09 22:42:45 +03:00
left = size;
count = 0;
while (left > 0) {
if ((result = recv(sock, buf + count, left, 0)) == SOCKET_ERROR)
return SOCKET_ERROR;
if (result == 0)
break;
left -= result;
count += result;
}
return count;
}
int WriteSocket(SOCKET sock, const char *buf, int size)
{
int left, count;
int result;
left = size;
count = 0;
while (left > 0) {
if ((result = send(sock, buf + count, left, 0)) == SOCKET_ERROR)
return SOCKET_ERROR;
if (result == 0)
break;
left -= result;
count += result;
}
return count;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Garantili n byte okumak için UNIX/Linux sistemlerinde aşağdaki gibi bir fonksiyon yazılabilir. Bu sistemlerde blokeli modda zaten send fonksiyonu
tüm bilgi network tampouna yazılana kadar bloke oluşturmaktadır. Windows sistemleri (Winsock) bunun garantisini vermemektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
int read_socket(int sock, char *buf, int size)
{
int left, count;
int result;
left = size;
count = 0;
while (left > 0) {
if ((result = recv(sock, buf + count, left, 0)) == -1)
return -1;
if (result == 0)
break;
left -= result;
count += result;
}
return count;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Text tabanlı mesajlaşmada bir taraf diğerine sonu \n ile biten ya da \r\n ile biten bir satırlık yazı gönderir. Karşı taraf bu yazıyı parse eder
ve gerekeni yapar. Ancak soketten bir satırlık bilginin okunması o kadar kolay değildir. Soketten karakter karakter okuma yapmak kötü bir tekniktir.
Onun yerine bir grup byte bir tampona yerleştirilip o tampondan '\n' ya da '\r\n' görünene kadar ilerlenir. Aşağıda Effective TCP/IP kitabında da
benzeri bulunan soketten bir satır okuyan bir fonksiyon örneği verilmiştir. Bu fonksiyon karşı taraf bir satır okunmadan soketi kapatmışsa 0 değerine geri döner.
Normal durumda satırdaki byte sayısına geri dönmektedir. Soket hatasında ise fonksiyon -1 değerine geri dönmektedir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
int ReadLineSocket(SOCKET sock, char *buf, int size)
{
char *bufx = buf;
static char *bp;
static int count = 0;
static char b[2048];
char ch;
while (--size > 0) {
if (--count <= 0) {
count = recv(sock, b, sizeof(b), 0);
if (count == SOCKET_ERROR)
return SOCKET_ERROR;
if (count == 0)
return 0;
bp = b;
}
ch = *bp++;
*buf++ = ch;
if (ch == '\n') {
*buf = '\0';
return buf - bufx;
}
}
return SOCKET_ERROR;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukardaki fonksiyonun UNIX/Linux sistemlerindeki karşılığı olan fonksiyon şöyle yazılabilir:
-------------------------------------------------------------------------------------------------------------------------------------------*/
int readline_socket(int sock, char *buf, int size)
{
char *bufx = buf;
static char *bp;
static int count = 0;
static char b[2048];
char ch;
while (--size > 0) {
if (--count <= 0) {
count = recv(sock, b, sizeof(b), 0);
if (count == -1)
return -1;
if (count == 0)
return 0;
bp = b;
}
ch = *bp++;
*buf++ = ch;
if (ch == '\n') {
*buf = '\0';
return buf - bufx;
}
}
return -1;
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yazısal biçimde komutlar alarak dört işlem yapan örnek bir client/server TCP uygulaması aşaüıda verilmiştir. Uygulamada server için
thread modeli, kullanılmıştır. Yani her client bağlandıkça server bir thread yaratır ve client ile o thread konuşur. Buradaki uygulama katmanı protokolü şöyledir:
- Client server'a fiziksel olarak bağlandıktan sonra mantıksal bağlantı oluşturmak için ona LOGIN komutunu gönderir. Komutun genel biçimi şöyledir:
LOGIN user_name password\n
Server kendisine kayıtlı olan kullanıcıları gözden geçirip bu istedği kabul eder ya da etmez. Eğer server bağlantı isteğini kabul ederse client'a
LOGIN_ACCEPTED mesajı göndermktedir. Bu mesaj şöyledir:
LOGIN_ACCEPTED\n
Bundan sonra client server'a aşağıdaki komutları gönderir:
ADD op1 ope\n
SUB op1 op2\n
MUL op1 op2\n
DIV op1 op2\n
Server'da bu hesaplamaları yaparak client'a sonucu RESULT mesajı ile göndermektedir:
RESULT result\n
Eğer client server'a uygun bir komnut göndermezse server client'a ERROR mesajı ile yanıt vermektedir:
ERROR mesaj\n
Client server'a gönderdiği her mesaj için server da client'a bir mesaj göndermektedir. İletişim yine el sıkışmayla sonlandırılmaktadır. Bunun için
client server'a LOGOUT komutunu gönderir. Server kabul ederse client'a LOGOUT_ACCEPTED mesajıyla yanıt verir. Artık ikiş taraf soketlerini kapatarak
iletişimi sonlandırırlar.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* Server.c */
#include <stdio.h>
2024-06-09 22:42:45 +03:00
#include <stdlib.h>
#include <winsock2.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define BUFFER_SIZE 2048
#define MAX_PARAMS 32
typedef struct tagCLIENT_REQUEST {
char buf[BUFFER_SIZE];
char *params[MAX_PARAMS + 1];
int nparams;
} CLIENT_REQUEST;
typedef struct tagCLIENT_INFO {
SOCKET sock;
char addr[16];
int port;
} CLIENT_INFO;
typedef struct tagUSER_INFO {
char *userName;
char *password;
} USER_INFO;
enum COMMANDS {
LOGIN_COMMAND,
ADD_COMMAND,
SUB_COMMAND,
MUL_COMMAND,
DIV_COMMAND,
LOGOUT_COMMAND,
};
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
DWORD __stdcall ClientThreadProc(LPVOID lpParam);
int ReadLineSocket(SOCKET sock, char *buf, int size);
int WriteSocket(SOCKET sock, const char *buf, int size);
void ParseCommand(char *buf, CLIENT_REQUEST *clientRequest);
BOOL ProcessCommand(const CLIENT_INFO *clientInfo, const CLIENT_REQUEST *clientRequest);
BOOL LoginCommandProc(const CLIENT_REQUEST *clientRequest, char *buf);
void OpCommandProc(const CLIENT_REQUEST *clientRequest, int commandCode, char *buf);
void LogoutCommandProc(const CLIENT_REQUEST *clientRequest, char *buf);
USER_INFO g_users[] = {
{"student", "12345"},
{"kaan", "xxxxx"},
{NULL, NULL}
};
int main(void)
{
WSADATA wsaData;
DWORD dwResult;
SOCKET serverSock, clientSock;
struct sockaddr_in sinServer, sinClient;
int addrLen;
HANDLE hThread;
DWORD dwThreadId;
CLIENT_INFO *clientInfo;
if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", dwResult);
if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) /* third parameter IPPROTO_TCP */
ExitSys("socket", WSAGetLastError());
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(SERVER_PORTNO);
sinServer.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(serverSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
if (listen(serverSock, 8) == SOCKET_ERROR)
ExitSys("listen", WSAGetLastError());
printf("Waiting for client connection....\n");
for (;;) {
addrLen = sizeof(sinClient);
if ((clientSock = accept(serverSock, (struct sockaddr *)&sinClient, &addrLen)) == INVALID_SOCKET)
ExitSys("accept", WSAGetLastError());
if ((clientInfo = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) {
fprintf(stderr, "Cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
clientInfo->sock = clientSock;
strcpy(clientInfo->addr, inet_ntoa(sinClient.sin_addr));
clientInfo->port = ntohs(sinClient.sin_port);
printf("Connected client %s:%d\n", clientInfo->addr, clientInfo->port);
if ((hThread = CreateThread(NULL, 0, ClientThreadProc, clientInfo, 0, &dwThreadId)) == NULL)
ExitSys("CreateThread", WSAGetLastError());
CloseHandle(hThread);
}
closesocket(serverSock);
WSACleanup();
return 0;
}
DWORD __stdcall ClientThreadProc(LPVOID lpParam)
{
CLIENT_INFO *clientInfo = (CLIENT_INFO *)lpParam;
char buf[BUFFER_SIZE];
int result;
CLIENT_REQUEST clientRequest;
for (;;) {
if ((result = ReadLineSocket(clientInfo->sock, buf, BUFFER_SIZE)) == SOCKET_ERROR) {
fprintf(stderr, "Error: ReadLineSocket, ClientConnection is closing...\n");
break;
}
if (result == 0)
break;
printf("Client request: %s", buf);
ParseCommand(buf, &clientRequest);
if (!ProcessCommand(clientInfo, &clientRequest))
break;
}
shutdown(clientInfo->sock, SD_BOTH);
closesocket(clientInfo->sock);
free(clientInfo);
return 0;
}
int ReadLineSocket(SOCKET sock, char *buf, int size)
{
char *bufx = buf;
static char *bp;
static int count = 0;
static char b[2048];
char ch;
while (--size > 0) {
if (--count <= 0) {
count = recv(sock, b, sizeof(b), 0);
if (count == SOCKET_ERROR)
return SOCKET_ERROR;
if (count == 0)
return 0;
bp = b;
}
ch = *bp++;
*buf++ = ch;
if (ch == '\n') {
*buf = '\0';
return (int)(buf - bufx);
}
}
return SOCKET_ERROR;
}
int WriteSocket(SOCKET sock, const char *buf, int size)
{
int left, count;
int result;
left = size;
count = 0;
while (left > 0) {
if ((result = send(sock, buf + count, left, 0)) == SOCKET_ERROR)
return SOCKET_ERROR;
if (result == 0)
break;
left -= result;
count += result;
}
return count;
}
void ParseCommand(char *buf, CLIENT_REQUEST *clientRequest)
{
char *str;
int count;
strcpy(clientRequest->buf, buf);
for (count = 0, str = strtok(buf, " \t\n"); str != NULL && count < MAX_PARAMS; str = strtok(NULL, " \t\n"), ++count)
clientRequest->params[count] = str;
clientRequest->nparams = count;
}
BOOL ProcessCommand(const CLIENT_INFO *clientInfo, const CLIENT_REQUEST *clientRequest)
{
static char *clientRequests[] = { "LOGIN", "ADD", "SUB", "MUL", "DIV", "LOGOUT", NULL};
int i;
int result;
char buf[BUFFER_SIZE];
BOOL retVal = TRUE;
if (clientRequest->nparams == 0) {
sprintf(buf, "ERROR Empty command\n", clientRequest->params[0]);
if ((result = WriteSocket(clientInfo->sock, buf, (int)strlen(buf))) == SOCKET_ERROR)
fprintf(stderr, "Error: WriteSocket\n");
return retVal;
}
for (i = 0; clientRequests[i] != NULL; ++i)
if (!strcmp(clientRequests[i], clientRequest->params[0]))
break;
if (clientRequests[i] == NULL) {
sprintf(buf, "ERROR Command not found \"%s\"\n", clientRequest->params[0]);
if ((result = WriteSocket(clientInfo->sock, buf, (int)strlen(buf))) == SOCKET_ERROR)
fprintf(stderr, "Error: WriteSocket\n");
return retVal;
}
switch (i) {
case LOGIN_COMMAND:
if (!LoginCommandProc(clientRequest, buf)) {
retVal = FALSE;
break;
}
break;
case ADD_COMMAND:
OpCommandProc(clientRequest, ADD_COMMAND, buf);
break;
case SUB_COMMAND:
OpCommandProc(clientRequest, SUB_COMMAND, buf);
break;
case MUL_COMMAND:
OpCommandProc(clientRequest, MUL_COMMAND, buf);
break;
case DIV_COMMAND:
OpCommandProc(clientRequest, DIV_COMMAND, buf);
break;
case LOGOUT_COMMAND:
LogoutCommandProc(clientRequest, buf);
retVal = FALSE;
break;
}
if ((result = WriteSocket(clientInfo->sock, buf, (int)strlen(buf))) == SOCKET_ERROR) {
fprintf(stderr, "Error: WriteSocket\n");
retVal = FALSE;
}
return retVal;
}
void OpCommandProc(const CLIENT_REQUEST *clientRequest, int commandCode, char *buf)
{
double op1, op2, opResult;
if (clientRequest->nparams == 3)
if (sscanf(clientRequest->params[1], "%lf", &op1) == 1)
if (sscanf(clientRequest->params[2], "%lf", &op2) == 1) {
switch (commandCode) {
case ADD_COMMAND:
opResult = op1 + op2;
break;
case SUB_COMMAND:
opResult = op1 - op2;
break;
case MUL_COMMAND:
opResult = op1 * op2;
break;
case DIV_COMMAND:
opResult = op1 / op2;
break;
}
sprintf(buf, "RESULT %f\n", opResult);
}
else
sprintf(buf, "ERROR Invalid command operand\n");
else
sprintf(buf, "ERROR Invalid command operand\n");
else
sprintf(buf, "ERROR Invalid command operand\n");
}
BOOL LoginCommandProc(const CLIENT_REQUEST *clientRequest, char *buf)
{
int i;
for (i = 0; g_users[i].userName != NULL; ++i) {
if (!strcmp(g_users[i].userName, clientRequest->params[1]) && !strcmp(g_users[i].password, clientRequest->params[2])) {
sprintf(buf, "LOGIN_ACCEPTED\n");
return TRUE;
}
}
sprintf(buf, "ERROR Invalid user name or password\n");
return FALSE;
}
void LogoutCommandProc(const CLIENT_REQUEST *clientRequest, char *buf)
{
if (clientRequest->nparams == 1)
sprintf(buf, "LOGOUT_ACCEPTED Logout Accepted\n");
else
sprintf(buf, "ERROR Invalid command operand\n");
}
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/* Client.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define SERVER_PORTNO 55000
#define CLIENT_PORTNO 62000
#define SERVER_NAME "185.86.155.34"
#define BUFFER_SIZE 2048
#define MAX_USER_NAME 32
#define MAX_PASSWORD 32
typedef struct tagSERVER_RESPONSE {
char *command;
char *text;
} SERVER_RESPONSE;
enum COMMANDS {
LOGIN_ACCEPTED_COMMAND,
RESULT_COMMAND,
ERROR_COMMAND,
LOGOUT_ACCEPTED_COMMAND,
};
int ReadLineSocket(SOCKET sock, char *buf, int size);
int WriteSocket(SOCKET sock, const char *buf, int size);
BOOL LoginProc(SOCKET clientSock, char *buf, SERVER_RESPONSE *serverResponse);
void ParseCommand(char *buf, SERVER_RESPONSE *serverResponse);
BOOL ProcessCommand(const SERVER_RESPONSE *serverResponse);
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
int main(void)
{
WSADATA wsaData;
DWORD dwResult;
SOCKET clientSock;
struct sockaddr_in sinServer;
struct hostent *hostEnt;
char buf[BUFFER_SIZE];
int result;
SERVER_RESPONSE serverResponse;
if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", dwResult);
if ((clientSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
ExitSys("socket", WSAGetLastError());
/*
{
struct sockaddr_in sinClient;
sinClient.sin_family = AF_INET;
sinClient.sin_port = htons(CLIENT_PORTNO);
sinClient.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(clientSock, (struct sockaddr *)&sinClient, sizeof(sinClient)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
}
*/
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(SERVER_PORTNO);
sinServer.sin_addr.s_addr = inet_addr(SERVER_NAME);
if (sinServer.sin_addr.s_addr == INADDR_NONE) {
if ((hostEnt = gethostbyname(SERVER_NAME)) == NULL)
ExitSys("gethostbyname", WSAGetLastError());
memcpy(&sinServer.sin_addr.s_addr, hostEnt->h_addr_list[0], hostEnt->h_length);
}
if (connect(clientSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("connect", WSAGetLastError());
printf("Connected...\n");
if (!LoginProc(clientSock, buf, &serverResponse))
goto FAILED;
printf("Client connected...\n");
for (;;) {
printf("Calculator>");
fflush(stdout);
fgets(buf, BUFFER_SIZE, stdin);
if ((result = WriteSocket(clientSock, buf, (int)strlen(buf))) == SOCKET_ERROR)
ExitSys("WriteSocket", WSAGetLastError());
if (result == 0)
break;
if ((result = ReadLineSocket(clientSock, buf, BUFFER_SIZE)) == SOCKET_ERROR)
ExitSys("ReadLineSocket", WSAGetLastError());
if (result == 0)
break;
ParseCommand(buf, &serverResponse);
if (!ProcessCommand(&serverResponse))
break;
}
FAILED:
shutdown(clientSock, SD_BOTH);
closesocket(clientSock);
WSACleanup();
return 0;
}
int ReadLineSocket(SOCKET sock, char *buf, int size)
{
char *bufx = buf;
static char *bp;
static int count = 0;
static char b[2048];
char ch;
while (--size > 0) {
if (--count <= 0) {
count = recv(sock, b, sizeof(b), 0);
if (count == SOCKET_ERROR)
return SOCKET_ERROR;
if (count == 0)
return 0;
bp = b;
}
ch = *bp++;
*buf++ = ch;
if (ch == '\n') {
*buf = '\0';
return (int) (buf - bufx);
}
}
return SOCKET_ERROR;
}
int WriteSocket(SOCKET sock, const char *buf, int size)
{
int left, count;
int result;
left = size;
count = 0;
while (left > 0) {
if ((result = send(sock, buf + count, left, 0)) == SOCKET_ERROR)
return SOCKET_ERROR;
if (result == 0)
break;
left -= result;
count += result;
}
return count;
}
BOOL LoginProc(SOCKET clientSock, char *buf, SERVER_RESPONSE *serverResponse)
{
char userName[MAX_USER_NAME];
char password[MAX_PASSWORD];
int result;
char *str;
printf("User name:");
fgets(userName, MAX_USER_NAME, stdin);
if ((str = strchr(userName, '\n')) != NULL)
*str = '\0';
printf("Password:");
fgets(password, MAX_PASSWORD, stdin);
if ((str = strchr(password, '\n')) != NULL)
*str = '\0';
sprintf(buf, "LOGIN %s %s\n", userName, password);
if ((result = WriteSocket(clientSock, buf, (int)strlen(buf))) == SOCKET_ERROR || result == 0) {
fprintf(stderr, "Error WriteSocket!..\n");
return FALSE;
}
/* buffer problem */
if ((result = ReadLineSocket(clientSock, buf, BUFFER_SIZE)) == SOCKET_ERROR || result == 0) {
fprintf(stderr, "Error ReadLineSocket!..\n");
return FALSE;
}
ParseCommand(buf, serverResponse);
if (strcmp(serverResponse->command, "LOGIN_ACCEPTED") != 0) {
printf("%s\n", serverResponse->text);
return FALSE;
}
return TRUE;
}
void ParseCommand(char *buf, SERVER_RESPONSE *serverResponse)
{
char *str = buf;
2024-06-09 22:42:45 +03:00
str = buf;
serverResponse->command = str;
2024-06-09 22:42:45 +03:00
while (*buf != ' ' && *buf != '\n')
++buf;
if (*buf == '\n') {
*buf = '\0';
serverResponse->text = NULL;
return;
}
*buf++ = '\0';
str = buf;
while (*buf != '\n')
++buf;
*buf = '\0';
serverResponse->text = str;
}
2024-06-09 22:42:45 +03:00
BOOL ProcessCommand(const SERVER_RESPONSE *serverResponse)
{
2024-06-09 22:42:45 +03:00
static char *serverCommands[] = { "LOGIN_ACCEPTED", "RESULT", "ERROR", "LOGOUT_ACCEPTED", NULL};
int i;
BOOL retVal = TRUE;
2024-06-09 22:42:45 +03:00
for (i = 0; serverCommands[i] != NULL; ++i)
if (!strcmp(serverCommands[i], serverResponse->command))
break;
2024-06-09 22:42:45 +03:00
switch (i) {
case RESULT_COMMAND:
case ERROR_COMMAND:
printf("%s\n", serverResponse->text);
break;
case LOGOUT_ACCEPTED_COMMAND:
printf("%s\n", serverResponse->text);
retVal = FALSE;
break;
default:
printf("Unknown server command: %s\n", serverResponse->text);
}
2024-06-09 22:42:45 +03:00
return retVal;
}
2024-06-09 22:42:45 +03:00
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
2024-06-09 22:42:45 +03:00
LPTSTR lpszErr;
2024-06-09 22:42:45 +03:00
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
2024-06-09 22:42:45 +03:00
Aslında IP ailesinin uygulama katmanındaki protokollerin büyük çoğunluğu aslında yazısal kıomutlarla yukarıdakşi örnekte olduğu gibi
haberleşme yapmaktadır. Örneğin POP3 protokolünde çnce client server'a kendini aşaüıdaki komut ile tanıtır:
2024-06-09 22:42:45 +03:00
USER user_name\r\n
PASS password\r\n
2024-06-09 22:42:45 +03:00
Sonra client server'daki posta kutusundaki postalar hakkında bilgiyi LIST komutuyla elde eder. Her postanın bir indeks numarası vardır. Client
RETR komutuyla indeks numarasını belirterek server'dan posta kutusundaki belli bir postayı istemektedir. Belli bir postayı silmek için client
server'a DELE komutunu gönderir. Tüm komutlar yazısal olarak sonu CR/LF ile bitecek biçimde gönderilmktedir. Server her komut için client'a bir
yanıt göndermektedir. Olumlu yanıtlar için +OK, olumsuz yanıtlar için -ERR mesaj komutları kullanılmaktadır. Protokolün detayları için RFC-1939
dokümanına başvurabilirsiniz.
2024-06-09 22:42:45 +03:00
Aşağıdaki örnekte komut satırından girilen komutlar POP3 server'a gönderilip oradaki yanıtlar alınarak ekrana yazdırılmıştır. Bıu işlemi telnet
denilen hazır programla da yapabilirsiniz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* pop3.c */
#include <stdio.h>
2024-06-09 22:42:45 +03:00
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 110
#define BUFFER_SIZE 2048
#define SERVER_NAME "mail.csystem.org"
2024-06-09 22:42:45 +03:00
int ReadLineSocket(SOCKET sock, char *buf, int size);
int WriteSocket(SOCKET sock, const char *buf, int size);
DWORD __stdcall Pop3ReadThreadProc(LPVOID lpvParam);
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
int main(void)
{
2024-06-09 22:42:45 +03:00
WSADATA wsaData;
DWORD dwResult;
SOCKET clientSock;
struct sockaddr_in sinServer;
struct hostent *hostEnt;
char buf[BUFFER_SIZE + 1];
int result;
char *str;
HANDLE hThread;
DWORD dwThreadId;
2024-06-09 22:42:45 +03:00
if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", dwResult);
2024-06-09 22:42:45 +03:00
if ((clientSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
ExitSys("socket", WSAGetLastError());
/*
{
struct sockaddr_in sinClient;
2024-06-09 22:42:45 +03:00
sinClient.sin_family = AF_INET;
sinClient.sin_port = htons(CLIENT_PORTNO);
sinClient.sin_addr.s_addr = htonl(INADDR_ANY);
2024-06-09 22:42:45 +03:00
if (bind(clientSock, (struct sockaddr *)&sinClient, sizeof(sinClient)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
}
*/
2024-06-09 22:42:45 +03:00
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(SERVER_PORTNO);
2024-06-09 22:42:45 +03:00
sinServer.sin_addr.s_addr = inet_addr(SERVER_NAME);
if (sinServer.sin_addr.s_addr == INADDR_NONE) {
if ((hostEnt = gethostbyname(SERVER_NAME)) == NULL)
ExitSys("gethostbyname", WSAGetLastError());
2024-06-09 22:42:45 +03:00
memcpy(&sinServer.sin_addr.s_addr, hostEnt->h_addr_list[0], hostEnt->h_length);
}
if (connect(clientSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("connect", WSAGetLastError());
2024-06-09 22:42:45 +03:00
if ((hThread = CreateThread(NULL, 0, Pop3ReadThreadProc, (LPVOID)clientSock, 0, &dwThreadId)) == NULL)
ExitSys("CreateThread", GetLastError());
2024-06-09 22:42:45 +03:00
for (;;) {
Sleep(500);
2024-06-09 22:42:45 +03:00
printf("POP3>");
fflush(stdout);
fgets(buf, BUFFER_SIZE, stdin);
if ((str = strchr(buf, '\n')) != NULL)
*str = '\0';
2024-06-09 22:42:45 +03:00
strcat(buf, "\r\n");
if ((result = WriteSocket(clientSock, buf, (int)strlen(buf))) == SOCKET_ERROR)
ExitSys("WriteSocket", WSAGetLastError());
if (result == 0) {
printf("Other side closed connection...\n");
break;
}
if (!strcmp(buf, "QUIT\r\n"))
break;
2024-06-09 22:42:45 +03:00
}
shutdown(clientSock, SD_BOTH);
closesocket(clientSock);
CloseHandle(hThread);
2024-06-09 22:42:45 +03:00
WSACleanup();
2024-06-09 22:42:45 +03:00
return 0;
}
2024-06-09 22:42:45 +03:00
int ReadLineSocket(SOCKET sock, char *buf, int size)
{
2024-06-09 22:42:45 +03:00
char *bufx = buf;
static char *bp;
static int count = 0;
static char b[2048];
char ch;
2024-06-09 22:42:45 +03:00
while (--size > 0) {
if (--count <= 0) {
count = recv(sock, b, sizeof(b), 0);
if (count == SOCKET_ERROR)
return SOCKET_ERROR;
if (count == 0)
return 0;
bp = b;
}
ch = *bp++;
*buf++ = ch;
if (ch == '\n') {
buf[-2] = '\0';
return (int)(buf - bufx);
}
}
2024-06-09 22:42:45 +03:00
return SOCKET_ERROR;
}
2024-06-09 22:42:45 +03:00
int WriteSocket(SOCKET sock, const char *buf, int size)
{
int left, count;
int result;
2024-06-09 22:42:45 +03:00
left = size;
count = 0;
2024-06-09 22:42:45 +03:00
while (left > 0) {
if ((result = send(sock, buf + count, left, 0)) == SOCKET_ERROR)
return SOCKET_ERROR;
if (result == 0)
break;
left -= result;
count += result;
}
2024-06-09 22:42:45 +03:00
return count;
}
2024-06-09 22:42:45 +03:00
DWORD __stdcall Pop3ReadThreadProc(LPVOID lpvParam)
{
char buf[BUFFER_SIZE + 1];
int result;
SOCKET sock = (SOCKET)lpvParam;
2024-06-09 22:42:45 +03:00
for (;;) {
if ((result = recv(sock, buf, BUFFER_SIZE, 0)) == SOCKET_ERROR)
ExitSys("recv", WSAGetLastError());
buf[result] = '\0';
printf("%s", buf);
}
2024-06-09 22:42:45 +03:00
return 0;
}
2024-06-09 22:42:45 +03:00
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
LPTSTR lpszErr;
2024-06-09 22:42:45 +03:00
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
2024-06-09 22:42:45 +03:00
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
UDP bağlantısız (connectionless), datagram tabanlı, güvenli olmayan hızlı bir protokoldür. Aslında IP protokolünün kendisine çok benzemektedir.
Bilgileri paket halinde gönderir. Hiç bağlantı yapmadan bilgi gönderimşi ve alımı mümkündür. Server taraf soketi yarattıktan sonra yine bind eder.
Sonra da döngü içerisinde recvfrom fonksiyonu ile datagram paketlerini okur. Clieng taraf bilgi alımı yapmayacaksa bind uygulamaz. Doğrudan
soketi yaratıp sendto fonksiyonu ile gönderimi yapar. Genellikle UDP'de alan tarafa server, gönderen tarafa client denilmektedir. Ancak client ve
server rolleri bu protokolde çok belli değildir. Bir server'a hiç bağlantı yapmadan pek çok cliengt bilgi gönderebilir. recvfrom fonksiyonu
datagram paketini alırken aynı zamanda onun kişmden geldiğini de elde etmektedir.
2024-06-09 22:42:45 +03:00
Aşağıdaki programda tipik bir UDP server örneği verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* Server.c */
#include <stdio.h>
#include <stdlib.h>
2024-06-09 22:42:45 +03:00
#include <winsock2.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define BUFFER_SIZE 1024
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
int main(void)
{
2024-06-09 22:42:45 +03:00
WSADATA wsaData;
DWORD dwResult;
SOCKET serverSock;
struct sockaddr_in sinServer, sinClient;
int addrLen;
char buf[BUFFER_SIZE + 1];
int result;
2024-06-09 22:42:45 +03:00
if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", dwResult);
2024-06-09 22:42:45 +03:00
if ((serverSock = socket(AF_INET, SOCK_DGRAM, 0)) == INVALID_SOCKET) /* third parameter IPPROTO_UDP */
ExitSys("socket", WSAGetLastError());
2024-06-09 22:42:45 +03:00
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(SERVER_PORTNO);
sinServer.sin_addr.s_addr = htonl(INADDR_ANY);
2024-06-09 22:42:45 +03:00
if (bind(serverSock, (struct sockaddr*)&sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("bind", WSAGetLastError());
2024-06-09 22:42:45 +03:00
printf("Waiting for messages...\n");
for (;;) {
addrLen = sizeof(sinClient);
if ((result = recvfrom(serverSock, buf, BUFFER_SIZE, 0, (struct sockaddr*)&sinClient, &addrLen)) == SOCKET_ERROR)
ExitSys("recvfrom", GetLastError());
buf[result] = '\0';
printf("%d bytes received from %s:%d \"%s\"\n", result, inet_ntoa(sinClient.sin_addr), ntohs(sinClient.sin_port), buf);
}
2024-06-09 22:42:45 +03:00
closesocket(serverSock);
WSACleanup();
return 0;
}
2024-06-09 22:42:45 +03:00
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/* Client.c */
#include <stdio.h>
2024-06-09 22:42:45 +03:00
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define BUFFER_SIZE 1024
#define SERVER_NAME "127.0.0.1"
2024-06-09 22:42:45 +03:00
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr);
2024-06-09 22:42:45 +03:00
int main(void)
{
2024-06-09 22:42:45 +03:00
WSADATA wsaData;
DWORD dwResult;
SOCKET clientSock;
struct sockaddr_in sinServer;
struct hostent *hostEnt;
char buf[BUFFER_SIZE];
char *str;
2024-06-09 22:42:45 +03:00
if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
ExitSys("WSAStartup", dwResult);
2024-06-09 22:42:45 +03:00
if ((clientSock = socket(AF_INET, SOCK_DGRAM, 0)) == INVALID_SOCKET)
ExitSys("socket", WSAGetLastError());
sinServer.sin_family = AF_INET;
sinServer.sin_port = htons(SERVER_PORTNO);
2024-06-09 22:42:45 +03:00
sinServer.sin_addr.s_addr = inet_addr(SERVER_NAME);
if (sinServer.sin_addr.s_addr == INADDR_NONE) {
if ((hostEnt = gethostbyname(SERVER_NAME)) == NULL)
ExitSys("gethostbyname", WSAGetLastError());
2024-06-09 22:42:45 +03:00
memcpy(&sinServer.sin_addr.s_addr, hostEnt->h_addr_list[0], hostEnt->h_length);
}
2024-06-09 22:42:45 +03:00
for (;;) {
printf("Text:");
2024-06-09 22:42:45 +03:00
fgets(buf, BUFFER_SIZE, stdin);
if ((str = strchr(buf, '\n')) != NULL)
*str = '\0';
if (!strcmp(buf, "exit"))
break;
2024-06-09 22:42:45 +03:00
if (sendto(clientSock, buf, (int)strlen(buf), 0, (struct sockaddr *) &sinServer, sizeof(sinServer)) == SOCKET_ERROR)
ExitSys("send", WSAGetLastError());
}
closesocket(clientSock);
2024-06-09 22:42:45 +03:00
WSACleanup();
2024-06-09 22:42:45 +03:00
return 0;
}
2024-06-09 22:42:45 +03:00
void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr)
{
LPTSTR lpszErr;
2024-06-09 22:42:45 +03:00
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
2024-06-09 22:42:45 +03:00
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıdaki programın UNIX/Linux sistemlerindeki karşılığı aşağıdaki gibi oluşturulabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
2024-06-09 22:42:45 +03:00
/* udp-server.c */
#include <stdio.h>
#include <stdlib.h>
2024-06-09 22:42:45 +03:00
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
2024-06-09 22:42:45 +03:00
#define SERVER_PORTNO 55000
#define BUFFER_SIZE 1024
2024-06-09 22:42:45 +03:00
void exit_sys(const char *msg);
2024-06-09 22:42:45 +03:00
int main(void)
{
int server_sock;
struct sockaddr_in sin_server, sin_client;
socklen_t addrlen;
char buf[BUFFER_SIZE + 1];
int result;
2024-06-09 22:42:45 +03:00
if ((server_sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
exit_sys("socket");
2024-06-09 22:42:45 +03:00
sin_server.sin_family = AF_INET;
sin_server.sin_port = htons(SERVER_PORTNO);
sin_server.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1)
exit_sys("bind");
2024-06-09 22:42:45 +03:00
printf("Waiting for messages...\n");
for (;;) {
addrlen = sizeof(sin_server);
if ((result = recvfrom(server_sock, buf, BUFFER_SIZE, 0, (struct sockaddr*)&sin_client, &addrlen)) == -1)
exit_sys("recvfrom");
buf[result] = '\0';
printf("%d bytes received from %s:%d \"%s\"\n", result, inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port), buf);
}
2024-06-09 22:42:45 +03:00
close(server_sock);
2024-06-09 22:42:45 +03:00
return 0;
}
2024-06-09 22:42:45 +03:00
void exit_sys(const char *msg)
{
perror(msg);
2024-06-09 22:42:45 +03:00
exit(EXIT_FAILURE);
}
2024-06-09 22:42:45 +03:00
/* udp-client.c */
#include <stdio.h>
#include <stdlib.h>
2024-06-09 22:42:45 +03:00
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define SERVER_PORTNO 55000
#define BUFFER_SIZE 1024
#define SERVER_NAME "127.0.0.1"
void exit_sys(const char *msg);
int main(void)
{
2024-06-09 22:42:45 +03:00
int client_sock;
struct sockaddr_in sin_server;
struct hostent *hoste;
char buf[BUFFER_SIZE];
char *str;
2024-06-09 22:42:45 +03:00
if ((client_sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
exit_sys("socket");
sin_server.sin_family = AF_INET;
sin_server.sin_port = htons(SERVER_PORTNO);
2024-06-09 22:42:45 +03:00
sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME);
if (sin_server.sin_addr.s_addr == INADDR_NONE) {
if ((hoste = gethostbyname(SERVER_NAME)) == NULL)
exit_sys("gethostbyname");
2024-06-09 22:42:45 +03:00
memcpy(&sin_server.sin_addr.s_addr, hoste->h_addr_list[0], hoste->h_length);
}
2024-06-09 22:42:45 +03:00
for (;;) {
printf("Text:");
2024-06-09 22:42:45 +03:00
fgets(buf, BUFFER_SIZE, stdin);
if ((str = strchr(buf, '\n')) != NULL)
*str = '\0';
if (!strcmp(buf, "exit"))
break;
2024-06-09 22:42:45 +03:00
if (sendto(client_sock, buf, (int)strlen(buf), 0, (struct sockaddr *) &sin_server, sizeof(sin_server)) == -1)
exit_sys("sendto");
}
close(client_sock);
2024-06-09 22:42:45 +03:00
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Eskiden diskler elektromekanik bir birim olarak üretiliyordu. Bunlara "hard disk" denilmektedir. Ancak artık son 10 senedir elektromekanik
harddisk kullanımı oldukça azalmıştır. Artık SSD denilen tamamen yarıiletken EEPROM teknolojisiyle üretilen bellekler disk olarak kullanılmaktadır.
İster hard disk olsun isterse SSD disk olsun bir diskten okunabilen ya da yazılabilen en küçük birime "sektör (sector)" denilmektedir. Yani örneğin biz diskten
1 byte okumak istersek aslında bu byte'ın içinde bulunduğu sektörü okumak durumunda kalırız. Benzer biçimde diskte 1 byte değiştirmek için önce
onun bulunduğu sektörü okuruz. Değişikliği ana bellek üzerinde yaparız. Sonra o sektörü yeniden diske yazarız. Bir sektör 512 byte uzunluğundadır.
Diskteki sektörleri görüntülemek ve güncellemek için kullanılan araçlara "disk editörleri" denilmektedir. Windows, UNIX/Linux ve Mac OS X için
çeşitli disk editörleri bulunmaktadır. Windows için bedava en iyi seçeneklerden biri HxD isimli programdır. Linux'ta komut satırından görüntüleme
yapan od gibi hd gibi programlar kullanılmaktadır. İyi bir GUI seçenek de wxHexEditor isimli programdır.
Donanımsal olarak nasıl RAM'deki her byte'ın bir adresi varsa disk biriminde de her sektörün bir numarası vardır. Numaralandırma ilk sektör 0 olmak üzere
artan sırada sayılarla yapılmaktadır. Sistem programcısı olarak bu alanda yapılacak ilk şey nasıl disk editörler sektörleri okuyup görüntüleyebiliyorsa
onların yaptığı gibi sektörleri okuyup yazabilmektir. Sektör okuyup yazabilmek için işletim sistemlerinde "aygıt sürücüler" bulundurulmuştur. Yani programcı
bu aygıt sürücüleri kullanarak sektör okuma yazma işlemlerini yapar.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Windows'ta diski bir bütün olarak ele alan aygıt sürücünün ismi \\.\PhysicalDriveN ismindedir. Buradaki N değeri birinci disk için 0, ikinci disk için 1
biçiminde artan sırada bir sayı belirtmektedir. Disk disk bölümlerinden (disk partitions) oluşmaktadır. Bir disk bölümüne "manıksal sürücü (logical drive)" da
denilmektedir. Disk bölümleri aslında bir diskin içerisindeki ardışıl N tane sektörden oluşmaktadır. Her disk bölümü kendi içerisinde sanki ayrı bir diskmiş gibi
davranabilmektedir. Bu biçimde disk bölümünü sanaki ayrı bir diskmiş gibi ele almak için \\.\X:" ismindeki aygıt sürücüler kullanılmaktadır.Burada X harfi sürücünün
harfini belirtmektedir.
Aşağıdaki örnekte Windows'ta aygıt sürücü kullanarak sektör okuması ve yazması yapan fonksiyonlar oluşturulmuştur.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* DiskIO.h */
/* DiskIO.h */
#ifndef DISKIO_H_
#define DISKIO_H_
#include <Windows.h>
/* Symbolic Constants */
#define BYTES_PER_SECTOR 512
/* Function Prototypes */
HANDLE OpenDisk(int disk, DWORD dwDesiredAccess);
HANDLE OpenVolume(int drive, DWORD dwDesiredAccess);
BOOL ReadSector(HANDLE hDrive, long long sector, DWORD count, void *buf);
BOOL WriteSector(HANDLE hDrive, long long sector, DWORD count, const void *buf);
#endif
/* DiskIO.c */
#include <stdio.h>
#include <Windows.h>
#include "DiskIO.h"
/* static Function Prototypes */
static BOOL SetFilePosition(HANDLE hDrive, long long distance, DWORD MoveMethod);
/* Function definitions */
HANDLE OpenDisk(int disk, DWORD dwDesiredAccess)
{
char driverName[32];
DWORD shareMode = 0;
sprintf(driverName, "\\\\.\\PhysicalDrive%d", disk);
if (dwDesiredAccess & GENERIC_READ)
shareMode |= FILE_SHARE_READ;
if (dwDesiredAccess & GENERIC_WRITE)
shareMode |= FILE_SHARE_WRITE;
return CreateFile(driverName, dwDesiredAccess, shareMode, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
}
HANDLE OpenVolume(int drive, DWORD dwDesiredAccess)
{
char driverName[32];
DWORD shareMode = 0;
sprintf(driverName, "\\\\.\\%c:", drive + 'A');
if (dwDesiredAccess & GENERIC_READ)
shareMode |= FILE_SHARE_READ;
if (dwDesiredAccess & GENERIC_WRITE)
shareMode |= FILE_SHARE_WRITE;
return CreateFile(driverName, dwDesiredAccess, shareMode, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
}
BOOL ReadSector(HANDLE hDrive, long long sector, DWORD count, void *buf)
{
long long location = (long long)sector * BYTES_PER_SECTOR;
DWORD dwBytes = count * BYTES_PER_SECTOR;
DWORD dwRead;
if (!SetFilePosition(hDrive, location, FILE_BEGIN))
return FALSE;
if (!ReadFile(hDrive, buf, dwBytes, &dwRead, NULL))
return FALSE;
return TRUE;
}
BOOL WriteSector(HANDLE hDrive, __int64 sector, DWORD count, void *buf)
{
long long location = (long long)sector * BYTES_PER_SECTOR;
DWORD dwBytes = count * BYTES_PER_SECTOR;
DWORD dwRead, status;
if (!SetFilePosition(hDrive, location, FILE_BEGIN))
return FALSE;
if (!DeviceIoControl(hDrive, FSCTL_DISMOUNT_VOLUME, NULL, 0, NULL, 0, &status, NULL))
return FALSE;
if (!WriteFile(hDrive, buf, dwBytes, &dwRead, NULL))
return FALSE;
return TRUE;
}
static BOOL SetFilePosition(HANDLE hDrive, __int64 distance, DWORD MoveMethod)
{
LARGE_INTEGER li;
li.QuadPart = distance;
li.LowPart = SetFilePointer(hDrive, li.LowPart, &li.HighPart, MoveMethod);
if (li.LowPart == 0xFFFFFFFF && GetLastError() != NO_ERROR)
return FALSE;
return TRUE;
}
/* Test.c */
#include <stdio.h>
#include <windows.h>
#include "DiskIO.h"
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hDisk;
HANDLE hVolume;
unsigned char buf[BYTES_PER_SECTOR];
int i;
if ((hDisk = OpenDisk(1, GENERIC_READ)) == INVALID_HANDLE_VALUE)
ExitSys("OpenDisk");
if (!ReadSector(hDisk, 409966399, 1, buf))
ExitSys("ReadSector");
for (i = 0; i < 512; ++i)
printf("%02X%c", buf[i], i % 16 == 15 ? '\n' : ' ');
CloseHandle(hDisk);
printf("-------------------------------------------------------\n");
if ((hVolume = OpenVolume('H' - 'A', GENERIC_READ)) == INVALID_HANDLE_VALUE)
ExitSys("OpenDisk");
if (!ReadSector(hVolume, 0, 1, buf))
ExitSys("ReadSector");
for (i = 0; i < 512; ++i)
printf("%02X%c", buf[i], i % 16 == 15 ? '\n' : ' ');
CloseHandle(hVolume);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Linux sistemlerinde yine disk sektörlerini okumak ve yazmak için aygıt sürücüler bulundurulmuştur. /dev/sda aygıt sürücüsü birinci diski bütün olarak
temsil etmektedir. Bu diskteki disk bölümler /dev/sda1, /dev/sda2 vs. biçimindedir. Diskin bölüm yapısını görmekl için df komutunu ya da fdisk --list
komutunu kullanabilirsiniz. Yine bu aygıt sürücler open fonksiyonuyla açılıp lseek fonksiyonuyla konumlandırma yapılıp read ve write fonksiyonlarıyla okuma ve yazma
işlemlerine sokulabilmektedir.
Aşağıdaki Linux sektör okumasına bir örnek verilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* diskio.h */
#ifndef DISKIO_H_
#define DISKIO_H_
/* Symbolic Constants */
#define BYTES_PER_SECTOR 512
/* Function Prototypes */
int read_sector(int fd, size_t sector, size_t count, void *buf);
int write_sector(int fd, size_t sector, size_t count, const void *buf);
#endif
/* diskio.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "diskio.h"
int read_sector(int fd, size_t sector, size_t count, void *buf)
{
if (lseek(fd, sector * BYTES_PER_SECTOR, SEEK_SET) == -1)
return -1;
if (read(fd, buf, BYTES_PER_SECTOR * count) != BYTES_PER_SECTOR * count)
return -1;
return 0;
}
int write_sector(int fd, size_t sector, size_t count, const void *buf)
{
if (lseek(fd, sector * BYTES_PER_SECTOR, SEEK_SET) == -1)
return -1;
if (write(fd, buf, BYTES_PER_SECTOR * count) != BYTES_PER_SECTOR * count)
return -1;
return 0;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include "diskio.h"
void exit_sys(const char *msg);
int main(void)
{
int fd;
unsigned char buf[BYTES_PER_SECTOR];
int i;
if ((fd = open("/dev/sda", O_RDONLY)) == -1)
exit_sys("open");
if (read_sector(fd, 24065464, 1, buf) == -1)
exit_sys("read_sector");
for (i = 0; i < BYTES_PER_SECTOR; ++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);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
FAT12 ve FAT16 sistemlerinde boot sektör içerisindeki BPB (Bios Parameter Block) bilgilerini boot sektörden okuyarak onu bir yapıya yerleştiren
GetBPB isimli bir fonksiyon aşağıda verilmiştir. Buradaki BPB yapısında (A) ile belirtilen yapı elemanları doğrudan BPB'de karşılığı olan elemanlardır.
Yanında (A) olmayan elemanlar mevcut bilgilerden hareketle elde edilen bilgileri belirtir. Örneğin BPB alanın içerisinde doğrudan daata bölümünün
hangi sektörden başladığı bilgisi yoktur. Ancak mevcut bilgilerden hareketle bu bilgi elde edilebilmektedir. GetBPB fonksiyonu sayesinde biz
artık FAT12 ve FAT16 volümlerine ilişkin bütün bilgileri elde etmiş oluruz.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* FatSys.h */
#ifndef FATSYS_H_
#define FATSYS_H_
#include <Windows.h>
/* Symbolic contants */
#define FILE_INFO_LENGTH 32
/* Type Declaration */
typedef struct tagBPB {
HANDLE hVolume; /* volume */
WORD fatLen; /* number of sectors of FAT (A) */
WORD rootLen; /* number of sectors of ROOT */
WORD fatCopyNum; /* number of copies of FAT (A) */
DWORD totalSec; /* total sector (A) */
WORD bps; /* byte per sector(A) */
WORD spc; /* sector per cluster(A) */
WORD reservedSect; /* reserved sector(A) */
BYTE medDes; /* media descriptor byte(A) */
WORD spt; /* sector per track(A) */
WORD rootEntryNum; /* root entry(A) */
WORD headNum; /* number of heads(A) */
WORD hidNum; /* number of hidden sector(A)*/
WORD tph; /* track per head */
WORD fatOrigin; /* fat directory location */
WORD rootOrigin; /* root directory location */
WORD dataOrigin; /* first data sector location */
DWORD serialNumber; /* Volume Serial Number (A) */
BYTE volumeName[12]; /* Volume Name (A) */
} BPB;
/* Function Prototypes */
BOOL GetBPB(HANDLE hVolume, BPB *pBPB);
#endif
/* FatSys.c */
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include "DiskIo.h"
#include "FatSys.h"
BOOL GetBPB(HANDLE hVolume, BPB *pBPB)
{
BYTE bootSect[BYTES_PER_SECTOR];
if (!ReadSector(hVolume, 0, 1, bootSect))
return FALSE;
pBPB->hVolume = hVolume;
pBPB->bps = *(WORD *)(bootSect + 0x0B);
pBPB->spc = *(BYTE *)(bootSect + 0x0D);
pBPB->reservedSect = *(WORD *)(bootSect + 0x0E);
pBPB->fatLen = *(WORD *)(bootSect + 0x16);
pBPB->rootLen = *(WORD *)(bootSect + 0x11) * FILE_INFO_LENGTH / pBPB->bps;
pBPB->fatCopyNum = *(BYTE *)(bootSect + 0x10);
if (*(WORD *)(bootSect + 0x13))
pBPB->totalSec = *(WORD *)(bootSect + 0x13);
else
pBPB->totalSec = *(DWORD *)(bootSect + 0x20);
pBPB->medDes = *(bootSect + 0x15);
pBPB->spt = *(WORD *)(bootSect + 0x18);
pBPB->rootEntryNum = *(WORD *)(bootSect + 0x11);
pBPB->headNum = *(WORD *)(bootSect + 0x1A);
pBPB->hidNum = *(WORD *)(bootSect + 0x1C);
pBPB->tph = (WORD)(pBPB->totalSec / pBPB->spt / pBPB->headNum);
pBPB->fatOrigin = pBPB->reservedSect;
pBPB->rootOrigin = pBPB->reservedSect + pBPB->fatLen * pBPB->fatCopyNum;
pBPB->dataOrigin = pBPB->rootOrigin + pBPB->rootLen;
pBPB->serialNumber = *(DWORD *)(bootSect + 0x27);
memcpy(pBPB->volumeName, bootSect + 0x2B, 11);
pBPB->volumeName[11] = '\0';
return TRUE;
}
/* Test.c */
#include <stdio.h>
#include <windows.h>
#include "DiskIO.h"
#include "FatSys.h"
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hVolume;
BPB bpb;
if ((hVolume = OpenVolume('H' - 'A', GENERIC_READ)) == INVALID_HANDLE_VALUE)
ExitSys("OpenDisk");
if (!GetBPB(hVolume, &bpb))
ExitSys("GetBPB");
printf("Success...\n");
printf("Fat'in başlangıç sektör numarası: %u\n", bpb.fatOrigin);
printf("Fat'in sektör uzunluğu: %u\n", bpb.fatLen);
printf("Root Dir bölümünün başlangıç sektör numarası: %u\n", bpb.rootOrigin);
printf("Root Dir bölümünün sektör uzunluğu: %u\n", bpb.rootLen);
printf("Data bölümünün başlangıç sektör numarası: %u\n", bpb.dataOrigin);
printf("Volümdeki toplam sektör sayısı: %lu\n", bpb.totalSec);
printf("Bir cluster'ın kaç sektörden oluştuğu: %u\n", bpb.spc);
CloseHandle(hVolume);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Boot Sektör BPB bilgileri okunduktan sonra artık bir cluster okuyan ve yazan fonksiyonlar yazılabilir. Data bölümünün ilk clusetr'ı 2'den başladığına göre
okunacak cluster numarası clu olmak üzere bu cluster'ın bşlangıç sektör numarası bpb.dataOrigin + (clu - 2) * bpb.spc ifadesi ile elde edilebilir.
O halde ReadClutser ve WriteCluster fonksiyonları şöyle yazılabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
BOOL ReadCluster(const BPB *pBPB, WORD clu, void *pData)
{
return ReadSector(pBPB->hVolume, pBPB->dataOrigin + (clu - 2) * pBPB->spc, pBPB->spc, pData);
}
BOOL WriteCluster(const BPB *pBPB, WORD clu, const void *pData)
{
return WriteSector(pBPB->hVolume, pBPB->dataOrigin + (clu - 2) * pBPB->spc, pBPB->spc, pData);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Yukarıdaki işlemler UNIX/Linux sistemlerinde aşağıdaki gibi yapılabilir. Bu örnekte aygıt olarak /dev/loop0 kullanılmıştır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* fatsys.h */
#ifndef FATSYS_H_
#define FATSYS_H_
#include <stdint.h>
#include <stdbool.h>
/* Symbolic contants */
#define FILE_INFO_LENGTH 32
/* Type Declaration */
typedef struct tagBPB {
int volume; /* volume */
uint16_t fatLen; /* number of sectors of FAT (A) */
uint16_t rootLen; /* number of sectors of ROOT */
uint16_t fatCopyNum; /* number of copies of FAT (A) */
uint32_t totalSec; /* total sector (A) */
uint16_t bps; /* byte per sector(A) */
uint16_t spc; /* sector per cluster(A) */
uint16_t reservedSect; /* reserved sector(A) */
uint8_t medDes; /* media descriptor byte(A) */
uint16_t spt; /* sector per track(A) */
uint16_t rootEntryNum; /* root entry(A) */
uint16_t headNum; /* number of heads(A) */
uint16_t hidNum; /* number of hidden sector(A)*/
uint16_t tph; /* track per head */
uint16_t fatOrigin; /* fat directory location */
uint16_t rootOrigin; /* root directory location */
uint16_t dataOrigin; /* first data sector location */
uint32_t serialNumber; /* Volume Serial Number (A) */
uint8_t volumeName[12]; /* Volume Name (A) */
} BPB;
/* Function Prototypes */
bool get_bpb(int volume, BPB *bpb);
bool read_cluster(const BPB *bpb, uint16_t clu, void *data);
#endif
/* fatsys. c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include "diskio.h"
#include "fatsys.h"
bool get_bpb(int volume, BPB *bpb)
{
uint8_t bootSect[BYTES_PER_SECTOR];
if (read_sector(volume, 0, 1, bootSect) == -1)
return false;
bpb->volume = volume;
bpb->bps = *(uint16_t *)(bootSect + 0x0B);
bpb->spc = *(uint8_t *)(bootSect + 0x0D);
bpb->reservedSect = *(uint16_t *)(bootSect + 0x0E);
bpb->fatLen = *(uint16_t *)(bootSect + 0x16);
bpb->rootLen = *(uint16_t *)(bootSect + 0x11) * FILE_INFO_LENGTH / bpb->bps;
bpb->fatCopyNum = *(uint8_t *)(bootSect + 0x10);
if (*(uint16_t *)(bootSect + 0x13))
bpb->totalSec = *(uint16_t *)(bootSect + 0x13);
else
bpb->totalSec = *(uint16_t *)(bootSect + 0x20);
bpb->medDes = *(bootSect + 0x15);
bpb->spt = *(uint16_t *)(bootSect + 0x18);
bpb->rootEntryNum = *(uint16_t *)(bootSect + 0x11);
bpb->headNum = *(uint16_t *)(bootSect + 0x1A);
bpb->hidNum = *(uint16_t *)(bootSect + 0x1C);
bpb->tph = (uint16_t)(bpb->totalSec / bpb->spt / bpb->headNum);
bpb->fatOrigin = bpb->reservedSect;
bpb->rootOrigin = bpb->reservedSect + bpb->fatLen * bpb->fatCopyNum;
bpb->dataOrigin = bpb->rootOrigin + bpb->rootLen;
bpb->serialNumber = *(uint32_t *)(bootSect + 0x27);
memcpy(bpb->volumeName, bootSect + 0x2B, 11);
bpb->volumeName[11] = '\0';
return true;
}
bool read_cluster(const BPB *bpb, uint16_t clu, void *data)
{
return read_sector(bpb->volume, bpb->dataOrigin + (clu - 2) * bpb->spc, bpb->spc, data) == 0;
}
bool write_cluster(const BPB *bpb, uint16_t clu, const void *data)
{
return write_sector(bpb->volume, bpb->dataOrigin + (clu - 2) * bpb->spc, bpb->spc, data) == 0;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include "diskio.h"
#include "fatsys.h"
void exit_sys(const char *msg);
int main(void)
{
int volume;
BPB bpb;
uint8_t *buf;
int i, k;
if ((volume = open("/dev/loop0", O_RDONLY)) == -1)
exit_sys("open");
if (!get_bpb(volume, &bpb))
exit_sys("getbpb");
printf("Fat'in başlangıç sektör numarası: %u\n", bpb.fatOrigin);
printf("Fat'in sektör uzunluğu: %u\n", bpb.fatLen);
printf("Root Dir bölümünün başlangıç sektör numarası: %u\n", bpb.rootOrigin);
printf("Root Dir bölümünün sektör uzunluğu: %u\n", bpb.rootLen);
printf("Data bölümünün başlangıç sektör numarası: %u\n", bpb.dataOrigin);
printf("Volümdeki toplam sektör sayısı: %u\n", bpb.totalSec);
printf("Bir cluster'ın kaç sektörden oluştuğu: %u\n\n", bpb.spc);
if ((buf = (uint8_t *)calloc(bpb.spc * bpb.bps, 1)) == NULL) {
fprintf(stderr, "Cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
if (!read_cluster(&bpb, 3, buf))
exit_sys("WriteCluster");
for (i = 0; i < 1024; ++i) {
printf("%02X ", buf[i]);
if (i % 16 == 15) {
for (k = 0; k < 16; ++k)
putchar(isalnum(buf[i - 15 + k]) ? buf[i - 15 + k] : '.');
putchar('\n');
}
}
free(buf);
close(volume);
return 0;
}
void exit_sys(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
FAT bölümü bir bağlı liste biçimindedir. Bir dosyanın ilk clutser numarası 32!lik dizin girişlerinde tutulur. Sonra FAT zinciri izlenerek
dosyanın cluster zinciri elde edilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/* fatsys.h */
#ifndef FATSYS_H_
#define FATSYS_H_
#include <Windows.h>
/* Symbolic contants */
#define FILE_INFO_LENGTH 32
#define MAX_CLUSTER_CHAIN 65536
/* Type Declaration */
typedef struct tagBPB {
HANDLE hVolume; /* volume */
WORD fatLen; /* number of sectors of FAT (A) */
WORD rootLen; /* number of sectors of ROOT */
WORD fatCopyNum; /* number of copies of FAT (A) */
DWORD totalSec; /* total sector (A) */
WORD bps; /* byte per sector(A) */
WORD spc; /* sector per cluster(A) */
WORD reservedSect; /* reserved sector(A) */
BYTE medDes; /* media descriptor byte(A) */
WORD spt; /* sector per track(A) */
WORD rootEntryNum; /* root entry(A) */
WORD headNum; /* number of heads(A) */
WORD hidNum; /* number of hidden sector(A)*/
WORD tph; /* track per head */
WORD fatOrigin; /* fat directory location */
WORD rootOrigin; /* root directory location */
WORD dataOrigin; /* first data sector location */
DWORD serialNumber; /* Volume Serial Number (A) */
BYTE volumeName[12]; /* Volume Name (A) */
} BPB;
/* Function Prototypes */
BOOL GetBPB(HANDLE hVolume, BPB *pBPB);
BOOL ReadCluster(const BPB *pBPB, WORD clu, void *pData);
BOOL WriteCluster(const BPB *pBPB, WORD clu, const void *pData);
WORD *GetClusterChain16(BYTE *pFat, WORD firstCluster, WORD *pCount);
WORD *GetClusterChain12(BYTE *pFat, WORD firstCluster, WORD *pCount);
#endif
/* fatsys.c */
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include "DiskIo.h"
#include "FatSys.h"
BOOL GetBPB(HANDLE hVolume, BPB *pBPB)
{
BYTE bootSect[BYTES_PER_SECTOR];
if (!ReadSector(hVolume, 0, 1, bootSect))
return FALSE;
pBPB->hVolume = hVolume;
pBPB->bps = *(WORD *)(bootSect + 0x0B);
pBPB->spc = *(BYTE *)(bootSect + 0x0D);
pBPB->reservedSect = *(WORD *)(bootSect + 0x0E);
pBPB->fatLen = *(WORD *)(bootSect + 0x16);
pBPB->rootLen = *(WORD *)(bootSect + 0x11) * FILE_INFO_LENGTH / pBPB->bps;
pBPB->fatCopyNum = *(BYTE *)(bootSect + 0x10);
if (*(WORD *)(bootSect + 0x13))
pBPB->totalSec = *(WORD *)(bootSect + 0x13);
else
pBPB->totalSec = *(DWORD *)(bootSect + 0x20);
pBPB->medDes = *(bootSect + 0x15);
pBPB->spt = *(WORD *)(bootSect + 0x18);
pBPB->rootEntryNum = *(WORD *)(bootSect + 0x11);
pBPB->headNum = *(WORD *)(bootSect + 0x1A);
pBPB->hidNum = *(WORD *)(bootSect + 0x1C);
pBPB->tph = (WORD)(pBPB->totalSec / pBPB->spt / pBPB->headNum);
pBPB->fatOrigin = pBPB->reservedSect;
pBPB->rootOrigin = pBPB->reservedSect + pBPB->fatLen * pBPB->fatCopyNum;
pBPB->dataOrigin = pBPB->rootOrigin + pBPB->rootLen;
pBPB->serialNumber = *(DWORD *)(bootSect + 0x27);
memcpy(pBPB->volumeName, bootSect + 0x2B, 11);
pBPB->volumeName[11] = '\0';
return TRUE;
}
BOOL ReadCluster(const BPB *pBPB, WORD clu, void *pData)
{
return ReadSector(pBPB->hVolume, pBPB->dataOrigin + (clu - 2) * pBPB->spc, pBPB->spc, pData);
}
BOOL WriteCluster(const BPB *pBPB, WORD clu, const void *pData)
{
return WriteSector(pBPB->hVolume, pBPB->dataOrigin + (clu - 2) * pBPB->spc, pBPB->spc, pData);
}
WORD *GetClusterChain16(BYTE *pFat, WORD firstCluster, WORD *pCount)
{
static WORD chain[MAX_CLUSTER_CHAIN];
WORD cluster;
int count;
cluster = firstCluster;
count = 0;
while (cluster < 0xFFF8) {
chain[count++] = cluster;
cluster = *(WORD *)(pFat + cluster * 2);
}
*pCount = count;
return chain;
}
WORD *GetClusterChain12(BYTE *pFat, WORD firstCluster, WORD *pCount)
{
static WORD chain[MAX_CLUSTER_CHAIN];
WORD cluster;
int count;
cluster = firstCluster;
count = 0;
while (cluster < 0xFF8) {
chain[count++] = cluster;
if (cluster % 2 == 0)
cluster = *(WORD *)(pFat + cluster * 3 / 2) & 0x0FFF;
else
cluster = *(WORD *)(pFat + cluster * 3 / 2) >> 4;
}
*pCount = count;
return chain;
}
/* app.c */
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <windows.h>
#include "DiskIO.h"
#include "FatSys.h"
void ExitSys(LPCSTR lpszMsg);
int main(void)
{
HANDLE hVolume;
BPB bpb;
BYTE *pFat;
WORD *pClusterChain;
WORD nclusters;
int i;
if ((hVolume = OpenVolume('H' - 'A', GENERIC_READ)) == INVALID_HANDLE_VALUE)
ExitSys("OpenDisk");
if (!GetBPB(hVolume, &bpb))
ExitSys("GetBPB");
printf("Fat'in başlangıç sektör numarası: %u\n", bpb.fatOrigin);
printf("Fat'in sektör uzunluğu: %u\n", bpb.fatLen);
printf("Root Dir bölümünün başlangıç sektör numarası: %u\n", bpb.rootOrigin);
printf("Root Dir bölümünün sektör uzunluğu: %u\n", bpb.rootLen);
printf("Data bölümünün başlangıç sektör numarası: %u\n", bpb.dataOrigin);
printf("Volümdeki toplam sektör sayısı: %lu\n", bpb.totalSec);
printf("Bir cluster'ın kaç sektörden oluştuğu: %u\n\n", bpb.spc);
if ((pFat = (BYTE *)malloc(bpb.fatLen * bpb.bps)) == NULL) {
fprintf(stderr, "Cannot allocate memory!..\n");
exit(EXIT_FAILURE);
}
if (!ReadSector(hVolume, bpb.fatOrigin, bpb.fatLen, pFat))
ExitSys("ReadSector");
pClusterChain = GetClusterChain12(pFat, 10, &nclusters);
for (i = 0; i < nclusters; ++i)
printf("%d ", pClusterChain[i]);
printf("\n");
CloseHandle(hVolume);
return 0;
}
void ExitSys(LPCSTR lpszMsg)
{
DWORD dwLastErr = GetLastError();
LPTSTR lpszErr;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) {
fprintf(stderr, "%s: %s", lpszMsg, lpszErr);
LocalFree(lpszErr);
}
exit(EXIT_FAILURE);
}
/*------------------------------------------------------------------------------------------------------------------------------------------
Alt dizinler tamamen bir dosya gibi organize edilmektedir. Nasıl dosyaları oluşturan cluster'larda dosya bilgileri varsa dizinleri oluşturan cluster'larda da
o dizin içerisindeki dosyalara ilişkin bilgiler vardır. Bir dosya bilgisi 32 byte uzunluğunda dizin girişlerinde saklanır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
FAT12 ve FAT16 sistemlerinde FAT elemanları 12 bit 16 bit olduğu için bu dosya sistemleriyle formatlanacak diskin kapasitesi de oldukça sınırlıdır.
Bir cluster en fazla 64 sektör olabilmektedir. Bu nedenle FAT16 volümlerinin de maksimum kapasitesi 2GB olabilir. Microsoft FAT32 ile birlikte
FAT dosya sistemine şu eklemeleri yapmıştır:
1) Boot sektör içerisindeki BPB alanını büyütmüştür.
2) FAT elemanlarını 28 bite çıkartmiştır. Ancak FAT elemanları 32 bit yer kaplar yüksek anlamlı 4 bit kullanılmamaktadır.
3) Kök dizin alt dizinler gibi DATA bölümünün içerisine alınmıştır. Kök dizinin hangi cluster'dan başladığı da BPB içerisinde tutulmaktadır.
Dolayısıyla kök dizin artık istenildiği kadar büyük olabilir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
UNIX/Linux sistemlerinde kullanılan dosya sistemleri birbirlerinden farklı olabilse de bunlar bir aile olarak genel yapı bakımından birbirlerine
benzemektedir. Bu aileye "I-Node Tabanlı Dosya Sistemleri" denilmektedir. Bugün bu ailenin en yaygın kullanılan dosya sistemleri ext-2, ext-3
gibi sistemlerdir. Linux sistemleri ağırlıklı Ext-2 dosya sistemini kullanmaktadır.
I-Node Tabanlı dosya sistemlerinde volüm dört mantıksal bölüme ayrılmıştır:
1) Boot Block
2) Super Block
3) I-Node Block
4) Data Block
Boot Block volümün ilk iki sektörünü oluşturur. 1024 byte uzunluktadır. Burada işletim sistemini yükleyen program kodu bulundurulur. Bu anlamda
Boot Block FAT dosya sistemlerindeki Boot sektöre işlevsel olarak benzetilebilir. Boot bloğunu I-Node Block denilen yine 1024 byte uzunlukta olan
Super Block izler. Super Block FAT dosya sistemindeki BPB bölümüne benzetilebilir. Dosya sistemin tüm parametrik bilgileri Super blokta tutulmaktadır.
I-Node blok I-Node elemanlarından oluşmaktadır. Bir dosyanın bütün bilgileri i-node elemanında saklanmaktadır. Gerçekten de stat, fstat gibi fonksiyonlar
dosya bilgilerini bu dosya için ayrılmış olan i-node elemanının içerisinden alırlar. Her dosya için I-Node Block içerisinde bir i-node elemanı bulunmaktadır.
I-Node bloktaki i-node elemanlarının ilk eleman 0 olmak üzere birer indeks numarası vardır. ls komutunda -i seçeneği dosyaların i-node numaralarını da göstermektedir.
Data Block FAT dosya sistemindeki Data Block ile aynı anlamdadır. Yani dosyaların parçalarının turulduğu en geniş mantıksal bölümdür. Ancak terminolojide
küçük farklılık vardır. Anımsanacağı gibi FAT dosya sistemlerinde dosyanın parçası olabilecek en küçük birime "cluster" deniyordu. Halbuki I-Node tabanlı dosya
sistemlerinde "cluster" sözcüğü yerine "block" sözcüğü kullanılmaktadır. Bir bloğun kaç byte'tan oluştuğu bilgisi Super blokta tutulmaktadır. Data Block içerisindeki
her bloğa ilki 0 olmak üzere bir blok numarası karşı getirilmiştir.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
Ext-2 Dosya sistemi bazı ayrıntılar içeren I-Node tabanlı bir dosya sistemidir. Bu dosya sisteminde yine volümün ilk 1024 byte'ında
Boot Block bulunur. Onu yine 1024 byte uzunluğunda Super Block izler. Ancak Ext-2'de Super Bloğu I-Node Block izlemez. Çünkü Ext-2 adeta
küçük bölümlerin toplamından oluşan bir dosya sistemi sunmaktadır. Bu bölümlere "Block Group" denilmektedir. Block Group'lar hakkında önemli bazı
bilgiler "Block Group Descriptor Table" isimli bir veri yapısında saklanır. Bu veri yapısı 1K blok yapısından oluşan volümlerde 3K (3072) sınırından,
1K'dan daha büyük blok yapısındna oluşan volümlerde 4K sınırından (4096) başlamaktadır.Block Group Descriptor Table denilen veri yapısı her biri 32 byte
uzunlupunda olan "Block Group Descriptor" denilen ve bir block group hakkında bilgi veren yapılardan oluşan bir dizidir. Örneğin volümümüzde
üç block group varsa Block Group Descriptor Table'da her biri 32 byte'tan oluşan 3 tane Block Group Descriptor.
Pekiyi Group Block'lar içerisinde ne vardır? İşte group block'ların başında Super Block'un ve Grooup Block Descriptor Table'ın birer kopyası bulunur.
Bu kopyalardan sonra DataBlcok Bitmap ve I-Node bitmap denilen iki bit tablosu bulunmaktadır. Bu bit tablolarından sonra da her block group için
I-Node Block ve Data Block bulunmaktadır. Görüldüğü gibi aslında ı_Node block ve Data Block volümde toplamda bir tane değildir. Her Block Group'ta
ayrı bir I-Node Block ve Data Block bulunmaktadır.
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/
/*------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------*/