UNIX/Linux sistemlerinde her prosesin proses tablosu yoluyla erişilen bir dosya betimleyici tablosu (file descriptor table) vardır. Dosya betimleyici tablosu bir gösterici dizisi biçimindedir. Betimleyci tablo içersindeki her gösterici açılmış bir dosyanın bilgilerinin tutulduğu ve ismine dosya nesnesi (file object) denilen bir veri yapısını gösterir. open fonksiyonundan elde edilen dosya betimleyicisi (file descriptor) prosesin dosya betimleyici tablosunda bir indeks belirtmektedir.
Dosya nesnesi dosya işlemlerini gerçekleştirmek için gerekli tüm bilgileri tutmaktadır. Dosya betimleyicisi bir sistem fonksiyonuna parametre olarak geçirildiğinde ilgili fonksiyon betimleyici tablosunun betimleyici değeri ile belirtilen indeksinden dosya nesnesinin adresini elde eder ve açık olan dosyanın bilgilerine erişir. Her dosya açma işlemi sonucunda yeni bir dosya nesnesi oluşturulmakta ve betimleyici tablosunda yeni bir giriş tahsis edilmektedir. Örneğin yukarıdaki şekilde belirtilen durumda open fonksiyonuyla yeni bir dosyayı daha açtığımızı düşünelim:
int fd;
...
fd = open("test.dat", O_RDONLY);
Dosya betimleyici tablosunun yeni durumu şöyle olacaktır:
Yeni yaratılan dosya nesnesini kesikli çizgilerle gösterdik. Şekilden de gördüğünüz gibi dosya açma işlemi sonucunda open fonksiyonundan 5 numaralı betimleyici değeri elde edilmiştir. Yani örneğimizde fd’nin içerisinde 5 değeri bulunacaktır.
Disk tabanlı bir dosya açıldığında işletim sistemi diskten dosya bilgilerini elde eder ve dosyayı yönetebilmek için bellekte çeşitli veri yapılarını oluşturur. Yukarıda da belirttiğimiz gibi, dosyalar üzerinde işlem yapabilmek için gerekli tüm bilgiler dosyanın açılması sırasında işletim sistemi tarafından dosya nesnelerinin içerisine yerleştirilmektedir. Dosya betimleyicilerinin yalnızca birer anahtar değer (handle) işlevi gördüğüne dikkat ediniz. Şimdi farklı iki dosya betimleyicisinin aynı dosya nesnesini gösterdiğini düşünelim. Bu durumda aslında her iki dosya betimleyicisi de aynı dosyaya ilişkin olur değil mi? Örneğin, yukarıdaki şekilde 1 ve 2 numaralı betimleyicileri aynı dosya nesnesini gösteriyor. O halde biz o dosya üzerinde işlem yapmak için 1 numaralı betimleyiciyi de 2 numaralı betimleyiciyi de kullanabiliriz.
Dosya nesnelerinin içerisinde hangi bilgilerin bulunduğunu merak ediyor olabilirsiniz. Bazılarını söyleyelim:
- Dosya işlemleri için gerekli dosyanın bloklarına ilişkin tüm disk bilgileri.
- Dosyanın sahiplik bilgileri ve erişim bilgileri.
- Dosya göstericisinin konumu.
- Dosya nesnesinin kullanım sayacı.
- ...
open fonksiyonunun dosya betimleyici tablosundaki ilk boş betimleyiciyi tahsis edeceği POSIX standartlarında garanti altına alınmıştır. Örneğin, prosesin betimleyici tablosunun yalnızca 0, 1, 2, 3, 4, 7 numaaralı betimleyicileri dolu olsun. open fonksiyonuyla bir dosya açmak istediğimizi düşünelim. Başarı durumunda open fonksiyonu kesinlikle 5 numaralı betimleyiciyi tahsis edecek ve 5 değeriyle geri dönecektir. Bu örnekte aynı zamanda dosyaların açılıp kapatılmasıyla zamanla betimleyici tablosunda çeşitli boşlukların oluşabileceğine dikkatinizi çekmek istiyoruz.
Her prosesin ayrı bir proses tablosuna dolayısıyla da ayrı bir betimleyici tablosuna sahip olduğunu belirtmiştik. İşte bu nedenle dosya betimleyicileri proses düzeyinde anlamlı değerlerdir. Örneğin biz X prosesinde open fonksiyonuyla bir betimleyici elde edip o betimleyicinin değerini Y prosesine proseslerarası haberleşme yöntemlerinden biriyle göndermiş olalım. Bu betimleyicinin artık Y prosesinde bir anlamı olmayacaktır. Çünkü bu betimleyici Y prosesinde kullanılırken artık Y prosesinin dosya betimleyici tablosunda bir indeks belirtecektir. Bu nedenle gönderilen bu betimleyici Y prosesi için ya başka bir dosyaya ilişkin betimleyici durumundadır ya da geçersiz bir betimleyici durumundadır.
Aynı dosya open fonksiyonuyla birden fazla kez açılabilir. open fonksiyonu her çağrıldığında yeni bir dosya nesnesi ve yeni bir betimleyici tahsis edilir. Böylesi bir durum aynı dosya üzerinde birden fazla dosya nesnesi ile işlem yapmak anlamına gelir. Dosya göstericisinin dosya nesnesinin içerisinde tutulduğunu anımsayınız. Bu durumda iki betimleyicinin gösterdiği dosya nesnelerindeki dosya göstericileri farklı konumlarda olabilirler değil mi? Örneğin:
int fd1, fd2;
...
if ((fd1 = open("test.dat", O_RDONLY)) < 0) {
perror("open");
exit(EXIT_FAILURE);
}
if ((fd2 = open("test.dat", O_RDONLY)) < 0) {
perror("open");
exit(EXIT_FAILURE);
}
lseek(fd1, 100, SEEK_SET);
lseek(fd2, 200, SEEK_SET);
Oluşan durumu şekilsel olarak şöyle gösterebiliriz:
Burada fd1 ve fd2 betimleyicilerinin her ikisi de “test.dat” dosyasına ilişkindir. Biz fd1 betimleyici ile bu dosyanın 100 numaralı offset'inden, fd2 betimleyici ile de 200 numaralı offset'inden işlem yapabiliriz.
UNIX/Linux sistemlerinde eldeki bir dosya betimleyicisini kullanarak o dosya betimleyicisi ile aynı dosya nesnesini gösteren başka bir betimleyici oluşturulabilir. Bu işlem dup fonksiyonuyla yapılmaktadır:
#include <unistd.h>
int dup(int filedes);
Fonksiyonun parametresi daha önce açılmış olan bir dosyaya ilişkin betimleyicidir. Fonksiyon parametresiyle aldığı betimleyici ile aynı dosya nesnesini gösteren yeni bir betimleyici tahsis eder ve bu betimleyici ile geri döner. dup fonksiyonunun da başarı durumunda betimleyici tablosundaki ilk boş betimleyici ile geri döneceği POSIX standartlarında garanti altına alınmıştır. Fonksiyon başarısızlık durumunda -1 değerine geri döner. Örneğin:
int fd1, fd2;
if ((fd1 = open("sample.dat", O_RDONLY)) < 0) {
perror("open");
exit(EXIT_FAILURE);
}
if ((fd2 = dup(fd1)) < 0) {
perror("dup");
exit(EXIT_FAILURE);
}
Burada fd1 ve fd2 aynı dosya nesnesini gösteren betimleyicilerdir. Oluşan durumu aşağıdaki şekille gösterebiliriz:
dup fonksiyonu başarısız olduğunda errno değişkeninin alabileceği önemli değerler şunlardır:
EBADEF | : | Fonksiyonun parametresi açık bir dosyaya ilişkin geçerli bir dosya betimleyicisi değildir. |
EINTR | : | Fonksiyon bir sinyalle sonlandırılmıştır. |
... | : | ... |
fd1 ve fd2'nin aynı dosya nesnesine ilişkin iki betimleyici olsun. close fonksiyonu ile fd1 betimleyicisini kapatmış olalım. Bu durumda dosya nesnesi fd2 betimleyicisi tarafından da kullanıldığı için yok edilmez. Bir dosya nesnesinin kaç betimleyici tarafından kullanıldığı sistem tarafından dosya nesnesinin içerisindeki bir sayaç yoluyla izlenmektedir. close fonksiyonu parametresiyle belirtilen betimleyiciyi boşaltır ve dosya nesnesinin sayacını bir eksiltir. Ancak nesne sayacı sıfıra geldiğinde dosya nesnesi yok edilmektedir.
open fonksiyonu ile “test.txt” isminde bir dosya açtığımızı ve fd isimli bir betimleyici elde ettiğimizi varsayalım. Aşağıdaki kod parçası çalıştıktan sonra ne olacaktır?
close(1);
dup(fd1);
printf("bu yazi nereye yazilacak?\n");
1 numaralı dosya betimleyicisinin stdout dosyasına ilişkin olduğunu belirtmiştik. dup fonksiyonu da ilk boş betimleyiciyi elde ettiğine göre şöyle bir durumla karşılaşılacaktır:
printf fonksiyonu aygıta aktarım sırasında stdout dosyasını (yani 1 numaralı betimleyciyi) kullandığına göre artık yazılanlar "test.txt" dosyasına yazılacaktır değil mi?
Bir dosyaya ilişkin betimleyicinin başka bir dosya nesnesini gösterir duruma getirilmesine yönlendirme (redirection) denilmektedir. Yukarıdaki örneğimizde stdout dosyasını “test.txt” isimli bir dosyaya yönlendirmiş olduk. Aşağıdaki programı çalıştırarak oluşturulan "test.txt" dosyasını inceleyiniz:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
int fd;
int i;
if ((fd = open("test.txt", O_WRONLY|O_CREAT|O_TRUNC)) < 0) {
perror("open");
exit(EXIT_FAILURE);
}
close(1);
dup(fd);
for (i = 0; i < 10; ++i)
printf("Test %d\n", i);
close(fd);
return 0;
}
Yukarıdaki gibi yapılan yönlendirme işleminde küçük bir sorundan bahsedelim. close(1) işlemi ile dup(fd) işlemi arasında bir kesilme olur da başka bir dosya open fonksiyonu ile açılırsa stdout yanlışlıkla o dosyaya yönlendirilebilir. Tabi böyle bir sorunun ortaya çıkması için bu kesilmenin bizim prosesimizde oluşması gerekir. Her prosesin ayrı bir betimleyici tablosu olduğunu biliyorsunuz. Eğer kendi programınızda böyle bir kesilme olasılığı yoksa ya da kesilme oluştuğunda başka bir dosyanın açılması söz konusu değilse kaygılanmanıza da gerek yok. Ancak çok thread'li uygulamalarda ya da sinyal mekanizmasının kullanıldığı uygulamalarda bu durumu göz önüne almanız gerekebilir.
dup2 fonksiyonu close ile çiftleme işlemlerinin atomik bir biçimde yapıldığı daha gelişmiş bir dup fonksiyonudur:
#include <unistd.h>
int dup2(int fd1, int fd2);
Fonksiyon önce fd2 betimleyicisi üzerinde close işlemi uygular. Daha sonra fd2 betimleyicisinin fd1 betimleyicisi ile aynı dosya nesnesini göstermesini sağlar. Yani çağırma sonrasında fd2 betimleyicisi fd1 betimleyicisi ile aynı dosya nesnesini gösteriyor olacaktır. Bu durumu şekilsel olarak şöyle açıklayabiliriz:
dup2 fonksiyonu size biraz karışık gelmiş olabilir. İşlevini şöyle aklınızda tutabilirsiniz: dup2, fd1 betimleyicisini çiftlemektedir. Fakat elde edilen betimleyici en düşük numaralı betimleyici değil fd2 numaralı betimleyici olur. Ayrıca dup2 fonksiyonunda ikinci parametreyle belirtilen fd2 betimleyicnin açık bir dosyaya ilişkin olması da gerekmemektedir. Yani fd2 boş bir betimleyici değerini belirtiyor olabilir. dup2 fonksiyonu eğer fd2 betimleyicisi boş değilse o betimleyiciyi kapatmaya çalışmaktadır.
dup2 fonksiyonu başarı durumunda fd2 betimleyici değeriyle başarısızlık durumunda -1 değeriyle geri döner. Eğer fd1 ve fd2 aynı değerdeyse dup2 dosyayı kapatmaz. fd2 değerine (fd1 ile aynı değerdir) geri döner. Başarısızlık durumunda errno değişkeninin alacağı önemli değerler şunlardır:
EBADEF | : | Fonksiyonun parametresi açık bir dosyaya ilişkin geçerli bir dosya betimleyicisi değildir. |
EINTR | : | Fonksiyon bir sinyalle sonlandırılmıştır. |
... | : | ... |
stdout dosyasının yönlendirilmesini dup2 fonksiyonunu kullanarak şöyle de yapabiliriz:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
int fd;
int i;
if ((fd = open("test.txt", O_WRONLY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR)) < 0) {
perror("open");
exit(EXIT_FAILURE);
}
dup2(fd, 1);
for (i = 0; i < 10; ++i)
printf("Test %d\n", i);
close(fd);
return 0;
}
Şimdi prosesin dosya betimleyici tablosunun ve dosya nesnelerinin veri yapılarını biraz daha somut bir biçimde inceleyelim. Linux 2.6 çekirdeğindeki konuyla ilgili veri yapıları şöyledir:
Linux sistemlerinde proses tablosu task_struct yapısı ile, proses dosya tablosu files_struct yapısı ile ve dosya nesnesi de file yapısı ile temsil edilmektedir. Bu yapılar üzerinde bazı temel bilgileri verelim:
struct task_struct {
/* ... */
struct files_struct *files;
/* ... */
};
struct files_struct {
atomic_t count;
struct fdtable *fdt;
struct fdtable fdtab;
spinlock_t file_lock ____cacheline_aligned_in_smp;
int next_fd;
struct embedded_fd_set close_on_exec_init;
struct embedded_fd_set open_fds_init;
struct file * fd_array[NR_OPEN_DEFAULT];
};
Burada prosesin dosya bilgilerine erişmekte kullanılan ana eleman files_struct yapısı içerisindeki fdt göstericisidir. Bu gösterici işin başında (yani fork işlemi sırasında) aynı yapı içerisindeki fdtable türünden fdtab isimli yapı nesnesini gösterir duruma getirilmektedir. fdtable yapısı şöyledir:
struct fdtable {
unsigned int max_fds;
struct file ** fd; /* current fd array */
fd_set *close_on_exec;
fd_set *open_fds;
struct rcu_head rcu;
struct fdtable *next;
};
fdtable yapısının fd elemanı dosya betimleyici tablosunu göstermektedir. fd_set türünün bir bit dizisi olarak kullanıldığını belirtelim. close_on_exec göstericisi “close on exec” bayrağı set edilmiş betimleyicileri, open_fds ise açık dosya betimleyicilerini tutmakta kullanılır. Her fork işleminde fazla sayıda tahsisat yapmamak için işin başında bu göstericilerin files_struct yapısı içerisindeki önceden tahsis edilmiş elemanları göstermesi sağlanmıştır. Yani işin başında fdtable yapısının fd elemanı files_struct yapısının fd_array elemanını, fdtable yapısının close_on_exec elemanı files_struct yapısının close_on_exec_init elemanını ve fdtable yapısının open_fds elemanı files_struct yapısının open_fds_init elemanını göstermektedir. Açılan dosya sayısının artması durumunda fdtable yapısının elemanları için çekirdek heap alanı içerisinde yeni dizilerin tahsis edileceğini belirtelim. fork işlemi sonrasındaki files_struct yapısının durumunu aşağıdaki şekille betimleyebiliriz:
Linux sistemlerindeki file yapısı ise aşağıdaki gibidir:
struct file {
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op;
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
void *private_data;
#ifdef CONFIG_EPOLL
struct list_head f_ep_links;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};
Yapının f_mode dosyanın erişim bilgilerini, f_pos elemanı dosya göstericisinin yerini, f_count elemanı nesne sayacını belirtmektedir. Bu yapının ayrıntılı açıklaması için başka kaynaklara başvurabilirsiniz.
Kaynaklar
1. Aslan, K. (2002). UNIX/Linux Sistem Programlama Kurs Notları. Istanbul: C ve Sistem Programcıları Derneği.
2. Bovet, D. and Cesati, M. (2005). Understanding the Linux Kernel. Oreilly & Associates Inc.
3. Mauerer W. (2008). Professional Linux Kernel Architecture. Wiley Publishing, Inc: Indianapolis
4. Maurice, B. (1986). The Design Of The UNIX Operating System. Prentice Hall: New Jersey.
5. Rodriguez C., Fisher G., Smolski S. (2006). The Linux Kernel Primer. Prentice Hall.
6. Stevens, R., Rago, S. A. (2005). Advanced Programming in the UNIX(R) Environment (2nd Edition). Addison-Wesley Professional.