/* 
   Bu belgenin telif hakları Necati Ersen ŞİŞECİ'ye aittir. 
   Kök: http://acikkod.org 
   İlk baskı: 2004-02-24
   Son değişiklik: 2004-02-24 
   Bu döküman Açıkkod.ORG Belge Yazım ve Dağıtım Lisansı ile dağıtılmaktadır. 
*/ 

Linux Kernel Modülleri ve Sistem Çağrıları


Linux'te aygıt sürücüleri, iki şekilde kullanılabilir.
Birincisi, direk olarak çekirdeğe gömülü olarak çalışmasıdır(static loading).
Bu durumda çekirdek başladıktan sonra bu modulu de çalıştıracaktır. İkincisi
ise çekirdeğin içinde olmayıp, sonradan yüklenecek şekilde kullanılır(dynamic
loading). Bu işlem ise, çekirdek sistemde çalışırken yapılır.

Dinamik yuklenen bir modulun içindeki, bir fonksiyon, mödül yüklenmeden önce
çalıştırılmak istenirse hata verecektir. Modül yüklendikten sonra bu fonksiyon
rahatlıkla kullanılabilecektir.

Linux, açılırken tipik olarak bazı modulleri kendisi yükler. Sistem açıldıktan
sonra yüklenen modulleri (statik yada dinamik) /proc/modules dosyasından
görebiliriz.

Modüller genel olarak aygıt sürücüsü olarak kullanılırlar. Bununla birlikte
modüller herhangi bir fonksiyonu da gerçekleştirebilir.


Modül Organizasyonu

Bir modül yüklenirken, çekirdek adres aralığında (kernel space) süper kullanıcı
modunda çalıştırılır. Yüklenen modül, kernel adres aralığındaki veri
yapılarından okuyabilir ya da yazabilir.

Genel olarak kernel tarafından, kullanıcılara sunulan (export) fonksiyonlar ve
hangi fonksiyonların hangi modül tarafından kullanıldığını /proc/ksyms
dosyasına bakarak görebiliriz. Çekirdek tarafından export edilmiş fonksiyonlar,
modül tarafından direk olarak kullanabilir. Bir modül, başka bir modül
tarafından export edilmiş fonksiyon ve değişkenleri kullanabilir yada kendisi,
fonksiyon ve değişken export edebilir. Modül yazılırken dikkat edilmesi gereken
noktalardan birisi de, export edilmiş değişkenlerin değerini değiştirmektir.
Çünkü, bu değerler sistemin kararlılığını etkileyebilir. Bu yüzden ne
yazdığımızdan emin olmadan bu değişkenlerin değeri değiştirilmemeli, sadece
okunmalıdır.

Kernel konfigürasyonunu hatırlayalım.
SCSI desteği kısmına göz atalım.

Eğer derleyeceğimiz çekirdekte SCSI desteği olmayacaksa SCSI support seçili
olmamalıdır.
< > SCSI support

SCSI desteğini modül olarak sağlamak için aşağıdaki şekilde kullanırız.
<M> SCSI support
--- SCSI support type (disk, tape, CD-ROM)
<M> SCSI disk support (NEW)

SCSI desteği çekirdeğin içinde gömülü olması için ise bu şekilde kullanırız.
<*> SCSI support
--- SCSI support type (disk, tape, CD-ROM)
<*> SCSI disk support (NEW)


Modullerin Yapısı

Dinamik olarak yüklenecek çekirdek modüllerinin en az iki fonksiyon
içermelidir. Bu fonksiyonlar modül yüklenirken ve bellekten atılırken
kullanılan init_module() ve cleanup_module() fonksiyonlarıdır.

#define __KERNEL__
#define MODULE
#define LINUX

#include <linux/kernel.h>
#include <linux/module.h>
#include ...

...

int init_module (void)
{
...
}

void cleanup_module (void)
{
...
}
...

Genel olarak bir çekirdek modülünün yapısı bu şekildedir. Bu şekilde sadece
iki fonksiyonu olan çekirdek modülü kullanmak mümkündür.

init_module() ve cleanup_module() fonksiyonlarına, modulun ilk yüklendiği
zaman ve bellekten atıldığı zaman yapması gereken işler yazılır. Örneğin
bir aygıtın register edilmesi ve unregister edilmesi bu fonksiyonlarla
yapılmalıdır.

init_module() fonksiyonu, modül yüklenirken çalıştırılır.
/sbin/insmod komutu ile bir modülü yüklemek istediğimiz zaman,
sistem tarafından modülün içerisinde bulunan init_module fonksiyonunu
çalıştırır.

Bir modül sisteme yüklendiği zaman, çekirdeğin yeni eklenen fonksiyonları
kullanabilmesi için bu fonksiyonların register edilmesi gerekir. Eğer
modül statik olarak yüklenmişse, çekirdek boot işlemi sırasında bu
fonksiyonları register eder. Ama dinamik olarak yüklenen bir modül için bu
işi init_module() fonksiyonu yapar. Bellekten atılan modüller içinde
unregister olayı, aynı şekilde geçerlidir. Bu işlemi ise, cleanup_module()
fonksiyonu yapar.

