> UNIX/Linux sistemlerinde "IPC" fonksiyonları denildiğinde varsayılan durumda akla üç adet fonksiyon gelir. Bunlar Mesaj Kuyrukları, Paylaşılan Bellek Alanları ve Semafor fonksiyonlarıdır. Her ne kadar borular da birer "IPC" fonksiyonu olsalar da hal arasında öyle bilinmezler. Semaforlar senkronizasyonla alakalı olduklarından, bu başlık altında o konu işlenmeyecektir. "System 5 IPC" fonksiyonları 70'li yıllardan beridir POSIX dünyasında var olan fonksiyonlardır. Fakat bir takım eksik yönlerinden dolayı 90'lı yıllarda yenilenmiş versiyonları da POSIX dünyasına eklenmiştir. İşte 70'li yıllardan bari dilde var olan versiyonlarına "System 5 IPC", 90'lı yıllarda yenilenmiş hallerine ise "POSIX IPC" fonksiyonları denir. İşin özünde her ikisi de POSIX çatısı altındadır. Pekiyi yeni versiyonlarına neden "POSIX IPC" fonksiyonları denir? Aslında bakarsanız POSIX standartları kendi içerisinde uyum kategorilerine ayrılır. Örneğin, "XSI", "SHM" isimli uyum kategorisi. "XSI" uyum kategorisi evvelden beri var olan ve "UNIX Single Specification" u oluşturan kategoridir. Fonksiyonların imzalarının yanında destekledikleri uyum kategorisi de belirtilmiştir. "UNIX" ve "AT&T" nin "System 5" versiyonunda ilk defa bu mekanizma kullanıldığı için fonksiyonlara "System 5 IPC" fonksiyonları denmektedir. Halk arasında "XSI IPC" fonksiyonları denmektedir. 90'lı yıllarda dile eklenenlere ise POSIX dünyasında daha sonra katıldıkları için o ismi almışlardır. Özetlemek gerekirse 3 adet mekanizma bulunmaktadır: Mesaj Kuyrukları, Paylaşılan Bellek Alanları ve Semaforlar. Her ne kadar borular da bir "IPC" mekanizması olsalar da borular ayrı ele alınmaktadır. Bizler kurs sırasında "System 5" ve "POSIX" "IPC" fonksiyonlarını ilgili konular altında karşılıklı göreceğiz. İlgili fonksiyonların isimlendirmesi kabaca şu şekildedir: System 5 IPC - POSIX IPC Paylaşılan Bellek Alanları => shmget - shm_open Mesaj Kuyrukları => msgget - mq_open Semaforlar => semget - sem_open Bir sistem programcısı olarak her iki gruptakileri bilmek iyi olsa da "POSIX IPC" fonksiyonlarını kullanmalıyız. Öte yandan proseslerin "System 5 IPC" mekanizmasını kullanabilmesi için "key-value" kavramı kullanılmaktadır. Haberleşme yapacak olan prosesler "xxxget" fonksiyonuna sayısal bir değer olan "key" değerini argüman olarak geçmeleri halinde, ismine "ID" diyeceğimiz "value" değerini elde edeceklerdir. Aynı "key" değerini fonksiyona geçen iki farklı proses, aynı "value" değerine sahip olacaktır. Dolayısıyla haberleşme yapacak olan iki proses aynı "key" değerini "xxxget" fonksiyonuna geçmelidir. Bu fonksiyondan elde ettiğimiz "ID" değerini okuma/yazma yapmak için mekanizmadaki diğer fonksiyonlara geçeceğiz. İşte "System 5 IPC" mekanizmasının bir anomalisi burada ortaya çıkmaktadır. Ya bambaşka bir proses bizim kullanacağımız "key" değerini kullanmışsa? Bu da bazı kontrollerin yapılmasını gerektirmektedir. Öte yandan "xxxget" fonksiyonlarının bize döndürdüğü "ID" değeri sistem geneli eşsiz bir değerdir. Eğer bu "ID" değeri bir şekilde karşı prosese ulaştırılırsa, o proses "xxxget" yapmasına gerek kalmaz. Almış olduğu bu "ID" değerini direkt kullanabilir. Yine değinilmesi gereken bir diğer nokta da "xxxget" fonksiyonuna geçilen "key" değeri Mesaj Kuyruğu söz konusu olduğunda başka bir isim alanı, Paylaşılan Bellek Alanı olduğunda başka bir isim alanı ve Semaforlar olduğunda başka bir isim alanındadır. Ayrıca "System 5 IPC" nesneleri, boruların aksina, sistemin ilk "reboot" anına kadar sistemde kalmaktadır eğer biz "xxxctl" ile yok etmezsek. Yani proses sonlansa bile ilgili nesneler hayatta kalmaya devam etmektedir. Buna "Kernel Persistance" denmektedir. Fakat borular, prosesin sonlanmasıyla birlikte yol olmaktadır. >> Mesaj Kuyrukları: Bildiğiniz üzere borular "stream" tarzı haberleşme sunmaktadır. Yani bir taraf boruya bir miktar yazıyor, karşı taraf yazılanların bir miktarını okuyor vs. Bu haberleşmenin bir alternatifi ise paket tarzı haberleşmedir. Protokol ailelerinde buna "Datagram" haberleşmesi de denir. Bir taraf bir "paket" bilgiyi karşı tarafa gönderiyor, karşı taraf ise bu paketin TAMAMINI ALIYOR. Burada gönderilen paketin bir kısmını almak gibi bir durum söz konusu değildir. İşte mesaj kuyrukları böylesi bir haberleşme yapmaktadır. Mesaj adı altında bir grup paket, kuyruk halindedir. Şekil olarak bağlı liste örnek gösterilebilir. Öte yandan borular ise şekil olarak dizilere benzetilebilir. Mesaj kuyrukları iki proses arasında haberleşme yapmaktadır. >>> "System 5 IPC" Fonksiyonları: "msgget", "msgsend", "msgcrv" ve "msgctl" fonksiyonlarıdır. Bu fonksiyonların isimlerinin altı karakterden oluştuğuna dikkat ediniz. Bu fonksiyonların işlevleri sırasıyla şu şekildedir: Bir mesaj kuyruğu hayata getirmek ya da var olanı açmak, bir mesaj kuyruğuna bayt yazmak, bir mesaj kuyruğundan bayt okumak/almak, bir mesaj kuyruğu üzerinde bir takım işlemler gerçekleştirmek(silme işlemi de dahil). Bir sistemdeki "System 5 IPC" nesnelerini "shell" programı üzerinden "ipcs" komutunu çalıştırarak görebiliriz. Unutulmamalıdır ki bu nesneler prosesler sonlansa bile ömürlerine devam eder. Ta ki sistem "reboot" gerçekleştiğinde ya da "msgctl" fonksiyonu ile silinirler. "ipcs" komutu ekrana bastığı "key" değerleri için 16'lık tabanı kullanmaktadır. Şimdi de bu fonksiyonları irdeleyelim: >>>> "msgget": Mesaj kuyruğu hayata getirir ya da halihazırdaki bir mesaj kuyruğunu açar. Bu yönüyle "open" fonksiyonuna benzetebiliriz. Fonksiyonun prototipi şöyledir: #include int msgget(key_t key, int msgflg); Fonksiyonun birinci parametresi tam sayı biçiminde bir "key" belirtmektedir. İkinci parametre ise şu bayrakları alabilir: -> IPC_CREAT: İlgili anahtara ilişkin bir mesaj kuyruğu varsa, olan mesaj kuyruğu açılır. Ancak yoksa, yeni bir mesaj kuyruğu oluşturulur. Buradaki semantik "open" fonksiyonundaki "O_CREAT" semantiğine benzemektedir. Öte yandan bu bayrak kullanılmışsa, yeni oluşturulacak mesaj kuyruğunun da erişim haklarını belirtmesi gerekmektedir. Bu fonksiyon üçüncü bir parametre almadığı için, erişim hakları da bu bayral ile "Bitwise-OR" işlemine sokulmalıdır. Burada kullanılacak olan erişim bayrakları, dosyalarda gördüğümüz "S_Ixxxx" olan bayraklardır. Tabii halihazırda bir mesaj kuyruğu varsa bu bayrakların bir önemi KALMAYACAKTIR. -> IPC_EXCL: Bu bayrak tek başına değil, "IPC_CREAT" ile birlikte, "IPC_CREAT | IPC_EXCL | ..." biçiminde kullanılır. İlgili "key" değerine karşılık gelen bir mesaj kuyruğu yoksa, yani sıfırdan bir mesaj kuyruğunu açmak için kullanılır. Eğer bu "key" değerine karşılık gelen mesaj kuyruğu varsa fonksiyon başarısız olacaktır. Fonksiyon başarısız olduğunda da "errno" değişkeni "EEXIST" değerini alır. Yine buradaki semantik de "open" fonksiyonundaki "O_EXCL" bayrağı gibidir. Haberleşecek her iki proses de bu bayrağı kullanabilir. -> Bu parametreye "0" değerini geçebiliriz. Dolayısıyla var olan açılacaktır. Fonksiyonun örnek kullanımı aşağıdaki gibi olabilir: ... msgid = msgget(0x12345, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); ... Tabii buradaki "S_Ixxx" bayraklarına karşılık sayısal değerler de getirilmiştir. Dolayısıyla yukarıdaki işlemi şu şekilde olabilir: ... msgid = msgget(0x12345, IPC_CREAT | 0644); ... Bu fonksiyonun birinci parametresine "IPC_PRIVATE" geçmemiz durumunda, daha önce kullanılmamış bir "key" değerinin geçildiği varsayılacaktır. Artık bu durumda "key" çakışmasının önüne geçilmiş olacak. Fakat fonksiyonun geri döndürdüğü "id" değerini de karşı prosese bir şekilde ulaştırmamız gerekmektedir çünkü karşı fonksiyon da bu bayrağı kullanırsa, o farklı bir "id" değerini elde edecektir. Dolayısıyla bu bayrak pek bir kullanışlı kalmamaktadır. Karşı tarafa göndermek için başka bir haberleşme yöntemi, komut satırı argümanı olarak gönderim vb. yöntemler izlenebilir. "msgget" fonksiyonu başarısız olduğunda "-1" ile başarılı olduğunda da pozitif bir "id" değerine geri döner. * Örnek 1, #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); int main(int argc, char** argv) { int msgid; /* * Aşağıda bu "key" değerine ilişkin bir mesaj kuyruğu yok ise yeni bir tanesi * oluşturulacaktır. */ if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "msgsend": Mesaj kuyruğuna bir mesaj göndermek için kullanılır. Prototipi şöyledir: #include int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); Fonksiyonun birinci parametresi, "msgget" fonksiyonuna "key" vererek elde ettiğimiz, "id" değeridir. Bu "id" değeri sistem geneli tekil olduğu için ve proses bu "id" değerini zaten biliyorsa, tekrardan "msgget" çağrısına gerek yoktur. İkinci parametresi ise gönderilecek bayt yığınının başlangıç adresidir. Üçüncü parametre ise mesajın uzunluğudur, ilgili bayt yığınının büyüklüğü DEĞİL. Kuyruğa yazılacak olan bayt yığınları şu şekilde oluşturulması bir ZORUNLULUKTUR: -> Mesajın ilk kısımları "long" türden olacak ve mesajın türü anlamına gelecek. Yani o mesajın "id" değerini bizler bayt yığınının baş kısmında bulundurmak zorundayız. Bu "id" değerinin "0" dan büyük olması gerekmektedir. -> Yukarıdaki "long" alanın bittiği yerde göndermek istediğimiz mesajın baytları gelmelidir. Böylelikle "long" alan ile bizim göndereceğimiz mesajın baytları ardışıl olmalıdır. Bu şartları sağlamanın en kolay yolu bir "struct" kullanmaktır. Çünkü "struct" ın ilk elemanı, aynı zamanda o "struct" ın başlangıç adresidir ve "struct" içerisindeki elemanların baytları da ardışıl vaziyettedir. Bir diğer alternatif yöntem ise bir "char" türden dizi oluşturup "memcpy" fonksiyonu ile bu diziye baytları yazmaktır. Aşağıda bu "struct" için bir örnek verilmiştir: struct MYMSG{ long mtype; // Mesajın türü. Bir nevi bu mesajın "id" değeridir. // Mesajın kendisi. "msgsnd" fonksiyonunun üçüncü parametresine bu büyüklük geçilecek. char msg[1024]; }; Yukarıdaki yapının son elemanı olan dizinin boyutunu, C99 sonrası dönemde "flexible array member" olarak, yazmayabiliriz. Bu eleman için bir alan tahsisi yapılmıyor. Aksi halde ilgili dizinin boyutunu ya "1024" olarak takribi belirtmeli ya da "1" belirtip daha sonra dinamik bellek yönetimi ile daha sonra büyütmeliyiz. Aşağıda ise ilgili dizinin uzunluğunun "1" seçilmesi durumuna dair bir örnek verilmiştir: //... struct MSG{ long mtype; char msg[1]; }; //... int main() { //... struct MSG* msg; msg = (struct MSG*)malloc(sizeof(long) + n); msg->mtype = 1; memcpy(msg->msg, /*message content*/, ...); //... free(msg); } Görüldüğü üzere dinamik bellek yönetimi biraz zahmetli. Bu zahmete girmemek adına, mesajın uzunluğu takribi 8192 olarak seçilebilir. Öte yandan bu yapının ilk elemanını mesajın "id" değeri olarak kullanılabileceğinden bahsetmiştir. Pekiyi nedir bu mesajın "id" değeri mevzusu? Burada mesajı alan taraf işin içine girmektedir. Şöyleki; -> Mesajı alan taraf spesifik bir "id" değerine ilişkin mesajları almak isteyebilir. Bu yönüyle "client-server" haberleşmesinde kullanılabilir. -> Mesajı alan taraf en yüksek "id" değerine sahip olanı ilk olarak almak isteyebilir. Bu yönüyle "priority queue" gibi de kullanılır. Son parametre ise gönderim sırasında kullanılabilen özel bir bayraktır. Bu bayrak "Bitwise-OR" işlemine tabi tutulabilir. Fakat POSIX standartlarınca sadece tek bir bayrak vardır o da "IPC_NOWAIT" bayrağıdır. Pekiyi bu bayrak ne işe yarar? Normalde mesaj kuyruğu doluysa, bu fonksiyon blokeye yol açmaktadır. Bu bayrağı kullanırsak blokede beklemeyiz ve fonksiyon başarısız olup "-1" ile geri döner ve "errno" değişkeni de "EAGAIN" değerine çekilir. Bu bayrağı kullanmak istemiyorsak, "0" geçebiliriz. Ek olarak belirtmek gerekir ki mesaj kuyruklarının da bir limiti vardır. Bu limit değerine ulaşıldığında, iş bu fonksiyon eğer "IPC_NOWAIT" bayrağı olmadan çağrılmışsa, prosesin bloke olmasına neden olur. Aksi halde fonksiyon başarısız olacaktır. Velevki mesaj kuyruğunda yer açılırsa bloke kalkar. Görüyoruz ki tıpkı borulardaki senkronizasyon burada da sağlanmış durumdadır. Son olarak "msgsnd" fonksiyonu, başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri döner ve "errno" uygun değeri alır. * Örnek 1, Aşağıdaki örnek "msgget" sırasında kullanılan örneğin üzerine eklenmiştir: #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; int value; }; int main(int argc, char** argv) { int msgid; /* * Aşağıda bu "key" değerine ilişkin bir mesaj kuyruğu yok ise yeni bir tanesi * oluşturulacaktır. */ if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; for(int i = 0; i < 10; ++i) { /* * Eğer mesajın "id" değeri önemsiz ise "0" hariç böylesi sabit bir değer geçebiliriz. */ msg.mtype = 1; msg.value = i; /* * Fonksiyonun; * Üçüncü parametresine ilgili mesajın uzunluğunun geçildiğine dikkat ediniz. * Dördüncü parametresine "0" geçildiği için "blocking" modda çalışacaktır. */ if(msgsnd(msgid, &msg, sizeof(int), 0) == -1) exit_sys("msgsnd"); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "msgrcv" : Mesaj kuyruğundan bir mesaj okumak için kullanılır. Fonksiyonun prototipi şöyledir: #include ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); Fonksiyonun birinci parametresi, "msgget" fonksiyonuna "key" vererek elde ettiğimiz, "id" değeridir. Bu "id" değeri sistem geneli tekil olduğu için ve proses bu "id" değerini zaten biliyorsa, tekrardan "msgget" çağrısına gerek yoktur. Fonksiyonun ikinci ve üçüncü parametreleri, kuyruktan alınan mesajın yerleştirileceği alanın başlangıç adresi ve mesajın uzunluk bilgisidir. Tabii ikinci parametreye geçilen adresin ilk kısmı, yine "long" türden olmalıdır. Bu "long" türden sonra bizim mesajın başladığı adres gelmektedir. Özetle; ikinci parametre ilgili "struct" yapısının adresiyken, üçüncü parametre ise bu "struct" içerisindeki mesajın uzunluk bilgisidir. İkinci ve üçüncü parametrede kullanılacak "struct" aşağıdaki gibi olabilir: struct MSG{ long mtype; char msg[8192]; }; Kuyruktaki mesajın içeriği yukarıdaki yapınıın içerisine yerleştirilecektir. Fonksiyonun dördüncü parametresi ise alınacak mesajların "id" bilgisini belirtmektedir. Bu parametre aşağıdaki durumlardan birisini karşılamalıdır: -> "0" geçilmesi durumunda, kuyrukta "FIFO" sistemi uygulanacaktır. Yani sıradaki ilk mesaj okunacaktır. "0" değerinin geçerli bir "id" değeri olmadığını UNUTMAYINIZ. -> "0" dan büyük pozitif bir değer geçilirse, o değere sahip ilk mesaj kuyruktan okunur. Örneğin, bu parametreye "100" geçilmiş olsun. Kuyruğun muhtelif yerlerinde de bu "id" değerine sahip birden fazla mesaj paketi olsun. Bizler en öndeki paketi almış olacağız. -> "0" dan küçük negatif bir değer geçilirse, "Priority Queue" sistemi uygulanacaktır. Şöyleki; geçilen negatif değerin mutlak değeri alınır. İş bu değere eşit ya da bu değerden küçük olan en küçük "id" değerine sahip paket ilk okunur. Örneğin, "-10" değeri geçilmiş olsun. Kuyruktaki mesajların "id" değerleri de aşağıdaki gibi olsun: 20 5 30 2 8 40 Mutlak değeri alındığında, "10" değeri elde edilmiş olur. İlk okumada "10" dan küçük en düşük "id" değeri olan "2" okunur.Daha sonra "5" ve en sonunda "8". Bu aşamada tekrar okuma yapılırsa fonksiyon blokeye yol açacaktır. Çünkü uygun "id" değerine sahip mesaj paketi kalmamıştır. Burada en düşük "id" değerine sahip olan paket en öncelikli paket haline gelmiştir. Fonksiyonun son parametresi ise mesajın alımına ilişkin bayrak değeridir. Buraya şu aşağıdaki bayraklar geçilebilir: -> "IPC_NOWAIT" : Artık ilgili fonksiyon "non-blocking" modda çalışacaktır. Kuyrukta okunacak uygun "id" değerine sahip mesaj kalmadıysa, fonksiyon başarısız olacaktır. "errno" değeri de "EAGAIN" değerine çekilir. -> "MSG_NOERROR" : Kuyruktaki paketin içerisindeki mesajın uzunluğu, bu fonksiyonun üçüncü parametresiyle geçtiğimiz uzunluktan büyükse, ilgili mesaj kırpılarak alınır. Böylelike parçalı okuma yapılmış olur. Bu bayrak bunu mümkün kılmaktadır. Bu bayrak kullanılmazsa ve yukarıdaki durum gerçekleştiğinde, fonksiyon başarısız olacaktır. "errno" değeri de "E2BIG" değerine çekilir. -> "0" geçilmesi durumunda, yukarıdaki semantikler uygulanmayacaktır. Fonksiyonun geri dönüş değeri ise başarı durumunda alınan paketin içerisindeki mesajın uzunluğu, başarısızlık durumunda ise "-1" değerindedir. Bu noktada bir hatırlatma yapmak gerekir ki mesaj kuyruklarında karşı tarafın mesaj kuyruğunu kapatması gibi bir durum SÖZ KONUSU DEĞİLDİR. Kuyrukta okunacak uygun paket kalmadıysa, okuma yapan taraf ya bloke olacaktır ya da başarısız olup sonlanacaktır. * Örnek 1, Aşağıdaki örnek "msgget" sırasında kullanılan örneğin üzerine eklenmiştir: #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; int value; }; int main(int argc, char** argv) { int msgid; /* * Aşağıda bu "key" değerine ilişkin bir mesaj kuyruğu yok ise yeni bir tanesi * oluşturulacaktır. */ if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; for(;;) { /* * Aşağıda son parametre "0" geçildiği için, alınan mesajın kırpılırsa, * fonksiyon başarısız olacaktır. Dolayısıyla kaç bayt alındığının bir * önemi kalmamıştır. */ if(msgrcv(msgid, &msg, sizeof(int), 0, 0) == -1) exit_sys("msgrcv"); printf("%ld. [%d]\n", msg.mtype, msg.value); /* Bu aşamada okunacak uygun mesaj paketi yok ise proses bloke olacaktır.*/ } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıda "IPC_NOWAIT" bayrağının kullanımına bir örnek verilmiştir: #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; int value; }; int main(int argc, char** argv) { int msgid; /* * Aşağıda bu "key" değerine ilişkin bir mesaj kuyruğu yok ise yeni bir tanesi * oluşturulacaktır. */ if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; for(;;) { /* * Aşağıda son parametre "IPC_NOWAIT" geçildiği için bloke olmayacaktır. */ if(msgrcv(msgid, &msg, sizeof(int), 0, IPC_NOWAIT) == -1 && errno == EAGAIN) break; else exit_sys("msgcrv"); printf("%ld. [%d]\n", msg.mtype, msg.value); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekte ise özel bir mesaj alınması taktirde kuyruktan okuma işlemi sonlanacaktır: #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; int value; }; int main(int argc, char** argv) { int msgid; /* * Aşağıda bu "key" değerine ilişkin bir mesaj kuyruğu yok ise yeni bir tanesi * oluşturulacaktır. */ if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; for(;;) { if(msgrcv(msgid, &msg, sizeof(int), 0, 0) == -1) exit_sys("msgcrv"); /* Paket içerisindeki mesaj "31" ise okuma sonlanacaktır. */ if(msg.value == 31) break; printf("%ld. [%d]\n", msg.mtype, msg.value); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi de mesaj kuyruklarını kullanarak iki prosesi haberleştirelim. Bu iki prosesten birisi klavyeden okuduklarını kuyruğa yazacak, diğeri de kuyruktakileri okuyarak ekrana basacaktır. * Örnek 1, Aşağıda "FIFO" semantiği uygulanmıştır. /* write */ #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; msg.mtype = 1; // Bu değer ile işimiz olmadığından, baştan "1" atıyoruz. char* str; for(;;) { printf("Message Text: "); fflush(stdout); /* * Klavyeden okuduklarımızı ilgili diziye yazıyoruz. * Burada '\n' karakteri de diziye alınmaktadır. */ if(fgets(msg.buffer, 8192, stdin) == NULL) continue; /* * Daha sonra bu '\n' karakterini '\0' ile değiştiriyoruz. */ if((str = strchr(msg.buffer, '\n')) != NULL) *str = '\0'; /* * "strlen" fonksiyonu yazının sonundaki '\0' karakterini * dahil etmediği için, o karakter kuyruğa aktarılmayacaktır. * Eğer bu karakteri de dahil etmek isteseydik, şöyle bir uzunluk * girmeliydik => " strlen(msg.buffer) + 1" */ if(msgsnd(msgid, &msg, strlen(msg.buffer), 0) == -1) exit_sys("msgsnd"); /* "quit" yazısı paketlenirse, bu proses sonlanacaktır. */ if(!strcmp(msg.buffer, "quit")) break; } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; ssize_t result; for(;;) { if((result = msgrcv(msgid, &msg, 8192, 0, 0)) == -1) exit_sys("msgcrv"); /* "quit" yazısı alınırsa, bu proses sonlanacaktır. */ if(!strcmp(msg.buffer, "quit")) break; /* * Karşı taraftan '\0' karakteri gönderilmediği için, * bizler eklemek istiyoruz. Çünkü birazdan ekrana basacağız. * Eğer karşı taraf bu karakteri de gönderseydi, bu satıra ve * "result" değerine gerek kalmayacaktı. */ msg.buffer[result] = '\0'; printf("%ld. [%s]\n", msg.mtype, msg.buffer); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıda ise "Priority Queue" semantiği uygulanmıştır. /* write */ #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); void clear_stdin(void); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; char* str; for(;;) { printf("Message Type: "); scanf("%ld", &msg.mtype); /* * "scanf" fonksiyonu klavyeden girilen '\n' * karakterini almayacaktır. Dolayısıyla bizler * "stdin" tamponunu boşaltmalıyız. */ clear_stdin(); printf("Message Text: "); fflush(stdout); if(fgets(msg.buffer, 8192, stdin) == NULL) continue; if((str = strchr(msg.buffer, '\n')) != NULL) *str = '\0'; /* Yazının sonundaki '\0' karakteri de gönderilecektir. */ if(msgsnd(msgid, &msg, strlen(msg.buffer) + 1, 0) == -1) exit_sys("msgsnd"); if(!strcmp(msg.buffer, "quit")) break; } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void clear_stdin(void) { while(getchar() != '\n'); } /* read */ #include #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; ssize_t result; for(;;) { /* * Dördüncü parametre negatif bir sayı olduğundan, * "id" değeri en küçük olan paket ilk alınacaktır. * Böylelikle "FIFO" yerine "Priority Queue" semantiği * uygulanmış oldu. */ if((result = msgrcv(msgid, &msg, 8192, -100, 0)) == -1) exit_sys("msgcrv"); if(!strcmp(msg.buffer, "quit")) break; printf("%ld. [%s]\n", msg.mtype, msg.buffer); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi de mesaj kuyrukları kullanılarak "Client-Server" haberleşmesi yapalım. Burada toplamda iki adet mesaj kuyruğu yeterli gelecektir. Bir tanesi "Client" lardan "Server" a, diğer ise "Server" dan "Client" a. Bu uygulama aşağıdaki şu şartları sağlayacaktır: -> "Client" programlar kendi proses "id" değerlerini, gönderilecek mesaj paketlerinin "id" değeri olarak kullanacaklardır. Aslında mesajın tür değeri, o prosesin "id" değeri oluyor. -> Paketlenen mesajlar, "ServerQueue" diyeceğimiz bir mesaj kuyruğuna gönderilsin. -> "Server" programın "FIFO" semantiğini uygulamasını isteyelim. Dolayısıyla "msgrcv" fonksiyonunun dördüncü parametresi "0" olmalıdır. Sırayla aldığı paketlerdeki istekleri yerine getirir. -> İsteklerin cevaplarını tekrar paket haline getirir. Bu paketlerin "id" değerleri, yine "client" programların proses "id" değerleri olur. -> Paketlenen mesajlar bu sefer başka bir mesaj kuyruğuna eklenir. İsmini de "ClientsQueue" diyelim. -> "ClientsQueue" den okuma yapacak olan "Client" ler, "msgrcv" fonksiyonunun dördüncü parametresine kendi proses "id" değerlerini geçerler. Böylelikle kendilerini ilgilendiren paketleri çekerler. Daha önceki derste "Client-Server" haberleşmesi için mesaj kuyrukları kullanılacaksa iki adet mesaj kuyruğunun kullanılması gerektiğinden bahsetmiştik. Pekiyi bizler sadece ve sadece bir adet mesaj kuyruğu kullansak nasıl olur? Teknik olarak böyle bir şey mümkündür. "Client" programlar yine kendi proses "id" değerlerini gönderilecek mesaj paketlerniin "id" değeri olarak kullanırlar. "Server" ise yanıtları yine aynı "id" değerini kullanarak aynı kuyruğa yazar. Fakat bu yöntem beraberinde "deadlock" problemini de getirebilir. >>>> "deadlock": Her ne kadar bu konu ileride ele alınacak olsa, kısaca şöyle bir açıklama yapılabilir; Birisi birisini beklerken, başka birisi de başka birisini beklemek durumudur. Yani prosesler birbirlerini bekledikleri için sistem kitleniyor. Bu sorun bir senkronizasyon problemidir ve dikkatsizlik sonucu ortaya çıkar. Örneğin, "Server" program almış olduğu isteği işlerken diğer "Client" programlar kuyruğu istekler ile doldururlar. Sonuçta mesaj kuyruklarının da bir limiti vardır ve kuyruğa yazma fonksiyonu blokeli modda kullanıldığında ilgili prosesin bloke olmasına yol açarlar. Artık kuyruk dolduğu için "Server" program isteğin cevabını da yazamaz. Öte yandan, dikkat edilmezse, "Client" program kuyruğa yazdığı kendi paketi okuyabilir. Bu da başlı başına bir problemdir. Ek olarak bahsetmek gerekirse; iki kuyruklu model de kullansak, tek kuyruklu model de kullansak veya her bir "Client" için ayrı bir mesaj kuyruğu da oluşturak, bir "Client" kuyruktan mesaj okumaz ise o kuyruk dolağı için kilitlenme oluşacaktır. Tabii "Client" programları da bizler yazacağımız için böylesi bir genellikle durum gerçekleşmez. Ancak "Client" programlar, yanlış programlama tekniklerinden dolayı bir şekilde bloke olsalar, yine kilitlenme gerçekleşebilir ve diğer "Client" programlar bu durumdan olumsuz etkinelebilir. Dolayısıyla hangi yöntemin kullanılacağı işin başında tartışılmalıdır. Şimdi de elimizdeki en son "System 5 IPC" fonksiyonu olan "msgctl" fonksiyonunu inceleyelim: >>>> "msgctl": Bu fonksiyon, mesaj kuyrukları üzerinde bir takım işlemleri yapabilmemize olanak sağlar. Fonksiyonun prototipi şu şekildedir: #include int msgctl(int msqid, int cmd, struct msqid_ds *buf); Fonksiyonun birinci parametresi üzreinde işlem yapılacak mesaj kuyruğununun "id" değeridir. Bildiğiniz üzere bu "id" değeri "msgget" fonksiyonuna bir "key" değeri geçilerek elde ediliyordu. Fonksiyonun ikinci parametresi ise mesaj kuyruğuna uygulanacak işleme dair parametredir. Bu parametre şu değerlerden birisi olmalıdır: "IPC_STAT", "IPC_SET" ve "IPC_RMID". Fonksiyonun üçüncü parametresi ise "msqid_ds" türünden bir yapının adresidir. Bu yapı "sys/msg.h" isimli başlık dosyasında tanımlanmış olup aşağıdaki şekildeki gibidir: structure msqid_ds { struct ipc_perm msg_perm // Operation permission structure. msgqnum_t msg_qnum // Number of messages currently on queue. msglen_t msg_qbytes // Maximum number of bytes allowed on queue. pid_t msg_lspid // Process ID of last msgsnd(). pid_t msg_lrpid // Process ID of last msgrcv(). time_t msg_stime // Time of last msgsnd(). time_t msg_rtime // Time of last msgrcv(). time_t msg_ctime // Time of last change. }; Şimdi de "msqid_ds" yapı türünün elemanlarını sırasıyla inceleyelim: >>>>> "msg_perm" : Bu elemanın türü "ipc_perm" yapı türündendir. Bu tür, "sys/ipc.h" isimli başlık dosyasında şu şekilde tanımlanmıştir: struct ipc_perm { uid_t uid // Owner's user ID. gid_t gid // Owner's group ID. uid_t cuid // Creator's user ID. gid_t cgid // Creator's group ID. mode_t mode // Read/write permission. }; Şimdi de "ipc_perm" yapı türünün elemanlarını sırasıyla inceleyelim: >>>>>> "uid" : Mesaj kuyruğunda en son "msgctl" ile değişiklik yapan prosesin "Etkin Kullanıcı ID" değerini göstermektedir. >>>>>> "gid" : Mesaj kuyruğunda en son "msgctl" ile değişiklik yapan prosesin "Etkin Grup ID" değerini göstermektedir. >>>>>> "cuid": Mesaj kuyruğunu ilk oluşturan prosesin "Etkin Kullanıcı ID" değerini göstermektedir. >>>>>> "cgid": Mesaj kuyruğunu ilk oluşturan prosesin "Etkin Grup ID" değerini göstermektedir. >>>>>> "mode": Mesaj kuyruğunun sahip olduğu erişim haklarını belirtmektedir. >>>>> "msg_stime" : Mesaj kuyruğuna yapılan en sonki "msgsnd" işleminin tarihini belirtmektedir. "epoch" dan geçen zamandır. >>>>> "msg_rtime" : Mesaj kuyruğuna yapılan en sonki "msgrcv" işleminin tarihini belirtmektedir. "epoch" dan geçen zamandır. >>>>> "msg_ctime" : Mesaj kuyruğuna yapılan en sonki "msgctl" işleminin tarihini belirtmektedir. "epoch" dan geçen zamandır. >>>>> "msg_qnum" : Mesaj kuyruğunda o anda bulunan mesaj paket sayısını verir. Bu elemanın türü "unsigned integer" olmak zorundadır. En azından "unsigned short" türünden olmalıdır. >>>>> "msg_qbytes": Mesaj kuyruğundaki paketlerin içerisindeki mesajların toplam uzunluğunu verir. "MSGMNB" değeri bu değerin limit değeridir. Bu elemanın türü "unsigned integer" olmak zorundadır. En azından "unsigned short" türünden olmalıdır. >>>>> "msg_lspid" : Mesaj kuyruğuna en son "msgsnd" yapan prosesin "id" değerini verir. >>>>> "msg_lrpid" : Mesaj kuyruğundan en son "msgrcv" yapan prosesin "id" değerini verir. Fonksiyon başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri dönmektedir. Pekiyi bütün bunlar ne anlama gelmektedir? Fonksiyonun ikinci parametresi "IPC_STAT", "IPC_SET" ve "IPC_RMID" değerlerinden birisini geçmemiz gerekmektedir. Şimdi de bu değerlerin ne anlama geldiğine bakalım: >>>>> "IPC_STAT" : İkinci parametreye bu değeri geçtiğimiz zaman, ilgili fonksiyonun üçüncü parametresine geçilen "msqid_ds" yapısının elemanları, üzerinde çalışılan mesaj kuyruğunun bilgileri ile doldurulur. * Örnek 1, Aşağıdaki örnekte "msgctl" fonksiyonunun ikinci parametresine "IPC_STAT" değeri geçilerek üzerinde çalışılan mesaj kuyruğununun bilgileri elde edilmiştir. #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if((msgid = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; msg.mtype = 1; strcpy(msg.buffer, "test"); if(msgsnd(msgid, &msg, 4, 0) == -1) exit_sys("msgsnd"); struct msqid_ds msginfo; if(msgctl(msgid, IPC_STAT, &msginfo) == -1) exit_sys("msgctl"); /* * "msginfo.msg_qbytes" ve "msginfo.msg_qnum" isimli elemanlar en az "unsigned short" * olmak zorundalar fakat şu anki sistemde neye karşılık gelmediğinden, C11 ile dile * eklenen "uintmax_t" türünü kullandık. Bu tür, o sistemdeki en geniş "unsigned integer" * türüne denk gelmektedir. */ printf("Maximum # of bytes: %ju\n", (uintmax_t)(msginfo.msg_qbytes)); printf("Maximum # of message packages: %ju\n", (uintmax_t)(msginfo.msg_qnum)); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>>> "IPC_SET" : İkinci parametreye bu değeri geçtiğimiz zaman üzerinde çalışılan mesaj kuyruğunun bazı bilgilerini değiştirebiliriz. Sadece şu bilgiler değiştirilebilir: "msg_perm.uid", "msg_perm.gid", "msg_perm.mode" ve "msg_qbytes". Bu elemanlar aslında "msqid_ds" türündeki yapının "msg_perm" isimli elemanlarıdır. Bahsi geçen elemanların isimlerinden de anlaşılacağı üzere, bu elemanlar erişim kontrolü için kullanılan elemanlardır. Ek olarak "IPC_SET" bayrağını kullanabilmemiz için prosesimizin ya "appropriate priviledged" olması ya da "Etkin Kullanıcı ID" değerinin "msqid_ds" yapısının "msg_perm" isimli elemanının "cuid" veya "uid" değerine eşit olması gerekmektedir. Öte yandan prosesimiz ilgili mesaj kuyruğuna "write" hakkı varsa kuyruğa mesaj paketi gönderebilir, "read" hakkı varsa kuyruktan mesaj paketi okuyabilir. Fakat "owner", "group" ve "other" kontrolü şu şekilde sağlanmaktadır: -> "msqid_ds" yapısının "msg_perm" isimli elemanının "uid" isimli elemanı ile bizim prosesimizin "Etkin Kullanıcı ID" değeri aynıysa, bizler mesaj kuyruğu için "owner" statüsündeyiz. -> "msqid_ds" yapısının "msg_perm" isimli elemanının "gid" isimli elemanı ile bizim prosesimizin "Etkin Grup ID" değeri aynıysa, bizler mesaj kuyruğu için "group" statüsündeyiz. -> Yukarıdaki şartlar karşılanmamışsa, bizler "other" statüsündeyiz. Son olarak "IPC_SET" işlemi yapılırken "msqid_ds" yapısının diğer elemanları dikkate alınmamaktadır. Tabii sadece belli bir değeri değiştireceksek, önce "IPC_STAT" yapmalı daha sonra "IPC_SET" uygulamalıdır. Çünkü "IPC_SET" işlemi elemanların hepsine etki etmektedir. * Örnek 1, #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if((msgid = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct msqid_ds msginfo; if(msgctl(msgid, IPC_STAT, &msginfo) == -1) exit_sys("msgctl"); printf("Maximum # of bytes: %ju\n", (uintmax_t)(msginfo.msg_qbytes)); msginfo.msg_qbytes = 3000; /* * Bu işlemi yapabilmemiz için prosesimizin bir takım yetkinliklere veya mesaj kuyruğunu * oluşturan proses olmalı ya da mesaj kuyruğunda en son "stat" işlemi uygulamış bir proses * olmalıdır. Aksi halde değiştirme işlemi başarısız olacaktır. */ if(msgctl(msgid, IPC_SET, &msginfo) == -1) exit_sys("msgctl"); if(msgctl(msgid, IPC_STAT, &msginfo) == -1) exit_sys("msgctl"); printf("Maximum # of bytes: %ju\n", (uintmax_t)(msginfo.msg_qbytes)); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>>> "IPC_RMID" : Bir mesaj kuyruğunu silmek için kullanılır. İkinci parametreye bu değer geçildiğinde "msgctl" fonksiyonunun üçüncü parametresine neyin geçildiği önemli kalmaktadır. Fakat bazı sistemler üçüncü parametreye geçilen değeri "ignore" ederken bazıları etmemektedir. POSIX standartları "ignore" etmektedir. "IPC_SET" yapabilmek için prosesimizin sahip olması gereken "id" değerleri, bu işlemi yapmak için de geçerlidir. Yani prosesimiz prosesimizin ya "appropriate priviledged" olması ya da "Etkin Kullanıcı ID" değerinin "msqid_ds" yapısının "msg_perm" isimli elemanının "cuid" veya "uid" değerine eşit olması gerekmektedir. Eğer bir mesaj kuyruğu bu işlem ile silinmez ise sistem "reboot" olduğunda yine silinecektir. Son olarak belirtmekte fayda varki bir proses mesaj kuyruğu üzerinde işlem yaparken bir başka proses mesaj kuyruğunu silerse, ilgili mesaj kuyruğu anında silinecektir. Dolayısıyla mesaj kuyruğu üzerinde işlem yapan diğer prosesler de hata durumunda düşecektir. Burada, dosyalardaki silme işleminin aksine, en son proses de sonlandıktan sonra gerçek silme GERÇEKLEŞMİYOR. DİREKT OLARAK SİLİNİYOR. * Örnek 1, #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 void exit_sys(const char*); int main(int argc, char** argv) { int msgid; key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if((msgid = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); if(msgctl(msgid, IPC_RMID, NULL) == -1) exit_sys("msgctl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Diğer yandan "System 5 IPC" fonksiyonlarının önemli bir handikapı ilgili mesaj kuyruklarını açarken kullanılan "key" değerinin bir çakışmaya mahal vermesidir. Yani kullandığımız "key" değeri halihazırda başka programlar tarafından kullanılma durumudur. Her ne kadar bu problemi gidermek etmek için "IPC_PRIVATE" bayrağı kullanılsa da, elde ettiğimiz "id" değerinin diğer prosese bir şekilde aktarılması gerekmektedir. İşte bu çakışma ihtimalini daha da azaltan bir fonksiyon POSIX standartlarına eklenmiştir. İsmi "ftok" olan bu fonksiyon aşağıdaki parametrik yapıya sahiptir: #include key_t ftok(const char *path, int id); Burada fonksiyonun birinci parametresine varolan bir dosyanın yol ifadesi, ikinci parametresine de bir sayı gireceğiz. Başarılı olursa da bizlere bir "key" değeri dönecek. Eğer bu iki parametreyi aynı tutarsak, elde edeceğimiz "key" değeri aynı olacaktır. Fakat en az birisi farklı olursa, elde edeceğimiz "key" değeri de farklılaşacaktır. Öte yandan yol ifadesindeki dosyayı silip aynı isimle tekrar oluştursak, aynı "key" değerini elde edemeyebiliriz. Bu yönde bir garanti sunulmamaktadır. Ek olarak ikinci parametreye geçilen sayının düşük anlamlı 8 bit'i kaale alınacaktır. Eğer iş bu bit değerleri "0" olursa, sonucun ne olacağı "unspecified" olarak belirlenmiştir. Son olarak fonksiyon başarısız olursa "-1" ile geri dönüp "errno" değişkenini uygun değere çekecektir. Bu fonksiyonun yegane amacı, "System 5 IPC" nesneleri için tekil bir "key" değeri üretmek. Libc kütüphanesinde, yol ifadesiyle belirtilen dosyanın "I-node" numarası ile ikinci parametresindeki "id" değeri kombine edilmektedir. Burada tam manasıyla tekil bir "key" değerinin üretileceği garanti edilmemiştir. Her ne kadar dosyaların "I-node" numaraları tekil olsa da ikinci parametreyle geçilen "id" değeriyle yapılan kombinasyon işlemi sonucu aynı "key" değeri elde edilebilir. * Örnek 1, /* write */ #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 #define NORMAL_MSG 1 #define QUIT_MSG 2 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if((msgid = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); size_t len; struct MSG msg; char* str; for(;;) { printf("Message Text: "); fflush(stdout); if(fgets(msg.buffer, 8192, stdin) == NULL) continue; if((str = strchr(msg.buffer, '\n')) != NULL) *str = '\0'; if(!strcmp(msg.buffer, "quit")) { msg.mtype = QUIT_MSG; len = 0; } else { msg.mtype = NORMAL_MSG; len = strlen(msg.buffer) + 1; } if(msgsnd(msgid, &msg, len, 0) == -1) exit_sys("msgsnd"); if(msg.mtype == QUIT_MSG) break; } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 #define NORMAL_MSG 1 #define QUIT_MSG 2 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if((msgid = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; ssize_t result; for(;;) { if((result = msgrcv(msgid, &msg, 8192, 0, 0)) == -1) exit_sys("msgcrv"); if(msg.mtype == QUIT_MSG) break; printf("%ld. [%s]\n", msg.mtype, msg.buffer); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } "System 5 IPC" nesneleri için iki önemli "shell" komutu vardır. Bunlar "ipcs" ve "ipcrm". Bizler daha önce zaten "ipcs" komutunu görmüştük. Anımsanacağı üzere bu komut "/proc/sysvipc" dizinindeki "msg", "sem" ve "shm" isimli dosyalara başvuararak işlem yapmaktadır. "ipcrm" komutu ise belli bir "ipc" nesnesini silmek için kullanılmaktadır. Silme işlemi "key" değerine göre veya "id" değerine göre yapılabilir. Bu değerleri görmek için yine "ipcs" komutunu kullanabiliriz. "ipcrm" komutunun seçeneklerinden küçük harfli olanlar "id", büyük harfli olanlar ise "key" değerine göre silme işlemi yaptırtmaktadır. "-q/-Q" seçeneği ise mesaj kuyruğu silmek için kullanılır. Paylaşılan bellek alanları için de "-m/-M" seçeneklerini kullanmalıyız. >>> "POSIX IPC" Fonksiyonları: Anımsanacağınız üzere "IPC" fonksiyonları iki gruba ayrılmıştır. Bir grup "System 5 IPC", diğer gruba ise "POSIX IPC" fonksiyonları denmektedir. Bir tanesi 70'li yıllardan beri varolurken, "POSIX IPC" nesneleri 90'lı yıllardan itibaren hayatımıza girmeye başlamıştır. Bu gruplardaki fonksiyonların arayüzleri, kendi grupları içerisinde birbirine benzemektedir. Fakat birbirleriyle karşılaştırıldıklarında işin yapılış şekli değişmektedir. Örneğin "POSIX IPC" fonksiyonları "key" değeri yerine direkt bir dosya ismi kullanmaktadır. Tıpkı isimli borularda olduğu gibi. Fakat bu dosyanın, bir dizin girişi biçiminde bulundurulması zorunluluk değildir. Öte yandan POSIX standartlarına göre bu dosyanın kök dizinde olması gerekmektedir. Yani kullanacağımız isim, kök dizindeki bir dosyaya ilişkin olmalıdır. Örneğin, "/my_message_queue" şeklinde. Ama "ls" kabuk komutu çalıştırdığımız zaman "my_message_queue" ismini görmeyeceğiz. Fakat işletim sistemi izin veriyorsa kök dizin içerisindeki başka dizinleri de kullanabiliriz. Fakat unutmamalıyız ki isimlerin de çakışabilmesi mümkünse de bu olasılık "key" değerlerinin çakışma ihtimalinden daha düşüktür. Buraya kadar yazılanlardan da anlaşılacağı üzere "POSIX IPC" fonksiyonları tıpkı dosya fonksiyonlarına benzemektedir. Mesela "System 5 IPC" fonksiyonlarında kuyruğu sildiğimiz zaman direkt olarak silme işlemi gerçekleşmektedir. Kuyruk üzerinde işlem yapanlar beklenmemektedir. Fakat "POSIX IPC" fonksiyonlarında kuyruk silme işlemi yaptığımız zaman direkt olarak silme GERÇEKLEŞMİYOR. Kuyruk üzerinde işlem yapan son proses de işini bitirdikten sonra gerçek manada silme gerçekleşmektedir. Bütün bunlara ek olarak her iki "IPC" grubundaki nesneler "kernel persistance" biçimindedir. Yani ya direkt olarak elle silinmelidir ya da ilk "reboot" gerçekleştikten sonra silinecektir. Pekiyi bizler hangi fonksiyonları kullanmalıyız? Her iki fonksiyon grubunun da birbirinden üstün olduğu, zayıf olduğu noktaları vardır. "POSIX IPC" fonksiyonları daha güncel olması hasebiyle bu gruptakileri kullanmaya çalışmalıyız. Son olarak "POSIX IPC" nesneleri "libc" kütüphanesinde değil, "librt" kütüphanesinde tanımlanmıştır. Dolayısıyla dosyaları derlerken "-lrt" seçeneğini de kullanmamız gerekiyor. Şimdi "POSIX IPC" mesaj kuyruklarını inceleyelim. Mesaj kuyruklarında "POSIX IPC" fonksiyonlarını kullanabilmek için şu sırayı gözetmemiz gerekmektedir: -> Üzerinde çalışılacak mesaj kuyruğu, "mq_open" fonksiyonu ile açılır. Burada bütün prosesler bu fonksiyonu geçerken aynı ismi kullanmaldır. "mq_open" fonksiyonunun protipi şu şekildedir; #include mqd_t mq_open(const char *name, int oflag, ...); Fonksiyon ya iki argüman ile ya da dört argüman ile çağrılabilir. Eğer mesaj kuyruğu var ise iki argümanla, mesaj kuyruğu yeniden oluşturulacak ise dört argüman ile çağrılmalıdır. Buradaki dört argümandan son iki argüman mesaj kuyruğunun bir takım özelliklerini belirtmektedir. Öte yandan fonksiyonun birinci ve ikinci parametresi ise sırasıyla açılacak/oluşturulacak mesaj kuyruğunun ismini ve yapılacak isme dair argümanlardır. İkinci parametre şu üç değerden sadece birisini alabilir; "O_RDONLY", "O_WRONLY" ve "O_RDWR". Fakat bunlardan bir tanesi şu değerler ile "bitwise-OR" işlemine sokulabilir; "O_CREAT", "O_EXCL" ve "O_NONBLOCK". Unutmamalıyız ki "O_EXCL" bayrağı "O_CREAT" ile birlikte kullanılır ve o isme dair bir kuyruk varsa fonksiyon başarısız olacaktır. Öte yandan "O_NONBLOCK" bayrağı ise okuma ve yazma işlemlerinin blokesiz modda yapılacağını belirtir. Anımsanacağı üzere bu bayrak, "SYSTEM 5 IPC" fonksiyonlarında, "msgsnd" ve/veya "msgrcv" fonksiyonlarında kullanılmaktaydı. Bütün bunlardan hareketle "O_CREAT" bayrağı kullanılmışsa, "mq_open" fonksiyonuna üçüncü ve dördüncü parametrelerini geçmeliyiz. Çünkü yeni bir mesaj kuyruğu oluşurken bu parametrelerdeki bilgileri kullanmaktadır. Pekiyi nedir bu üçüncü ve dördüncü parametreler? Üçüncü parametre, mesaj kuyruğunun sahip olacağı erişim haklarına ilişkindir ve dosyalarda kullandığımız "S_IXXX" sembolik sabitleri kullanılır. Bu sembolik sabitleri "bitwise-OR" işlemine sokabiliriz. Dördüncü parametre ise bir "struct" türünden nesnenin adresi olmalıdır. Bu yapının türü ise "mq_attr" türündendir. Bu yapının tanımı aşağıdaki gibidir; struct mq_attr { long mq_flags; /* Flags (ignored for mq_open()) */ long mq_maxmsg; /* Max. # of messages on queue */ long mq_msgsize; /* Max. message size (bytes) */ long mq_curmsgs; /* # of messages currently in queue (ignored for mq_open()) */ }; Yapının "mq_flags" isimli elemanı sadece "O_NONBLOCK" bayrağını içerebilir. "mq_maxmsg" elemanı ise kuyrukta tutulabilecek maksimum mesaj paketinin sayısını belirtmektedir. "mq_msgsize" ise bir mesajın kaplayabileceği maksimum büyüklüğü belirtmektedir. Bu büyüklük bir mesaj paketinin büyüklüğü değil, o paket içerisindeki mesajın büyüklük bilgisidir. "mq_curmsgs" ise kuyrukta o anda bulunan mesaj paket adedini belirtmektedir. Şimdi bu yapının "mq_flags" ve "mq_curmsgs" isimli elemanları "mq_open" fonksiyonunca göz ardı edilir. Dolayısıyla bizler "mq_attr" türünden bir nesnenin adresini "mq_open" fonksiyonuna geçmeden evvel sadece "mq_maxmsg" ve "mq_msgsize" isimli elemanlarını değiştirmeliyiz. "mq_open" fonksiyonu, bu iki elemanı dikkate alacaktır. Linux standart dökümanlarında "mq_open" fonksiyonunun dördündü paremetresi "non-const" olarak belirtilmiştir fakat görüleceği üzere bu parametre "set" konusunda bir işlevi yoktur. Dolayısıyla bizler "const mq_attr" türden nesnenin adresini geçmeliyiz. Öte yandan "mq_open" fonksiyonunun bu dördüncü parametresine "NULL" değeri de geçilebilir. Bu durumda oluşturulacak mesaj kuyruğu varsayılan değerler ile hayata gelecektir. Bu varsayılan değerler sistemden sisteme değişmektedir. Linux sistemlerinde "mq_maxmsg" değeri "10" iken, "mq_msgsize" değeri "8192" dir. Ek olarak "mq_open" fonksiyonunda kuyruk özelliklerini girerken "mq_maxmsg" ve "mq_msgsize" değerleri için o işletim sistemi alt ve üst limit de belirlemiş olabilir. Eğer bizim gireceğimiz değerler bu limitlerin dışında kalırsa, fonksiyon başarısız olur ve "errno" değişkeni "EINVAL" değerine çekilir. İlaveten "priviledged user" olan prosesler "mq_maxmsg" değişkenini arttırabilirken, "non-priviledged users" olanlar arttıramamaktadır. POSIX standartları bu limitler hakkında bir şey söylememiştir. İleride göreceğimiz "getrlimit" ve "setrlimit" fonksiyonları ile bu limit değerlerini değiştirebiliriz. Fonksiyonun geri dönüş değeri de bir "handle" değeridir. Bu değer Linux standartlarınca "fd" olarak ele alınmış fakat POSIX bu konuda "file descriptor" olmalıdır değil, herhangi bir "descriptor" olmalıdır demiştir. Özetle Linux sistemlerinde geri dönüş değeri, prosesin "Dosya Betimleyici Tablosunda" bir indeks numarasıdır. Fakat POSIX standartlarınca böyle bir zorunluluktan bahsedilmemiştir. Öte yandan Linux sistemlerinde geri dönüş değerinin "Dosya Betimleyici Tablosundaki" en düşük betimleyici olacağına dair bir şey söylenmemiştir. Fakat fonksiyon başarısız olursa "-1" ile geri dönecektir. Bizler mesaj kuyruğu üzerinde işlem yaparken bu "descriptor" değerini kullanacağız. Fonksiyonun geri dönüş değer türü "mqd_t" hangi türe karşılık geleceğine dair POSIX standartları bir şey söylememiştir. Yapı türleri de dahil olmak üzere herhangi bir tür olabilir. -> Mesaj kuyruğunu oluşturduktan sonra mesaj paketi göndermek için "mq_send" fonksiyonunu kullancağız. Fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio); Fonksiyonun birinci parametresi, "mq_open" ile elde ettiğimiz "descriptor" değeridir. Fonksiyonun ikinci parametresi ise gönderilecek mesajı belirtmektedir. Bu parametrenin "const char*" olmasından dolayı bizler göndereceğimiz şeyi "const char*" türüne "cast" ettikten sonra bu fonksiyona geçmeliyiz. Fonksiyonun üçüncü parametresi ise gönderilecek mesajın uzunluğunu, son parametre ise iş bu mesajın öncelik derecesini belirtmektedir. Bu öncelik derecesi "0" a eşit ya da "0" dan büyük girilmelidir. Bu öncelik derecesi ise şöyle işlemektedir; büyük değere sahip mesajlar kuyruğun önünde yer alırken, küçük değere sahipler kuyruğun arkalarında yer almaktadır. Eğer aynı değere sahip mesajlar varsa, yeni gelen mesaj eski mesajların arkasına yerleştirilmektedir. Anımsanacağı üzere "System 5 IPC" mekanizmasında öncelik değeri küçük olanlar mesajın baş kısmına, büyük olanlar ise mesajın arka kısmına yerleştirilmekteydi. Öte yandan "POSIX IPC" mesaj kuyruklarında, belli bir değeri kuyruktan almak gibi bir durum SÖZ KONUSU DEĞİLDİR. Son olarak bu parametreye geçeceğimiz değer "MQ_PRIO_MAX" sembolik sabit değerinden de KÜÇÜK OLMALIDIR. İş bu sembolik sabit ise "limits.h" içerisinde tanımlanmıştır. Fonksiyon başarı durumund "0" değerine, başarısızlık durumunda "-1" ile geri dönüp kuyruğa hiç mesaj yazmayacaktır. Son olarak "POSIX" mesaj kuyruklarının da bir limiti vardır. Kuyruğu açış sırasında kullanılan "O_NONBLOCK" bayrağına göre bu fonksiyon ya başarısızla geri dönecek ya da ilgili prosesi bloke edecektir. -> Mesaj kuyruğundan paket okuması yapabilmek için "mq_receive" fonksiyonunu kullanmalıyız. Fonksiyonun parametresi şu şekildedir; #include ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio); Fonksiyonun ilk parametresi yine "mq_open" ile elde ettiğimiz "descriptor" değeri. İkinci parametre ise mesajın yerleştirileceği alanın başlangıç adresi. Bu adres "char*" türden olduğu için bizler "char*" türüne "cast" yapmak durumunda kalabiliriz. Üçüncü parametre ise başlangıç adresi ikinci parametre olan alanın genişliğidir. Fakat bu parametreye dikkat etmeliyiz çünkü bu parametrenin, mesaj kuyruğu oluşturulurken dördüncü parametresine geçilen "mq_attr" yapı türünün "mq_msgsize" isimli elemanının değerinden küçük olmaması gerekir. Aksi halde "mq_receive" fonksiyonu direkt başarısız olmakta ve "errno" değişkenini uygun değere çekecektir. Pekiyi iş bu dördüncü parametre kullanılmamışsa, yani mesaj kuyruğu varsayılan değerler ile oluşturulmuşsa ya da bizler bu dördüncü parametredeki yapı türünün elemanlarınının değerini bilmiyorsak, ne yapmalıyız? İşte bu durumda devreye "mq_getattr" fonksiyonu girmektedir. Bu fonksiyonun ilgili mesaj kuyruğunun özelliklerini "get" eden bir fonksiyondur. Fakat bu "get" işlemi çalışma zamanında faal olacağı için, bizler "malloc" fonksiyonunu da kullanmamız gerekmektedir. "mq_receive" fonksiyonunun son parametresi ise kuyruktan alınan mesajın öncelik derecesidir. Kuyruktan alınan mesajın önceliği, bu adrese yazılacaktır. Tabii "NULL" adresini de geçebiliriz. Fonksiyonun geri dönüş değeri de başarı durumunda alınan mesajın uzunluğunu, başarısız durumunda ise "-1" ile geri döner. Başarısızlık durumunda kuyruktaki mesajı ALMAYACAKTIR. -> Pekiyi haberleşme nasıl sonlandırılmalıdır? Burada da tıpkı "System 5 IPC" mesaj kuyruklarındaki gibi özel bir mesajın kuyruğa yazılması gerekmektedir. Buradaki özel mesaj "0" uzunlukta bir mesaj olabileceği gibi belli bir değere sahip mesaj da olabilir. -> Son olarak "mq_close" fonksiyonu ile ilgili "descriptor" kapatılmalıdır. Fonksiyonun parametrik yapısı şöyledir: #include int mq_close(mqd_t mqdes); Bu fonksiyon çağrısını haberleşen her bir prosesin yapması tavsiye edilir. Fakat proses sonlandığında otomatik olarak ilgili "descriptor" kapatılacaktır. -> "POSIX IPC" mesaj kuyruklarının da "kernel persistance" olduğundan bahsetmiştik. Pekiyi bizler mesaj kuyruklarını nasıl sileceğiz? (To be continued...) Şimdi de bu vakte kadar gördüklerimize dair bir örnek yapalım: * Örnek 1, /* write */ #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void exit_sys(const char*); int main(int argc, char** argv) { struct mq_attr attr; // Bir şekilde sistemimizdeki en büyük değerin "10" olduğunu öğrendik diyelim. attr.mq_maxmsg = 10; // Bu değeri biz belirledik. Dolayısıyla kuyruktan okuma yaparken bu "32" değerini // kullanabiliriz. attr.mq_msgsize = 32; mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, &attr)) == -1) exit_sys("mq_open"); for(int i = 0; i < 100; ++i) { if(mq_send(mqd, (const char*)&i, sizeof(int), 0) == -1) exit_sys("mq_send"); } mq_close(mqd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void exit_sys(const char*); int main(int argc, char** argv) { mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL)) == -1) exit_sys("mq_open"); /* * Kuyruğu biz oluşturduğumuz için mesajın genişlik bilgisini biliyorduk. */ char buffer[32]; int value; for(;;) { if(mq_receive(mqd, buffer, 32, NULL) == -1) exit_sys("mq_receive"); value = *(const int*)buffer; printf("%d\n", value); fflush(stdout); if(99 == value) break; } printf("\n"); mq_close(mqd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Daha önceki derste yaptığımız örneklerde mesaj kuyruğunu biz oluşturduğumuz için mesajın kaplayacağı alanı da biliyorduk. Dolayısıyla mesajı alırken ayırmamız gereken alan bilgisi bizde zaten vardı. Pekiyi bizler bu bilgiyi bilmiyorsak nasıl davranmalıyız? Çünkü karşı taraf mesaj kuyruğunu varsayılan değerlerle hayata getirebileceği gibi kendisi de bu değerleri belirleyebilir. Anımsanacağı üzere, böylesi bir durumda "mq_getattr" isimli fonksiyon devreye girecektir. İş bu fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int mq_getattr(mqd_t mqdes, struct mq_attr *mqstat); Fonksiyonun birinci parametresi, mesaj kuyruğuna ilişkin olan "descriptor" değeridir. İkinci parametre ise "mq_attr" yapı türünden bir nesnenin adresi. Mesaj kuyruğunun bilgileri, bu adresteki nesneye yerleştirilecektir. Geri dönüş değeri ise başarı durumunda "0", aksi halde "-1" biçimindedir. "mq_getattr" fonksiyonunun "set" işlemi yapan versiyonu da mevcuttur. Bu fonksiyon, mesaj kuyruğu oluşturulduktan sonra bazı özelliklerini değiştirmek için kullanılır. Fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int mq_setattr(mqd_t mqdes, const struct mq_attr * mqstat, struct mq_attr * omqstat); Fonksiyonun birinci parametresi, mesaj kuyruğuna ilişkin olan "descriptor" değeridir. İkinci parametre, yeni özelliklerin bulunduğu "mq_attr" yapı türünden bir nesnenin adresidir. Son parametre ise değiştirilmeden önceki bilgileri "get" etmek için kullanılan ve "mq_attr" yapı türünden bir nesnenin adresidir. Eğer eski değerleri istemiyorsak, üçüncü parametreye "NULL" değeri geçebiliriz. Geri dönüş değeri ise başarı durumunda "0", aksi halde "-1" biçimindedir. Pekiyi bizler bu fonksiyon ile "mq_attr" yapısındaki hangi elemanları değiştirebiliriz? Sadece ve sadece "mq_flags" isimli elemanı. Yapının diğer elemanları göz ardı edilecektir. Şimdi ise bu anlatılanların işlendiği bir örnek yapalım: * Örnek 1, Aşağıdaki örnekte öncelik değeri yüksek olan mesaj kuyruğun başında yer alırken, düşük olan kuyruğun sonunda yer almaktadır. Bunu görmek için ilk önce "write" programını çalıştırıp kuyruğu doldurmalı, sonrasında "read" fonksiyonu ile kuyruktakileri okumalıyız. /* write */ #include #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void clear_stdin(void); void exit_sys(const char*); int main(int argc, char** argv) { mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL)) == -1) exit_sys("mq_open"); int prio; char str*; char buffer[8192]; for(;;) { /* Kullanıcıdan gönderilecek mesaj alınır. */ printf("Message Text: "); fflush(stdout); if(fgets(buffer, stdin, 8192) == NULL) continue; if((str = strchr(buffer, '\n')) != NULL) *str = '\0'; /* Kullanıcıdan gönderilecek mesajın öncelik bilgisi de alınır. */ printf("Priority: "); fflush(stdout); scanf("%d", &prio); clear_stdin(); /* * İlgili mesajın sonundaki '\0' karakteri gönderilmeyecektir. * Eğer isteseydik, "strlen(buffer) + 1" dememiz gerekiyordu. */ if(mq_send(mqd, buffer, strlen(buffer), prio) == -1) exit_sys("mq_send"); if(!strcmp(buffer, "quit")) break; } mq_close(mqd); /* Mesaj kuyruğu bu noktada silinmiştir. */ return 0; } void clear_stdin(void) { int ch; while((ch = getchar() != '\n') && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void exit_sys(const char*); int main(int argc, char** argv) { mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL)) == -1) exit_sys("mq_open"); /* Kuyruğun özelliklerini bilmediğimiz için ilk önce öğreniyoruz. */ struct mq_attr attr; if(mq_getattr(mqd, &attr) == -1) exit_sys("mq_getattr"); /* * Mesajın ne kadarlık yer kaplayacağını öğrendikten sonra, o kadarlık bir alanı ayırıyoruz. * "attr.mq_msgsize + 1" dememizin sebebi, ayrılan bu ekstralık alana daha sonra '\0' karakteri * ekleneceği içindir. Karşı tarafın mesajın sonunda '\0' ekleyip eklemediği bilinmediği için * biz en kötüyü düşünerek hareket ettik. Eğer ekleyerek göndermişse, yazının sonunda iki adet * '\0' karakterinden olacaktır. */ char* buffer; if((buffer = malloc(attr.mq_msgsize + 1)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } int prio; ssize_t result; for(;;) { if(mq_receive(mqd, buffer, attr.mq_msgsize, &prio) == -1) exit_sys("mq_receive"); buffer[result] = '\0'; printf("%d. %s\n", prio, buffer); if(!strcmp(buffer, "quit")) break; } printf("\n"); free(buffer); mq_close(mqd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi sıra "POSIX IPC" mesaj kuyruklarının sistemden nasıl silineceğine geldi. Burada devreye "mq_unlink" fonksiyonu devreye girmektedir. Anımsanacağı üzere mesaj kuyrukları silme işlemi uygulanmadığı veya sistem "reboot" edilmediği sürece sistemde varlığını sürdürecektir. Öte yandan, burada direkt olarak mesaj kuyruğu silinmemektedir, tıpkı dosyalarda olduğu gibi. Ancak ve ancak bu kuyruk üzerinde işlem yapan prosesler işlemlerini bitirdikten sonra gerçek anlamda silinme gerçekleşecektir. Şunu da belirtmekte fayda vardır ki mesaj kuyruğunu oluşturan prosesin silmesi uygun görülmektedir. Eğer kuyruğu oluşturan kişi bilinmiyorsa, taraflardan birisi kuyruğu silebilir. Pekiyi nedir bu "mq_unlink" fonksiyonu? İlgili mesaj kuyruğu aşağıdaki parametrik yapıya sahiptir; #include int mq_unlink(const char *name); Fonksiyon, silinecek olan mesaj kuyruğunun ismini almaktadır. Başarı durumunda "0", başarısızlık durumunda "-1" ile geri dönmektedir. Aşağıdaki biçimde de bu fonksiyonu kullanabiliriz; //... #define MSG_QUEUE_NAME "/my_message_queue" //... if(mq_unlink(MSG_QUEUE_NAME) == -1) exit_sys("mq_unlink"); Malumunuz olduğu üzere "System 5 IPC" mesaj kuyruklarının bir limiti vardır. Bu limit kavramı yine "POSIX IPC" mesaj kuyrukları için de geçerlidir. Fakat POSIX standartları bu konuda bir şey söylememektedir. Linux sistemlerinde ise bir mesaj paketi içerisindeki mesajın uzunluğunun 8192 bayt, bir kuyruğun alabileği toplam mesaj paketi adedi de 10'dur. Pekiyi bu değerler nereden gelmektedir? "/proc/sys/fs/mqueue" dizini içerisindeki dosyalardan bu değerler belli olmaktadır. İş bu dosyalar şu şekildedir; "msg_default", "msg_max", "msgsize_default", "msgsize_max" ve "queues_max". Şimdi bu dosyaları inceleyelim: >>>> "msg_default" : Bir kuyruğu varsayılan değerler ile oluştururken, o kuyrukta bulunabilecek maksimum mesaj paketi adedini belirtil. Bu değer, birazdan göreceğimiz "msg_max" değerinden büyük olamaz. >>>> "msg_max" : "mq_open" ile bir mesaj kuyruğu oluştururken, varsayılan değerler yerine bizim atayacağımız değerler ile oluşturulmasına olanak veren "mq_attr" yapısının "mq_maxmsg" isimli elemanının alabileceği maksimum değeri belirler. Yani bizler mesaj kuyruğu yaratırken, "msg_max" değerinden daha büyük bir değeri "mq_attr.mq_maxmsg" elemanına veremeyiz. >>>> "msgsize_default": Bir kuyruğu varsayılan değerler ile oluştururken, o kuyruktaki mesaj paketlerinin içindeki mesajların alabileceği maksimum uzunluk değeridir. Bu değer, birazdan göreceğimiz "msgsize_max" değerinden büyük olamaz. >>>> "msgsize_max" : "mq_open" ile bir mesaj kuyruğu oluştururken, varsayılan değerler yerine bizim atayacağımız değerler ile oluşturulmasına olanak veren "mq_attr" yapısının "mq_msgsize" isimli elemanının alabileceği maksimum değeri belirler. Yani bizler mesaj kuyruğu yaratırken, "msgsize_max" değerinden daha büyük bir değeri "mq_attr.mq_msgsize" elemanına veremeyiz. >>>> "queues_max" : Sistem geneli oluşturulabilecek maksimum mesaj kuyruğu adedidir. Bu adetten daha büyük bir mesaj kuyruğunu hayata getiremeyiz. Öte yandan "root" olan prosesler, yukarıda detayları açıklanan "limit" değerlerinden etkilenmemektedir. Fakat "root" prosesin de takıldığı bir "limit" değeri vardır. Ek olarak, yukarıdaki beş "limit" dosyası, "root" tarafından değiştirilebilir. İlgili "limit" değerleri için bkz. (https://man7.org/linux/man-pages/man7/mq_overview.7.html). Fakat unutmamalıyız ki buradaki "limit" değerleri Linux sistemleri için geçerlidir. Diğer sistemlerde, o sistemin kaynak dökümanlarına bakılması gerekmektedir. Bütün bunlar bir yana olsun bazı POSIX türevi sistemlerde mesaj kuyrukları, özel bir dosya biçiminde, "mount" edilbilmektedir. Örneğin, Linux sistemlerinde mesaj kuyruklarına ilişkin bu özel dosya, "/dev/mqueue" dizini üzerine "mount" edilmiştir. Yani "/dev/mqueue" dizini içerisine girdiğimizde, "ls" kabuk komutuyla sistemde bulunan bütün "POSIX" mesaj kuyruklarını görebilir ve "rm" kabuk komutu ile bunları silebiliriz. Linux sistemlerinde, yukarıdaki "mount" işlemi otomatik olarak gerçekleştirilir. Tabii bizler de istediğimiz başka dizinler üzerinde bu özel dosyayı "mount" işlemi gerçekleştirebiliriz. Bunun için de kabuk programından şu aşağıdaki komutu çalıştırmalıyız: sudo unmount /dev/mqueue Artık ilgili özel dosya, "unmount" edilmiştir. Şimdi bizler bu özel dosyayı başka bir yere de "mount" etmemiz gerekiyor. Bunun için ilk olarak bir dizin oluşturmamız gerekiyor. Dolayısıyla önce aşağıdaki kabuk komutunu çalıştırmalıyız: mkdir my_message_queue_point Daha sonra aşağıdaki komutu çalıştırmalıyız. Fakat bizler "appropriate priviledged" olmamız gerekmektedir: sudo mount -t mqueue somerandomname my_message_queue_point Şimdi bizler "mount" işlemini gerçekleştirdik. "ls -l" çektiğimiz zaman "my_message_queue_point" isimli dizin girişi karşımıza çıkacaktır. "my_message_queue_point" içerisine girip "ls -l" çekersek, sistemdeki POSIX mesaj kuyruklarını görebiliriz. Şimdi burada bir noktaya dikkat çekmek gerekmektedir. "mount" işlemi sırasında "my_message_queue_point" isimli dizinin "sticky_bit" değeri de "set" edilmektedir. Dolayısıyla bu dizinin sahibi bile olsak, bu dizin içerisinde istediğimiz bir dosyayı silemiyorduk("appropriate priviledged" olmadğımız varsayılmıştır). Yalnızda kendimize ait dosyaları silebiliyor, sahibi başka olan dosyaları silemiyor idik. Halbuki iş bu "sticky_bit" değeri "set" edilmeseydi, ilgili dizin içerisinde istediğimiz dosyaları silebiliyorduk. Öte yandan bizler tekrardan "unmount" işlemi yaparsak, "my_message_queue_point" ismi dizin girişinde hala görülecektir fakat içi boş bir dizin gibi işlevi olacaktır. Yani bu aşamada "ls -l" çeksek, bir şey göremeyeceğiz. Pekiyi "mount" işlemi sırasında kullandığımız "somerandomname" değeri ne manaya gelmektedir? Sistem geneli "mount" edilen bütün özel dosyaların tutulduğu bir dosya düşünün. Bu dosya içerisinde "mount" edilen noktayı işaret eden isim olarak kullanılmaktadır. Yani bütün "mount point" ler bir isim ile ilişkilendirilmektedir. İş bu dosyayı görebilmek için "mount" isimli kabuk komutunu çalıştırmamız yeterli olacaktır. Mesaj kuyrukları bahsini kapatmadan evvel bahsedilmesi gereken iki fonksiyon daha vardır; "mq_timedsend" ve "mq_timedreceive" isimli fonksiyonlardır. Bu fonksiyonlar sırasıyla "mq_send" ve "mq_receive" fonksiyonlarının zaman aşımlı versiyonlarıdır. Pekiyi nedir bu zaman aşımı durumu? Üzerinde çalışılan kuyruk boşken ya da doluyken belirlenen zaman çerçevesinde blokeye sebebiyet vermekte, zaman aşımı olması durumunda da ilgili fonksiyonların başarısız olmaları demektir. "errno" değişkeni de uygun değere çekilecektir. Fonksiyonların prototipleri şu şekildedir; #include #include int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio, const struct timespec *abstime); ssize_t mq_timedreceive(mqd_t mqdes, char *restrict msg_ptr, size_t msg_len, unsigned *msg_prio, const struct timespec *abstime); Bu fonksiyonların "mq_send" ve "mq_receive" fonksiyonlarından tek farklı, son parametre olan zaman aşımı parametresidir. Bu parametre "timespec" yapı türünden bir nesnenin adresidir. "const" olduğuna dikkat ediniz. "timespec" yapısı aşağıdaki gibi tanımlanmıştır; #include struct timespec { time_t tv_sec; /* Seconds. */ long tv_nsec; /* Nanoseconds. */ }; 01.01.1970'den itibaren geçen saniyeyi tutmaktadır. "tv_nsec" ise bu saniyenin nanosaniye cinsinden değeridir. Pekiyi bizler zaman aşım süresini nasıl belirleyeceğiz? Şöyleki; -> "clock_gettime()" fonksiyon çağrısı ile şimdiki zamana ilişkin "timespec" değeri elde edilir. "clock_gettime" fonksiyonu aynı zamanda bir standart C fonksiyonudur. Fonksiyon aşağıdaki parametrik yapıya sahiptir; #include int clock_gettime(clockid_t clock_id, struct timespec *tp); Fonksiyonun birinci parametresi saat türüne ilişkin bir parametredir. Bu parametrenin "CLOCK_REALTIME" sembolik sabiti alınması uygun olur. Fonksiyonun ikinci parametresi ise içi doldurulacak "timespec" yapısının adresidir. -> Elde edilen bu "timespec" değerine istenilen zaman aşım süresi eklenir. -> Yeni "timespec" değeri ise "mq_timedsend"/"mq_timedreceive" fonksiyonlarında kullanılır. Aşağıda bu fonksiyonun kullanımına ilişkin bir örnek yapılmıştır. * Örnek 1, /* write */ #include #include #include #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void clear_stdin(void); void exit_sys(const char*); int main(int argc, char** argv) { mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL)) == -1) exit_sys("mq_open"); struct timespec ts; if(clock_gettime(CLOCK_REALTIME, &ts) == -1) exit_sys("clock_gettime"); // printf("Total Sec. since Epoch: %lld\n", (long long)ts.tv_sec); /* Şu ankinin üzerine 10 saniye ekledik. */ ts.tv_sec += 10; char buffer[100]; if(mq_timedsend(mqd, buffer, 100, 0, &ts) == -1) { if(errno == ETIMEDOUT) { /* Timedout oluştu. Program çalıştıktan sonra 10 saniye sonra akış buraya gelecektir. */ } else { exit_sys("mq_timedsend"); } } mq_close(mqd); if(mq_unlink(MSG_QUEUE_NAME) == -1) exit_sys("mq_unlink"); return 0; } void clear_stdin(void) { int ch; while((ch = getchar() != '\n') && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void exit_sys(const char*); int main(int argc, char** argv) { mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL)) == -1) exit_sys("mq_open"); struct mq_attr attr; if(mq_getattr(mqd, &attr) == -1) exit_sys("mq_getattr"); char* buffer; if((buffer = malloc(attr.mq_msgsize)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } int prio; ssize_t result; for(;;) { if(mq_receive(mqd, buffer, attr.mq_msgsize, &prio) == -1) exit_sys("mq_receive"); buffer[result] = '\0'; printf("%d. %s\n", prio, buffer); if(!strcmp(buffer, "quit")) break; } printf("\n"); free(buffer); mq_close(mqd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Son olarak belirtmemiz gerekiyor ki zaman aşımlı işlemler blokesiz işlemler değillerdir. Blokesiz işlemlerde hiç bloke oluşmazken, bu tip zaman aşımlı işlemlerde belirlenen süre boyunca bloke oluşur. Artık mesaj kuyrukları bahsini bitirmiş oluyoruz. Öte yandan mesaj kuyruklarının da bir limiti vardır. Pekiyi bu limit değerleri nelerdir? POSIX standartları bu limitlerin neler olduğundan BAHSETMEMİŞTİR. Sadece bir limitin olduğundan bahsetmiştir. Öte yandan Linux sistemleri üç adet limit kullanmaktadır. Bu limitler sırasıyla şunlardır; MSGMAX, MSGMNI, MSGMNB. Şimdi bu limitlerin detaylarına bakalım: >>> MSGMAX: Bir mesaj paketi içerisindeki mesajın toplam uzunluğu. Mesajın tür bilgisi hariç, bizzat mesajın kendisinin tutulduğu alanın büyüklüğüdür burada kastedilen. Tipik olarak bu uzunluk 8192'dir. Bu değer "proc" dosya sistemindeki "/proc/sys/kernel" dizini içerisindeki "maxmsg" isimli dosya üzerinden değiştirilebilir. Tabii bunu yapabilmek için prosesin "appropriate priviledged" olması gerekmektedir. >>> MSGMNI: Bu limit sistem geneli oluşturulacak mesaj kuyruklarının maksimum adedini belirtmektedir. Yine bu değeri "/proc/sys/kernel" dizini içerisindeki "msgmni" isimli dosyadan elde edebiliriz. Tipik olarak bu uzunluk 32000'dir. >>> MSGMNB: Bu limit, bir mesaj kuyruğunda bulunan paketlerin içerisindeki mesajların toplam uzunluk değeridir. "msgsnd" fonksiyonu blokeli modda çalışırken bu değer açılırsa, bloke oluşur. Yine bu değeri "/proc/sys/kernel" dizinindeki "msgmnb" isimli dosyadan elde edilebilir. Tipik olarak değeri 16384 bayttır. Bizler bu değerleri "kernel" derlemesi sırasında değiştirilebiliriz. Bütün bunlar bir yana, borulardaki sıfır bayt yazma durumu mesaj kuyrukları için de geçerli midir? Burada boruların aksine mesaj kuyruğuna sıfır bayt yazılması özel bir şeydir. Yine bir paket oluşturulup kuyruğa yazım gerçekleşiyor. Dolayısıyla kuyruğu doldurabiliriz. Bu tür paketlerin içerisindeki mesajların uzunluğu işlemci tarafından bir baytmış gibi ele alınır. Pekiyi böylesi mesajlar neden gönderilir? En sık karşılaşılan yöntem, haberleşmenin sonlanma bilgisini karşı tarafa iletmek içindir. Yukarıdaki örnekte "quit" yazısı gönderildiğinde haberleşme sonlanmaktaydı. Bunun yerine kullanabiliriz. * Örnek 1, Aşağıda sıfır bayt yazma iletişimi sonlandırmak amacıyla gönderilmektedir. /* write */ #include #include #include #include #include #define MSG_KEY 0x1234567 #define NORMAL_MSG 1 #define QUIT_MSG 2 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); size_t len; struct MSG msg; char* str; for(;;) { printf("Message Text: "); fflush(stdout); if(fgets(msg.buffer, 8192, stdin) == NULL) continue; if((str = strchr(msg.buffer, '\n')) != NULL) *str = '\0'; if(!strcmp(msg.buffer, "quit")) { msg.mtype = QUIT_MSG; len = 0; } else { msg.mtype = NORMAL_MSG; len = strlen(msg.buffer) + 1; } if(msgsnd(msgid, &msg, len, 0) == -1) exit_sys("msgsnd"); if(msg.mtype == QUIT_MSG) break; } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include #include #define MSG_KEY 0x1234567 #define NORMAL_MSG 1 #define QUIT_MSG 2 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; ssize_t result; for(;;) { if((result = msgrcv(msgid, &msg, 8192, 0, 0)) == -1) exit_sys("msgcrv"); if(msg.mtype == QUIT_MSG) break; printf("%ld. [%s]\n", msg.mtype, msg.buffer); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> Paylaşılan Bellek Alanları: İki prosesin "Sayfa Tablolarının" sol sütunlarındaki farklı Sanal Sayfa Numaraları, aynı tablonun sağ sütunundaki aynı Fiziksel Sayfaya(RAM'deki karşılığına) ilişkin olması durumudur. Daha öncesinde de kabaca bir açıklama yapılmıştır. İki prosesin de aynı fiziksel sayfaya eriştiği; birisi yazma yaparken diğeri okuma yapabilmektedir. Fakat bu haberleşme yöntemi, mesaj kuyruklarındakinin ve/veya borulardakinin aksine, kendi içerisinde bir senkronizasyona sahip değildir. Senkronizasyonun sağlanabilmesi için de "semaphore" gibi senkronizasyon nesnelerine ihtiyaç duymaktadır. İşte bu yüzdendir ki "IPC" nesnelerine "semaphore" lar eklenmiştir. Bizler bu "semaphore" konusunu "thread" konusunda göreceğiz. Eğer senkronizasyon sağlanamaz ise üzerine yazma, iki defa okuma gibi sorunlar meydana gelebilir. >>> "System 5" paylaşılan bellek alanları aşağıdaki aşamalardan geçerek kullanılmaktadır; -> "shmget" fonksiyonuna "key" değeri verilerek bir "id" elde edilir. Duruma göre ya yeni bir paylaşılan bellek alanı oluşturularak ya da var olan açılarak bir "id" elde edilir. Diğer prosesler bu "id" değerini biliyorsa, "shmget" fonksiyonunu kullanmalarına gerek kalmayacaktır. "shmget" fonksiyonu aşağıdaki parametrik yapıya sahiptir; #include int shmget(key_t key, size_t size, int shmflg); Fonksiyonun birinci parametresi, "id" elde etmek için kullanılacak "key" değeridir. Aynı "id" değerini elde etmek için aynı "key" değerleri kullanmalıyız. Tabii yine "ftok" fonksiyonunu "key" elde etmek için de kullanabilir ya da "IPC_PRIVATE" sembolik bayrağını kullanabiliriz. Tabii, böylesi bir bayrak kullanımı sonrasında elde edilen "id" değerini karşı proseslere bir şekilde ulaştırmamız gerekmektedir. Fonksiyonun ikinci parametresi, paylaşılacak bellek alanının büyüklüğünü belirtmektedir. Bu değerin o sistemdeki sayfa sayısının kapladığı alan kadar olması tavsiye edilir. Fakat POSIX standartlarınca böyle bir zorunluluk yoktur. Örneğin, sistemimizde bir sayfa 4096 bayt yer kaplasın. Bizler de fonksiyonun ikinci parametresine 5000 değerini girelim. İşletim sistemi iki adet sayfayı bize ayıracaktır. Dolayısıyla ikinci sayfadaki 3192 bayt boşa gidecektir. Çünkü bellekteki en küçük birim sayfalardan oluşmaktadır (bkz. "internal fragmantation"). Öte yandan Linux sistemlerinde, fonksiyonun ikinci parametresine geçilen değer sayfa katlarına yuvarlanmaktadır. Bizim örneğimiz için 5000 değeri 8192 değerine yuvarlanacaktır. "shmget" fonksiyonunun üçüncü parametresi, tıpkı "msgget" fonksiyonunda olduğu gibi, erişim hakları ve yeni bir paylaşılan bellek alanı oluştururken kullanılacak bayraklara ilişkin seçenekleri belirtmektedir. Yine burada erişim hakları için "S_IXXX" biçimindeki sembolik sabitleri, yeni oluşturulacak paylaşılan bellek alanları için "IPC_CREAT" ya da "IPC_CREAT | IPC_EXCL" bayrakları kullanılmaktadır. Fonksiyonun geri dönüş değeri ise başarı durumunda "id" değeri, başarısızlık durumunda ise "-1" değeridir. * Örnek 1, Aşağıdaki örnekte, bir adet Paylaşılan Bellek Alanı nesnesi oluşturulmuş ve ona dair "id" değeri elde edilmiştir. "write" yapacak olan proses de "read" yapacak olan proses de şimdilik aynı kodları kullanacağı için kodlar ayrı ayrı belirtilmemiştir. #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shmget"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Anımsanacağınız üzere sistem geneli bütün "IPC" nesnelerini görmek için "ipcs" kabuk komutunu kullanıyorduk. Mesaj kuyruklarını görmek için "-q" seçeneğini kullanırken, paylaşılan bellek alanları için "-m" seçeneğini kullanacağız. Tabii iş bu kabuk komutu da "/proc/sysvipc" dizinindeki dosyalara bakarak iş yapmaktadır. -> Şimdi sıradaki işlem halihazırda oluşturulmuş olan paylaşılan bellek alanı nesnesini, prosesin "Sayfa Tablosuna", yeni bir giriş oluşturarak, ekliyoruz. Artık o prosesin sayfa tablosunun o indeksi, ilgili paylaşılan bellek alanını göstermektedir. Tabii bu işlemi haberleşecek diğer proseslerin de yapması gerekmektedir. Böylelikle iki farklı prosese ait sayfa tablolarının sol taraflarındaki indisler, bellek üzerinde aynı yeri gösterir olacaklardır. "System 5" terminolojisinde bu bağlama işlemine "attach" denmektedir. Dolayısıyla bu "attach" işlemi sonrasında ilgili sayfa tabloları aşağıdaki biçimde olacaktır: Proses - I Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 95134 45321 ... ... Proses - II Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 32148 45321 ... ... Pekiyi bizler bu "attach" işlemini nasıl gerçekleştireceğiz? Tabii ki "shmat" isimli POSIX fonksiyonuyla. Bu fonksiyon aşağıdaki parametrik yapıya sahiptir: #include void *shmat(int shmid, const void *shmaddr, int shmflg); Fonksiyonun birinci parametresi, paylaşılan bellek alanı nesnesinin "id" değeridir. İkinci parametre ise talep edilen sanal adresi belirtmektedir. Bir nevi işletim sistemine "O sanal adres yolu ile paylaşılan bellek alanı nesnesine erişmek istiyorum" demektir. Fakat bu sanal adres halihazırda kullanılıyorsa ya da işletim sistemi tarafından kullanılamaz bir adres olması durumunda, fonksiyon başarısız olacaktır. Bu parametreye "NULL" değeri de geçilebilir. Bu durumda işletim sistemi sanal adresi kendisi seçecektir. Tavsiye edilen yol ise iş bu ikinci parametreye "NULL" geçilmesidir. Son olarak bu parametreye değer geçeceksek, ilgili değerin sayfa katları biçiminde olması beklenir. Örneğin, sayfa sayısı 4096 ise bizim geçeceğimiz değer 4096'nın katları biçiminde olmasıdır. Fonksiyonun son parametresi ise bir takım bayraklar almaktadır. Bunlar "SHM_RND" ve "SHM_RDONLY" bayraklarıdır. Eğer bu bayrakları kullanmak istemiyorsak, "0" değerini kullanabiliriz. Şimdi bu bayraklar için bir takım nüanslar vardır. "SHM_RND" bayrağını ele alalım; şimdi fonksiyonun üçüncü parametresinde bu bayrak kullanılmazsa ve fonksiyonun ikinci parametresine geçilen değer de sayfa sayısı katlarında değilse, "shmat" fonksiyonu başarısız olacaktır. Öte yandan üçüncü parametrede "SHM_RND" bayrağı kullanılırsa ve ikinci parametreye geçilen değer de sayfa sayısı katlarında DEĞİLSE, bu sefer bu değer sayfa sayısının katları olacak biçimde aşağı doğru YUVARLANIR. POSIX standartlarında bu aşağıdaki biçimde tarif edilmektedir: "shmaddr - ((uintptr_t)shmaddr & SHMLBA)" Burada "SHMLBA" sembolik sabiti, o sistemdeki sayfa katına karşılık gelmektedir. "%" işlemi ile de bölme işleminden kalan değer elde edilmektedir. Şimdi de "SHM_RDONLY" bayrağını ele alalım; işletim sistemi ilgili paylaşılan bellek alanı nesnesine "read-only" olarak işaretlemektedir. Bu alana yazma amacıyla erişirsek, "fault" oluşacak ve işletim sistemi prosesi sonlandıracaktır. Eğer "SHM_RDONLY" bayrağı kullanılmazsa, erişim "read-write" biçiminde olacaktır. Fonksiyon, başarı durumunda paylaşılan bellek alanı nesnesine erişmede kullanılan sanal adrese, başarısızlık durumunda ise "(void*)-1" değerine geri dönmektedir. * Örnek 1, Aşağıdaki örnek, bir önceki örneğin devamı niteliğindedir. /* write */ #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { /* * Bu proses "read" yapacağı için "shmget" fonksiyonunun * son parametresine "0" geçilmiştir. */ int shmid; if((shmid = shmget(SHM_KEY, 4096, 0)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Şimdi haberleşecek prosesler iş bu paylaşılan bellek alanı nesnesini kullanabilirler. Fakat unutmamalıyız ki "Paylaşılan Bellek Alanları" kendi içerisinde senkronizasyon sağlamamaktadır. Bizler bu senkronizasyonu dışarıdan sağlamamız gerekmektedir. Halbuki borular ve mesaj kuyruklarında senkronizasyon otomatik olarak sağlanmaktadır. * Örnek 1, Aşağıdaki örnekte senkronizasyon yöntemi çok ilkel bir yolla sağlanmaya çalışılmıştır. Haberleşmeyi sağlamak için ilk olarak "write" programı çalıştırılmalı ve bellek alanına yazılmalı, daha sonra "read" programı çalıştırılmalı ve bellek alanından okuma yapılmalıdır. Aşağıdaki örnek, bir önceki örneğin devamı niteliğindedir. /* write */ #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); printf("Virtual address of shared memory: [%p]\n", shmaddr); strcpy(shmaddr, "This is a test message!...\n"); printf("The message has been written to the [%p]\n", shmaddr); printf("Press ENTER to continue...\n"); getchar(); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, 0)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); printf("Virtual address of shared memory: [%p]\n", shmaddr); printf("A message has been read : [%s]\n", shmaddr); printf("Press ENTER to continue...\n"); getchar(); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Pekiyi haberleşme nasıl sonlanmalıdır? Görüleceği üzere bizler ilk önce bir nesne oluşturup, bunu prosesin bellek alanına "attach" ettik. Sonlandırma yaparken ilk önce ilgili nesne ilgili bellek alanından "detach" edilmeli ve daha sonra bu nesne yok edilmelidir. İşte "detach" işlemi için "shmdt" fonksiyonu, yok etme işlemi için de "shmctl" fonksiyonu kullanılır. "shmdt" fonksiyonunun prototipi şöyledir; #include int shmdt(const void *shmaddr); Fonksiyon, "shmat" fonksiyonu ile elde ettiğimiz sanal adresi alır. Başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri dönecektir. Öte yandan bu "detach" işlemi yapılmazsa, proses sonlandığında otomatik olarak yapılmaktadır. "detach" işleminden sonra paylaşılan bellek alanı nesnesini de yok etmemiz gerektiğini söylemiştik. Çünkü ya bizler "shmctl" ile silmeliyiz. Aksi halde sistem ilk "reboot" edilene kadar hayatta kalacaktır. "shmctl" fonksiyonu aşağıdaki parametrik yapıya sahiptir; #include int shmctl(int shmid, int cmd, struct shmid_ds *buf); Fonksiyonun üçüncü parametresi "shmid_ds" yapı türünden bir adres değeridir. Bu yapı türü, ilgili paylaşılan bellek alanı nesnesi için bir takım bilgiler tutmaktadır. Bu yapı türü "sys/shm.h" başlık dosyasında aşağıdaki gibi tanımlanmıştır; struct shmid_ds{ struct ipc_perm shm_perm; /* Operation permission structure. */ size_t shm_segsz; /* Size of segment in bytes. */ pid_t shm_lpid; /* Process ID of last shared memory operation. */ pid_t shm_cpid; /* Process ID of creator. */ shmatt_t shm_nattch; /* Number of current attaches. */ time_t shm_atime; /* Time of last shmat */ time_t shm_dtime; /* Time of last shmdt */ time_t shm_ctime; /* Time of last change by shmctl */ }; Bu yapının, -> "shm_perm" isimli elemanı "ipc_perm" yapı türündendir. "System 5 IPC" Mesaj Kuyrukları bahsinde zaten bu yapı türünü görmüştük. Hatırlayacak olursa; struct ipc_perm { uid_t uid // Owner's user ID. gid_t gid // Owner's group ID. uid_t cuid // Creator's user ID. gid_t cgid // Creator's group ID. mode_t mode // Read/write permission. }; Bu yapı türü ise "sys/ipc.h" başlık dosyasında belirtilmiştir. Bu yapının elemanlarının sırasıyla; -> o anki "Kullanıcı ID" ve "Group ID" değerleri, -> ilgili "IPC" nesnesini oluşturan prosesin "Kullanıcı ID" ve "Group ID" değerleri, -> ilgili "IPC" nesnesinin sahip olduğu erişim hakları şeklindedir. -> "shm_atime", "shm_dtime" ve "shm_ctime" isimli elemanları sırasıyla ilgili "IPC" nesnesinin en son ki "attach" zamanı, "detach" zamanı ve "shmid_ds" yapısındaki değiştirilme zaman bilgisini tutmaktadır. -> "shm_segsz" isimli elemanı ise ilgili "IPC" nesnesinin büyüklük bilgisini tutmaktadır. -> "shm_lpid" ve "shm_cpid" isimli elemanları sırasıyla "attach"/"detach" yapan prosesin "id" değeri ve ilgili "IPC" nesnesini oluşturan prosesin "id" değerini tutmaktadır. -> "shm_nattch" isimli veri elemanı ise kaç defa "attach" yapıldığı bilgisini tutmaktadır. Fonksiyonun ikinci parametresi, iş bu paylaşılan bellek alanı nesnesini yok etmek ya da "shmid_ds" yapı nesnesini "get" ve/veya "set" etmek için kullanılır. Dolayısıyla bu parametre şu bayraklardan birisini alabilir; "IPC_STAT", "IPC_SET" ve "IPC_RMID". Şimdi de bu bayrakları irdeleyelim: -> "IPC_STAT" bayrağı kullanıldığında, ilgili "IPC" nesnesinin özellikleri "get" edilir ve fonksiyonun üçüncü parametresine geçilen yapı türü bu bilgiler ile doldurulur. -> "IPC_SET" bayrağı kullanıldığında, ilgili "shmid_ds" yapısının bazı özellikleri "set" edilir. Bu özellikler şunlardır; "shmid_ds.uid", "shmid_ds.gid" ve "shmid_ds.mode". Tabii bu özellikleri değiştirebilmek için, tıpkı mesaj kuyruklarında olduğu gibi, prosesimizin "Appropriate Priviledged" olması ya da prosesin "Etkin Kullanıcı ID" değerinin "shmid_ds" yapısının "shm_perm" isimli elemanının ki bu eleman da aslında "ipc_perm" yapı türündendir, "uid" ya da "cuid" değerine eşit olması gerekmektedir. -> "IPC_RMID" bayrağı kullanıldığında, ilgili "IPC" nesnesi yok edilecektir. Tabii bu bayrağı kullanabilmek için, "IPC_SET" bayrağını kullanmak için sahip olmamız gereken koşullara sahip olmalıyız. Ek olarak, bu bayrak kullandıldığı taktirse ilgili fonksiyonun son parametresine hangi adresin geçildiğinin bir önemi kalmaz. Bu durumda "NULL" adresini geçebiliriz. Fonksiyonun birinci parametresi ise ilgili paylaşılan bellek alanının "id" değeridir. Bu fonksiyonun işlevini "msgctl" fonksiyonuna benzetilebilir. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", hata durumunda "-1" ile geri dönmektedir. Pekiyi ilgili "IPC" nesnesini kim yok etmelidir? Burada tavsiye edilen, hayata getiren proses tarafından silinmesidir. Ayrıca şunu da belirtmek gerekir ki "System 5" mesaj kuyruklarında, mesaj kuyruğu silindiğinde, direkt olarak silme işlemi gerçekleşiyor ve mesaj kuyruğu üzerinde işlem yapan prosesler hata durumuna düşmekteydi. Fakat "System 5" paylaşılan bellek alanları için böyle bir şey söz konusu değildir. İlgili "IPC" nesnesinin gerçek manada silinmesi için, "attach" yapan bütün proseslerin "detach" yapması gerekmektedir. * Örnek 1, Aşağıdaki örnek, bir önceki örneğin devamı niteliğindedir. İlgili "IPC" nesnesini oluşturan proses, silme işlemini gerçekleştirmiştir. /* write */ #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); printf("Virtual address of shared memory: [%p]\n", shmaddr); strcpy(shmaddr, "This is a test message!...\n"); printf("The message has been written to the [%p]\n", shmaddr); printf("Press ENTER to continue...\n"); getchar(); if(shmdt(shmaddr) == -1) exit_sys("shmdt"); /* * Tam manasıyla silme işlemi, bütün prosesler "detach" yaptığında * gerçekleşeceği için, yukarıdaki gibi herhangi bir bekleme * yapılmamıştır. */ if(shmctl(shmid, IPC_RMID, NULL) == -1) exit_sys("shmctl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, 0)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); printf("Virtual address of shared memory: [%p]\n", shmaddr); printf("A message has been read : [%s]\n", shmaddr); printf("Press ENTER to continue...\n"); getchar(); if(shmdt(shmaddr) == -1) exit_sys("shmdt"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>> "POSIX" paylaşılan bellek alanlarını görmeye geçebiliriz. "POSIX" paylaşılan bellek alanları, daha önce gördüğümüz "POSIX" mesaj kuyruklarına benzer bir kullanıma sahiptirler. "System 5" versiyona nazaran burada "key" değerleri değil, dosya ismi kullanılmaktadır. Dolayısıyla sanki iş bu "IPC" nesnesi bir dosya gibi ele alınmaktadır. Ayrıca "POSIX" paylaşılan bellek alanları, "Memory Mapped File" denilen olguyla da birleştirilmiştir. "POSIX" versiyonlar daha genç olduğu için yine taşınabilirlik problemi çıkartabilir ama pek çok sistem bu arayüzü destekler niteliktedir. Yine hatırlatmakta fayda vardır ki bu versiyona ait fonksiyonlar "librt" kütüphanesinde olduğu için derleme aşamasında "-lrt" seçeneği kullanılmalıdır. "POSIX" paylaşılan bellek alanları aşağıdaki aşamalardan geçerek kullanılmaktadır; -> "POSIX" paylaşılan bellek alanı nesnesini açmak ya da oluşturmak için "shm_open" kullanılır. Fonksiyonun prototipi aşağıdaki gibidir; #include int shm_open(const char *name, int oflag, mode_t mode); Fonksiyonun birinci parametresi, paylaşılan bellek alanı nesnesini belirtmektedir. Tıpkı "POSIX" mesaj kuyruklarında olduğu gibi bu ismin de kök dizindeki bir dosya ismi olarak verilmesi gerekmektedir. Fakat bazı sistemler, ilgili dosya ismi olarak, başka dizinlerdeki dosyaların ismini de kabul etmektedir. Fonksiyonun ikinci parametresi, açış bayraklarını belitmektedir. Bu bayraklar şunlardan birisi olabilir; "O_RDONLY", "O_RDWR", "O_CREAT", "O_EXCL" ve "O_TRUNC". Bu bayrakların açıklamaları da şu şekildedir; -> "O_RDONLY" bayrağı, ilgili bellek alanının "read-only" modunda açılır. Sadece okuma yapabiliriz. -> "O_RDWR" bayrağı, ilgili bellek alanının "read-write" modunda açılır. Hem okuma hem yazma yapılabilir. -> "O_CREAT" bayrağı, ilgili bellek alanı yoksa yeni bir tanesinin oluşturulacağını belirtir. Halihazırda varsa eğer, var olan bellek alanı açılacaktır. -> "O_EXCL" bayrağı ise aslında "O_CREAT" ile birlikte kullanılır ve halihazırda bir tane paylaşılan bellek alanı var ise ilgili fonksiyonun başarısız olmasını sağlar. Aksi halde yeni bir bellek alanı oluşturulur. -> "O_TRUNC" bayrağı ise ilgili bellek alanının sıfırlanarak açılmasını sağlar. Yani içerisindeki bilgiler sıfırlandıktan sonra açılır. Bu bayraklardan "O_RDONLY" ve "O_RDWR" bayraklarını aynı anda kullanamayız. Fakat diğer üç bayrağı, bu iki bayraktan birisi/bir kaçı ile "bitwise-OR" işlemine sokabiliriz. Fonksiyonun üçüncü parametresi ise ilgili bellek alanına erişim bayraklarını belirtmektedir. Bu bayraklar "S_IXXX" biçimindeki bayraklardır. Eğer fonksiyonun ikinci parametresinde "O_CREAT" bayrağı kullanılmış ise iş bu üçüncü parametredeki bayraklar önem arz etmektedir. Aksi halde buraya geçilen argümanların bir önemi kalmamaktadır. Fonksiyonun geri dönüş değeri ise başarı durumunda bir "file descriptor", yani "fd" değeri döndürür. Halbuki "POSIX" mesaj kuyruklarında geri dönüşün "file descriptor" olması bir zorunluluk değildir. Başarısızlık durumunda ise "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. Aşağıda bu hususa ilişkin bir örnek verilmiştir: * Örnek 1, Aşağıdaki örnekte bir adet paylaşılan bellek alanı nesnesi oluşturulmuştur. /* write */ #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Örnekte de görüleceği üzere paylaşılan bellek alanının boyutu hakkında bir bilgi kullanmadık. Çünkü "POSIX" paylaşılan bellek alanlarını oluştururken değil, oluşturduktan sonra boyutlamamız gerekmektedir. -> "ftruncate" fonksiyonu bir "POSIX" fonksiyonu olup, iş bu boyutlandırmayı yapacak olan fonksiyondur. Daha evvel işlediğimiz yardımcı dosya fonksiyonlarında bu fonksiyona değinmediğimiz için şimdi değineceğiz. Fonksiyonun parametrik yapısı aşağıdaki gibidir; #include int ftruncate(int fildes, off_t length); Fonksiyonun birinci parametresi, üzerinde işlem yapılacak dosyaya ait "fd" değerini almaktadır. İkinci parametre ise o dosyanın yeni uzunluk değeridir. Bu fonksiyon genellikle küçültme işlemi için kullanılır ve küçültme sırasında dosyanın sonundaki ilgili kısım budanır/yok edilir. Küçültme işleminden sonra dosyanın yeni boyutu, bu fonksiyonun ikinci parametresi olacaktır. Öte yandan dosyanın boyutunu da büyütebiliriz. Büyütme işlemi sırasında, yeni gelen ekstra kısım sıfırlar ile doldurulur. Bu günkü sistemde, büyütme sonrası yeni gelen kısım, Dosya Delikleri ile sağlanmaktadır. Tabii ilgili dosya sisteminin de Dosya Deliklerini desteklemesi gerekmektedir. Fonksiyon başarı durumunda "0", başarısızlık durumunda "-1" ile geri döner ve "errno" değişkeni de uygun değere çekilir. Son olarak bu fonksiyonun işlev görebilmesi için prosesin ilgili dosya nezdinde "write" hakkında sahip olması gerekir. * Örnek 1, Aşağıdaki örnekte ilgili fonksiyonun kullanımına bir örnek verilmiştir. #include #include #include #include #define SHM_NAME "/sample_POSIX_shared_memory" void exit_sys(const char*); int main(int argc, char** argv) { int fd; if((fd = open("SHM_NAME", O_RDWR)) == -1) exit_sys("open"); /* Artık ilgili dosyanın büyüklüğü "1000" olmuştur. */ if(ftruncate(fd, 1000) == -1) exit_sys("ftruncate"); printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Tabii "ftruncate" fonksiyonunun "fd" yerine argüman olarak dosya ismi alan versiyonu da bulunmaktadır. Bu versiyonunun ismi ise "truncate" biçimindedir. İşlev bakımından iki fonksiyonun bir farkı bulunmamaktadır. Fonksiyonun parametrik yapısı aşağıdaki gibidir; #include int truncate(const char *path, off_t length); Fonksiyonun birinci parametresi üzerinde işlem yapılacak dosyanın yol ifadesi, ikinci parametresi ise o dosyanın yeni uzunluk bilgisidir. Fonksiyon başarı durumunda "0", başarısızlık durumunda "-1" ile geri döner ve "errno" değişkeni de uygun değere çekilir. Son olarak aynı boyut değerini bu iki fonksiyona geçmemize bir değişikliğe yol açmayacaktır. * Örnek 1, Aşağıdaki kodlar, bir önceki paragraftaki örnekteki kodların güncellenmiş halidir. /* write */ #include #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Şimdi sırada ilgili paylaşılan bellek alanı nesnesini bir yere "map" etmeliyiz. "System 5" paylaşılan bellek alanlarında, "map" etme yerine "attach" etme terimi kullanılmaktaydı. Bunun için "mmap" isimli POSIX fonksiyonu kullanılmaktadır. Fakat "mmap" isimli bu fonksiyon, sadece bu noktada "map" işlemi için değil daha genel bir kullanımı olan detaylı bir fonksiyonudur. Bu fonksiyon bir sistem fonksiyonudur ve detayları ileride ele alınacaktır. Kabaca şu şekilde özetleyebiliriz; #include void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off); Fonksiyonun birinci parametresi, "map" işlemi için önerilen adresi belirtmektedir. Tıpkı "System 5" paylaşılan bellek alanlarında olduğu gibi belli bir adresin kullanılması mümkündür fakat bu tavsiye edilmez. Bunun yerine bu parametre için "NULL" değerinin geçilmesi tavsiye edilir. Fonksiyonun ikinci parametresi ne kadarlık bir alanın "map" işlemine tabii tutulacağını belirtmektedir. Böylelikle paylaşılan bellek alanı nesnesini kısmet "map" edebiliriz. Öte yandan bu parametreye geçilen değerin sayfa katlarında olması bir zorunluluk değildir fakat bir çok sistem buraya geçilen değeri sayfa katı olacak şekilde yukarı doğru yuvarlamaktadır. Örneğin, bizler bu parametre için 100 bayt geçtiğini düşünelim. Sistem, 100 bayt yerine sayfa katı olacak şekilde yukarı doğru yuvarlayacağı için, 4096 bayt olarak işleyecektir. Ek olarak, erişim için burada belirtilen değeri kullanmalıyız. Bu değerden öte bir değeri erişim için kullanmak "Tanımsız Davranış" olacaktır. Yine buraya geçilen değerin "0" olmaması gerekmektedir. Fonksiyonun üçüncü parametresi ise "map" edilen sayfaların "protection" özelliklerini belirtmektedir. İşlemci düzeyindeki koruma özellikleridir bunlar. Anımsanacağın üzere sayfa tablosundaki sayfalar bir takım "attribute" lere sahiptir. İşte bizler bu parametreler ile iş bu "attribute" leri atıyoruz. Bu parametreye şu bayraklardan bir ya da bir kaç tanesini geçebiliriz; "PROT_READ", "PROT_WRITE", "PROT_EXEC" ve "PROT_NONE". Birden fazlasını bir arada kullancaksak, yine "bit-wise OR" işlemine sokmamız gerekmektedir. Bu bayrakların açıklamaları şu şekildedir; -> "PROT_READ" bayrağı, ilgili sayfanın "read-only" olacağını belirtmektedir. Böylesi sayfalara yazma yapılacağı zaman, işlemci "exception" oluşturur ve proses "SIGSEGV" sinyali ile sonlandırılır. -> "PROT_WRITE" bayrağı, ilgili sayfanın "write-only" olacağını belirtir. Eğer "PROT_READ | PROT_WRITE" biçiminde bir kombin yaparsak, ilgili sayfa hem okumaya hem yazmaya müsait olacaktır. -> "PROT_EXEC" bayrağı ise ilgili sayfada çalıştırılabilir bir kodun olması durumunda, iş bu kod parçacığının çalıştırılabileceğin belirtmektedir. -> "PROT_NONE" bayrağı ise ilgili sayfaya herhangi bir erişimin mümkün olamayacağını belirtmektedir. Yani bu bayrağa sahip olan bir sayfa, ne okunabilir ne de üzerinde yazma yapılabilir. Aslında bütün işlemciler buradaki özelliklerin hepsini desteklemeyebilir. Örneğin, Intel işlemcileri "PROT_WRITE" bayrağı okuma özelliğini de kapsamaktadır. Son olarak bu özellikler daha sonra da değiştirilebilir. Fonksiyonun dördüncü parametresi ise bir takım değerlerden oluşmaktadır. Bu değerler; "MAP_SHARED", "MAP_PRIVATE" ve "MAP_FIXED". Bu bayrakların açıklamaları şu şekildedir; -> "MAP_SHARED" bayrağı ise yazma işleminin normal bir şekilde yapılacağını ve karşı tarafın da yazılanları okuyacağını belirtmektedir. Normalde hemen her zaman bu bayrak kullanılmaktadır. -> "MAP_PRIVATE" bayrağı, "copy-on-write" semantiği için düşünülmüştür. Yazma yapmaya kalktığımız zaman, bu sayfanın bir kopyası oluşturulur ve bizler bu kopyaya yazma yaparız. Dolayısıyla karşı taraf, bizim yazdıklarımızı göremeyecektir. Tabii çıkartılan bu kopya, yine bizim prosesimize özgü bir kopyadır. Bu bayrağı kullanan proses, öteki proses bu alana yazma yaparsa, yazılanları okuyup okumayacağı POSIX standartlarınca "unspecified" olarak belirtilmiştir. Linux sistemlerinde de bu "unspecified" durum korunmuştur. Bu bayrağın kullanılması bazı özel durumlarda tercih edilmektedir. Örneğin, işletim sistemleri çalıştırılabilir dosyaların ".data" bölümlerini belleğe yüklerken "MAP_PRIVATE" bayrağını kullanıyor. Eğer aynı çalıştırılabilir dosya ikinci defa çalıştırılmışsa, ilk çalıştırılan ile ikincinin ".data" bölümü aynı yer oluyor. Ta ki yazma yapılana kadar. Yazma işleminden sonra ".data" bölümleri ayrılmaktadır. -> "MAP_FIXED" bayrağı, fonksiyonun birinci parametresindeki adres olarak "NULL" geçilmemesi durumunda, geçilen adres değerinin aynen kullanılmasını sağlar. Ek olarak bu bayrağı kullanmışsak, birinci parametredeki adresin sayfa katlarında olması, POSIX standartlarınca bir zorunluluk değildir. Eğer bu bayrağı kullanmazsak, birinci parametreye geçeceğimiz adres değeri işletim sistemi tarafından bir ipucu olarak ele alınacaktır. Bu bayraklardan "MAP_SHARED" ve "MAP_PRIVATE" olanlardan sadece birisi kullanılabilirken, "MAP_FIXED" bayrağı bu iki bayrak ile "bitwise-OR" işlemine sokulabilir. Linux sistemlerinde ise bu bayraklara ek olarak bayraklar da vardır. Bu bayraklardan en önemlisi "MAP_ANONYMUS" bayrağıdır. Bu bayrak kullanılarak yapılan "map" işlemine ise "anonymus mapping" denir. Fakat böylesi bir "map" işlemi dosya üzerinde yapılamamaktadır. Dolayısıyla bu işlem sırasında fonksiyona geçilen "fd" ve "offset" parametreleri dikkate alınmamaktadır. Artık "map" edilen yerin karşılığı "swap" dosyası olacaktır. Bu da bir nevi "malloc" ile bellek tahsisatı anlamına gelmektedir. Zaten "glibc" kütüphanesindeki "malloc" fonksiyonları arka planda "anonymus mapping" işlemi yapmaktadır. Pekiyi bizler "anonymus mapping" işlemini direkt olarak "malloc" yerine kullanabilir miyiz? Aslında evet. Fakat unutmamalıyız ki "malloc" bayt temelli tahsisat yaparken, "anonymus mapping" sayfa katları temelli tahsisat yapmaktadır. İş bu sebepten dolayıdır ki büyük miktarda tahsisat yaparken "anonymus mapping" yapmak daha hızlı ve etkili olabilir. Son olarak belirtmekte fayda vardır ki "MAP_ANONYMUS" bayrağı "MAP_PRIVATE" ya da "MAP_SHARED" ile birlikte kullanılır. Fakat ikisi arasında da şöyle bir fark vardır; "fork" işlemi sırasında "map" edilen alan da "child" prosese aktarılır. Eğer "MAP_ANONYMUS | MAP_PRIVATE" yaparsak, alt proses ile üst prosesin kullanacağı "map" alanı ayrışmaktadır (copy on write). Eğer "MAP_ANONYMUS | MAP_SHARED" yaparsak, alt proses ile üst prosesin kullanacağı "map" alanı ayrışmamaktadır. Yani iki proses de aynı "swap" dosyasını kullancaktır. Dolayısıyla alt-üst proses birbiriyle de haberleşebilir. Öte yandan "fork" işlemi hiç yapılmazsa, iki kullanım arasında bir fark kalmayacaktır. Linux sistemlerindeki bir diğer önemli bayrak da MAP_UNINITIALIZED" bayrağıdır. Bu bayrak kullanıldığında, tahsis edilen alan SIFIRLANMAYACAKTIR. Çünkü normal şartlarda, "anonymus mapping" sırasında, "mmap" ile tahsis edilen alanlar sıfırlanmaktadır. Fonksiyonun beşinci ve altıncı parametreleri bir adet "fd" ve "offset" değeri içindir. Buradaki "offset" değeri, paylaşılan bellek alanının o "offset" değerinden itibaren "map" edileceğini belirtmek için kullanılır. Örneğin, paylaşılan bellek alanımız 4MB olsun. Biz bu nesnenin 1MB'sinden itibarek, 64K'lık kısmını "map" edebiliriz. Fonksiyon başarı durumunda, "map" işlemi sonrasındaki sanal adrese geri dönmektedir. Başarısızlık durumunda ise "MAP_FAILED" değerine geri dönmektedir. Pek çok sistemde bu sembolik sabit aşağıdaki biçimde tanımlanmıştır; #define MAP_FAILED ((void*)-1) Öte yandan bu altıncı parametre, dördüncü parametrede "MAP_FIXED" kullanılması durumunda, birinci parametrenin gereksinim duyduğu hizalamada olmak zorundadır. Yani burada kastedilen şey şudur; Fonksiyonun birinci parametresine geçilen değer ile altıncı parametreye geçilen değerin, sayfa katlarına bölümünden elde edilen kalanı aynı olmalıdır. Örneğin, fonksiyonun birinci parametresine geçilen değer "5000" olsun. Sayfa katları da "4096" nın katları biçiminde olsun. İşte bu durumda "5000 % 4096" işleminden bizler "4" değerini kalan olarak elde edeceğiz. İşte altıncı parametreye geçilen değerin yine "4096" a bölümünden kalanı "4" olması gerekmektedir. Eğer "MAP_FIXED" kullanılmamış ise böyle bir zorunluluk yoktur. * Örnek 1, Aşağıdaki örnekte senkronizasyonu karadüzen sağlanmış iki proses paylaşılan bellek alanı kullanarak haberleşmektedir. /* write */ #include #include #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); void *shmaddr; if((shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); char buffer[4096]; char* str; for(;;) { printf("Text: "); if(fgets(buffer, 4096, stdin) == NULL) continue; if((str = strchr(buffer, '\n')) != NULL) *str = '\0'; strcpy((char*)shmaddr, buffer); if(!strcmp((char*)shmaddr, "quit")) break; } printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); void *shmaddr; if((shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); for(;;) { printf("Press ENTER to read...\n"); getchar(); puts((char*)shmaddr); if(!strcmp((char*)shmaddr, "quit")) break; } printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "anonymus mapping" yapılarak alan tahsisatı yapılmıştır: #include #include #include #include #include void exit_sys(const char*); int main(void) { char* faddr; /* * "ANONYMOUS MAPPING" sırasında "fd" yerine "-1" geçilmesi Linux dökümanlarında belirtilmiştir. "offset" * değeri de dikkate alınmayacağı için "0" geçilmiştir. Tıpkı "malloc" ile yer tahsisatı yapmışız gibi * olacaktır. */ if((faddr = (char*)mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0)) == MAP_FAILED) exit_sys("mmap"); strcpy(faddr, "ulya_yuruk"); printf("Press ENTER to continue!...\n"); getchar(); puts(faddr); if(munmap(faddr, 4096) == -1) exit_sys("unmap"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Paylaşılan Bellek Alanı nesnesini kullandıktan sonra, "map" edilen alanın geri verilmesi yani boşaltılması gerekmektedir. Nasılki "malloc" çağrısı için "free" çağrısını yapıyorsak, "map" işlemi için de "unmap" işlemini yapmamız gerekmektedir. Bu işi görecek POSIX fonksiyonu ise "munmap" fonksiyonudur. Fonksiyonun parametrik yapısı aşağıdaki gibidir; #include int munmap(void *addr, size_t len); Fonksiyonun birinci parametresi, daha önce "map" edilen alanın başlangıç adresidir. Yani "mmap" fonksiyonunun geri dönüş değeri. Fonksiyonun ikinci parametresi ise "unmap" edilecek alanın uzunluk bilgisidir. Bu fonksiyon, birinci parametresindeki adresten başlayan ve ikinci parametresindeki uzunluk kadar olan alanı "unmap" etmektedir. Eğer başlangıç noktası bir sayfanın ortalarında ve bitiş noktası ise başka sayfanın ortalarına denk geliyorse, bu iki sayfa ve aradaki sayfaların hepsi komple "unmap" edilmektedir. Öte yandan POSIX standartları, birinci parametredeki adresin sayfa katlarında olması için bir zorlama yapabilir fakat Linux standartları bunu zorunlu kılmaktadır. Ek olarak, birinci parametreye geçilen adres değeri daha önce "mmap" ile elde edilmemişse, bu durum "unspecified" olarak POSIX standartlarında tanımlanmıştır. Eğer daha önce "unmap" edilmiş bir alanı tekrar "unmap" edersek, fonksiyon herhangi bir değişikliğe sebebiyet vermeyecektir. Böylesi bir durumda da fonksiyon BAŞARISIZ olmamaktadır. Fonksiyonun geri dönüş değeri ise başarı durumuna göre "0", başarısızlık durumuna göre "-1" biçimindedir ve "errno" uygun değere çekilir. Son olarak şu noktaya dikkat etmeliyiz; uzun bir "map" edilmiş alanımız olsun. Bizler "unmap" ederken bu alanın başlangıç ve bitiş noktalarını değil, orta kısımlardan iki nokta seçmiş olalım. "unmap" işlemi sonrasında artık iki adet "map" edilmiş alanımız oluşacaktır. Çünkü bizler orta kısımları "unmap" ettiğimiz için, kenarda kalanlar bundan etkilenmeyecektir. Buna da "Partial Mapping" diyebiliriz. Unutmamalıyız ki prosesin ömrü bittiğinde sistem tarafından otomatik olarak "unmap" işlemi gerçekleştirilecektir. -> Artık paylaşılan bellek alanı nesnesi yok edilebilir. Bunun için "shm_unlink" fonksiyonunu kullanmalıyız. Eğer ilgili nesne yok edilmezse, sistem ilk "reboot" olana dek, hayatını sürdürecektir. İlgili fonksiyonun prototipi şu şekildedir; #include int shm_unlink(const char *name); Burada fonksiyonumuz paylaşılan bellek alanı nesnesinin ismini alır ve onu yok eder. Başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri döner. Tabii yok etme işinin de yine hayata getiren prosese bırakılması tavsiye edilendir. Gerçek manada silmenin gerçekleşmesi için yine ilgili paylaşılan bellek alanı nesnesini kullanan bütün proseslerin, bu nesneyi "unmap" etmesi gerekmektedir. * Örnek 1, /* write */ #include #include #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); void *shmaddr; if((shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); char buffer[SHM_SIZE]; char* str; for(;;) { printf("Text: "); if(fgets(buffer, SHM_SIZE, stdin) == NULL) continue; if((str = strchr(buffer, '\n')) != NULL) *str = '\0'; strcpy((char*)shmaddr, buffer); if(!strcmp((char*)shmaddr, "quit")) break; } printf("Ok\n"); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); if(shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); void *shmaddr; if((shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); close(shmfd); for(;;) { printf("Press ENTER to read...\n"); getchar(); puts((char*)shmaddr); if(!strcmp((char*)shmaddr, "quit")) break; } printf("Ok\n"); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi yapılan örneklerde de görüldüğü üzere, "shm_open" ile elde ettiğimiz "fd" değişkeni "main" fonksiyonlarının değişik yerlerinde "close" çağrıları ile geri verilmiştir. Çünkü bu kapatma işleminin "mmap" işlemine bir etkisi yoktur. Tabii eğer "stat/fstat" gibi fonksiyon çağrıları yapacaksak, kapatma işlemini bu çağrılardan sonra yapmalıyız. Yani burada kastedilen şey kapatma işlemi ile "mapping" işleminin birbirinden bağımsız olmasıdır. "close" ile "unmap" YAPILMAMAKTADIR. Paylaşılan Bellek Alanlarında bizim dikkat etmemiz gereken şey, ister "System 5" ister "POSIX" olsun, ilk önce iş bu nesneyi oluşturuyor, daha sonra bu nesnenin büyüklüğünü belirtiyor ve son olarak bu nesneyi "attach/map" yaparak proseslerin sayfa tablolarına eklemekteyiz. Son olarak bir paylaşılan bellek alanı nesnesi, bir prosesin sanal tablosuna birden fazla kez "attach/map" edilebilir. Tıpkı mesaj kuyruklarında olduğu gibi "System 5" paylaşılan bellek alanları da bir takım limitlere sahiptir. Her ne kadar POSIX standartları bu limitlerin detayları hakkında bir şey söylemese de Linux sistemi şu limit değerlerine sahiptir; "SHMALL", "SHMMIN", "SHMMAX", "SHMMNI". Şimdi de bunları irdeleyelim: -> "SHMALL" : Sistem geneli Paylaşılan Bellek Alanları nesnelerinin kaplayabileceği toplam alanın, sayfa sayısı cinsinden, değeridir. Bu değer "/proc/sys/kernel" dizinindeki "shmall" isimli dosyadan elde edilir ve değiştirilebilirdir. Buradaki varsayılan değer çok büyüktür. Adeta bir sınır olmadığını belirtir. -> "SHMMAX" : Bir Paylaşılan Bellek Alan nesnesinin maksimum büyüklüğüdür. "/proc/sys/kernel" dizini içerisindeki "shmmax" dosyasından elde edilir. Buradaki varsayılan değer çok büyüktür. Adeta bir sınır olmadığını belirtir. -> "SHMMIN" : Bir Paylaşılan Bellek Alan nesnesinin minimum büyüklüğüdür. Şimdiki sistemlerde "1" bayt değerindedir. Fakat "shmget" fonksiyonu ile paylaşılan bellek alanı oluşturmak istersek, sistem bize bir sayfa kadar yer ayıracaktır. Ek olarak bu değer için "proc" dosya sisteminde bir dosya mevcut değildir. -> "SHMMNI" : Sistem geneli oluşturulabilecek maksimum Paylaşılan Bellek Alanı nesnesinin maksimum adet bilgisidir. Bu değer "/proc/sys/kernel" dizinindeki "shmmni" isimli dosyadan elde edilir. Diğer yandan paylaşılan Bellek Alanları nesnesini kullanırken dikkat etmemiz gereken nokta, eğer bir kuyruk sistemi oluşturulmamışsa, yeni bilgiler eskilerinin üzerine yazılmaktadır. Şu vakte kadarki örneklerde, hep üzerine yazma yapılmıştır ve senkronizasyon kara düzen sağlanmıştır. Bütün bunlara ek olarak belirtmekte fayda vardır ki paylaşılan bellek alanları nesneleri "Bellek Tabanlı Dosya" mekanizması ile de ilişki içerisindedir. >>> Bellek Tabanlı Dosyalar ("Memory Mapped Files"): Bu mekanizma, 90'lı yıllarda "POSIX IPC" nesneleriyle birlikte, UNIX türevi sistemlere eklenen bir mekanizmadır. Benzer yıllarda da Windows ve macOS türevi işletim sistemlerine de eklenmiştir. Pekiyi nedir bu mekanizma? Diskteki bir dosyanın belleğe çekilmesi ve bellekte yapılan işlemlerin diskteki dosya üzerinde de etkisinin görülmesidir. Anımsanacağınız üzere diskteki bilgileri değiştirmek için ilk önce ilgili dosyayı açıyor, daha sonra dosya sonuna kadar "read" ile okuyup bilgileri belleğe çekiyorduk. Devamında değişiklik yapıp, yeni halini "write" ile tekrardan dosyaya yazıyorduk. Fakat bu mekanizmaya sahip dosyaları bizler yine açıyoruz. Açma işleminden sonra ilgili dosya, prosesin sanal bellek alanına enjekte edilmektedir. Dolayısıyla bu aşamada yapacağımız değişiklikler dosyaya direkt yansımaktadır. Prosesin sanal bellek alanına enjekte edildiği için, aynı dosyayı başka prosesler de açarsa, prosesler arası haberleşme olarak da kullanılabilir. Pekiyi bizler bu mekanizmayı nasıl kullanabiliriz? Şu adımları takip ederek, bu mekanizmadan faydanalabiliriz: -> Açılacak olan dosya belirlendikten sonra, "open" fonksiyonu ile bu dosya açılır. -> Daha sonra direkt olarak "mapping" işlemi açılmış olan bu dosyaya uygulanır. -> "mapping" işleminden sonra ya da "main" fonksiyonunun sonunda "close" çağrısı ile "open" ile elde ettiğimiz "fd" değeri geri verilebilir. -> Her ne kadar prosesin sonlanması ile "map" edilen alanlar otomatik olarak "unmap" edilecekse de bizler elle "unmap" işlemi yapmalıyız. -> Eğer bizler kendimizin hayata getirdiği dosyayı açmıyorsak, "unlink" ya da "remove" fonksiyon çağrıları yapmamalıyız. Aşağıda bu hususa ilişkin örnek verilmiştir: * Örnek 1, Aşağıda, yukarıda anlatılanlar için, bir örnek bulundurulmuştur. #include #include #include #include #include #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* i. Dosyayı açtık. */ int fd; if((fd = open("main.c", O_RDWR)) == -1) exit_sys("open"); /* ii. Dosyanın uzunluk bilgisini temin ettik. */ struct stat finfo; if(fstat(fd, &finfo) == -1) exit_sys("fstat"); /* * iii. Dosyayı, prosesin sanal bellek alanına "map" ettik. "finfo.st_size" değerinin * "0" olmadığından emin olunuz. Aksi halde "mmap" fonksiyonu başarısız olacaktır. */ char* faddr; if((faddr = (char*)mmap(NULL, finfo.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); /* iv. Daha sonra dosyadakileri işledik. */ for(int i = 0; i < finfo.st_size; ++i) putchar(faddr[i]); memcpy(faddr, "ankara", 6); /* * v. Programın akışı tam bu noktadayken ilgili dosyayı kontrol ettiğimiz zaman * göreceğiz ki "ankara" yazısı dosyanın başına yazılmıştır. Dosyada halihazırda * karakter bulundurduğu için, oradaki karakterlerin üzerine yazılacaktır. Yapılan * değişiklik direkt olarak dosyaya yansıyacaktır. */ getchar(); /* vi. Artık "unmap" yapabiliriz. */ if(unmap(faddr, finfo.st_size) == -1) exit_sys("unmap"); /* vii. Artık "fd" değerini geri verebiliriz. */ close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bu mekanizmayı kullanırken dikkat etmemiz gereken noktalardan birisi de "open" fonksiyonuna geçtiğimiz açış mod bilgileri ile "mmap" fonksiyonuna geçilen bilgilerin birbiriyle uyumlu olmasıdır. Hakeza "mmap" fonksiyonua geçilen bilgilerin de birbiriyle uyumlu olması gerekmektedir. Aksi halde fonksiyonlar başarısız olacak ve "errno" değişkeni uygun değere çekilecektir. Örneğin, bizler dosyayı açarken "O_RDWR" modunda açalım. "mmap" fonksiyonunda da yalnızca "PROT_READ" modunu kullanalım. Bu durumda, prosesimizin dosyaya yazma hakkı olsa bile, "PROT_READ" bayrağını kullandığımız için "mmap" fonksiyonu başarısız olacaktır. Benzer biçimde "open" fonksiyonunda "O_WRONLY" bayrağı kullansak ve "mmap" için de "PROT_WRITE" bayrağını kullansak, yine sorunla karşılaşabiliriz. Çünkü bazı sistemlerdeki "PROT_WRITE" bayrağı aynı zamanda "PROT_READ" bayrağını da içermektedir. Dolayısıyla dosyayı açarken "O_RDWR" modunun kullanılmasını istemektedir. Buradan hareketle yalnızca "O_WRONLY" bayrağını kullanmamalıyız. Bu kural POSIX standartlarında bellek tabanlı dosyaları açarken ve "shm_open" fonksiyonu için geçerlidir. Kural gereği açım sırasında "read" özelliğinin de bulunması gerekmektedir. Son olarak Bellek Tabanlı Dosyalar büyütülemez. Fakat bizler normal dosyaları "truncate"/"ftruncate" fonksiyonlarıyla ya da dosya konum göstericisini "EOF" konumuna çekip, bu konumdan itibaren yazma yaparak büyütebiliriz. Diğer yandan bizler, dosyanın büyüklüğünden daha büyük alanları "map" edebiliriz. Örneğin, dosyamızın büyüklüğü "10278" bayt olsun. Bizler de "mapping" sırasında büyüklük olarak "20000" değerini, "offset" olarak da "0" değerini geçelim. Şimdi "10278" bayt büyüklüğündeki bir dosya 3 sayfa yer kaplayacaktır "(4096 * 2 + 2086)". Fakat üçüncü sayfadaki "2010" (4096*3 - 10278) bayt boş kalacaktır. Eğer bizler üçüncü sayfadaki boş kalan "2010" baytlık alana erişmek istersek bir sorun olmayacaktır. Fakat dördüncü sayfanın başından itibaren, "20000" inci bayta kadarlık alana erişmeye çalışırsak, yani "12288" bayt ile "20000" bayt arası, programımız çökecektir. O vakit bizler hangi durumlarda, 3. sayfada boş kalan "2010" bayta erişmeli hangi durumlarda "12288" bayt ile "20000" bayt arasına erişmeye çalışmalıyız? Anımsanacağınız üzere bir dosyayı büyütmek için, dosya konum göstericisini dosyanın sonuna çekmek ve o konumdan itibaren "write" işlemi yapmamız gerekiyordu. Yöntemlerden birisi de buydu. Fakat Bellek Tabanlı Dosyaları, dosyanın büyüklüğünden daha fazla büyüklükte "map" etmek ve fazla kısma "write" yaparak, büyütemeyiz. Öte yandan Bellek Tabanlı Dosyalarda yapılan değişikliklerin direkt olarak disk üzerinde değişikliğe yol açtığından daha önce bahsetmiştik. Aslında bu durum, POSIX standartlarınca belli şartlar altında garanti edilmiştir. Pekiyi nedir bu şartlar? "msync" isimli fonksiyonun çağrılması. İş bu fonksiyon çağrıldığı müddetçe Bellek Tabanlı Dosya'da yapılan değişiklik, diske yansıtılacak ya da diskte yapılan değişiklik Bellek Tabanlı Dosya'ya yansıtılacaktır. >>>> "msync" : Fonksiyon aşağıdaki prototipe sahiptir; #include int msync(void *addr, size_t len, int flags); Fonksiyonun birinci parametresi, "mmap" ile elde ettiğimiz adres bilgisidir. Bu adres bilgisinin sayfa katlarında olması POSIX tarafından bir zorunluluk değildir. Fakat Linux türevi sistemlerde böylesi bir zorunluluk vardır. Fonksiyonun ikinci parametresi ise "flush" edilecek alanın büyüklük bilgisidir. Tipik olarak bu değer, "mmap" fonksiyonunda kullandığımız büyüklük bilgisi olabilir. Fakat burada kullandığımız değer, sayfa katları olacak şekilde yukarı doğru yuvarlanmaktadır. Örneğin, sayfa katları 4096 biçiminde olsun. Bizler de 3000 değerini girelim. Yukarı doğru yuvarlandığı için sanki 4096 değerini geçmişiz gibi olacaktır. Fonksiyonun son parametresi ise şu bayraklardan birisini almaktadır; "MS_SYNC", "MS_ASYNC" ve "MS_INVALIDATE". Bu bayraklarnda, >>>>> "MS_SYNC" : Buradaki yön bilgisi bellekten diske doğrudur. Yani Bellek Tabanlı Dosya üzerinde bir değişiklik yapıldığında, ilgili değişikliğin diskteki dosya üzerinde yansıtılmasıdır. Tabii, "msync" fonksiyonu geri döndükten sonra, "flush" işleminin bitmesi garantidir. >>>>> "MS_ASYNC" : "MS_SYNC" bayrağı gibidir. Ancak bu bayraktan farkı, "flush" işlemi başlatıldıktan sonra "msync" fonksiyonunun hemen geri dönmesidir. Dolayısıyla "flush" işleminin bitmiş olması garnati değildir. >>>>> "MS_INVALIDATE" : Buradaki yön bilgisi diskten belleğe doğrudur. Yani başka bir proses diskteki dosyayı güncellediğinde, ilgili değişikliğin Bellek Tabanlı Dosya üzerinde yansıtılmasıdır. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda "-1" biçimindedir. Öte yandan "munmap" işlemi sırasında da "msync" işlemi gerçekleştirilmektedir. Dolayısıyla bir proses "munmap" yapmadan sonlansa bile "munmap" çağrılacağı için yine "msync" işlemi uygulanacaktır. Öte yandan Linux sistemleri, başka bir yaklaşık sergilemektedir. Şimdi bu yaklaşımı incelemeden evvel "write" ve "read" fonksiyonlarının nasıl çalıştığını detaylarıyla öğrenmeliyiz; >>>> "write" fonksiyonu ile normal bir dosyaya yazma yaptığımız zaman aslında direkt olarak diske yazma işlemi gerçekleşmez. İlk önce "kernel" içerisinde bulunan "buffer cache" veya "page cache" isimli bir tampona yazılır. Bu tampon, ismine "kernel daemon" diyeceğimiz bir başka akış tarafından belirli aralıklarla "flush" edilerek içerisindekiler diske aktarılır. Böylelikle disk üzerindeki yazma işlem adedinin azaltılarak ömrünün uzun olması, işi yapan sistem fonksiyonunu daha az çağırarak sistem performansını arttırmak vb. şeyler hedeflenmektedir. Bu konu esasında "IO Scheduling" konusu ile ilgilidir. Tabii iş bu tamponun "flush" edilme sıklığı da önemlidir. Çünkü çok fazla beklenirse, elektrik kesilmesi gibi durumlarda, bilgi kayıpları oluşabilir. >>>>> "IO Scheduling" : Diske yazılacak ya da diskten okunacak bilgilerin bazılarının bir araya getirilerek belli bir sırada işleme sokulmasıdır. >>>> "read" fonksiyonu ise "write" fonksiyonunun kullandığı tamponları kullanmaktadır. Çünkü POSIX standartlarınca "write" fonksiyonu geri döndüğünde, artık aynı dosyaya yapılan bir sonraki "read" işlemiyle o yazdıklarımızın okunması garanti altındadır. Linux sistemlerinde Bellek Tabanlı Dosyalar ise "msync" fonksiyon çağrısı yerine, haberleşmek için kullanılacak olan ve prosesin sanal sayfa tablosuna enjekte edilen bellek alanı, aslında yukarıda zikredilen "buffer cache" / "page cache" isimli tamponlara yönlendirilmiştir. Dolayısıyla haberleşecek iki proses, aslında bu "buffer cache" / "page cache" isimli tamponları kullanarak haberleşmektedir. Bu duruma ise "Unified File System" tasarımı denmektedir. Tabii taşınabilirlik açısından bizlerin "msync" kullanması tavsiye edilmektedir. * Örnek 1, Aşağıdaki örnekte "msync" kullanılmamıştır. Haberleşmenin sağlanması için içi dolu "test.txt" dosyasının mevcut olması gerekmektedir. Daha sonra ilk "write", sonrasında da "read" programını çalıştırmalıyız. Görüleceği üzere prosesler hayatlarını devam ettiriyor olmalarına rağmen yapmış oldukları değişiklik yansıtılmaktadır. Çünkü Linux sistemlerinde kullanılan "Unified File System" tasarımı kullanılmıştır. /* read */ #include #include #include #include #include #include #include void exit_sys(const char*); int main(void) { int fd; if((fd = open("test.txt", O_RDWR)) == -1) exit_sys("open"); struct stat finfo; if(fstat(fd, &finfo) == -1) exit_sys("fstat"); printf("Press ENTER to read!...\n"); getchar(); char* faddr; if((faddr = (char*)mmap(NULL, finfo.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); printf("[%lld] : ", (long long)finfo.st_size); for(int i = 0; i < finfo.st_size; ++i) putchar(faddr[i]); printf("\nPress ENTER to exit!...\n"); getchar(); if(munmap(faddr, finfo.st_size) == -1) exit_sys("unmap"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* write */ #include #include #include #include void exit_sys(const char*); int main(void) { int fd; if((fd = open("test.txt", O_WRONLY)) == -1) exit_sys("open"); printf("Press ENTER to write!...\n"); getchar(); if(write(fd, "abbccc", 6) == -1) exit_sys("write"); printf("Press ENTER to exit!...\n"); getchar(); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "msync" isimli fonksiyon kullanılmıştır. #include #include #include #include #include #include #include void exit_sys(const char*); int main(void) { int fd; if((fd = open("test.txt", O_RDWR)) == -1) exit_sys("open"); struct stat finfo; if(fstat(fd, &finfo) == -1) exit_sys("fstat"); char* faddr; if((faddr = (char*)mmap(NULL, finfo.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); memcpy(faddr, "ulya_yuruk", 10); /* * Bellekte yapılan değişiklikler, diske yansıtılmıştır. */ if(msync(faddr, finfo.st_size, MS_SYNC) == -1) exit_sys("msync"); printf("Press ENTER to exit!...\n"); if(munmap(faddr, finfo.st_size) == -1) exit_sys("unmap"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Anımsanacağınız üzere "private mapping" yapmanın asıl amacı "Copy On Write" mekanizmasından faydalanmaktır. Böylelikle haberleşecek iki proses aynı "page cache" isimli tamponu kullanacak, ancak "write" yaptığında bu tamponun bir kopyası çıkartılıp ona yazılacaktır. Bu mekanizma özellikle şu noktalarda kullanılmaktadır; "executeable" dosyalar ve "dynamic libraries" dosyalarının belleğe yüklenmesi. Örneğin, "sample" isimli çalıştırılabilir bir dosyamız olsun. Bu dosyamız ise "ELF" dosya türünden olsun. Bu dosya kabaca şu şu içeriklere sahiptir; ".text", ".data", ".bsdd", vb. "exec" fonksiyonları "ELF" formatında bir dosyayı belleğe aktarırken her bir bölümü "private mapping" yaparak yüklemektedir. Dolayısıyla aynı programı ikinci kez çalıştırdığımızda, mümkün olduğunda aynı fiziksel bellek kullanılmaktadır. Eğer bunlardan birisi "write" işlemi yaparsa, "copy on write" mekanizmasıyla, ikisinin fiziksel belleği farklılaştırılmaktadır. Diğer yandan "exec" fonksiyonları, yukarıda belirtilen bölümleri belleğe aktarırken, ilk değer verilmemiş global değişkenlerin bulunduğu ".bss" alanını sıfırlayarak belleğe aktarmaktadır. Buradaki sıfırlama işlemi "mapping" sırasında yapılmaktadır. > Hatırlatıcı Notlar: >> "mq_open" fonksiyonunda "O_EXCL" bayrağını yalnız başına kullanırsak yani "O_CREAT" ile birlikte kullanmazsak tanımsız davranış oluşacaktır. >> Bizler burada yazarken "POSIX IPC" mesaj kuyrukları tabiri ya da "System 5 IPC" mesaj kuyrukları tabiri kullandık. Aslında "IPC" kelimesi başlı başına prosesler arası haberleşmeyi kastetmektedir. Dolayısıyla aslında kastettiğimiz şey "POSIX" mesaj kuyrukları ve "System 5" mesaj kuyruklarıdır. >> Linux standartlarınca "mq_open" fonksiyonun dördüncü parametresi için "struct mq_attr*" belirtilmiş. Fakat POSIX standartları bu parametrenin "const" mu "non-const" mu olduğu konusunda bir şey söylememişler. Fakat bu yapı "global namepsace" içerisinde tanımlanarak kullanıldığında herhangi bir problemle karşılaşılmamıştır. Dolayısıyla bizler "const" olarak kullanacağız. >> Borular "stream" tabanlıyken, mesaj kuyrukları paket tabanlıdır. >> Ek olarak "POSIX" Paylaşılan Bellek Alanları nesnelerini Linux sistemlerinde "/dev/shm" dizini içerisinde görüntüleyebiliriz. >> Dosya Delikleri, bazı dosya sistemlerinin desteklediği bir özelliktir. Bir dosyayı "ftruncate" ya da "truncate" ile büyütmek istediğimiz zaman dosya gerçekte büyümüyor. Yani, disk üzerinde yeni alan için henüz bir alan tahsisi gerçekleştirmiyor. Fakat dosya sisteminde bu yeni alanın bilgilerini bir şekilde saklıyor ve dosyanın boyutunu yeni boyut olarak gösteriyor. Eğer okuma yaptığımız nokta, bu yeni alana denk gelirse bizlere "0" gösteriyor. Fakat ne zamanki bu yeni alana bir yazma işlemi yapalım, işte o vakit gerçekten de disk üzerinde alan tahsisi yapıyor.