ENUX KERNEL DENEMESI Enux Free Lisans(Herhangi bir lisans belirlenmedi.) ile birlikte gelen bir kernel denemesidir. Su an mevcut standart isletim sistemi ozelliklerinin bazilarini desteklemektedir. Bunlardan baslicalari: surec scheduling, exception handling, keyboard ve bazi temel konsol islemleri. Calismalar isletim sisteminin POSIX standartlarina uygun olmasi yonunde ilerlemektedir. Ilerleyen bolumlerde su ana kadar yazilan kodlarin aciklamalarini isletim sistemi kavramlariyla birlikte anlatmaya calisacagim. Enux dizini icinde ilk olarak boot dizini icindeki kodlari aciklamaya baslayip daha sonra da kernel dizini icindeki yuksek seviye c kodlarini hakkinda bilgi vermeye calisacagim. Mevcut kod su an herhangi bir dosyalama sistemi kullanmamaktadir. Boot dizini altindaki boot.s dosyasi as ile derlendikten sonra dd ile floppy diskin 0. (boot sektor) sektorune yazilir. Daha sonra da init.s dosyasi as ile derlendikten sonra floppy diskin 1 numarali sektorunden itibaren 5 sektor yazilir. 1-5 sektorler init.s dosyasinin binary halini icermektedir. Bu ilksel bilgilerden sonra sirasiyla boot.s ve init.s iceriklerini inceleyelim: BOOT.S ------ Bu dosya, sistemin bios post asamasindan sonra boot edecegi (biostan secildikten sonra ) floppy nin ilk bakacagi olan boot sectorune yazilir. Bu kodun yaptigi is sirasiyla 1-6 sektorler arasinda yerlesmis olan init.s kodlarini hafizaya yuklemek ve buna ek olarak init kodlarindan sonra yer alan kernel kodunu hafizaya yuklemek ve daha sonra da init kodlarina ziplamak.. Butun yapilan is bu kadardir. boot.s dosyasinda, .code16 .text .global _boot_start sirasiyla 16 bitlik kodlama sistemi kullanilacagi belirtilmis .text ile cod segment icinde oldugumuz ve .global ise de bizim kodumuzun C dilinde karsiligi main olan kodlarin ilk calisacaklari yerin etiketini belirtmektedir. movw $0x07c0, %ax movw %ax, %ds movw %ax, %es ile birlikte bios tarafindan okunan boot sektorun otomatik olarak kopyalanacagi segmentlerin adresleri belirtiliyor. Boylece kod segmentte calisan islemci orada tanimlanmis verilere ulasmak icin bu segment numaralarini kullanacaktir. Simdi sirasiyla floppyden init.s ve kernel kodlarini bellegin bizim keyfi olarak sectigimiz real mod segment adreslerine kopyalama islemlerini yapacagiz. Herhangi bir disk islemi yapmadan once disk kontrolculerini resetlememiz gerekmektedir (fdc ya da hdc - harddisk controller). Butun bu islemlerin 512 byte sigmasi icin onceden tanimli bios kesmelerinden yararlanmamiz gerekmektedir. movb $0x00, %bl xorw %ax, %ax int $0x13 islem grubuyla birlikte floppy disk kontrollerini resetliyoruz. Bundan sonra yapacagimiz is artik kopyalanacak verinin bulundugu C/H/S - cylinder, head ve sector bilgilerini uygun kayitcilar(register)a kopyalayip bios kesmelerini kullanarak floppy uzerinde bulunan veriyi hafizanin belli bolgesine yuklemek. Bios kesmesini kullanarak veri kopyalama islemini yapabilmemiz icin kopyalanacak hafiza bolgesinin %es segment kayitcisina belirtmemiz ve offset adresini de %bx kayitcisina belirtmemiz gerekmektedir. Bu islemleri de yaptiktan sonra movw $0x0205, %ax movw $0x0002, %cx xorw %dx, %dx int $0x13 burada %ah kayitcisinda 0x02 bilgisi ve %al kayitcisinda da 0x05 bilgisi bulunmaktadir. Bu sirasi ile okuma islemi (0x02) ve okunacak sektor sayisi (0x05) anlamina gelmektedir. %cl kayitcisinda bulunan 0x02 degeri de kacinci sektorden itibaren kopyalama yapilacagini belirtmektedir. Assembly ve bios da kullanilan tarza gore sektorler 1 numarali sektorden baslamaktadir. Bizim 0.sektor olarak belirtilen boot sektoru bios rutinlerinde 1 numarali sektor olarak belirtilmistir. Yani 1-5 sektor arasinda bulunan init kodlari bios rutinlerinde 2-6 arasinda bulunmaktadir. O yuzden 0x02 numarali sektorden itibaren kopyalama islemini yapiyoruz. 1. sektorde ise boot.s bulunmaktadir. Bu islemlerden sonra init kodlarini 0x4000 numarali segmentin 0x0000 inci ofsetinden itibaren yerlestirdik. Simdi sira kernel kodlarini floppyden alip hafizanin belirli bolgesine kopyalamaya geldi. Bunun icin de yukaridaki izledigimiz yolun aynisini kullanacagiz. Kernel kodlarinin kopyalanacagi adres grubu ise 0x1000 segmenti ve 0x0000 ofsetidir. butun bu degerler keyfidir. Bios'un 0xa000:0x0000 - 0xffff:0x000f arasinda bulundugunu bilerek bu adresler haricinde butun heryere bu kodlari kopyalayabiliriz. Dikkat ederseniz yine kopyalama islemi yapmadan once floppy denetcisini onceki gibi resetledik. Bu islemle beraber artik bootstrap'in isi bitmistir. Sira bootstrap kodu ile yukledigimiz init kodlarini calistirmakta. Bunun icin init kodlarini yukledigimiz hafiza bolgesine kücük bir ziplama yapiyoruz. ljmp $0x4000, $0x0000 ile birlikte artik init kodlarindayiz. INIT.S ------ Bu dosyada kernel'in calisacagi islemci modu olan korumali mod(protected mode)a gecis yapacagiz. Daha sonra da bootstrap'da yukledigimiz 32 bit kernel kodlarini calistiracagiz. Diger ust seviyeli ilkleme islemlerini orada yapacagiz. Sirasiyla init.s kodlarinda 32 bit adresleme yapabilmemiz icin gerekli olan islemcinin 20. pinini aktif hale getirilmesi ve APIC(Advanced Programmable Interrupt Controller)'in maskelenmesi var. Cunku korumali modda kesme sistemi gercek moddan (real mode) cok farkli calismaktadir. Daha sonra da segmentasyonu ayarlayip son olarak da kernel koduna ziplayacagiz. Ilk olarak bir kac tane ekrana yazi yazdirma ile ilgili makrolar gorulmektedir. __start kisminda bulunan jmp firsthere ile direk olarak bu makrolar asilir ve goruldugu uzre %ds(data segment) su an icinde oldugumuz segment bilgisi ile yuklenir. Eger bunu yapmasaydik bir onceki %ds degeri bootstrap da verdigimiz deger olan 0x07c0 olacak ve yaptigiz herbir referans anlamsiz olacakti. Basit bir string ekrana bastirdiktan sonra A20 pinini aktif edecegimiz koda geliyoruz. A20 pin i iki turlu ilklenebilir; birincisi biosu kullanarak digeri de klavye kontrolcusunun bulundugu portu kullanarak. Kulaga cok alakasiz gelmekte ilk olarak fakat bunu yapmak zorundayiz. Cok anlam verememekle birlikte A20 pininin aktif edilmesi ile ilgili kodlarin cogunu linux'un kodlarindan almis bulunmaktayim. Acikcasi bu noktada kodlarin ne oldugu cok onemli degil. Daha sonra gelen kisim da yine bir yapilmasi gereken ve cok kafa patlatmamiz gereken kisim APIClerin tanimi. Burada IRQ degerleri ile gelecekte bahsedecegim IDT nin nasil kullanilacagi bir nevi eslestirilecegi belirtilmektedir. Burada yapilan iki islem onemli onlar da sirasiyla; movb $0x70, %al outb %al, $0x21 .word 0x00eb, 0x00eb movb $0x78, %al outb %al, $0xa1 ilk kisimda master PIC (0x21) in bagli bulundugu I/O portundan gelen irq'larin 0x70 numarali kesme istegini dondurecegi ve slave PIC(0xa1) in de 0x78'den baslayan kesme isteklerini dongurecegi belirtilmektedir. Yani IRQ0 -> 0x70 numarali IDT kesmesini IRQ1 -> 0x71 numarali IDT kesmesini .. .. IRQ7 -> 0x77 numarali IDT kesmesini bundan sonraki kisim da ikinci PIC ile ilgili kisim IRQ8 -> 0x78 numarali IDT kesmesini IRQ9 -> 0x79 numarali IDT kesmesini ... dondurecegi tanimlanmaktadir. En son kod kisminda ise portlara 0xff gondererek bunlari simdili inaktif duruma sokuyoruz. Bu IRQ degerlerini kullanacagimiz zaman teker teker aktif hale getirecegiz APIC lerin o degerlerini. Bundan sonra korumali mod icin gerekli olan GDT ve IDT tablolarinin hafiza uzerindeki yerlerini tanimliyoruz, gerekli islemci kayitcilarina bu tablolarin hafiza uzerindeki yerlerini belirtiyoruz. GDT segmentasyon descriptor tablosu segment kayitcilari ile yapilacak adres araliginin bilgilerini tutmaktadir. I386 segmentasyon mantigini burada bahsetmeyecegim. Sadece hangi degerleri icedigiklerini ve hangi haklara sahip olduklarini gececegim ENUX altinda kullanilan segment descriptorlarinin. gdt: .word 0, 0, 0, 0 # they say dummy. .word 0xffff, 0x0000, 0x9a00, 0x00cf #cs base:0 limit:4gb page:4k read/exec [08] .word 0xffff, 0x0000, 0x9200, 0x00cf #ds base:0 limit:4gb page:4k read/write [10] .word 0xffff, 0x0000, 0xfa00, 0x00cf #user cs base:0 limit: 4gb page:4k read/exec [18] .word 0xffff, 0x0000, 0xf200, 0x00cf #user ds/ss base:0 limit: 4gb page : 4k read/write her bir descriptor 8 byteten olusmaktadir. Ilk segment dummy olarak adlandirilir ve kullanilmamaktadir. Diger sektor 4gb limitten olusan base adresi 0x00000000 olan 4k lik framelerle bolumlen direlebilecek read/exec flagleri olan bir segmenttir bu segmente yazilma cabasi GP(General Protection) excetionu ile karsilanacaktir. Diger segment ise ayni limit ve taban degerlerine sahip fakat read/write flagleri olan bir segmenttir. Bu iki segmenttin privilege seviyesi 0 dir. Yani en ayricaliklidir. Bundan sonra gelen iki segment base ve limit degerleri olarak onceki segmentlere benzemektedirler fakat ayricalik seviyeleri dusuktur. Genel Tanimlayici Tablosu (GDT)'unu tamimladiktan sonra sirasiyla bu tablonun adresini ve bir de icini doldurmadigimiz IDT(Interrupt Descriptor Table) nin adres degerini ve boyutunu GDTR ve IDTR kayitcilarina yukluyoruz. lgdt gdt_48 lidt idt_48 ile bu islemleri yapiyoruz. Artik bilgisayari korumali mod icin hazirladik tek yapagimiz is ise artik islemciyi korumali moda sokmak. Bunu da cr0 islemci kayitcisinin ilk bitini set etmek Bu iki yolla birincisi cr0 bilgisini alip 1 ile orlamak( or bit seviye islemi) ya da %ax kayitcisina $1 degerini atip lmsw assembly islemi ile bilgisayari korumali moda sokmak. Cachelerin temizlenmesi icin son: diye bir etikete zipliyoruz. Aslinda gorunurde cok anlamsiz gorunen bir islem. Korumali moda ilk girdigimiz zaman floppynin motor isiginin aktif oldugunu goreceksiniz. Yani motor devamli calisiyor demek. Bu cok olagan bir durumdur cunku eski sistemden cok farkli bir yere gectik ve kesmeler komple degisti. O yuzden floppy denetcisine io portlarindan moturunu durdurmasi ile ilgili komut gonderiyoruz. movw $0x3f2, %dx movb $0, %al outb %al, %dx Kod segmenti haric butun segmentleri 0x10 degeri yarni GDT ki 3. segment olan kernel data segmenti degeri ile yukluyoruz. Bundan sonra da korumali mod icinde kernelin bulundugu adrese zipliyoruz. Fakat bu islem 32 bitlik bir ziplama oldugu icin herhangi bir instruction yerine 48 bit oneki olan 0x66 bytini tanimladiktan sonra 0xea ziplama komutunu tanimliyoruz. Son olarak da ziplama yapacagimiz offset degeri ve segment bilgilerini yaziyoruz. %cs kayitcisi otomatik olarak 0x08 degeri ile yuklenecektir. Dikkat ederseniz bir onceki kisimda belirttigimiz uzre 0x08 numarali segment 0x00000000 adresinden baslamakta idi 0x00010000 ofseti direk olarak kernel kodunu gostermektedir. KERNEL Boot kodlariyla ilgili islemlermiz bitti. Sira kernel dizini icindeki kodlara geldi. Artik kodlarla oynamaktan hoslanacagimiz kisim burasi. Fakat bazen de o kadar emin olmamak gerek- mektedir. Yapilan yanlis bir hafiza referansi istenmeyen sonuclara bizi goturmektedir. init_console(); Kernel kodlarinin ilk calistirilan kisim yani 32 bit adres olarak 0x00010000 adresinde bulunan kisim kernel.c dosyasi icinde bulunan start prosedurudur. Bu prosedur kernelin ilkleme islemleri ni ardarda yapildigi yerdir. Simdi sirasiyla bu ilklemeleri islem sirasiyla gorelim. Ilk gelen kisim init_console(). init_console(), console.c dosyasi icinde tanimlanmistir. Temel isi sadece bios video buffer adresini bir degiskene atip, bundan sonra ekrana yapilacak olan her islemi bu degisken uzerinden onu bir karakter arraymis gibi dusunerek yapmaktir. Rom'dan bilindigi uzere video buffer (baska bir deyisle screen buffer) bios un 0xb8000 adresindan baslamaktadir. Her bir cift numarali adresler karakterlerin degerini ve tek numarali adresler ise de o hucrenin renk bilgisini tutar yani ; 0x000b8000 adresindeki bilgi ekranin 0,0 pozisyonundaki bilgidir. video[0] = 'a'; gibi yapilacak bir atama islemi direk olarak ekranin ilk satir ilk sutununa a karakterini yazmak anlamina gelmektedir. video[1] ise 0,0 pozisyonundaki 'a' karakterinin renk bilesenlerini tutmaktadir. Su an rengin bir onemi yoktur. init_console() in ilk yaptigi is direk olarak onceden de belirtildigi gibi video degiskenine video bufferin adres degerini tasiyip butun ekran islemlerini bu degisken uzerinden yapmaktir. console.c dosyasinda ek olarak set_cursor prosedurunu deginmek gerekir. Global olarak tuttuguz locx ve locy(1 degerinden basliyor 0 degil) bilgilerini kullanarak bizim ekran uzerindeki cursorumuzu io portlari kullanarak set etmemize yaramaktadir. Linux'dan alinmistir :) init_exceptions(); Bu yordam sistemde bulunan exceptionlari idt tablosuna set etmek suretiyle gerceklestirilir. init_exceptions() yordami irq.c dosyasi icinde bulunmaktadir. Herbir exception, sisteme boyle bir kesme istegi geldigi zaman hangi prosedurun calisacagi, prosedurun hangi segment icinde oldugu, kac tane parametreye sahip oldugu (burada 0), kesmenin tipi (TRAP_GATE) ve tanimlayici nin mevcut oldugu bilgileriyle birlikte set_idt fonksiyon ile birlikte ilgili idt girdisine yazilir. set_idt(&__enux_idt[0], (unsigned long)divide_error, SELECTOR_KRN_CS, 0, TRAP_GATE|PRESENT); orneginde __enux_idt adli idt tablosunun 0 inci kesme istegi yani divide error exceptionu geldigi zaman sisteme divide_error proseduru calisacaktir. Bu prosedur SELECTOR_KRN_CS segmentinde TRAP_GATE olarak tanimlidir. Teker teker butun ontanimli exception bilgileri tabloya girildikten sonra, 17 ile 32 numarasi arasinda bulunan idt entryleri reserved seklinde ayriliyor. Su an hicbir istek orada isleme tabi tutulmayacak. Simdi artik elimizde konsolu tanimli ve exceptionlari tanimli bir sistemcik var. Fakat daha yolun cok basindayiz. Bundan sonra yapacagimiz ilk is klavyeyi tanimlamak olacak. init_keyboard(); Bu prosedur, klavye kesmesini aktif hale getirmektedir. Yapilan islem cok basittir. Ilk olarak 0x71 numarali idt girdisini keyboard icin tanimlamak ve her gelen 0x71 nolu kesme servisini keyboard_interrupt prosedurune yonlendirmek. Peki neden 0x71? Bundan once bilindigi üzre IRQ0 0x70 numarali kesme servisini veriyordu. IRQ1 kesmesini saglayan ise klavyedir. Tanim olarak keyboard.c dosyasi icindedir. Herhangi IRQ1 kesmesi geldigi zaman yani klavyeden bir girdi yapildigi zaman, keyboard.c dosyasinda da tanimladigi gibi entry.s icindeki keyboard_interrupt proceduru calisacaktir. Butun gerekli kayitcilarin bilgileri yigitta saklandiktan sonra ise keyboard.c dosyasi icinde tanimli olan keyboard_handler proseduru calisacaktir. Bu fonksiyonu cagirmadan once movb $0x20, %al outb %al, $0x20 assembly komutlari calistirilmistir. Bu PIC'lere interrupt bilginizi aldim, yeni kesme gonderebilirsiniz seklinde PIC'lerin sonraki kesme isteklerine cevap verebilmesine imkan saglayan bir nevi ACK sinyali gondermektedir. keyboard.c icindeki keyboard_handler fonksiyonu 0x60 numarali port olan keyboard portundan bilgiyi aliyor, yani basilmis olan karakterin scan kodunu aliyor, onu key degiskenine atiyor. Daha sonra genel bir switch sorgusuyla klavye denetcisinden gelen karakterine scan kodu belirli degerlerle karsilastirilip ascii karsiliklari ch degiskenine atiliyor. Artik bundan sonraki asamalarda, ch degeri, sistemin o an bulundugu konuma gore degerlendirilip isleme tabi tutuluyor. Ornegin, eger ch degeri 0 ise herhangi bir islem yapilmamaktadir, eger 0 (null) degilse global out degiskeni; bu keyboard_buffer in en son kacinci numarada oldugunu gosterir; 255 ile kontrol edilir, 255 ise maximum arabellek buyuklugudur. Eger keyboard_buffer dolu ise konsola cikti olarak, "Buffer Full" yazilir. Eger klavye arabelligimiz olan keyboard_buffer dolu degilse, backspace karakteri kontrolu yapiyoruz. Backspace karakteri basilmis ise o zaman keyboard_buffer index degiskeni olan out 0 ile kontrol ediliyor ve out un gosterdigi yer null degeri ile ataniyor sonra out bir azaltiliyor boylece keyboard_buffer icinde bacspace(geritus) islemi gerceklesmis oluyor. Eger durum boyle degilse yani out degeri 0 i goseriyorsa herhangi bir eksiltme yapilmayarak keyboard_buffer[-1] degeri elde etmeden - bu da page_fault a neden oluyor - if sorgusundan cikiyoruz. Daha sonra ekrana girdigimiz karakteri yazdiyor ve eger karakterimiz \n karakteri ise wake_up() prosedurunu calistiriyoruz. wake_up prosedurun yaptigi is wait_queue kuyrugunda bekleyen bir surec var mi diye kontrol etmek daha sonra da varsa bu surecin keyboard_buffer'ina keyboard_buffer i kopyalamak wait_queue den alip ready_queue'ye bu sureci eklemekten ibarettir. Ve sched() fonksiyonunu calistirarak surec gecis islemini gerceklestiriyoruz. Artik surecimiz calisan sureclerin icindedir. init_syscall(); syscall.c dosyasi icinde tanimlidir. init_syscall proseduru ile birlikte kesme tablosunun 0x80 inci girdi degerini bizim kullanici kesmemiz icin tanimliyoruz. Boylece kullanici surecleri icinde int 0x80 assembly kodu ile birlikte islemciye kesme istegi gonderebiliyor hale geliyoruz. "int 0x80" istegi ile birlikte entry.s dosyasi icinde __sys_call fonksiyonu calismaktadir. __sys_call onceki kesme isteklerinin calisma mekanizmasindan biraz daha farkli calismaktadir. Standart olarak butun kayitcilar yigit'da saklaniyor. Daha sonra %ecx e 2 degeri atanarak %eax kayitcisinin icerigi 2 sola shift ediliyor bu da %eax kayitcisini 4 ile carpmak demektir. Bunu yapmamizdaki amacimiz; __enux_syscall_table nin temel adresinden itibaren herbir girdinin 4 byte - 4 byte buyuklugunde bulunmasidir. Yani __enux_syscall_table diyelim ki 0x00800000 adresinde bulunsun __enux_syscall_table[1] ise 0x00800000 + 4 adresinde bulunmaktadir. Bunu yapmamizdaki amacimiz ise %eax ile gonderdigimiz degerin aslinda __enux_syscall_table icinde saklanmis olan prosedurun adresini tam olarak elde etmemizdir. Bu kaydirma isinden sonra ters olarak kayitcilari %esi, %edx, %ecx, %ebx olarak yigita sakliyoruz. ve %edi kayitcisina __enux_syscall_table 'nin %eax indexinin adresini atiyoruz. Daha sonra %edi degerini 0x800000 ile topluyoruz. Bunun neden, yani bu toplama isteminin nedeni, kullanici sureclerinin cekirdegin bulundugu yeri 0x800000 temel adresinden baslayarak gormesidir. ve normal olarak %edi de bulunan degeri call ediyoruz. Bundan sonra artik %eax ile gonderdigimiz servisimiz hangisi ise __enux_syscall_table icinden ayitlanacak ve o fonksiyon cagirilacaktir. Butun fonksiyonlarin standart olarak 4 tane parametresi olmak zorunda degildir. Kayitcilari dolayisiyla system_call a gonderdigimiz parametreleri tersten yigitta saklamis oldugumuz icin ve C'deki fonksiyonlarin parametreleri yigittan duz mantikta cektigi icin ilk gelen parametre her zaman %ebx ile gonderdigimiz parametre olacaktir. Simdi sirasiyla su ana kadar yazilmis olan sistem cagrilarini aciklayalim: Butun sistem cagrilarini aciklarken izleyecegimiz yol __enux_syscall_table deki siradir. Ilk gelen sistem cagrisi __enux_write'dir. Goruldugu uzre bu sistem cagrisi parametre olarak bir str degiskeni aliyor. Bu degisken printk cekirdek prosedurune yonlendiriliyor. str degiskeni %ebx ile gelen ekrana yazilacak olan dizgenin temel adresini saklamaktadir. Hemen burada ekleme yapmak istiyorum. Normal olarak kullanici sureclerinin cekirdegi 0x800000 adresinden itibaren gordugunu bildigimize gore aslinda bu fonksiyon icinde str = str + 0x800000 yapmamiz (bu mantiksal olarak gosterilmis halidir, casting islemlerini goz ardi ederek yaziyorum) gerekmektedir. Ikinci gelen sistem cagrisi metodu ise __enux_fork'dur su an cok ilkel bir cekirdek oldugu icin POSIX isletim sistemlerinde kullanilan fork ile yakindan alakasi yoktur. Parametre olarak olusturulacak surecin temel adresi girilmektedir. Ve bu parametre _exec kernel fonksiyonuna aktarilmaktadir. Ve temel adresi offset olan bir surec olusturulmus olmaktadir. Ucuncu olarak da __enux_read vardir. Bu da POSIX standartlarindaki bir isletim sistemindeki read sistem cagrisi ile uzaktan yakindan alakali degildir. Tek yaptigi klavyeden bilginin alindigini ve bu bilgiyi de referans olarak gonderdigimiz buffer degiskenine aktarmaktir. Söyle ki; boyle bir sistem cagrisi yapan sürec, calisan surecler listesi olan ready_queue'den silinir, daha sonra wait_queue'ye eklenir. Artik surecimiz calismamakta bir isin gerceklesmesini beklemektedir. Daha sonra current süreci calisan sureclerin basinda bulunan süreci alir ve surec takasi icin sched() calistirilir. Artik bizim sürec bekleme kuyrugundadir. Ve keyboard.c dosyasi icinde tanimladigimiz wake_up() proseduru calistirilmadigi surece beklemede kalacaktir. Bu surecin kaldigi kisim, yani uyandigi zaman devam edecegi kisim ise sched(); cagrisinin hemen altidir. Simdi surecimizin beklemede oldugunu dusunelim ve klavyeden rastgele biseyler girdigimizi ve enter tusuna(\n karakteri) bastigimizi dusunelim. Keyboard_handler icinde \n karakteri goruldugu zaman wake_up calistirilacak ve keyboard_buffer bilgisi surecin tty'si icindeki keyboard_bufferine yazilacaktir ve wait_queue de bulunan bu surec ready_queue'ye aktarilacaktir. Sonra da scheduler calistirilacaktir. Simdi artik bizim surec calisan surecler icindedir. Ve calisma ani geldigi zaman; for(i = 0; i < 255; i++) buffer[i] = current->tty->keyboard_buffer[i]; for(i = 0; i < 255; i++) current->tty->keyboard_buffer[i] = 0; kodlari calistiracaktir. Bunlar da surecin tty'si icindeki keyboard_bufferi referans olarak girdigimiz buffer degiskenine yazar ve tty icindeki keyboard_bufferi temizler. Boylece sistem cagrisi isini halletmis olmaktardir. Henuz __enux_open, __enux_close, __enux_cls sistem cagrilarini tamamlamadim. Sira __enux_exit e geldi. Bu sistem cagrisi o anki current degerine sahip surecin cikis islemini gerceklestir- mektedir. Yani ready_queue'den silinmesini.. init_paging(); Sayfalama(paging) mekanizmasinin aktiflerstirildigi bolumdur. Sirasiyla pg_dir (sayfa dizini), pg_table_0, pg_table_1 pg_table_2 sayfa tablolari ilklenecektir. Bir tane sayfa tablosu toplam olarak 4K x 1K = 4MB adresleme icin kullanilabilir. Bu da sayfa dizinde sadece bir girdiye estir. Sabit olarak pg_dir i 0x2000 adresine koyuyoruz. Sirasiyla pg_table_0 0x3000 adresine, pg_table_1 0x4000 adresine, ve pg_table_2 ise 0x5000 adresine konulmaktadir. Ve sayfa dizinine de bu sayfa tablolari ilklenmektedir. Gecerli bayrak degerleri ile birlikte. Sira geldi sayfa tablolarinin icerigini doldurmaya. for(i = 0; i < 1024; i++){ pg_table_0[i] = (i * 4096)|PTE_PRESENT|PTE_WRITE|PTE_USER; pg_table_1[i] = (i * 4096 + 0x400000)|PTE_PRESENT|PTE_WRITE|PTE_USER; pg_table_2[i] = (i * 4096)|PTE_PRESENT|PTE_WRITE|PTE_USER; if(i < 0x100) frame_map[i] = 1; } butun sayfa tablolarimiz gecerli degerlerle dolduruluyor. Dikkat edilirse pg_table_2 ile pg_table_0 ayni degerleri gostermetedir. Bunun amaci onceden de bahsettigimiz gibi kernel modunda calisan kullanici surecleri 0x800000 (8 mb = 3. page table) adresinden itibaren adresleme yapmaktadirlar. Ve en alttada 1 mb temel olmakla birlikte sayfalarin dolu ya da bos oldugunu (kullanip kullanilmadiklarini) gosteren bir array ilklenmektedir. bundan sonraki asama ise islemci duzeyinde sayfalama mekanizmasini aktif hale getirmektir. init_ttys(); Bu fonksiyon 4 tane konsol olusturmaktadir. Herbir konsol, 2 sayfa buyuklugunden olusmaktadir. bunu palloc ile birlikte ayarladigimiz bos sayfalara aktarmaktayiz. Sonra konsolun video arabellegini temizleyip klavye arabellegini de temizlemekteyiz. Daha sonra olusturulmus olan konsollarin descriptorlarinin adreslerini tty_addresses[] arrayinde tutmaktayiz. Sonra herbir teletype e write_to_tty ile ilk meslarini olan "press enter to activate ttyX" mesajini yazdiriyoruz. init_ttys() nin tanimli oldugu dosya olan tty.c de üzerinde durulmasi gereken 2 tane daha fonksiyon bulunmaktadir. Bunlardan birincisi load_tty, digeri de store_tty. Isimlerinden de anlasilacagi gibi load_tty istenilen konsolu atif konsol hale getirmek icin for(i = 0; i < 4000; i++){ video[i] = tty->video_buffer[i]; } dongusu ile video buffer bitmap i/o adresine olusturdugumuz teletype in icerigini aynen kopyaliyoruz ve tty mizin cursor lokasyonlarini global locx ve locy degerlerine atayip set_cursor() ile ekrandaki cursorun yerini tayin ediyoruz. store_tty ise tam tersi islemi yapmaktadir. mevcut screen buffer bilgilerini aktif olan tty degiskenine aktarmaktadir. Bu fonksiyonlari keyboard.c icinde tanimli olan keyboard_handler fonsiyonunda F1 tusuna basildigi zaman cagrilan change_tty fonsksiyonu icinde gerceklestirmekteyiz. init_task(); Sira init surecini olusturmaya geldi. init_task ile birlikte bir tane gecici TSS segmenti olusturuyoruz. sonra da _exec((unsigned long)init) ile birlikte init sureci olusturuyoruz. Simdi recursive olarak cagrilan fonksiyonlari inceleyelim. _exec(); process.c icinde tanimlidir ve create_task() fonksiyonu ile birlikte olusturulan new surec descriptorunun status degerini TASK_READY yapar, surecin calisacagi yani surec gecisi sirasinda calisacak olan eip degerini parametre olarak gelen code_offset degiskenin gosterdigi deger olarak atar ve ready_queue kuyruguna ekler. Boylece sched(); bu sureci secme firsati yakalamis olur. create_task(); process.c icinde tanimlidir ve 3 sayfa buyuklugunde bir alan ayarlar ve bu adresi surec descriptoru icin ayrilmis alan olarak atar. Yeni surecin adres araligini ayarlar, tss segmentini olusturur. Konsolunu atar ve olusturulmus olan tss segmentini global descriptor tablosuna yazar. set_address_space(); Herbir surecin bellegi gormesi farklidir. O yuzden herbir surec ilklenirken kendine ozgu sayfa tablolari ve dizinleri olusturulur. Bu islem sistem icin olusturdugumuz sayfa tablolarina benzemektedir.(memory.c) fakat sureclerin pg_directory[3] degeri kernelin pg_dir[2] degerine esittir. Bunun nedeni onceden belirtigimiz gibi surecler cekirdegi 0x800000 adresinden itibaren gormektedir. set_tss(); sürec icin ayarlanmis olan tss segmentinin iceriginin doldurulmasi islemleri burada gerceklestirilmektedir. set_task_gdt(); Icerigi hazirlanmis olan tss burada global descriptor tableye yazilir. butun hepsi bu kadar. :) Sira geldi kernel.c icindeki son cagri olan init_scheduler(); Scheduler algoritmasi sched.c dosyasi icinde tanimlidir. IRQ0 degeri ve idtki karsiligi 0x70 timer aygitina aittir. init_scheduler() icinde idt nin 0x70 = 112 degeri timer kesmesi olan timer_interrupt fonksiyonunu gosterecek sekilde aktif hale getirilir. Ve o IRQ degerinin maskesi kaldirilir. Boylece belli araliklarla timer kesmesi islemciye yapilir ve her gelen kesme sonrasi da timer_interrupt (entry.s) fonksiyonu cagrilir. Bu fonksiyon da gerekli kayitcilarin bilgilerini yigita attiktan sonra sched() C fonksiyonunu cagirir. sched() cok ilkel bir scheduler algoritmasidir. Dikkat edilirse, FCFS mantiginda ve cembersel bir mantik izlemektedir. Yani liste bir kere dolasildigi zaman bastan itibaren surecler aktif hale getirilmektedir. Burada bir kac noktaya deginmek istiyorum. Intel mimarisinin tanimi uzre calisan bir surec ten sonra kendisi tekrar cagrilamaz. Cunku bir surec islemcide calisiyorsa TSS deki busy bit set edilmis durumdadir. Ve switch_to ile busy olan bir TSS e jump edilemez. O yuzden current_id diye bir global degisken tuttuk ve boylece islemcide calisan surecin id'si o degiskende saklanmaktadir. ve switch_to oncesi gecis yapilacak surecin id'si ile islemcide calisan surecin id'si karsilastiriliyor, eger ayni degil ise gecis switch_to ile yapiliyor eger ayni ise mimari kisitlamasi oldugu icin ellenilmeyip yani gecis yapilmiyor ve ayni surec zaten istenildigi gibi islemcide calisiyor birakiliyor. Simdiye kadar yapilan calismalarin daha oturakli bir sekilde anlasilmasi icin ornek bir surec calisimi mekanizmasini hazir kod uzerinde uygulanmis halini anlatmak istiyorum. Dikkat edilirse; process.c icinde _exec((unsigned long)init); diye bir init sureci olusturulmus. Bu init sureci init.c dosyasi icinde tanimlidir. Bu surecin yaptigi islere bakacak olursak. void init(void) { int i; printk("INIT: executing shell...\n"); do_syscall(1,shell,0,0,0); do_syscall(1,forever,0,0,0); for(;;); } kernel seviyesinde tanimli printk ile konsola mesaj yazidiyor ve sistem cagrilariyle shell adresindeki bir surec olusturuluyor. Bu asamada sistemde 2 tane surec bulunmaktadir. Bir tanesi ilk olusan init sureci digeri de simdi olusturdugumuz shell sureci. Daha sonra tekrar gorulecegi gibi sistem cagrilariyla forever adresli bir surec daha olusturuluyor. forever adresi init.c dosyasinda tanimlidir. Ve init sureci sonsuz bir dongude beklemeye giriyor. Bu islemlerden sonra shell ve forever sureclerinin neler yaptigini gorelim. forever kodlardan da gorulecegi gibi void forever(void) { for(;;); } sonsuz bir dongu icinde calismaktadir. Shell ise belli basli bazi islemleri yapmasi amaciyla olusturulmus bir komut yorumlayicisidir. shell.c dosyasi icinde tanimlidir. void shell(void) { char *prompt = "$ "; char buffer[256]; while(1){ do_syscall(0, (unsigned long)prompt, 0,0,0); do_syscall(2, (unsigned long)buffer,0,0,0); //wait on keyboard if(strcmp(buffer, "ps",2) == 0) ps_command(); else if(strcmp(buffer, "halt", 4) == 0){ do_syscall(0,"System halted...\n",0,0,0); cli(); for(;;); } else if(strcmp(buffer, "exit", 4) == 0){ do_syscall(5,0,0,0,0); } else if(strcmp(buffer, "help", 4) == 0){ print_help(); } else do_syscall(0, "Unrecognized command\n",0,0,0); } shell_exit: } sistem cagrilariyla ekrana ilk olarak prompt degiskenininde tutulan dizge yazilmaktadir. Daha sonra 2 numarali sistem cagrisi olan __enux_read cagrilmaktadir. Bu da klavye enter tusuna basilasaya kadar beklemekte ve enter tusuyla alinan arabellek bilgisini buffer degiskenine atmaktadir. Daha sonra bu buffer degiskeni icinde gelen dizge sirasiyla ps, halt, exit ve help dizgeleriyle karsilastirilmakta ve karsilastirmalarin uygun olan kisimlari calisirilmaktadir. Burada onemli bir nokta butun mesaj ve okuma islemlerinin sistem cagrilariyla yapilmasidir. Enux'un ilkel kodlarini http://www.enderunix.org/halil/enux.tar.gz adresinden indirebilirsiniz Halil Demirezen [halil at enderunix.org]