Kullanıcı tarafından, modül içinde bulunan bir fonksiyon kullanılmak istendiği
zaman bir sistem çağrısı yapılır, ve çekirdek, programı kullanılmak istenen
fonksiyona yönlendirir.

Bir modül derlendikten sonra çalıştırılabilir bir kod haline gelir.
Modül yüklenirken;
1. Ilk olarak bir çekirdek fonksiyonu olan create_module() fonksiyonu çalışır
   ve yüklenecek olan modülü çekirdek adres aralığına yükler.
2. Bu işlemden sonra modülün kullandığı daha önceden export edilmiş çekirdek
   sembolleri aranır. Bu yüzden export edilmiş bir sembol kullanan modül,
   bu sembolü export eden modülden sonra yüklenmek zorundadır. Bu işlemi bir
   çekirdek fonksiyonu olan get_kernel_syms() yapar.
3. create_module() fonksiyonu, modül için bellek aralığı ayırır.
4. init_module() sistem çağrısı ile modül yüklenir. Burada daha sonradan
   yüklenecek modüller tarafından kullanılacak olan semboller export edilir.
5. Son olarak insmod, init_module() fonksiyonunu çağırır.

cleanup_module() fonksiyonu ise, modul bellekten atılırken çalıştırılır.
/sbin/rmmod komutu, daha önceden yüklenmiş bir modülü bellekten atmak
için kullanılır ve sistem cleanup_module() fonksiyonunu çalıştırır.

Basit bir modülü inceleyelim.

#define __KERNEL__
#define MODULE
#define LINUX

#include <linux/module.h>
#include <linux/version.h>
#include <linux/fs.h>

int init_module (void)
{
  printk (KERN_EMERG "Module yükleniyor.!\n");
  return 0;
}

void cleanup_module (void)
{
 printk (KERN_EMERG "Module bellekten atılıyor.\n");
}

#define __KERNEL__ 
Programın normal bir program olmadığını, kernel ile ilgili olduğunu gösterir.

#define MODULE 
Programın bir kernel modülü olduğunu gösterir.

#define LINUX
Linux için olduğunu gösterir.

#include <linux/module.h>
Modul ile ilgili tanımlamaların bulunduğu header dosyasıdır.

#include <linux/version.h>
Kernel'in versiyon bilgisini gosterir.

#include <linux/fs.h>
Veri yapıları gibi önemli tanımlamaları içerir.


Yukarıdaki modulu bir obje olarak derlememiz gerekmektedir.
Derleyiciye, yazdığımız kodun normal bir program olmadığını,
-Wall opsiyonu ile loader ı geçmesini ve
-c opsiyonu ile de linker ı çağırmaması gerektiğini belirmemiz gerekir.

siseci:~/kernel# gcc -Wall -c ornek.c
siseci:~/kernel# ls -l ornek.o
-rw-------   1 root     root         1192 Jan  1 02:36 ornek.o
siseci:~/kernel# insmod ornek
Module yükleniyor.!

siseci:~/kernel# lsmod
Module                  Size  Used by
ornek                    196   0  (unused)
....

siseci:~/kernel# rmmod ornek
Module bellekten atılıyor.

MODULE tanımalaması, bir desteğin modül olarak mı yoksa çekirdeğin içinde
gömülü olarak mı derleneceğini belirtmek için kullanılır. Sürücüleri
incelediğimiz de;

#ifdef MODULE
int init_module(void)
#else
int i2c_tuner_init(void)
#endif

şeklinde bir #ifdef bloğu ile, modül olarak derlenmesi durumunda ve çekirdeğe
gömülü olacak şekilde derlendiğinde hangi fonksiyonun kullanıldığını, nasıl
anladığını görürüz.

MODULE olarak derlenecekse, gcc'nin -D parametresi ile belirtilmeli ve ya
define ile belirtilmelidir.

siseci:~/kernel# gcc -Wall -c -D__KERNEL__ -DMODULE -DLINUX ornek.c

Yukarıdaki örnekte #define ile belirtilmiş tanımalamaları kaldırıp, bu şekilde
de derleyebiliriz.

Ilk bakışta, C bilen birisi için iki nokta dikkati çeker. Birincisi main
fonksiyonun olmaması, ikincisi ise printf yerine printk kullanılmasıdır.

printk ile printf arasindaki tek fark, printk da loglevel i belirmemizdir.

#define KERN_EMERG    "<0>" 
#define KERN_ALERT    "<1>"  
#define KERN_CRIT     "<2>"  
#define KERN_ERR      "<3>"  
#define KERN_WARNING  "<4>"  
#define KERN_NOTICE   "<5>"  
#define KERN_INFO     "<6>"  
#define KERN_DEBUG    "<7>"  
Bu konu ile ilgili ayrıntılı bilgiyi syslog(2) man page inde bulabilirsiniz.


Sistem Çağrıları

