> UNIX/Linux Sistemlerinde Dosya Betimleyicileri: Anımsayacağınız gibi open fonksiyonu dosya başarılı bir biçimde açıldığında "dosya betimleyici (file descriptor)" denilen int türden bir handle değeri vermektedir. Bu dosya betimleyicisi read, write, lseek, close fonksiyonlarında hangi dosya üzerinde işlem yapılacağını belirlemek için kullanılmaktadır. UNIX/Linux sistemlerinde sistem programcılarının "dosya betimleyicilerinin (file descriptors)" ne anlam ifade ettiğini bilmesi gerekmektedir. UNIX/Linux sistemlerinde proses kontrol blok içerisinde "dosya betimleyici tablosu (file descriptor table)" bir tablonunadresini tutan bir eleman vardır. Dosya betimleyici tablosu dosya nesnelerini gösteren bir gösterici dizisidir. Dosya nesneleri (file object) çekirdeğin açık bir dosya üzerinde işlem yapabilmesi için gereken bilgileri tutmaktadır. Bu durumu aşağıdaki şekille temsil edebiliriz. Proses Kontrol Blok ... ... Dosya Betimleyici Tablosu ... pfds ----------------> 0 adres --------------------------> Dosya Nesnesi ... 1 adres --------------------------> Dosya Nesnesi (aşağıdaki ile aynı nesneyi gösteriyor) ... 2 adres --------------------------> Dosya Nesnesi (yukarıdaki ile aynı nesneyi gösteriyor) ... 3 BOŞ 4 BOŞ 5 BOŞ ... 1022 BOŞ 1023 BOŞ Örneğin Linux çekirdeğinde proses kontrol blok "task_struct" isimli yapı ile temsil edilmiştir. Dosya nesneleri de "file" isimli yapı ile temsil edilmiş durumdadır. Dosya betimleyici tablosu da aslında file türünden adreslerden olulan bir gösterici dizisidir. Bu gösterici dizisindeki her elemana bir slot da diyebiliriz. Buradaki her elemanın bir indeksi numarası vardır. Proses çalışmaya başladığında hemen her zaman dosya betimleyici tablosunun ilk üç slotu (yani 0, 1 ve 2 numaralı indeks elemanları) doludur. İşte "dosya betimleyicisi (file descriptor)" aslında dosta betimleyici tablosunda bir indeks belirtmektedir. 0 numaralı betimleyici (yani dizinin ilk slotu) "stdin dosyası" dediğimiz klavyeyi temsil eden terminal aygıt sürücüsüne ilişkin dosya nesnesini göstermektedir. 1 ve 2 numaralı betimleyiciler de ekranı temsil eden terminal aygıt sürücüsüne ilişkin aynı dosya nesnesini göstermektedir. Yani 0 numaralı betimleyici ile okuma yapıldığında klavyeden okuma yapılacak, 1 ve 2 numaralı betimleyici kullanılarak yazma yapıldığında yazılanlar ekrana çıkartılacaktır. open fonksiyonu ile bir dosya açıldığında işletim sistemi önce bir dosya nesnesi oluşturur. Sonra dosya betimleyici tablosundaki ilk boş slotun bu nesneyi göstermesini sağlar ve dosya betimleyicisi olarak bu slotun numarasıyla yani (dizideki indeks numarasıyla) geri döner. Örneğin yukarıdaki şekli temel alarak prosesin open fonksiyonuyla bir dosya açmış olduğunu varsayalım: Proses Kontrol Blok ... ... Dosya Betimleyici Tablosu ... pfds ----------------> 0 adres --------------------------> Dosya Nesnesi ... 1 adres --------------------------> Dosya Nesnesi (aşağıdaki ile aynı nesneyi gösteriyor) ... 2 adres --------------------------> Dosya Nesnesi (yukarıdaki ile aynı nesneyi gösteriyor) ... 3 adres --------------------------> Dosya Nesnesi 4 BOŞ 5 BOŞ ... 1022 BOŞ 1023 BOŞ Burada bize dosya betimeleyicisi olarak 3 değeri verilecektir. open fonksiyonunun dosya betimelyici tablosundaki ilk boş betimleyiciyi vermesi garanti edilmiştir. Yukarıda da belirttiğimiz gibi genellikle UNIX/Linux sistemlerinde proses çalışmaya başladığında 0, 1 ve 2 numaralı betimleyiciler dolu durumdadır. Dolayısıyla ilk boş betimleyici 3 numaralı betimleyicidir. * Örnek 1, Aşağıdaki programda önce bir dosya açılmış ve oradan 3 numaralı betimleyici elde edilmiştir. Sonra bir dosya daha açılmış oaradan da 4 numaralı betimleyici elde edilmiştir. Sonra 3 numaralı betimelyici kapatılıp yenidne bir dosya açıldığında 3 numaralı betimeleyici elde edilmiştir. Çünkü open fonksiyonu her zaman eldeki ilk boş betimleyiciyi vermektedir. #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd1, fd2, fd3; if ((fd1 = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); printf("%d\n", fd1); /* 3 */ if ((fd2 = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); printf("%d\n", fd2); /* 4 */ close(fd1); if ((fd3 = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); printf("%d\n", fd3); /* 3 */ return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Bir dosya ile ilgili işlem yaapabilmek için gereken her şey "dosya nesnesi (file object)" içerisinde bulunmaktadır. Dosya nesneleri diskteki normal bir dosyaya ilişkin olabildiği gibi aygıt sürücü dosyalarına da ilişkin olabilmektedir. Aslında dosya nesnelerinin içerisinde read, write, lseek, close gibi işlemlerde çağrılacak fonksiyonaların adresleri bulunmaktadır. fd ----> Dosya Nesnesi ... okuma fonksiyonunun adresi yazma fonksiyonun adresi konumlandırma fonksiyonun adresi kapatma fonksiyonun adresi ... Böylece örneğin bir betimleyici kullanılarak read fonksiyonu çağrıldığında aslında read fonksiyonu dosya nesnesinin içerisinde adresi bulunan okuma fonksiyonunu çağırmaktadır. Tabii işlemlerin bazı yarıntıları vardır. Biz burada bu ayrıntıları basitleştirerek genel mekanizmayı açıklamak istiyoruz: ssize_t sys_read(int fd, ...) { 1) prosesin dosya betimeleyici tablosunun fd numaralı elemanından dosya nesnesine eriş 2) Dosya nesnesinde belirtilen okuma fonksiyonunu çağır } write fonksiyonu ve temel dosya fonksiyonları da benzerdir. Linux sistemlerinde bir prosesin dosya betimleyici tablosu default olarak 1024 elemanlıdır. Dolayısıyla proses hiç kapatmadan en fazla 1024 dosyayı aynı anda açık tutabilir. Tabii işin başında dosya betimelyici tablosunun ilk üç girişi zaten dolu biçimdedir. Bu durumda proses onları kapamazsa ancak 1021 dosya açabilir. Aşağıda bu testi yapan bir program verilmiştir. * Örnek 1, #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; int i; for (i = 0;; ++i) { if ((fd = open("test.txt", O_RDONLY)) == -1) { perror("open"); break; } printf("%d\n", fd); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Linux sistemlerinde bir proses isterse dosya betimelyici tablosunu 1048576 kadar büyütebilir. Bunun için setrlimit POSIX fonksiyonu kullanılmaktadır. Dosya betiemleyici tablosunun uzunluğu ise getrlimit ya da sysconf fonksiyonu ile alınabilmektedir. Ancak bu konular kursumuzun kapsamı dışındadır. Aşağıda prosesin dosya betimelyici tablosunu 5000 uzunluğunda yapan örnek bir program verilmiştir. * Örnek 1, #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; struct rlimit rl; rl.rlim_cur = 5000; rl.rlim_max = 5000; if (setrlimit(RLIMIT_NOFILE, &rl) == -1) exit_sys("setrlimit"); printf("%ld\n", sysconf(_SC_OPEN_MAX)); for (int i = 0;; ++i) { if ((fd = open("sample.c", O_RDONLY)) == -1) exit_sys("open"); printf("%d\n", fd); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Mademki UNIX/Linux sistemlerinde open fonksiyonu en düşük boş betimleyiciyi bize vermektedir. O halde biz stdout aygıt sürücüne ilişkin 1 numaralı betimleyiciyi kapatıp hemen arkasından open fonksiyonu ile bir dosya açarsak open bize 1 numaralı betimleyiciyi verecektir. Yani artık 1 numaralı betimleyici stdout dosyasına ilişkin dosya nesnesini değil, disk dosyasına ilişkin dosya nesnesini gösteriyor durumda olur. İşte dosya yönlendirmeleri böyle bir mekanizmayla yapılmaktadır. Yukarıda da belirttiğimiz gibi printf, puts vs. gibi tüm standart C fonksiyonları ve diğer dillerdeki (Java, C#, Python vs.) ekrana yazan tüm fonksiyonlar eninde sonunda aslında write fonksiyonuyla 1 numaralı betimleyiciyi kullanarak yazımı yaparlar. Dolayısıyla bu yönlendirmeden sonra ekrana yazdırma için kullanılan tüm fonksiyonlar aslında yönlendirilen bu dosyaya yazacaktır.Aşağıda bu işleme bir örnek verilmiştir. * Örnek 1, Aşağıda örnekte açtığımız dosyayı biz kapatmadık. 0, 1 ve 2 numaralı betimelyiciler zaten program sona erdiğinde exit fonksiyonunda kapatılmaktadır. #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; close(1); if ((fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("open"); for (int i = 0; i < 10; ++i) printf("Test: %d\n", i); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte aynı fikirle stdin dosyası yönlendirilmiştir. Burada "test.txt" dosyası içerisinde boşluk karakterleriyle ayrılmış sayıların olduğunu varsayıyoruz. Burada scanf fonksiyonu klavyedne değil bu dosyadan okuma yapacaktır. #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; int val; close(0); if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); while (scanf("%d", &val) == 1) printf("%d\n", val); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Bir dosya betimelyicisnin gösterdiği dosya nesnesi başka bir betimelyici tarafında gösteriliyorsa ne olur? Örneğin fd1 betimelyeicisi ile fd2 betiemleyicisinin aynı dosya nesnesini gösterdiğini varsayalım. Bu durumda read, write, lseek gibi dosya fonksiyonlarına fd1 ya da fd2 betimeleyicilerinin geçirilmesinde bir fark oluşmayacaktır. Çünkü dosya işlemleri neticede bu dosya nesnesinin içerisindeki bilgilerden hareketle yapılmaktadır. Pekiyi böyle bir durumun bir faydası olabilir mi? Dosya betimleyicilerini çiftlemek (duplicate etmek) için dup ve dup2 isimli iki POSIX fonksiyonu kullanılmaktadır. Tabii bu POSIX fonksiyonları da aslında doğrudan işletim sisteminin sistem fonksiyonlarını çağırmaktadır. Bu fonksiyonlardan, >> "dup" : Fonksiyonunun prototipi şöyledir: #include int dup(int fd); Fonksiyon parametresi ile belirtilen dosya betimleyicisinin gösteridği dosya nesnesini gösteren yeni bir betimelyici tahsis etmektedir. Yani fonksiyon başarılı olursa fonksiyonun geri döndürdüğü dosya betimleyicisi ile parametrede belirtilen betimleyicinin aynı dosya nesnesini gösterir durumda olur. dup başarısızlık durumunda -1 değerine geri dönmektedir. dup fonksiyonunun dosya betimelyici tablosundaki ilk boş betimelyiciyi vermesi garanti edilmiştir. * Örnek 1, Aşağıdaki örnekte önce test.txt dosyası açılmış sonra da bu betimleyici çiftlenmiştir. Dosya göstericisinin dosya nesnesi içerisinde bulunduğunu anımsayınız. Dolayısıyla aşağıdaki örnekte dosyadan ıkuma yaparken hangi dosya betimleyicisinin kullanıldığının bir önemi kalmamaktadır. #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd1; int fd2; char buf[10 + 1]; ssize_t n; if ((fd1 = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); if ((fd2 = dup(fd1)) == -1) exit_sys("dup"); if ((n = read(fd1, buf, 10)) == -1) exit_sys("read"); buf[n] = '\0'; puts(buf); if ((n = read(fd2, buf, 10)) == -1) exit_sys("read"); buf[n] = '\0'; puts(buf); close(fd1); close(fd2); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >> "dup2" : dup2 fonksiyonu da dup fonksiyonu gibi dosya betimleyicisini çiftlemekte kullanılır. Fonksiyonun prototipi şöyledir. #include int dup2(int fd, int fd2); Fonksiyon birinci parametresiyle belirtilen dosya betimleyicisi ile aynı dosya nesnesini gösteren bir betimleyici oluşturur. Ancak bu betimleyici en düşük boş betimleyici değil ikinci parametrede belirtilen betimleyicidir. Yani fonksiyon başarılı olduğunda birinci ve ikinci parametresiyle belirtilen dosya betimleyicileri aynı dosya nesnesini gösteriyor durumda olur. Eğer ikinci parametreyle verilen betimleyici zaten açık bir dosyanın betimleyicisi ise bu durumda o dosya önce kapatılır, betimelyici boşaltılır sonra bu betimleyicinin birinci parametresiyle belirtilen betimelycinin gösterdiği dosya nesnesini göstermesi sağlanır. dup2 fonksiyonu başarısızlık durumunda yine -1 değerine geri dönmektedir. Dosya yönlendirmeleri genellikle dup2 fonksiyonuyla yapılmaktadır. Çünkü close işlemi ve open işlemi arasında prosese ilişkin bir thread varsa ve o thread de tesadüfen open fonksiyonunu çağırırsa ilk boş betimelyiciyi o thread elde edebilir. Ayrıca ilk boş betimeleyici duruma göre farklılıklar da gösterebilir. Örneğin stdout betimeleyicisini bir dosyaya yönlendirmek isteyelim. Bu işlemi şöyle yapabiliriz kontroller uygulanmamıştır: fd = open(....); dup2(fd, 1); close(fd); Aşağıda stdout dosyasının dup2 ile doğru teknik kullanılarak yönlendirilmesi örneği verilmiştir. * Örnek 1, #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("test.txt", O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("open"); if (dup2(fd, 1) == -1) exit_sys("dup2"); close(fd); for (int i = 0; i < 10; ++i) printf("test: %d\n", i); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi kabuk programları nasıl IO yönlendirmesi yapmaktadır. Kabuk programları tipik olarak komut satırından girilen program için bir kez fork uygulayıp alt yarattıktan sonra, alt proseste IO yönlendirmesini yapıp sonra exec uygulamaktadır. exec işlemi sırasında prsesin kontrol bloğu değişmediği için dosya betimleyici tablosu da değişmemektedir. Dolayısıyla exec sonrasında çalıştırılan program aslında IO yönlendirmesinin etkisi altında kalacaktır. UNIX/Linux sistemlerinde exec yapıldığı zaman o ana kadar açılmış olan dosyaların exec işleminden sonra açık olarak kalması istenmeyebilir. Örneğin biz 100 tane dosya açmış olalım. Sonra fork ve exec uygulamış olalım. Şimdi exec yaptığımız program çalışırken aslında kendisinin ilgilenmediği 100 dosyayı açık olarak görecektir. Bu da exec yapan kodun dosya betimleyici tablosunu boş değil kısmen dolu olarak çalışması anlamına gelecektir. İşte UNIX/Linux sistemlrinde her betimleyici için "close on exec" isimli bir bayrak tutulmaktadır. Eğer bu bayrak set edilmişse bu durumda exec işlemi sırasında o betimleyici kernel taarafından otomatik olarak kapatılmaktadır. Dosyalar açıldığında default durumda betimleyicinin "close on exec" bayrığı "reset" durumdadır. Yani dosya exec işlemleri sırasında kapatılmayacaktır. Betimleyicinin "close on exec" bayrağını set etmek için open fonksiyonunda O_CLOEXEC baurağı kullanılabilir. Ya da fcntl fonksiyonu ile bu işlem yapılabilir. Biz kursumuzda bu konunun ayrıntılarına girmeyeceğiz. * Örnek 1, Aşağıdaki örnekte "redirect" isimli bir program yazılmıştır. Program kabuk programının yaptığı yönlendirmenin benzerini yapmaktadır. Programın komut satırı argümanı kabukta girilen yönlendirme komutunu almaktadır. Örneğin: ./redirect "ls -l -i > test.txt" Programın içerisinde check_arg isimli fonksiyon '>' karakterinden yazıyı iki parçaya ayırmış ve soldaki programın komut satırı argümanlarını da ayrıştırmıştır. Yönlendirmenin aşağıdaki gibi yapıldığına dikkat ediniz: if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { /* child process */ if ((fd = open(rargs.redirect_path, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("open"); if (dup2(fd, 1) == -1) exit_sys("dup2"); close(fd); if (execvp(rargs.exe_args[0], rargs.exe_args) == -1) exit_sys("execvp"); /* unreacable code */ } Programın kodları ise aşağıdaki gibidir: /* redirect.c */ #include #include #include #include #include #include #include #include #define MAX_ARGS 1024 typedef struct tagREDIRECT_ARGS { char *exe_args[MAX_ARGS]; char *redirect_path; } REDIRECT_ARGS; bool check_arg(char *arg, REDIRECT_ARGS *rargs); void exit_sys(const char *msg); /* ./redirect "./sample > test" */ int main(int argc, char *argv[]) { char *arg; REDIRECT_ARGS rargs; pid_t pid; int fd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((arg = strdup(argv[1])) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if (!check_arg(arg, &rargs)) { fprintf(stderr, "invalid argument: \"%s\"\n", argv[1]); exit(EXIT_FAILURE); } if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { // child process if ((fd = open(rargs.redirect_path, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("open"); if (dup2(fd, 1) == -1) exit_sys("dup2"); close(fd); if (execvp(rargs.exe_args[0], rargs.exe_args) == -1) exit_sys("execvp"); /* unreacable code */ } if (wait(NULL) == -1) exit_sys("wait"); free(arg); return 0; } bool check_arg(char *arg, REDIRECT_ARGS *rargs) { char *str; size_t i = 0; if ((str = strchr(arg, '>')) == NULL || strchr(str + 1, '>') != NULL) return false; *str = '\0'; if ((rargs->redirect_path = strtok(str + 1, " \t")) == NULL) return false; for (str = strtok(arg, " \t"); str != NULL; str = strtok(NULL, " \t")) rargs->exe_args[i++] = str; if (i == 0) return false; rargs->exe_args[i] = NULL; return true; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } fork işlemi sırasında üst prosesin dosya betimleyici tablosu "sığ kopyalama (shallow copy)" yoluyla alt prosese kopyalanmaktadır. Sığ kopyalama bir nesnenin (yapının) elemanlarının diğerine kopyalanması anlamına gelmektedir. Eğer yapının bir elemanı başka bir nesneyi gösteriyorsa o nesnenin kopyasından çıkartılmamaktadır. Yalnızca ana nesnenin kopyasından çıkartılmaktadır. Dolayısıyşa sığ kopyalama sonucunda iki nesnenin gösterici elemanları aynı nesneyi gösteriyor durumda olur. fork işlemi sırasında alt prosesin kontrol bloğunda yeni bir dosya betimelyici tablosu oluşturulur. Üst prosesin dosya betimleyici tablosundaki dosya nesnelerinin adresleri alt prosesin dosya betimeleyici tablosuna kopyalanır. Böylece prosesle alt proses aynı dosya nesnesini gösteriyor durumda olur. Tabii bu surada dosya nesnelerinin referans sayaçları da 1 artırılmaktadır. fork işlemi sırasında işlemlerin bu biçimde yapıldığını şöyle ispatlayabiliriz. Örneğin dosya göstericisi dosya nesnesinin içerisinde tutulmaktadır. Bu durumda üst proses dosya nesnesini konumlandırırsa alt proses de onu konumlandırılmış görecektir. Aşağıdaki örnekte üst proses açtığı dosyanın dosya göstericisini 10'uncu offset'e konumlandırmıştır. Alt proses o betimelyciden okuma yaptığında 10 numaralı offset'ten itibaren okuma yapmış olacaktır. * Örnek 1, #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; pid_t pid; char buf[10 + 1]; ssize_t result; if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); if ((pid = fork()) == -1) exit_sys(""); if (pid != 0) { /* parent process */ lseek(fd, 10, SEEK_CUR); } else { /* child process */ sleep(1); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); buf[result] = '\0'; puts(buf); close(fd); exit(EXIT_SUCCESS); } if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); }