open()
ve close()
Bir önceki bölümlerde kendi user space programlarımızı nasıl ekleyeceğimizi ve
write()
, read()
fonksiyonlarını görmüştük. Aldığını geri basan yani loopback
yapan (ya da echo) bir program yazmıştık. Bu bölümde open()
fonksiyonu ile
tanışalım.
Artık Makefile
a ekleme yapma konusundan bahsetmeyeceğim fakat yazdığımız
programların önceki loop.c
gibi eklenmesi gerektiğini unutmayın.
I/O fonksiyonlarının en önemlilerinden biri de open()
ve kardeşi close()
fonksiyonlarıdır. open()
, write()
, read()
, close()
adeta
Voltran’ı oluşturur.
Not
Tamam, Voltran için 5 bileşen lazım. lseek()
olsaydı bence bu da 5. olurdu. Fakat xv6’da, lseek()
bulunmuyor. 🤖
Önceden de bahsettiğim gibi read()
ve write()
fonksiyonları birer
file descriptor, fd üzerinden çalışırlar. Bu fonksiyonlar dosya ismi bilmezler.
Önceki loop.c
örneğinde process başladığı zaman shell tarafından açılmış
stdout, 1
ve stdin, 0
fd’lerini kullanmıştık. Peki kendimiz bir dosya yazmak
istersek, örneğin not.txt
, bunu nasıl yapacağız?
open()
İşte burada open()
fonksiyonu devreye giriyor. open()
fonksiyonu diskte var
olan bir dosyanın açılmasını ya da dosya yoksa istenirse önce dosyanın yaratılmasını
sağlıyor.
Not
İlerleyen kısımlarda f5b93ef nolu commit’i referans alacağım.
Fonksiyon prototipine bir bakalım.
11//...
12int exec(const char*, char**);
13int open(const char*, int);
14int mknod(const char*, short, short);
15//...
open
bizden iki parametre istiyor. İlki bir char pointer, buraya açmak
istediğimiz dosyanın adını vereceğiz, "not.txt"
gibi. İkinci parametre ise
dosyanın açış modlarını belirliyor.
1#define O_RDONLY 0x000
2#define O_WRONLY 0x001
3#define O_RDWR 0x002
4#define O_CREATE 0x200
5#define O_TRUNC 0x400
Dikkat
Linux sistem programlama ile ilgileniyorsanız oradaki sembolik sabitin adı O_CREAT
olmaktadır, buradaki O_CREATE
.
Bu sembolik sabitleri OR’layarak open()
fonksiyonunun dosyayı nasıl açacağını
belirleyebiliyoruz. Sırası ile:
read only
write only
read ve write
eğer yoksa yarat
dosyanın içeriğini sil
gibi seçenekler verebiliyoruz. Elbette bunların tüm kombinasyonları anlamlı değil.
O_RDONLY | O_RDWR
mantıksız bir seçenek mesela, hem read only hem de okuma
yazma istiyoruz.
Bu fonksiyon günün sonunda kernel içerisinde bulunan sys_open
fonksiyonunu
çağrıyor. Bunun mekanizmasına sonraki bölümlerde bakarız, şu an amacımı kernel’i
çok fazla kurcalamadan kullanıcı olarak nasıl kod yazabiliriz ona bakmak.
open()
eğer bir hata ile karşılaşırsa -1
, karşılaşmasa da o dosyaya karşılık
gelen file descriptor, fd, sayısını dönüyor. Biz de bu sayıyı daha sonra
write()
ve read()
fonksiyonları ile işlem yapmak için kullanabiliyoruz.
close()
close()
ise açılmış bir dosyanın o process için kapatılmasını sağlıyor.
Bir adet parametre alıyor, kapatmak istediğimiz file descriptor ve eğer başarılı
olursa 0, eğer başarısız olursa (örneğin parametre hatası, geçersiz fd gibi) -1
dönüyor.
İşletim sistemi çekirdeği, hangi dosyaların kaç process tarafından açıldığını
takip ediyor. close()
işlemi aslında açık olan bir dosyayı o process için
kapatıyor ve kernel o dosyayı açmış olan process sayısını 1 eksiltiyor. Eğer o
dosyayı açmış olan başka process kalmadıysa yani sayı 0 olduysa dosya gerçekten
kernel tarafından kapatılıyor. Kapatma işleminin detaylarını ilerleyen
kısımlarda muhtemelen konuşuruz.
close()
işlemi sonunda kapatılan dosyanın file descriptor değeri boşa çıkıyor
ve bu noktadan sonra gelecek başka open()
işlemleri ile aynı descriptor değeri
başka dosya için kullanılabilir. Yani bir process’in ömrü boyunca bir file
descriptor değer, örneğin 3 diyelim, yapılan open()
ve close()
sayısına göre
farklı dosyaları gösteriyor olabilir.
Linux gibi sistemlerde bir process sonlandığı zaman açık olan tüm dosyalar
otomatik close()
ediliyor, xv6’da böyle mi bilmiyorum ama muhtemelen öyeldir.
İlerleyen zamanlarda umarım öğreniriz.
Örnekler
Şimdi biraz örnek yapalım.
1#include "kernel/types.h"
2#include "user/user.h"
3#include "kernel/fcntl.h" //O_WRONLY vs
4
5static const char not[] = "Merhaba Dunya!\nBen bir notum.\n";
6
7int main() {
8 int fd, result;
9
10 fd = open("not.txt", O_RDWR);
11
12 if (fd < 0){
13 fprintf(2, "fd = %d not.txt acilamadi!\n", fd);
14 exit(1);
15 }
16
17 printf("fd = %d\n", fd);
18
19 result = write(fd, not, sizeof(not) - 1);
20
21 if (result != sizeof(not) - 1){
22 fprintf(2, "result = %d yazma basarisiz\n", result);
23 exit(2);
24 }
25
26 result = close(fd);
27
28 if (result != 0) {
29 fprintf(2, "result =%d kapama basarisiz\n", result);
30 exit(3);
31 }
32
33 exit(0);
34}
Yukarıdaki programda önceden kullanmadığımız iki adet fonksiyon var: printf()
ve fprintf()
. Bunlar standart C fonksiyonları.
Fakat bunlar xv6 ile geliyorlar. Yani her ne kadar standart C fonksiyonları
olsalar da xv6 standart C fonksiyonları ile derlenmiyor.
61CFLAGS += -mcmodel=medany
62CFLAGS += -ffreestanding -fno-common -nostdlib -mno-relax
63CFLAGS += -I.
Burada -nostdlib
flag’ine dikkat.
İpucu
Standart C kütüphanesi olmadan Linux üzerinde program derlemeyle ilgili yazdığım bir yazı: Merhaba Dünya!
fprintf()
ve printf()
fonksiyonları user/printf.c
içerisinde implement
edilmiş durumda. Bu ikisi neredeyse aynı, fprintf(fd, printf kısmı)
ya da
printf(...) ≡ fprintf(1, ...)
gibi düşünülebilir yani fazladan bir file
descriptor alıyor. Her ikisi de ilgili dosya içerisindeki vprintf()
fonksiyonunu çağırıyor. printf()
in farkı fd 1’e yani stdout’a basması.
Ben burada hata durumlarını fd 2’ye yani stderr
ye bastırdım. Bunun çeşitli
avantajları var, ilerleyen zamanlarda görürüz. Yine de hem stdout
hem stderr
default olarak shell ekranına basmaktadır.
xv6’da kısıtlı sayıda standart C fonksiyonları implement edilmiş durumda. İlgili fonksiyonlar:
26// ulib.c
27int stat(const char*, struct stat*);
28char* strcpy(char*, const char*);
29void *memmove(void*, const void*, int);
30char* strchr(const char*, char c);
31int strcmp(const char*, const char*);
32void fprintf(int, const char*, ...);
33void printf(const char*, ...);
34char* gets(char*, int max);
35uint strlen(const char*);
36void* memset(void*, int, uint);
37void* malloc(uint);
38void free(void*);
39int atoi(const char*);
40int memcmp(const void *, const void *, uint);
41void *memcpy(void *, const void *, uint);
Bu programı make qemu
ile derleyip QEMU’da çalıştırdığımızda bize bir hata
veriyor ne yazık ki:
$ not
fd = -1 not.txt acilamadi!
Peki neden? Çünkü not.txt
dosyası yok ve open()
bunu açamadı.
İki seçeneğimiz var.
Seçeneklerden biri dosyayı önden oluşturmak diğer ise open()
a O_CREATE
flagini vermek.
//...
fd = open("not.txt", O_RDWR | O_CREATE);
//...
Bunu deyip derlersek işlem başarılı oluyor.
$ not
fd = 3
Gördüğünüz üzere open()
3 nolu file descriptor ile döndü. Neden 3? Çünkü
bir process hayatına başladığı zaman tipik olarak 0, stdin
, 1, stdout
ve
2, stderr
file descriptor’ları açık oluyor. open()
kullanılmayan en düşük
numaralı fd’yi verdiği için 3 numarasını aldık.
$ ls not.txt
not.txt 2 24 30
$ cat not.txt
Merhaba Dunya!
Ben bir notum.
Gördüğünüz üzere dosya sistemimizde not.txt
oluştu ve içinde yazı var.
Şimdi tekrar not
programını çalıştırıp not.txt
nin içeriğine bakalım.
$ not
fd = 3
$ cat not.txt
Merhaba Dunya!
Ben bir notum.
$ not
fd = 3
$ cat not.txt
Merhaba Dunya!
Ben bir notum.
Neden hep aynı içerik oluyor? Çünkü her seferinde dosyayı programımız açıyor ve
en başından itibaren aynı yazıyı yazdığı için dosya içeriği hep aynı oluyor.
xv6’de open()
fonksiyonunda Linux’ta olduğu gibi O_APPEND
gibi bir mod,
lseek()
biri bir sistem fonksiyonu da ya da implement edilmiş fseek()
fonksiyonu yok. Hal böyle olunca kolay bir şekilde append işlemi yapamıyoruz,
yani var olan içeriği koruyup write()
işlemini dosyanın sonuna yapamıyoruz.
Bunu yapabilmemiz için görünen tek yol önce read()
ile dosya içeriğinin bir
char
diziye okunması, bu dizinin sonuna eklemek istediğimiz yazının eklenmesi,
mesela strcat()
ile, daha sonra hepsinin write()
ile yazılması. Bunun da
kendine has zorlukları var, programımız içerisinde ayıracağımız dizinin yeteri
kadar büyük olması gerekiyor ama dosya boyutunu bilmiyorsak nasıl yapabiliriz?
Normalde bunu dinamik bellek yönetimi ile çözebiliriz, yani malloc()
kullanarak, xv6’da malloc()
mevcut. Fakat bunu daha sonraya bırakalım bence
Peki önceden dosyaya bir şeyler yazsak ne olacak?
$ rm not.txt
$ echo 0123456789012345678901234567890123456789 > not.txt
$ cat not.txt
0123456789012345678901234567890123456789
$ not
fd = 3
$ cat not.txt
Merhaba Dunya!
Ben bir notum.
0123456789
İlk olarak xv6’da bulunan echo
komutu ile not.txt
ye bir şeyler yazdık. Daha
sonra bizim programımızı çalıştırınca başını doldurdu devamı kalan içeriklerden
oldu.
Fakat O_TRUNC
ile açsaydık, bu sefer dosyanın içeriği open()
fonksiyonu
tarafından silinecekti, adeta yeni yaratılmış gibi olacaktı.
fd = open("not.txt", O_RDWR|O_CREATE|O_TRUNC);
ve sonuç:
$ echo 0123456789012345678901234567890123456789 > not.txt
$ cat not.txt
0123456789012345678901234567890123456789
$ not
fd = 3
$ cat not.txt
Merhaba Dunya!
Ben bir notum.
Gördüğünüz üzere dosyamız truncate oldu yani içeriği silindi.
Elbette, read only modda açarsak, write()
sırasında hata alırız.
Örneğin:
fd = open("not.txt", O_RDONLY|O_CREATE|O_TRUNC);
Tabii read only bir dosyaya O_CREATE
ve özellikle O_TRUNC
vermek biraz garip
ama göstermek istediğim şey, bir dosya read only açılırsa write()
yapılamaz.
Aşağıda göreceğiniz gibi open()
bize geçerli bir fd dönüyor fakat write()
bize hata dönüyor, çünkü dosyayı read only açtık.
$ not
fd = 3
result = -1 yazma basarisiz
Kaynaklar
Ayrıca bknz: genel kaynaklar