İşletim sistemlerinin temelinde, sistemde yapılacak tüm işlemleri yapan
fonksiyonlar vardır. Linux'te bu fonksiyonlar sistem çağrısı denir.

Kullanıcının yaptığı işlemler çekirdeğe iletilir ve çekirdek ilgili sistem
çağrısı çalıştırır.

Özet olarak, kullanıcı tarafından yapılan işlemlerin, çekirdek tarafından
yapılan kısmıdır diyebiliriz.

Dosya açmak, kapatmak, okumak, yazmak gibi bir çok sistem çağrısı vardır.
SYS_open, SYS_close dosya açarken (veya oluştururken) / dosyayı kapatırken,
SYS_read ve SYS_write ise okuma / yazma yaparken kullanılan sistem çağrılarıdır.

Sistem çağrıları /usr/include/sys/syscall.h dosyasında belirtilmiştir.

#define SYS_read                3
#define SYS_write               4
#define SYS_open                5
#define SYS_close               6

int sys_open (const char *filename, int mode);
int sys_close (unsigned int fd);
int sys_read (unsigned int fd, char *buf, unsigned int count);
int sys_write (unsigned int fd, char *buf, unsigned int count);

Linux, IA32 mimarisinde sistem cagrilarini karsilamak icin iki metod kullanir:
        1. lcall7/lcall27 gates
        2. INT 0x80 software interrupt.

Asil Linux uygulamalari INT 0x80'i kullanirken, diger bazi UNIX ler de lcall7
mekanizmasini kullanir.

Sistem çağrısı numarasını ve parametreleri register'lara yazar ve 0x80 nolu
interrupt'ı çağırır.

Sistem cagrisi istegini, cagri anindaki CPU register'larinin durumu belirler.
EAX register'inin alacagi deger, hangi sistem cagirisinin calistirilacagini
tespit eder. Diger register'lar, EAX'in aldigi deger gore parametrik deger
tasirlar.

Bu kısmın detayları için /usr/include/asm/unistd.h dosyasına göz atabilirsiniz.
Sistem çağrılarının nasıl çağrıldığını, sistem cağrılarının parametrelerinin
nasıl işlendiğini görebilirisiniz.

Bizlerde, kendi yaptığımız modüllerde sistem çağrılarını kullanabiliriz.

Linux'te strace komutu ile, çalıştırılan programların kullanığı sistem
çağrılarını görebilirsiniz.

1.c
--------------------
void main()
{
 printf("Deneme\n");
}

Derleyip, strace ile inceleyelim.

siseci:~/kernel# make 1
siseci:~/kernel# strace ./1
execve("./1", ["./1"], [/* 32 vars */]) = 0
...
...
write(1, "Deneme\n", 7)                 = 7
....


execve ile yeni bir process başlatılıyor.
write ile 1 ile gösterilen dosyaya 7 bayt uzunluğunde "deneme\n" yazılıyor.

ssize_t write(int fd, const void *buf, size_t count);

write fonksiyonun 3 parametresinden birincisi hangi dosyaya yazacağımızı
gösteriyor. Burada 1 olarak gösterilen dosya standart output (stdout) u
gösterir.

Linux'e baglandigimiz terminal de bir aygıttır. Aşağıdaki örnekte, tty1 i
kullanıdığımız görülmektedir.

siseci:~# ls -al /proc/self/fd/
total 0
lrwx------   1 root     root           64 Jan  1 03:05 0 -> /dev/tty1
lrwx------   1 root     root           64 Jan  1 03:05 1 -> /dev/tty1
lrwx------   1 root     root           64 Jan  1 03:05 2 -> /dev/tty1

0: stdin
1: stdout
2: stderr dir.


Sistem Cagrilarinin modullerde kullanilmasi

Modüllerimizde sistem çağrılarını aşağıdaki şekilde kullanabilirsiniz.

Aşağıdaki örneği inceleyelim.

#define MODULE
#define __KERNEL__
#define LINUX

#include <linux/module.h>
#include <linux/version.h>
#include <linux/fs.h>
#include <sys/syscall.h>

extern void* sys_call_table[];

asmlinkage int (*getuid_syscall)();

int init_module (void)
{
  getuid_syscall = sys_call_table[SYS_getuid];
  printk(KERN_EMERG "%d\n", getuid_syscall());
  return 0;
}

void cleanup_module (void)
{
 printk (KERN_EMERG "Module bellekten atılıyor.\n");
}

siseci:~/kernel# insmod 1.o
0
siseci:~/kernel# rmmod 1
Module bellekten atılıyor.


Bu örnekte, getuid sistem çağrısı kullanılıyor. Pointer yapısında bir fonksiyon
tanımlayıp, sys_call_table dan getuid() fonksiyonun adresini alip,
tanımladığımız fonksiyona atayıp kullanmamız gerekiyor.

Bu şekilde diğer sistem çağrılarını da kullanabilirsiniz.

Ilerleyen yazilarimizda, ornek bir aygit sürücüsünün nasıl çalıştığını inceleyeceğiz.

Necati Ersen SISECI

24 Şubat 2004