size_t
ve ssize_t
Sorunsalı
Bir önceki yazıda, write()
fonksiyonunu görmüş ve bununla
beraber iki adet tür eş ismi yani typedef ile tanışmıştık:
size_t
ve ssize_t
. Tekrar hatırlayalım:
size_t
, C standartlarında olan bir tür eş ismidir. Implementation defined bir tür olup,sizeof
operatörünün dönüş değeri bu türden olmaktadır. Tipik olarak array’deki eleman sayısı gibi nesne sayılarını göstermek için kullanılan bir türdür.size_t
türü, array’ler dahil olmak üzere var olabilecek en büyük nesnenin boyutunu tutabilmelidir. İşaretsiz bir tam sayı yani unsigned integer olacağı standartlarda garanti edilmiştir.unsigned int
,unsigned long
veunsigned long long
gibi bir tür olabilir. [1]ssize_t
ise C standartlarında olmayan fakat POSIX standartlarında olan bir tür eş ismidir. Byte sayısı saymak ya da hata bildirmek için kullanılır. [2] Özündesize_t
türünün işaretli versiyonu olarak düşünülmüştür. [3] İlgili sistemdesize_t
hangi türden ise, örneğinunsigned int
,ssize_t
onun işaretli versiyonu olabilir, örneğinsigned int
, ama böyle olmak zorunda değildir.size_t
türü[0, SIZE_MAX]
arasındaki değerleri tutabilmelidir.SIZE_MAX
,limits.h
içerisinde tanımlanmaktadır. [4] C99’dan itibarensize_t
türünün en az 16 bit genişliğinde olacağı yani 65535’e kadar olan tam sayı değerlerini tutabileceği garanti edilmiştir. [1]ssize_t
türü[-1, SSIZE_MAX]
arasındaki değerleri tutabilmelidir. [5] POSIX standartlarına göreSSIZE_MAX
değeri en az_POSIX_SSIZE_MAX
olmalıdır._POSIX_SSIZE_MAX
ise 32767 olarak tanımlanmıştır. [6] Yanissize_t
türünde tanımlanmış bir değişken, en kötü ihtimalle[-1, 32767]
arasındaki değeleri tutabilmektedir.
ssize_t
türü, size_t
türünün işaretli versiyonu olarak düşünülmüş ve
standartları okuduğumuz zaman bu durumu bir miktar gözlemleyebiliyoruz.
Programımızı C99 standartlarına uygun yazdığımız zaman C99, size_t
türünün en
az 16-bit genişliğinde olacağını garanti etmiştir. POSIX standartları da ssize_t
türünün en az [-1, 32767]
aralığını tutabileceği belirtiyor, yani aslında
işaretli 16-bit türünden bahsediyor. Yani her iki standarta göre her iki tür de
en az 16-bit genişliğinde olmalıdır.
size_t
türünden bir değişkende tutulan bir değerin, bir işaret hatası olmadan
ssize_t
türünden bir değişkende tutulabileceğinin garantisi yok. Örneğin her
ikisi de 16-bit genişliğinde ise, 16-bit genişliğindeki tüm işaretsiz tam sayılar
aynı genişlikteki işaretli bir değişkende tutulamaz. Örneğin 40000 (decimal)
değeri bir problem oluşturacaktır. Peki bundan bize ne?
write()
POSIX fonksiyonunu tekrar hatırlayalım:
#include <unistd.h>
ssize_t write(int fd, const void buf[.count], size_t count);
size_t
türünden olan 3. parametre, kaç byte yazma yapacağımızı söylememizi
sağlıyor. write()
ise bize kaç byte’lık veriyi başarılı bir şekilde yazdığını
dönüyor. Fakat dikkat ederseniz dönüş değerinin türü size_t
değil, ssize_t
.
Çünkü write()
, hata durumunda bize -1
dönüyor. size_t
işaretsiz bir tür
olduğu için dönüş değerini göstermek için kullanılamaz, o yüzden ssize_t
türünden
bir dönüş türüne sahip.
size_t
ve ssize_t
türlerinin genişliklerinin 16-bit olduğu örnekten devam
edelim. write(..,..,40000)
gibi bir çağrı yaparsak ve 40000
byte yazılırsa
ne olacak? write()
, 40000
değerini dönmek isteyecek fakat ssize_t
bu
değeri bir işaret hatası olmadan tutacak kadar geniş değil. İşte POSIX
standartları bu durum için böyle bir açıklama yapıyor: [7]
If the value of nbyte is greater than {SSIZE_MAX}, the result is implementation-defined.
Yani yazmak istediğimiz byte sayısı, ssize_t
nin tutabileceği maksimum değeri
aşıyorsa, sonuç implementation defined olur diyorlar. Bu da “Git, kodunun
çalışacağı işletim sisteminin dokümanına bak” demek. E biz de gidip Linux
dokümanlarına bakalım.
Elbette POSIX standartları geniş donanım ve işletim sistemi çeşitliliğini kapsamayı hedefliyor, C standartları gibi. Örneğin günümüzde pratikte görme ihtimalimiz düşük olsa da 16 bitlik bir işlemcide POSIX uyumlu bir işletim sistemi çalışıyor olabilir. Bu yüzden ilgili limitlerin minimum değerleri oldukça küçük. Fakat pratikte gömülü taraf da dahil olmak üzere bir Linux işletim sistemi çalışıtırıyorsak o işlemci en az 32 bit hatta büyük olasılıkla artık 64 bit olacaktır. Yine de sistem programlama yapan kişiler olarak bu limitleri ve durumları akılda tutmak iyi olacaktır.
Linux’ta implement edilmiş olan write()
fonksiyonun detayı ise şöyle: [8]
On Linux, write() (and similar system calls) will transfer at most 0x7ffff000 (2,147,479,552) bytes, returning the number of bytes actually transferred. (This is true on both 32-bit and 64-bit systems.)
Aynı açıklama henüz değinmediğimiz read()
fonksiyonu için de yapılmıştır: [9]
On Linux, read() (and similar system calls) will transfer at most 0x7ffff000 (2,147,479,552) bytes, returning the number of bytes actually transferred. (This is true on both 32-bit and 64-bit systems.)
32 ve 64 bit sistemlerin hepsinde maximum transfer boyutu 0x7ffff000
olarak
belirtilmiş. Yani write()
fonksiyonu bu değerden büyük bir değer dönemez. Bu
değer ise 32-bit genişliğinde bir işaretli tam sayının tutabileceği değer aralığı
arasında. Fakat hala problemlerimiz var: Linux üzerinde SSIZE_MAX
ne?
Yani ssize_t
türünün bu değeri bir işaret hatası oluşturmadan tutabileceğinden
emin miyiz?
C ve POSIX standartlarını beraber okuduğumuz zaman ssize_t
türünün aralığının
size_t
türünün aralığını kapsadığına dair bir çıkarım yapamıyoruz. Hatta
ssize_t
türünün, size_t
türünün işaretli türü olabileceği söyleniyor. Bu
zaten size_t
türünden bir değişkendeki “büyük” sayıların işaret hatası olmadan
ssize_t
türünde ifade edilemeyeceğini açıkça belirtiyor. Odağımızı Linux’la
sınırlasak bile ssize_t
türünün en az 32-bit genişliğinde olacağını garanti
eden bir durum yok. Fakat pratikte durum bu kadar kötü değil.
Genel POSIX sistemlerden ayrılıp Linux sistemlere bakacak olursak, eğer
SSIZE_MAX
değeri 0x7ffff000
dan büyük ise bir problemimiz yok. Çünkü zaten
Linux üzerinde write()
ve read()
fonksiyonları maksimum bu değeri
dönebiliyorlar ve ssize_t
bu değeri tutabilecek kadar geniş ise tamamız. 👍
Hadi gelin sistemimizdeki SSIZE_MAX
değerine bakalım:
ay@dsklin:~$ getconf -a | grep SSIZE_MAX
SSIZE_MAX 32767
_POSIX_SSIZE_MAX 32767
32767 mi? Şaka mı? 🤦 64 bit sistem üzerinde bu komutu çalıştırıyorum ve böyle
bir değer dönüyor. _POSIX_SSIZE_MAX
ın 32767 olması doğru, çünkü bu POSIX’in
belirlediği sabit bir sayı. Bu sayı SSIZE_MAX
ın alabileceği en düşük değeri
belirtiyor. Benim sistemde ise gerçekten SSIZE_MAX
en düşük değerde mi? Bir
de aynı değeri limits.h
içerisindeki makroyu kullanarak yazdıralım:
#include <stdio.h>
#include <limits.h> // For SSIZE_MAX
#include <unistd.h> // For sysconf
int main() {
// Print the SSIZE_MAX from limits.h
printf("SSIZE_MAX from limits.h: %zd\n", SSIZE_MAX);
// Get and print the SSIZE_MAX from sysconf
long sysconf_value = sysconf(_SC_SSIZE_MAX);
if (sysconf_value == -1) {
printf("Failed to get SSIZE_MAX from sysconf");
} else {
printf("SSIZE_MAX from sysconf: %ld\n", sysconf_value);
}
return 0;
}
Programı derleyip çalıştırdığımızda
SSIZE_MAX from limits.h: 9223372036854775807
SSIZE_MAX from sysconf: 32767
çıktısını elde ediyoruz. O uzun sayının hexadecimal karşılığı
0x7FFFFFFFFFFFFFFF
yani 64-bit genişliğindeki işaretli tam sayının alacağı en
yüksek değer. Bu 64 bir sistem için mantıklı ve Linux’un belirttiği 0x7ffff000
limit değerin de çok çok üstünde.
Gelin aynı kodu 32-bit bir sistem için derleyelim. Bunun için gcc -m32
şeklinde
bir derleme yapabiliriz fakat 64-bit bir sistemde bu şekilde derlerken hata
alıyorsanız sudo apt install gcc-multilib
(Ubuntu için verdim) şeklinde
32-bit derlemek için gerekli paketleri kurmanız gerekiyor. Bu noktadan sonra
-m32
ile derleme yapabilmeniz lazım. Bu şekilde aynı kodu derleyip
çalıştırdığımızda ise
SSIZE_MAX from limits.h: 2147483647
SSIZE_MAX from sysconf: 32767
çıktısını alıyoruz. Bu da 0x7FFFFFFF
demek yani 32-bit genişliğindeki işaretli
bir tam sayısının alacağı en yüksek değer.
Özetle pratikte gözüken hem 32-bit hem de 64-bit sistemlerde SSIZE_MAX
değeri, Linux’un belirlediği 0x7ffff000
limit değerinin üzerinde olduğu.
Pratikte bu sistemler ile çalışacağımız düşünürsek aslında bir problem yok.
Peki getconf
kabuk komutu veya sysconf()
fonksiyonu bize niye farklı
bir sayı dönüyor?
sysconf()
bir POSIX fonksiyonu [10] ve runtime sırasında uygulamanın
üzerinde çalıştığı sistemle ilgili çeşitli limit değerlerin sorgulanmasını
sağlıyor. Bu fonksiyon, Linux üzerinde de bulunuyor.[11]
#include <unistd.h>
long sysconf(int name);
Fakat yukarıdaki kodda bulunan sysconf(_SC_SSIZE_MAX)
çağrısı, glibc
ye
özgü bir çağrı çünkü _SC_SSIZE_MAX
sembolik sabiti POSIX standartlarında
belirtilen bir sabit değil. GNU dokümanlarında ise şöyle bir açıklama
yapılmış: [12]
...
_SC_SSIZE_MAX
Inquire about the maximum value which can be stored in a variable of type ssize_t.
Bize gerçekten de ssize_t
nin tutabileceği maksimum değeri vermesi gerekiyor.
Fakat işler tam da öyle değil. glibc
nin 2.39 sürümünün kaynak koduna baktığımızda
aslında bu sembolik sabit ile yapılan sysconf()
çağrısının bize _POSIX_SSIZE_MAX
değerini döndürdüğünü görüyoruz: [13]
115case _SC_NZERO:
116 return NZERO;
117
118case _SC_SSIZE_MAX:
119 return _POSIX_SSIZE_MAX;
120
121case _SC_SCHAR_MAX:
122 return SCHAR_MAX;
Neden POSIX’in belirlediği en düşük limiti dönüyor, bilmiyorum! Ama bizler
için doğru olmadığı kesin. getconf
kabuk komutu da aslında sysconf()
fonksiyonunu çağırdığı için terminalde de aynı değeri gördük [14].
Yapılacaklar
Bunu glibc gruplarına sorabilirsin.
Fakat GNU dokümanlarında sysconf()
dokümantasyonunda şöyle bir ifade mevcut:
[15]
We recommend that you first test for a macro definition for the parameter you are interested in, and call sysconf only if the macro is not defined
Yani diyorlar ki aradığınız parametreyi gösteren bir macro yok ise sysconf()
ile sorgulama yapın. Bizim durumumuzda aslında limits.h
içerisinde SSIZE_MAX
değeri mevcut. Bunu net belirtmemişler ama acaba bu macro tanımlı olduğunda
sysconf()
doğru değeri vermiyor (yani vermek zorunda değil) olabilir mi? Yani
SSIZE_MAX
zaten tanımlı olduğu için sysconf()
ile biz doğru olmayan bir
sonuç alıyor olabilir miyiz? Ama öte yandan da adamlar bam diye return _POSIX_SSIZE_MAX;
yazıp geçmişler yani SSIZE_MAX
tanımlı mı değil mi durumu
pek kontrol ediliyor gibi değil. Belki de glibc
için SSIZE_MAX
ın tanımlı
olmama durumu yoktur. Yine de bu mantığı çok anlamış değilim.
Ben bu konuyu SO’da sordum fakat pek tatmin edici bir cevap gelmedi ama bir
kişinin yorumuna göre limits.h
içerisindeki SSIZE_MAX
değerine güvenebiliriz.
[16] Yani hem 32-bit hem de 64-bit sistemlerde görünüşe göre güvendeyiz.
glibc’nin kaynak kodlarına baktığım zaman ise SSIZE_MAX
ile ilgili iki yerde
tanımlama görüyorum:
Bu [17]
164#ifndef SSIZE_MAX
165/* ssize_t is not formally required to be the signed type
166 corresponding to size_t, but it is for all configurations supported
167 by glibc. */
168# if __WORDSIZE == 64 || __WORDSIZE32_SIZE_ULONG
169# define SSIZE_MAX LONG_MAX
170# else
171# define SSIZE_MAX INT_MAX
172# endif
173#endif
ve bu [18]
153#ifndef SSIZE_MAX
154# define SSIZE_MAX ((ssize_t) (SIZE_MAX / 2))
155#endif
Bunlardan iki adet çıkarım yapabiliriz:
ssize_t
türü 32-bit sistemlerde 32-bit genişliğinde, 64-bit sistemlerde ise 64-bit genişliğinde.ssize_t
gerçekten desize_t
nin işaretli versiyonu olarak kullanılıyor.
Uçtan Uca
SSIZE_MAX
ın minimum 0x7fffffff
olacağını hem deneyimledik hem de kaynak
kodlardan gördük diyebiliriz. Linux’taki fonksiyonlar 0x7ffff000
dan büyük bir
değer dönmeyeceği için bir problem pratikte yok. Ama bu kadar dibine girmişken
kendi C programımızda write()
veya read()
fonksiyonlarını çağırdığımızda
neler oluyor, uçtan uca onu bir anlamaya çalışalım.
write()
fonksiyonu glibc tarafından implement edilmiş durumda. Fakat glibc
gibi büyük kütüphanelerin kodlarını takip etmek oldukça zor. Örneğin write()
fonksiyonu için stub benzeri yapılar kullanılmış. [19] Onun yerine daha
basit bir implementasyon olan musl
a bakalım: [20]
#include <unistd.h>
#include "syscall.h"
ssize_t write(int fd, const void *buf, size_t count)
{
return syscall_cp(SYS_write, fd, buf, count);
}
write()
fonksiyonu aslında doğrudan syscall yapan bir fonksiyon, sys_write
isimli syscall’ı çağırıyor. Bu syscall ise kernel içerisinde tanımlanmış durumda
[21]
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
return ksys_write(fd, buf, count);
}
Sonra [22]:
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos, *ppos = file_ppos(f.file);
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_write(f.file, buf, count, ppos);
if (ret >= 0 && ppos)
f.file->f_pos = pos;
fdput_pos(f);
}
return ret;
}
ve [23]:
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_WRITE))
return -EINVAL;
if (unlikely(!access_ok(buf, count)))
return -EFAULT;
ret = rw_verify_area(WRITE, file, pos, count);
if (ret)
return ret;
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT;
file_start_write(file);
if (file->f_op->write)
ret = file->f_op->write(file, buf, count, pos);
else if (file->f_op->write_iter)
ret = new_sync_write(file, buf, count, pos);
else
ret = -EINVAL;
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
return ret;
}
Burada MAX_RW_COUNT
dikkat çekici. İşte Linux’un koyduğu limit buradan geliyor.
Bakalım [24]:
#define MAX_RW_COUNT (INT_MAX & PAGE_MASK)
Burada mimariye özgü tanımlamalar olabiliyor ama sayfa boyutunu 12 bit, int
i de
32 bit olarak düşünürsek aslında 0x7ffff000 = 0x7fffffff - 0xfff
ilişkisinden
elde edilmektedir. count
yani gerçekten yazım yapılan değer ise aslında if
ile bu değere kernel içerisinde limitlenmektedir. x86 mimarilerde page size yani
sayfa boyutu tipik olarak 4K olmaktadır, yani 12 bit. ARM gibi mimarilerde ise
daha büyük sayfa boyutları olabiliyor, x86’da da olabiliyor elbette. Ama anlaşılan,
ilgili fonksiyonlar en büyük değer olarak 0x7ffff000
dönüyorlarsa en küçük
sayfa boyutu günümüzde kernel tarafından 4K olarak ayarlanıyor. Aksi taktirde
PAGE_MASK
değeri değişecek MAX_RW_COUNT
sembolik sabiti de bu değeri
aşacaktır.
Özet
Özetle, Linux üzerinde ssize_t
türü ve write()
/read()
fonksiyonları
düşünüldüğünde pratikte bir problem bulunmamaktadır. O kadar yazdık, sonuç bu.
😄