Temel Modül Fonksiyonları ve Kavramlar

Kernel modül yolculuğumuza devam ediyoruz.

printk(), pr_x()

Kernel modüller de daemon’lar gibi ekrana değil log dosyalarına yazar. Log üretmek için printk() fonksiyonunu kullanırız. k, kernel demektir. Kullanımı printf() gibidir. Varsayılan durumda bu fonksiyon mesajların /var/log/syslog dosyasına yazdırılmasını sağlar. Ama artık Debian 13 gibi modern sistemlerde bu dosya bulunmaz, onun yerine systemd tabanlı journal kullanılır. Biz çalışmalar yaparken sudo dmesg -W ile yeni logları takip edebiliriz arkadaki sistemin detaylarına takılmadan. Ya da dmesg --human vs.

Fonksiyon<linux/kernel.h> içerisindedir.

Kullanımı şu şekildedir:

printk(KERN_INFO "Merhaba Dunya!\n");

Burada virgül yoktur, dikkat. KERN_INFO bir macrodur.

Bakalım:

 5#define KERN_SOH	"\001"		/* ASCII Start Of Header */
 6#define KERN_SOH_ASCII	'\001'
 7
 8#define KERN_EMERG	KERN_SOH "0"	/* system is unusable */
 9#define KERN_ALERT	KERN_SOH "1"	/* action must be taken immediately */
10#define KERN_CRIT	KERN_SOH "2"	/* critical conditions */
11#define KERN_ERR	KERN_SOH "3"	/* error conditions */
12#define KERN_WARNING	KERN_SOH "4"	/* warning conditions */
13#define KERN_NOTICE	KERN_SOH "5"	/* normal but significant condition */
14#define KERN_INFO	KERN_SOH "6"	/* informational */
15#define KERN_DEBUG	KERN_SOH "7"	/* debug-level messages */

Burada ASCII SOH karakteri kullanılmaktadır. C’deki string concatenation kuralları ile birleşmektedir. O yüzden virgül kullanmamak lazım.


Bunun yerine doğruda pr_info(), pr_err() gibi makroları da kullanabiliriz. pr_info("Merhaba Dünya") gibi.

576/**
577 * pr_info - Print an info-level message
578 * @fmt: format string
579 * @...: arguments for the format string
580 *
581 * This macro expands to a printk with KERN_INFO loglevel. It uses pr_fmt() to
582 * generate the format string.
583 */
584#define pr_info(fmt, ...) \
585	printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)

Tekrar hatırlayalım: Kernel modüller kernelin içerisine yerleştirildiği için biz user mod’daki kütüphaneleri kullanamayız. Örneğin standart C ve POSIX fonksiyonlarını kullanamayız. Çünkü bunlar, user mode programlar için oluşturulmuş kütüphanelerin içerisindedir. Örneğin printf() fonksiyonu, kernel modülleri tarafından kullanılamaz.

Biz kernel modüllerinin içerisinde yalnızca export edilmiş kernel fonksiyonlarını kullanabiliriz. Örneğin buradaki gibi yerlerden bakabiliriz. Fakat dokümantasyon mükemmel değildir, kaynak koda bakmak gerekebilir.

/proc/modules ve lsmod

Yüklü olan modülleri procfs içerisinde görebiliriz. /proc/modules dosyasına bakarak yüklü olan modülleri görebiliriz. Dosyanın her satırında bir kernel modül bilgisi vardır.

Örnek:

$ grep helloworld /proc/modules
helloworld 12288 0 - Live 0x0000000000000000 (OE)

Bizim helloworld modülümüz şu anda kernelde yüklü.

Benzer şekilde lsmod komutu ile de listeleyebiliriz. Bu komut da zaten bilgileri bu dosyadan almaktadır.

İşte kanıtımsı çıktı:

$ sudo strace lsmod 2>&1 | grep -C 3 "/proc/modules"

