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.

user/user.h
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.

kernel/fcntl.h
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.

user/not.c
 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.

Makefile
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:

user/user.h
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