Hata Durumları, errno
Çağırdığımız sistem ya da POSIX fonksiyonları çeşitli durumlarda bize hata
dönebilirler, isteklerimizi yerine getiremeyebilirler. Birçok sistem fonksiyonun
dokümanında hata durumları için bir kısım bulunmaktadır. write() fonksiyonundan
devam edelim: [1]
ERRORS
EAGAIN The file descriptor fd refers to a file other than a
socket and has been marked nonblocking (O_NONBLOCK), and
the write would block. See open(2) for further details on
the O_NONBLOCK flag.
EAGAIN or EWOULDBLOCK
The file descriptor fd refers to a socket and has been
marked nonblocking (O_NONBLOCK), and the write would
block. POSIX.1-2001 allows either error to be returned
for this case, and does not require these constants to
have the same value, so a portable application should
check for both possibilities.
EBADF fd is not a valid file descriptor or is not open for
writing.
...
write() fonksiyonu birçok hata oluşturabiliyor, ben bir kısmını aldım.
Bazı fonksiyonlar ise çok az sayıda hata koşulu var iken bazı fonksiyonlar için
bu sayı fazla oluyor fakat hepsi için hataları yakalama yöntemi aynı: errno
errno
errno, C dili ile uğraştıysanız duymuş olabileceğiniz bir kelime. errno, C
standartlarında bulunuyor. Yani ister POSIX uyumlu bir yerde çalışın, ister uyumsuz
olsun, ister Windows üzerinde olsun errno C dilinde olan bir kavram, POSIX veya
Linux ile sınırlı değil. POSIX standartları da C dilinde var olan errno hata
bildirim yöntemini tercih etmiştir. [2] O zaman önce C dilindeki errno
kullanımına bakalım.
C89’dan yani ilk C standartından itibaren errno ve errno.h standartlar içerisinde
yer alıyor. Standart C kütüphane fonksiyonları, hata bildirimleri için bu yöntemi
kullanabiliyorlar. [3]
errno yu, int türden bir değişken gibi düşünebiliriz. Fakat aslında teknik
olarak bu tam doğru değil. C11 standartlarına kadar errno hakkında şöyle bir
ifade bulunuyordu:
It is unspecified whether errno is a macro or an identifier declared with external linkage.
C11 ile burada bir değişiklik yapıldı ve bu ifade kaldırıldı, errno nun
aslında bir önişlemci macro’su olması sağlandı. [4] Ama biz kullanıcılar
açısından bu fark çok önemli değil, sonuçta derleyici yazmıyoruz. errno.h
içerisinde extern int errno; şeklinde bildirimi yapılmış global bir değişken
gibi düşünebiliriz.
errno ya erişmek için kodumuza errno.h ı #include etmemiz gerekiyor. Bu
noktadan sonra errno yu rezerve edilmiş gibi global değişken gibi düşünebiliriz,
ama arka planda macro olabilir. Problem yaşamamız için errno yu #undef etmeye
çalışmamalıyız. Ayrıca POSIX standartları errno isminde bir şey tanımlamamamız
gerektiğini belirtiyor. [5]
It is unspecified whether errno is a macro or an identifier declared with external linkage. If a macro definition is suppressed in order to access an actual object, or a program defines an identifier with the name errno, the behavior is undefined.
Benzer ifade C standartlarında da var:
If a macro definition is suppressed in order to access an actual object, or a program defines an identifier with the name errno, the behavior is undefined.
Yani özetle errno.h ı include edin ya da etmeyin errno isminde kendiniz
bir şeyler tanımlamayın, başka bir isim bulun. (Yani aslında errno.h ı
kullanmıyorsanız errno tanımlamak problem de olmayabiliyor bazı durumlarda ama
başka isim mi bulamadınız kardeşim, bela aramayın!)
C’de bir kütüphane fonksiyonu başarısız olduğu zaman, 0 olmayan bir tam sayı ya
da NULL pointer dönebiliyor. İşte bu durumda bu fonksiyon tarafından errno,
daha detaylı bir hata bilgisi vermek için set edilebiliyor. Biz de bu değere
bakarak hata hakkında detaylı bilgi edinebiliyoruz. Aşağıdaki koda bakalım:
#include <stdio.h>
#include <errno.h>
int main(void)
{
printf("%d\n", errno); //0
FILE* f = fopen("olmayan-dosya.txt","r");
if (!f)
printf("%d\n", errno);
errno = 0; //bizim sorumluluğumuzda
}
Bu kodun çıktısı:
0
2
olmaktadır.
Program başlangıcındaki thread’te errno nun değerinin 0 olacağı C
standartları tarafından garanti edilmiştir. Çok thread’li programlarda her
thread için ayrı bir errno vardır. Standartlar, başlangıç thread’i hariç diğer
thread’lerdeki errno değerinin belirsiz olacağını belirtmektedir. Standart C
fonksiyonları, errno yu hata olsun olmasın bir pozitif sayıya set edebilirler.
Fakat C kütüphanesi fonksiyonları errno yu 0’a set etmezler. Dilersek biz elle
0 yazabiliriz.
Yukarıdaki kodda var olmayan bir dosya fopen() standart C fonksiyonu ile
açılmaya çalışılmış. Dosya olmadığı için fopen() bize NULL pointer dönmüş ve
errno yu set etmiştir.
Aslında bu örnek iyi bir örnek değildir. Çünkü C standartları gereğince
fopen() fonksiyonunun hata durumunda errno yu set etme gibi bir zorunluluğu
yoktur. Hata durumunda errno nun set edilmesi POSIX standartlarında zorunlu
kılınmıştır. [6] Fakat Microsoft C derleyicisi de uyumluluk adına errno yu
bu durumda set etmektedir. C standartlarında fgetpos() gibi fonksiyonların
errno yu set etmesi zorunlu kılınmıştır. Yine de basitlik açısından yukarıdaki
örneğin vermeyi tercih ettim. Ama yine de bu şekilde kullanım C standartları
açısından doğru değildir çünkü errno, fopen() tarafından set edilmeyebilir.
Peki 2 nolu hata ne demektir? Bunu anlamak için errno.h içerisindeki
sembolik sabitlere bakmamız gerekir. errno yu anlamlandırmak için standart C’de
bulunan bazı yardımcı fonksiyonları kullanabiliriz.
#include <stdio.h>
void perror(const char *s);
stdio.h içerisinde prototipi bildirilen perror() fonksiyonu standart hata
akımına, stderr, istediğimiz bir mesaj ile beraber hata bilgisi yazdırmaktadır.
[7] Kodumuzu değiştirelim:
#include <stdio.h>
#include <errno.h>
int main(void)
{
printf("%d\n", errno); //0
FILE* f = fopen("olmayan-dosya.txt","r");
if (!f){
printf("%d\n", errno);
perror("Patladık!");
}
errno = 0; //bizim sorumluluğumuzda
}
Çıktı:
0
2
Patladık!: No such file or directory
Gördüğünüz üzere bizim mesajımız ile beraber hatayı yazdırdı. Demek ki benim
sistemimde 2, bu demekmiş. Dikkat ederseniz perror() a ayrıca errno geçmiyoruz
çünkü errno adeta bir global değişken olduğu için perror() tarafından
okunabiliyor.
Bir diğer fonksiyon da string.h içerisindeki strerror() fonksiyonudur. Bu
fonksiyon da ilgili hata kodunu, string bir mesaja çevirmeye yarar.
for (int i = 0; i < 30; ++i)
printf("error code %2d: %s\n", i, strerror(i));
Dilersek bu şekilde hata kodlarının anlamlarını öğrenebiliriz. Dikkat ederseniz
bu fonksiyon global değişken gibi düşündüğümüz errno yu okumuyor, argüman olarak
bir hata kodu alıyor. Elbette bunu çağırırken strerror(errno) dersek, perror()
benzeri bir etki alırız. strerror(), standart bir C fonksiyonudur.
POSIX ve Sistem Fonksiyonları
Gelelim POSIX ve sistem fonksiyonları ile errno nun kullanımına. POSIX, kendi
fonksiyonları için sembolik sabit şeklinde hatalar tanımlamıştır. Formatları,
EXXX şeklindedir. EACCES, EAGAIN, EBADF gibi. Bu sembolik sabitlerin
değerleri ise POSIX standartlarında uspecified olarak bırakılmıştır. [8]
Standart C fonksiyonları gibi POSIX fonksiyonları da errno yu 0 değerine set
etmezler:
No function in this volume of POSIX.1-2017 shall set errno to zero.
errno ile ilgili yapılabilecek hatalardan biri de bir fonksiyon hata dönmemiş
olsa bile errno yu anlamlandırmaya çalışmaktır. errno, fonksiyon hata
döndüyse anlamlı olacaktır. Elbette her fonksiyonun dokümanına bakmalıyız.
errno Değerleri
Ubuntu üzerinde sudo apt install moreutils ile moreutils paketini yükleyerek
komut satırı üzerinden errno programını çalıştırıp hata kodlarını
öğrenebiliriz. errno -l diyerek tüm kodları listeleyebilir, errno 2 diyerek
ise 2 nolu hata hakkında bilgi alabiliriz.
Örnek olarak open() POSIX fonksiyonu ile aşağıdaki gibi bir kullanım düşünebiliriz:
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
int main(void)
{
int fd;
if ((fd = open("olmayandosya", O_RDONLY)) == -1) {
fprintf(stderr, "open failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("success\n");
return 0;
}
Çıktı:
open failed: No such file or directory
Ya da istersek kendimize boilerplate bir kod hazırlayabiliriz:
void exit_sys(const char* msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
//...
exit_sys("Elveda zalim dünya");