read(3, "BOOT_IMAGE=/vmlinuz-6.12.57+deb1"..., 4095) = 82
read(3, "", 4013)                       = 0
close(3)                                = 0
openat(AT_FDCWD, "/proc/modules", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
read(3, "helloworld 12288 0 - Live 0xffff"..., 1024) = 1024
read(3, "d_hda_intel,snd_hda_codec, Live "..., 1024) = 1024

lsmod daha güzel formatlama yapabilmektedir.

$ sudo lsmod | grep helloworld

helloworld             12288  0

Diğer Modüller ?

Eğer lsmod ya da /proc/modules ile bakarsanız aslında birçok kernel modülü olduğunu görebilirsiniz. Mesela

$ wc -l /proc/modules

96 /proc/modules

Bende 96 adet kernel modülü varmış o anda yüklü. Tamam, bir tanesi helloworld ama 95 adet de az değil hani!

Modüller boot sırasında yüklenebilir, işletim sisteminin bir parçası olarak yüklenebilir ya da bizim yaptığımız gibi daha sonradan yüklenebilir. Debian, Ubuntu, Fedora gibi genele hitap eden desktop dağıtımlarda kernel’i minimal tutup, statik kısmını, birçok özelliği modül olarak yüklemek başta sayı olarak fazla gözükse de günümüzde normal durumdadır. Gömülü bir Linux dağıtımı yapıyor olsaydık daha makul seviyelerde modül sayımız olabilirdi.

module_init() ve module_exit()

ELF formatı section’lardan oluşuyor. Şimdi bunların üzerinde bir duralım.

Öncelikle kaynak kodumuzu hatırlayalım:

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

MODULE_LICENSE("GPL");

int init_module(void){
  printk(KERN_INFO "Merhaba Dunya...\n");
  return 0;
}

void cleanup_module(void) {
  printk(KERN_INFO "Gule gule Dunya...\n");
}

Bunu derlediğimiz zaman çıkan dosya helloworld.ko olsun, onu biraz analiz edelim.

$ nm -n helloworld.ko | grep -E 'init_module|cleanup_module'

0000000000000000 T __pfx_init_module
0000000000000010 T init_module
0000000000000030 T __pfx_cleanup_module
0000000000000040 T cleanup_module

Her iki fonksiyonumuz da .text section içerisinde bulunuyor, yanlış bilmiyorsam T bu anlamda.

Alternatif:

$ readelf -S helloworld.ko
There are 48 section headers, starting at offset 0x291c8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000000  0000000000000000  AX       0     0     1
  [ 2] .text.unlikely    PROGBITS         0000000000000000  00000040
       0000000000000055  0000000000000000  AX       0     0     16

Index 1, .texte ait, 2 de öyle ama nadir çalışacak fonksiyonlar burada, derleyici bu şekilde çalışma sırasında cache locality vs iyileştirmek için bunları yapabiliyor, ama bu da .text alanı aslında.

$ readelf -Ws helloworld.ko | grep -E 'init_module|cleanup_module'

    44: 0000000000000040    21 FUNC    GLOBAL DEFAULT    2 cleanup_module
    46: 0000000000000010    28 FUNC    GLOBAL DEFAULT    2 init_module
    49: 0000000000000000    16 FUNC    GLOBAL DEFAULT    2 __pfx_init_module
    50: 0000000000000030    16 FUNC    GLOBAL DEFAULT    2 __pfx_cleanup_module

Bizim init ve exit fonksiyonlarımız 2 indeksli section’da yani .text.unlikely yani aslında .text içerisinde.

Burada ilginç bir şey yok


Biz istersek, ki artık önerilen budur, modülün giriş ve çıkış fonksiyonlarına rastgele isimler verip, bunları module_init() ve module_exit() makroları ile tanıtabiliriz. Eski kernellerde init_module() ve cleanup_module() kullanmak zorunluydu.

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

MODULE_LICENSE("GPL");

int merhaba(void){
  printk(KERN_INFO "Merhaba Dunya...\n");
  return 0;
}

void gulegule(void) {
  printk(KERN_INFO "Gule gule Dunya...\n");
}

module_init(merhaba);
module_exit(gulegule);

Bu kodu derlediğimi zaman, bu iki fonksiyonun da yine .text section’u içinde kaldığını görebiliriz.

Bu makro içerisindeki fonksiyonların öncesinde bildirilmiş olması gerekmektedir. O yüzden tipik olarak kodun en sonuna yazılırlar.

Bu iki macro şuna benzemektedir:

 80#ifndef MODULE
 81/**
 82 * module_init() - driver initialization entry point
 83 * @x: function to be run at kernel boot time or module insertion
 84 *
 85 * module_init() will either be called during do_initcalls() (if
 86 * builtin) or at module insertion time (if a module).  There can only
 87 * be one per module.
 88 */
 89#define module_init(x)	__initcall(x);
 90
 91/**
 92 * module_exit() - driver exit entry point
 93 * @x: function to be run when driver is removed
 94 *
 95 * module_exit() will wrap the driver clean-up code
 96 * with cleanup_module() when used with rmmod when
 97 * the driver is a module.  If the driver is statically
 98 * compiled into the kernel, module_exit() has no effect.
 99 * There can only be one per module.
100 */
101#define module_exit(x)	__exitcall(x);

Internal Linkage, static

C’de bir fonksiyon varsayılan olarak external linkage’a sahip olmaktadır. C’de namespace kavramı olmadığı için fonksiyonların isimlerinin çakışmasını önlemek, başımızın daha az ağırmasını sağlamak için olabilecek her yerde modüller içerisinde fonksiyonların static olarak tanımlanması önerilir. Aynısı global değişkenler için de geçerlidir.

Her ne kadar C’de namespace ya da scope özelliği yoksa da kernel modüllerinin ele alınış biçiminden dolayı problem çıkartmak çok da kolay değildir. Çünkü kernel modüllerin derlenmiş halleri aslında ELF dosyalarıdır ve modül yüklendiği zaman buralardaki sembollerin çalışan kernel sembolleri ile hemen çakışmasını beklemeyiz. Modüller bu açıdan biraz ayrıcalıklıdır, bir nevi ayrı bir program olarak yüklenir gibi düşünebiliriz sembol çözümleme vs açısından. AMA birçok sürücü/driver modül olarak derlendiği gibi statik olarak da kernel ile beraber derlenebilir. İşte bu durumda isim çakışmaları problem olacaktır.

Özetle, mümkün olduğunca static kullanmalıyız.

__init ve __exit

__init ve __exit birer makrodur.

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/init.h

adresinden tanımlamalarına bakabiliriz:

#define __init		__section(".init.text") __cold __latent_entropy	\
						__no_kstack_erase
//...
#define __exit          __section(".exit.text") __exitused __cold notrace

Görüldüğü üzere derleyici spesifik eklemeler ile fonksiyonların ELF dosyasında bulunacağı yerleri işaret ederler aslında.

Bunun çeşitli avantajları olabilir. Mesela __init ile işaretlenmiş fonksiyon, normalde init fonksiyonu, modül init’i bittikten sonra bellekten yer açmak için kernel tarafından atılabilir. Çünkü biz bu fonksiyonunun sadece init sırasında çağırılacağını belirttik. Benzer şekilde __exit fonksiyonu da eğer kernelin modül çıkarma, rmmod gibi, özelliği kapalıysa ya da modül doğrudan kernel içerisine derleniyorsa (bu durumda kernel kapanana kadar modül tek başına çıkış yapamaz zaten) hiç konmayabilir. Bu iki makro, bu tarz optimizasyonların yapılmasına imkan sağlamaktadır.

Bazen kernel açılırken,

Freeing unused kernel memory: 236k freed

gibi loglar görebiliriz. İşte bu tarz bellekte yer açmalar buralardan olur.

Hepsini birleştirdiğimizde modülümüz aşağıdaki gibi olmaktadır:

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

MODULE_LICENSE("GPL");

static int __init merhaba(void){
  printk(KERN_INFO "Merhaba Dunya...\n");
  return 0;
}

static void __exit gulegule(void) {
  printk(KERN_INFO "Gule gule Dunya...\n");
}

module_init(merhaba);
module_exit(gulegule);

Üstte belirtildiği gibi bakarsak bu sefer merhaba() fonksiyonun .text içerisinde değil .init.text içerisinde; gulegule() fonksiyonunun ise .exit.text içerisinde olduğunu görürüz.

sysfs ve Modüller

2003 yılında kernel 2.6 ile beraber sysfs dosya sistemi gelmiştir. procfs daha eski bir yapıdır ve sysfs, procfsteki çeşitli eksiklikleri gidermek için eklenmiştir. Güncel sistemlerde her iki dosya sistemi de bulunmaktadır tipik olarak. Her iki dosya sistemi de çekirdekle ilgili bilgileri dışarıya açmak ya da dışarıdan değiştirilmesine imkan sağlamak için vardır. sysfs, biraz da nesne yönelimli yaklaşımı benimser, daha derin hiyerarşide birçok dosya ve dizin barındırır. sysfs, tipik olarak /sys altına mount edilir.

Bir bir modül yüklediğimi zaman modül ile aynı isimde /sys/module/<isim> bir dizin yaratılır. Burası /proc/modulesten farkldır. sysfs çok daha detaylı bilgi verir.

Bizim modülümüz için bakalım:

$ ls -lah /sys/module/helloworld/

total 0
drwxr-xr-x   5 root root    0 Dec 20 19:53 .
drwxr-xr-x 190 root root    0 Dec 20 14:31 ..
-r--r--r--   1 root root 4.0K Dec 20 19:53 coresize
drwxr-xr-x   2 root root    0 Dec 20 19:53 holders
-r--r--r--   1 root root 4.0K Dec 20 19:53 initsize
-r--r--r--   1 root root 4.0K Dec 20 19:53 initstate
drwxr-xr-x   2 root root    0 Dec 20 19:53 notes
-r--r--r--   1 root root 4.0K Dec 20 19:53 refcnt
drwxr-xr-x   2 root root    0 Dec 20 19:53 sections
-r--r--r--   1 root root 4.0K Dec 20 19:53 taint
--w-------   1 root root 4.0K Dec 20 19:53 uevent

129-11920