Dosya Yaratma ve Açma: open()
Genel kavramları gördük, biraz ısındık diye düşünüyorum. Şimdi temel I/O fonksiyonlarından devam edelim.
Bir programın yaptığı temel işlemlerden biri I/O işlemleridir, yani giriş ve çıkış işlemleri. E bu da beklenen bir şey değil mi? Bilgisayar programları günün sonunda veri alıp, ki bu veri kullanıcı girdisi olabilir, bir dosyadaki kayıtlı veriyi okumak olabilir, bu veriyi işleyip dışarıya aktaran akışlardır. Durum böyle olunca I/O işlemleri programlamanın önemli bir kısmını oluşturmaktadır.
Linux gibi Unix türevi sistemlerde ise dosyalar daha da anlamlıdır. Çünkü bu
sistemlerde “(Neredeyse) Her şey dosyadır.” şeklinde bir tabir vardır,
duymuş olabilirsiniz. Bu betimlemeyi farklı perspektiflerden
değerlendirebiliriz. Linux sistemlerde kernel’e ait çeşitli parametreler bile
dosya sistemi üzerinde adeta diskte var olan dosyalardan bir değer okunuyormuş
gibi okunabilir ya da yazılıyormuş gibi değiştirilebilir, /sys
ve /proc
gibi. Linux sistemlerde diskte var olmayan ancak çeşitli işlemleri yapmak için
kullanılan sanal dosyalar vardır. Kullanıcı açısından bakıldığında böyledir.
Yazılımcı açısından bakıldığında ise Linux sistemlerdeki birçok bileşen bir
dosyaymış gibi gözükmektedir. Aslında bunun örneklerini önceki yazılarda biraz
gördük. Dosyalara yazmak için bulunan write()
fonksiyonu ile 1
nolu dosyaya,
file descriptor, bir veri yazdığımız zaman bu veriler terminal ekranımıza düştü.
İlerleyen kısımlarda göreceğiz ki başka bir numaraya yazdığımızda bu yazdıklarımız
gerçek bir dosyaya gidecek. Yani write()
fonksiyonunu bir dosya API’si olarak
düşünürsek aynı API fonksiyonu ile farklı işlemler yapabildik. Linux üzerinde
temel dosya işlemleri için open()
, close()
, read()
, write()
fonksiyonları
vardır. İşte biz bu fonksiyonları kullanarak gerçek dosyalara erişebilir, aygıt
sürücüler üzerinden donanıma da erişebiliriz. Programcı açısından Linux üzerinde
birçok bileşen temel dosya fonksiyonları ile kullanılabilir. Bu yüzden Linux
üzerinde I/O fonksiyonları ekstra kıymetlidir.
Sistem fonksiyonlarını düşündüğümüzde bildiğimiz türden (regular) dosyaları
yaratıp açmak için kullanabileceğimiz tek bir fonksiyon vardır, o da
open()
fonksiyonudur. Bunun dışında openat()
ve creat()
fonksiyonları olsa
da bunlar aslında open()
ile benzerdir, open()
çağırılıyor gibi
düşünebiliriz.
fopen()
gibi fonksiyonlar sistem fonksiyonu değildir. Bunlar, standart C
kütüphanesinde bulunan kütüphane fonksiyonlarıdır. Linux üzerinde fopen()
çağrısı yapan bir program C kütüphanesi aracılığı ile yine open()
sistem
fonksiyonunu çağıracaktır.
open()
sistem fonksiyonu da çoğu zaman doğrudan sys_open
isimli sistem
çağrısını, syscall, yapmaktadır. Günün sonunda bir dosyanın yaratılması veya
var olan bir dosyanın açılması kernel tarafından yapılır.
#include <fcnlt.h>
int open(const char* path, int flags, ...);
Prototip’i fcnlt.h
içerisinde olan open()
fonksiyonu aslında variadic bir
fonksiyondur, printf()
gibi. Fakat aslında iki kullanımı vardır: 2 argümanlı
veya 3 argümanlı. 3 argümanlı kullanılacaksa bu 3. argüman mode_t
türünden
olmalıdır. Yani
#include <fcnlt.h>
int open(const char* path, int flags);
//VEYA
int open(const char* path, int flags, mode_t mode);
olmalıdır. Diğer kullanımlar tanımsız davranış, UB, sebebidir.
İlk parametre açılacak dosya adıdır.
İkinci parametre ise dosyanın açış modunu ve çeşitli ayarları iletmek için
kullanılır. Bu parametre için tanımlanmış birçok sembolik sabit vardır. O_xxx
formatında olan bu sembolik sabitler de fcntl.h
başlık dosyası içerisindedir.
İkinci flags
isimli parametre bu sembolik sabitlerin OR’lanması, |
ile
oluşturulur. Burada uyulması gerekilen çeşitli kurallar vardır.
flags
parametresi, O_RDONLY
, O_WRONLY
veya O_RDWR
sembolik sabitlerinden
sadece bir tanesini içermek zorundadır. 0 adet veya 2, 3 adet içeremez, yalnızca
1 tanesi olmalıdır.
O_RDONLY
: Read-only açışO_WRONLY
: Write-only açışO_RDWR
: Read ve Write açış
Bunun dışında file creation flags ve file status flags sembolik sabitleri
vardır. Bunlar |
operatörü ile OR’lanabilir. Burada bulunan bazı sembolik
sabitler Linux’a özgüdür, POSIX standartında yoktur, O_DIRECT
gibi. Yani Linux,
POSIX standartını bazı noktalarda genişletmektedir.
open()
fonksiyonun ismi var olan dosyaların açılmasını çağrıştırsa da dosya
yaratmak için de bu fonksiyon kullanılmaktadır. İşte mode_t
türündeki 3 nolu
parametre bu noktada işe yaramaktadır. Eğer flags
ile O_CREAT
veya
O_TMPFILE
geçilirse bu durumda 3 nolu parametre olan mode
parametresinin
sağlanması zorunludur. Aksi taktirde open()
fonksiyonu stack’ten rastgele
değerleri çekip, mode
yerine bunları kullanacaktır. Bu da tanımsız
davranışlara yol açabilir. Eğer bu iki flag ten herhangi biri yoksa, mode
kullanılmaz ve 0
geçilebilir ya da open()
fonksiyonu 2 argümanla
çağrılabilir.
Burada bir parantez açmak istiyorum, C ve variadic fonksiyonlar ile ilgili.
Gördüğünüz ve belirttiğim üzere open()
variadic bir fonksiyon çünkü bazen
2 bazen ise 3 argüman alıyor. Burada bana ilk başta garip gelen şey, dokümanda
geçen bir ifade oldu. Yukarıda da belirttiğim gibi O_CREAT
ve O_TMPFILE
geçmesek bile 3 nolu argümanı dilersek geçebiliriz. Yani bu durumda open()
fonksiyonu 2 argüman okusa bile 3 argümanın geçilmesi problem yaratmıyor.
Peki C ve özellikle stack organizasyonu açısıdan bu nasıl olabilir?
İlk olarak gelin, musl kütüphanesindeki implementasyona bakalım. [1]
int open(const char *filename, int flags, ...)
{
mode_t mode = 0;
if ((flags & O_CREAT) || (flags & O_TMPFILE) == O_TMPFILE) {
va_list ap;
va_start(ap, flags);
mode = va_arg(ap, mode_t);
va_end(ap);
}
int fd = __sys_open_cp(filename, flags, mode);
if (fd>=0 && (flags & O_CLOEXEC))
__syscall(SYS_fcntl, fd, F_SETFD, FD_CLOEXEC);
return __syscall_ret(fd);
}
Gördüğünüz üzere O_CREAT
ve O_TMPFILE
flag’larinin en az birinin olduğu
durumda va_
macroları ile 3 nolu argümanın stack’ten alınması işlemi
yapılıyor. Diyelim ki bu flagleri geçmedik ama 3 adet argüman geçtik. Bu problem
değil mi? Aslında değil (en azından anladığım kadarıyla). Çünkü bir open()
ı
çağırırken 3 adet argümanı stack’e soktuk, open()
geldi 2 adedini çıkardı 1
adedi stack’te kaldı. Burada fonksiyon argümanlarının stack’e ters sırada
sokulduğunu varsayıyoruz. Yani stack’e ilk 3 nolu argüman, sonra 2 sonra 1
giriyor. Bu böyle olmak zorunda çünkü printf()
stack’ten kaç argüman çekmesi
gerektiğini ilk argümanı çekerek anlayabiliyor. Yani variadic fonksiyonların ilk
argümanı çağrılan fonksiyonda stack’ten çekilecek ilk argüman olmalı yani
stack’e en son push edilen argüman olmalı. Kalan 1 argümanın üzerinin otomatik
ömürlü yerel değişkenlerle vs yazılması veya kullanılmaması problem değil. Esas
problem, stack’ten fazla sayıda argüman çekmek. Şu SO
cevabı konuyu daha güzel
açıklıyor.
open()
fonksiyonu bize bir int
değer döndürmektedir. Eğer her şey yolunda
giderse bize negatif olmayan bir tam sayı dönecektir. İşte bu döndürdüğü şey
file descriptor, fd olarak adlandırılmaktadır. Fonksiyonun döndürdüğü fd
değeri 0 olabilir, detaylara geleceğiz. Eğer open()
başarısız olursa bize -1
döner ve errno
set olur. errno
ya bakarak hatayı anlayabiliriz.
Bknz: Hata Durumları, errno
open()
a ikinci argüman olarak geçilecek flags
kısmında birçok seçenek
vardır. Hepsini şimdi görmek çok anlamlı olmayacak fakat en sık kullanılanlardan
başlayalım. Bir kısmı Advanced I/O konularına gireceği için ilerleyen
kısımlarda değiniriz.
O_CREAT
seçeneği eğer dosya yoksa open()
fonksiyonun önce bu dosyayı
yaratıp, açmasını sağlar. CREAT
, CREATE
demektir (evet 1 harfin hesabını
yapmışlar). O_CREAT
verdiğimiz zaman, üçüncü argüman olan mode
argümanını
vermemiz gerekir. mode
argümanları S_xxx
formatındadır: S_IRWXU
, S_IROTH
gibi. Bu mode
değerleri dokümante edilmiştir. [2] Bunlar dosya izinleri ile
ilgilidir, sonra daha detaylı göreceğiz.
Hemen bir örnek yapalım.
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int fd = open("olmayandosya.txt", O_RDWR | O_CREAT, S_IRWXU | S_IRGRP | S_IROTH );
if (-1 == fd) {
perror("dosya acilamadi");
return 1;
}
printf("Dosya Acildi, fd = %d\n", fd);
}
Yukarıdaki kodu derleyip çalıştırdığımız zaman aşağıdaki çıktıyı alıyoruz:
Dosya Acildi, fd = 3
Programımızı çalıştırdığımız dizinde de olmayandosya.txt
oluşuyor:
-rwxr--r-- 1 alper alper 0 Jun 16 12:57 olmayandosya.txt*
Normalde bu dosya yoktu fakat O_CREAT
bayrağını verdiğimiz için open()
fonksiyonu bu dosyayı oluşturdu. Şimdi dosyamızı silelim ve O_CREAT
bayrağını
da kaldıralım, yani:
int fd = open("olmayandosya.txt", O_RDWR , S_IRWXU | S_IRGRP | S_IROTH );
şekline getirelim. Konuştuğumuz gibi bu durumda üçüncü argümanların bir önemi yok, bırakabiliriz fakat kafa karışıklığı yaratmamak adına silmek daha doğru olacaktır.
rm olmayandosya.txt
diyerek oluşmuş dosyayı silelim ve yeni programımızı
çalıştıralım.
dosya acilamadi: No such file or directory
Bu sefer open()
bize -1
yani hata döndü, biz de perror
ile hata mesajını
gördük. O_CREAT
bayrağı olmadığı için dosya yoksa dosya yaratılmıyor ve
olmayan dosya açılamadığı için hata alıyoruz.
Son olarak programımızı tekrar ilk haline getirelim, yani O_CREAT
bayrağı
olsun ve çalıştıralım. Sonra dosyamızı elle modifiye edelim, tekrar çalıştıralım
ve dosyanın bozulmadığını görelim:
alper@brs23-2204:~/temp$ ll olmayandosya.txt
ls: cannot access 'olmayandosya.txt': No such file or directory
alper@brs23-2204:~/temp$ ./a.out
Dosya Acildi, fd = 3
alper@brs23-2204:~/temp$ ll olmayandosya.txt
-rwxr--r-- 1 alper alper 0 Jun 16 13:15 olmayandosya.txt*
alper@brs23-2204:~/temp$ cat olmayandosya.txt
alper@brs23-2204:~/temp$ echo "deneme" > olmayandosya.txt
alper@brs23-2204:~/temp$ cat olmayandosya.txt
deneme
alper@brs23-2204:~/temp$ ./a.out
Dosya Acildi, fd = 3
alper@brs23-2204:~/temp$ cat olmayandosya.txt
deneme
Gördüğünüz üzere programımız çalıştığı zaman eğer o dosya varsa o dosyayı silmiyor,
var olanı açıyor. Yani O_CREAT
bayrağı dosya varsa etkili olmuyor.
Şimdi gelin O_EXCL
yani exclusive mode flag’ine bakalım. Bu flag O_CREAT
ile beraber kullanılmaktadır. Burada open()
fonksiyonu eğer dosyayı sıfırdan
yaratmıyorsa ve var olan dosyayı açıyorsa başarısızlıkla dönecektir. Yani
O_EXCL
ile open()
ın var olan dosyayı açmadığından, dosyayı yeni yarattığından
emin olabiliyoruz.
int fd = open("olmayandosya.txt", O_RDWR | O_CREAT | O_EXCL, S_IRWXU | S_IRGRP | S_IROTH );
şeklinde kodu değiştirip denemeler yapalım.
alper@brs23-2204:~/temp$ rm olmayandosya.txt
alper@brs23-2204:~/temp$ ./a.out
Dosya Acildi, fd = 3
alper@brs23-2204:~/temp$ ./a.out
dosya acilamadi: File exists
İlk olarak dosyamızı sildik, sonra programımızı çalıştırdık. Dosya open()
tarafından yaratıldı ve program sonlandı. Fakat daha sonra programı tekrar
çalıştırdığımızda File exists hatası aldık. Çünkü dosya zaten vardı ve
O_CREAT
ile dosya oluşturmadığımız için O_EXCL
bayrağından dolayı bu
open()
bize bu sefer hata döndü.
Şimdilik open()
fonksiyonuna ara verelim. Çünkü open()
ile ilgili bazı
kavramları anlamak için dosya izinleri, user, group id, process gibi kavramları
anlamamız gerekiyor. Biraz onlara bakarız. Ama genel olarak fonksiyonun yapısını
görmüş olduk. Detaylı kullanım bilgileri, kullanabileceğimiz mod ve bayraklar
her zaman olduğu gibi dokümantasyonda yer alıyor. [2] Konularda ilerledikçe
open()
a tekrar tekrar dönüp, diğer kullanım senaryolarına bakacağız.