Çekirdek Çıtlatması (Bölüm I)
by faik, 08.06.06 at 12:20 am :: Çekirdek :: permalink :: rss
İşletim sistemi çekirdekleri şüphesiz geliştirilmesi en karmaşık yazılımlardan sayılır. Merak eden geliştiriciler, özgür yazılım dünyasında yazılmış sayısız alternatifleri inceleme fırsatına sahiptir. Ancak kısa bir inceleme ve araştırma sonrasında farkedilir ki; çekirdek, geliştiricinin her ne kadar iyi bildiği bir dilde de yazılmış olsa, inceleme esnasında belirecek bir çok “neden” Ve “niçin” sorusuna cevap bulabilmek için, çekirdeğin üzerinde çalıştığı platform ve işlemci teknolojileri, genel derleyici ve çekirdek teknolojileri ve de çekirdeğin derlendiği derleyici hakkında genel bilgi sahibi olunmalıdır.
İşte yukarıda bahsi geçen teknolojilerin getirdiği ihtiyaçların bazılarını karşılamak amacıyla kullanılan ve Linux çekirdeğinde de oldukça sık rastlanan derleyici bariyerleri, hafıza bariyerleri, “asm volatile” ve “c volatile” anahtar sözcükleri üzerine bir yazı yazmak istedim. Bu konuda kaynak bulmak zor. Çok dağınık, yanıltıcı ve hatta yanlış kaynaklar mevcut. ilerde de bakacak derli toplu bir şeyler olur.
Modern işlemci ve derleyeciler uygulamalardan maksimum verim alınabilmesi için çeşitli optimizasyon yöntemleri kullanırlar. Modern işlemcilerde en çok bilinen
optimizasyon yöntemlerinin bir kaçına örnek vermek gerekirse “speculative execution”, “Superscalar mimari”, “pipelining” ve “out-of-order execution” verilebilir.
Modern derleyici optimizasyonlarından yine en çok bilinenlerine örnek olarak “register allocation” ve “instruction scheduling” verilebilir.
İlk konumuz bariyerler olduğu için bizi burada ilgilendiren, derleyiciler için yukarıda bahsi geçen “register allocation”, “instruction scheduling” ve işlemciler için “out-of-order execution” optimizasyonları.
Gcc’nin “instruction scheduling” optimizasyonunu görebilmek için bir kaç deneme yeterli oldu. Çıktılar gcc’nin 3.4.6 versiyonu ve -O2 ile derlenerek alındı.
test1.c
======
extern int address;
extern int cmd;
extern int action;
int startengine(void)
{
address = 0×1000;
cmd = 0×20;
action = 1;
return 0;
}
test1.s:
=======
startengine:
pushl %ebp
movl $1, %eax
movl %esp, %ebp
popl %ebp
movl $4096, %ecx
movl %eax, action
movl $32, %edx
xorl %eax, %eax
movl %ecx, address
movl %edx, cmd
ret
Dikkatli baktığımız zaman, derleyici’nin, address, cmd ve action adreslerine ilgili değerleri koddaki sırada değil de, pipeline’ın en verimli kullanılabileceği sırada yerleştirdiğini görüyoruz. action adresi için sabit değer önce eax yazmacına yazılmış. Yine 0×1000 değeri ecx yazmacına yazılmış. Daha sonra ecx ve eax yazmaçları ilgili adreslere kopyalanmış. Bu işlemler fonksiyon içine öyle serpiştirilmiş ki, ardarda gelen komutların birbirlerine bağımlılığı kalmamış ve bağımlılık olmadığı için de komutlar pipeline’da “bekleme yapmayacak” şekilde dizilmiş.
Şimdi bu kodun çekirdek için geliştirdiğiniz bir usb motor devresi sürücüsüne ait olduğunu düşünün. Tüm bu adreslerin de, devrenizdeki ilgili yazmaçların, bilgisayarınızın hafızasına map edilmiş hali olduğunu. (MMIO) Devreniz de öyle çalışıyor ki, önce adres ve cmd yazmaçlarına, daha sonra da işlemi başlatan action yazmacına sırası ile yazmanız gerekiyor. Siz kodu istediğiniz ve olması gerektiği sırada yazdınız. Kodunuzu derlediniz ve sürücünüzü yükleyerek çalıştırdınız. Kötü haber: Devreniz çalışmıyor.Çünkü derleyici sizden akıllı davrandı.
Derleyici komutların sırasını anladığı kadarıyla en optimum haline getirdi. Fakat kod sizin istediğiniz şekilde çalışmıyor. İşte bu noktada ihtiyacınız olan şey bir “derleyici bariyeri” (compiler barrier). Bunu yapmanın da gcc dilinde bir tek yolu var: inline assembly kullanarak clobbered listesine hafızayı ekleyeceksiniz.
Hemen öncesinde yine derleyici bariyeri gerektiren ikinci bir örneğe geçelim. Gcc’nin “register allocation” optimizasyonu’na basit bir örnek vermeye çalışalım. Yine benzer şekilde inout, cmd ve action hafıza adreslerinde bir usb bilet üretici aletimizin yazmaçları olsun. inout yazmacına rasgele üretilecek bilet numarası için bir ilk değer girelim. cmd yazmacına ilgili komutu verelim. Ve action yazmacına yazdığımız anda bilet üretilsin. Üretilen numara da yeniden inout yazmacına geri yazılsın. -O2 ile derleyerek, neler olduğuna bakalım.
test2.c
=======
#define udelay()
extern int inout;
extern int cmd;
extern int action;
extern int seed;
int getlotteryticket(void)
{
inout = seed;
cmd = 0×50;
action = 1;
udelay(300);
return inout; /* winner */
}
test2.s
=======
getlotteryticket:
pushl %ebp
movl seed, %eax
movl $80, %ecx
movl %ecx, cmd
movl %esp, %ebp
popl %ebp
movl $1, %edx
movl %eax, inout
movl %edx, action
ret
Üretilen kod’a baktığımız zaman, sıra ilk örnekte olduğu gibi yine hatalı görünüyor. Ama burada asıl dikkat etmemiz gereken yer, fonksiyonun sonu. Geri dönen değere bakarsak eax’in içine yazılmış olan seed’in döndüğünü görüyoruz. Harika! Seed’i biliyorsak (gettimeofday?) her zaman kazanabiliriz. Gcc inout değişkeni için eax yazmacını kullanıyor ve tekrar hafızadan okumadan “cache”‘den okuyor.
test1.c ve test2.c’de karşımıza çıkan her iki sorunun da çözümü, action değişkeni öncesine bir derleyici bariyer’i eklemek.
include/linux/compiler-gcc.h:
#define barrier() __asm__ __volatile__(”": : :”memory”)
Gcc inline assembly yapısı için oldukça fazla kaynak bulmak mümkün. Burada “clobber listesi”‘ndeki hafıza bildirimi, gcc’ye bu __asm__ blok’u sonrasında hafıza’nın bozulduğunu/bozulacağını söylüyor.
Bu da gcc’nin, blok sonrasında yazmaçlarda cache’lenen değerleri kullanamayacağı anlamına geliyor. gcc blok öncesinde tüm değerleri hafızaya yazıyor/sonrasında da ilk okumada yeniden hafızadan okuyor. Çünkü artık daha önce cachelediği değeri kullanamaz. Blok öncesinde kalan yazılacak değerlerin sırası yine farklı olabilir. İlk örneğimize bakarsak address ve cmd yine de farklı sıralarda yazılabilir. Eğer bu iki adresinde yazılma sırası önemli ise, o zaman aralarına bir derleyeci bariyeri daha koymak gerekir. Ama bizim için önemli olan action adresine, address ve cmd adreslerinin içerisi doldurulduktan sonra yazılması olduğu
için, tek bariyer yeterli.
test1.c ve test2.c uygulamalarında action öncesine barrier() koyarak üretilen kod’a bakalım:
test1-barrier.s
===============
startengine:
pushl %ebp
movl $4096, %ecx
movl $32, %edx
movl %ecx, address
movl %esp, %ebp
movl %edx, cmd
popl %ebp
movl $1, %eax
movl %eax, action
xorl %eax, %eax
ret
test2-barrier.s
===============
getlotteryticket:
pushl %ebp
movl seed, %eax
movl $80, %edx
movl %edx, cmd
movl %esp, %ebp
movl %eax, inout
movl $1, %eax
movl %eax, action
popl %ebp
movl inout, %eax
ret
İki örneğin çıktılarında da istediğimiz gibi bariyer öncesinde action adresi öncesi doldurulacak değerler doldurulmuş. Sonra da action adresine ilgili değer yazılmış. test2 çıktısına baktığımızda ise, dönülen değer önceden cachelenen eax değişkeni değil, artık cihaz tarafından doldurulmasını beklediğimiz inout adresindeki değer.
devam edecek… ![]()


No comments at the moment.
Add a comment