Kernel Modülü Karakter Sürücü Örneği

Askere gitmeden önce linux’ün çekirdeğinde yazılım geliştirmek ile ilgili bu yazı dizisinin sonunu getirmek istedim. Bir önceki yazımda Linux işletim sistemindeki her şeyin birer dosya olarak ifade edildiğini söylemiştim. Aynı zamanda sistem çağrılarından bahsetmiştim. Kernel uzayındaki sürücüler ile kullanıcı uzayındaki programlar, /dev klasörü altındaki ilgili sürücü dosyaları ile iletişim kurarlar. Sürücüden veri okumak, sürücüye veri göndermek ya da sürücüyü yapılandırmak gibi işlemleri bu dosya işlemleri ile yapıyoruz. Geçen yazımda bahsettiğim sistem çağrıları aslında bu işlemlerin kullanıcı uzayındaki isimdir. “open”, “read”, “write” gibi dosya işlemlerinin yapıldığı fonksiyon adresleri “linux/fs.h” dosyasında aşağıdaki şeklinde tanımlanmıştır. Bu fonksiyon adresleri sürücüden istenilen dosya işlemini gerçekleştirmek için sürücü programları içerisinde tanımlanır.

struct file_operations 
{
  struct module *owner;
   loff_t(*llseek) (struct file *, loff_t, int);
   ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);
   ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
   ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
   ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t,loff_t);
  int (*readdir) (struct file *, void *, filldir_t);
  unsigned int (*poll) (struct file *, struct poll_table_struct *);
  int (*ioctl) (struct inode *, struct file *, unsigned int,unsigned long);
  int (*mmap) (struct file *, struct vm_area_struct *);
  int (*open) (struct inode *, struct file *);
  int (*flush) (struct file *);
  int (*release) (struct inode *, struct file *);
  int (*fsync) (struct file *, struct dentry *, int datasync);
  int (*aio_fsync) (struct kiocb *, int datasync);
  int (*fasync) (int, struct file *, int);
  int (*lock) (struct file *, int, struct file_lock *);
   ssize_t(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
   ssize_t(*writev) (struct file *, const struct iovec *, unsigned long,loff_t *);
   ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);
   ssize_t(*sendpage) (struct file *, struct page *, int, size_t,loff_t *, int);
  unsigned long (*get_unmapped_area) (struct file *, unsigned long, unsigned long, unsigned long,unsigned long);
};

 

Tüm karakter sürücülerinin, kullanıcı uzayındaki programlarla dosya işlemleri üzerinden iletişim kurabilmesi için bu fonksiyonları tanımlaması gerekir. Örneğin sürücüden veri almak için “read” operasyonu tanımlanır.  Yukarıdaki dosya operasyon “structure”‘ı bu operasyonları gerçekleştiren asıl fonksiyonların adreslerini tutar ve yazılan sürücüler de ilgili fonksiyonları tanımlamak için bu adresleri kullanır.

Her dosya işleminin her sürücüde uygulanmasına gerek yoktur. Örneğin grafik kartı sürücüsü “read” gibi operasyona gerek duymaz.  Sürücüde gerek duyulmayan fonksiyonların NULL olarak belirtilmesi tavsiye edilmiştir.  Bu yüzden sürücü programımızın başında bu fonksiyonları tanımlayan bir struct oluşturup içerisinde gerçekleştirmek istediğimiz operasyonlara ait fonksiyonları belirtmemiz gerekiyor.

struct file_operations fops = {
  read: device_read,
  write: device_write,
  open: device_open,
  release: device_release
};

Örneğin yukarıdaki veri yapısına bakarsanız sadece “read”, “write”, “open”, “release” işlemlerini yapan fonksiyonlar alınmış.  Hangi fonksiyonların sürücü tarafından kullanılacağını sürücü programının başında tanımlayıp onu kernel’e bildirmemiz gerekiyor.  Genellikle bu işlem için tanımlanan structure ismi fops olur.

Bir önceki yazımda minor ve major numaralarından bahsetmiştim. Daha önce de belirttiğim gibi, bu numaralar hangi cihaz sürücülerinin hangi cihaz sürücülerine bağlı olduğunu gösterir.  Sürücünün minor numarası, sadece sürücünün kendisi tarafından ilgili sürücü dosyasını (nodu) oluşturmak için kullanılır. Sisteme sürücü eklemek, aslında sürücünün kernel’e kayıt edilmesiyle olur. Buna “registering driver” diyoruz. Kernel’e sürücünün kaydedilmesi ile, kernel sürücüye bir major numarası verir. Bu numara, sürücünün kernel operasyonlarının gerçekleştirebilmesi için kernel tarafından kullanılır. Bu yüzden sürücü yazarken ilk yapılması gereken şey, sürücüyü kernele kaydetmektir.  Bunu “register_chrdev” fonksiyonu ile yapıyoruz. Bu fonksiyonla aslında kernelde major numarası istiyoruz.  Bu fonksiyonun negatif değer dönmesi kayıt işleminin başarısız olduğu anlamına geliyor.  Dikkat ettiyseniz, bu fonksiyonla  kernel’e herhangi bir minor numarası bildirilmiyor.  Çünkü kernel minor numarası ile ilgilenmiyor.

Kernel’in verdiği mojar numaraları “Documentation/devices.txt” dosyasında belirtilmiştir. Bazen kernel zaten atanmış olan major numaraları verebiliyor. Bu da sürücünüzün ve diğer sürücülerin çalışmamasına sebep olabilir.  Bu yüzden kernel’e size atadığı major numaranın dinamik olup olmadığını sorabilirsiniz.  Eğer “register_chrdev” fonksiyonuna verdiğiniz major numarası değişkeni “0” olursa,  geri dönecek değer dinamik major numarası olacaktır.  Bunun dez avantajı sabit bir major numarası olamadığından sürücü dosyası “node” oluşturmak için major numarasını bir şekilde öğrenmeniz gerekmesidir. Major numarasının öğrenmek için sürücünün major numarasını printk gibi bir fonksiyonla bildirmesini sağlayabilirsiniz.

Cihaz kayıt işlemi tamamlandıktan sonra, cihaz sürücü dosyasını “/dev” kalasörü altında oluşturmalısınız. Bu sayede sürücü ile dosya işlemleri(sistem çağrıları) ile iletişime geçebilirsiniz. Bunun için  “mknod /dev/ <device major number> <device minor number> , <DEVICE_NAME>” komutunu kullanmanız gerekiyor.

Burada önemli olan bir nokta daha var. O da eğer bir cihaz sürücüsü kullanıcı uzayındaki herhangi bir program tarafından kullanılırken, bir şekilde “rmmod” komutu sürücü kaldırılmaya çalışırsa ne olacağı. Bu aslında sistemde öngörülemez hatalara sebep olabilir. Bu yüzden modülünüzün kaç program tarafından kullanıldığını ya da kullanımda olup olmadığını takip etmeniz gerekiyor.  Bu yüzden bir “counter” değişkeni kullanıyoruz.  Bu counter değişkeni “open” işlemi ile arttırılırken “release” işlemi ile azaltılıyor. Eğer bu değişken değeri “0” değilse, “rmmod” komutu işlevsiz oluyor.  Bu counter’ı kullanmak için şu iki fonksiyonu kullanıyoruz;

  • try_module_get(THIS_MODULE): counter’ı bir arttırır
  • module_put(THIS_MODULE): counter’ı bir azaltır.

Aşağıda adı chardev olan bir karakter sürücü örneği var.  Bu sürücüyü aşağıdaki makefile ile derleyebilirsiniz.

obj-m += chardev.o

all:
  make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
  make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

 

sürücüyü derleyip insmod komutuyla kernele yükleyin. Daha sonra ise mknod komutuyla sürücü dosyasını oluşturarak sürücü dosyası üzerinde “cat” ve “echo” gibi komutlarla okuma yazma işlemi gerçekleştirmeyi deneyin.  Daha sonra koda bakıp kodu daha iyi anlayacağınızı düşünüyorum.  Kolay Gelsin.

/*
 *  chardev.c: Creates a read-only char device that says how many times
 *  you've read from the dev file
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>	/* for put_user */

/*  
 *  Prototypes - this would normally go in a .h file
 */
int init_module(void);
void cleanup_module(void);
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char *, size_t, loff_t *);

#define SUCCESS 0
#define DEVICE_NAME "chardev"	/* Dev name as it appears in /proc/devices   */
#define BUF_LEN 80		/* Max length of the message from the device */

/* 
 * Global variables are declared as static, so are global within the file. 
 */

static int Major;		/* Major number assigned to our device driver */
static int Device_Open = 0;	/* Is device open?  
         * Used to prevent multiple access to device */
static char msg[BUF_LEN];	/* The msg the device will give when asked */
static char *msg_Ptr;

static struct file_operations fops = {
  .read = device_read,
  .write = device_write,
  .open = device_open,
  .release = device_release
};

/*
 * This function is called when the module is loaded
 */
int init_module(void)
{
        Major = register_chrdev(0, DEVICE_NAME, &fops);

  if (Major < 0) {
    printk(KERN_ALERT "Registering char device failed with %d\n", Major);
    return Major;
  }

  printk(KERN_INFO "I was assigned major number %d. To talk to\n", Major);
  printk(KERN_INFO "the driver, create a dev file with\n");
  printk(KERN_INFO "'mknod /dev/%s c %d 0'.\n", DEVICE_NAME, Major);
  printk(KERN_INFO "Try various minor numbers. Try to cat and echo to\n");
  printk(KERN_INFO "the device file.\n");
  printk(KERN_INFO "Remove the device file and module when done.\n");

  return SUCCESS;
}

/*
 * This function is called when the module is unloaded
 */
void cleanup_module(void)
{
  /* 
   * Unregister the device 
   */
  int ret = unregister_chrdev(Major, DEVICE_NAME);
  if (ret < 0)
    printk(KERN_ALERT "Error in unregister_chrdev: %d\n", ret);
}

/*
 * Methods
 */

/* 
 * Called when a process tries to open the device file, like
 * "cat /dev/mycharfile"
 */
static int device_open(struct inode *inode, struct file *file)
{
  static int counter = 0;

  if (Device_Open)
    return -EBUSY;

  Device_Open++;
  sprintf(msg, "I already told you %d times Hello world!\n", counter++);
  msg_Ptr = msg;
  try_module_get(THIS_MODULE);

  return SUCCESS;
}

/* 
 * Called when a process closes the device file.
 */
static int device_release(struct inode *inode, struct file *file)
{
  Device_Open--;		/* We're now ready for our next caller */

  /* 
   * Decrement the usage count, or else once you opened the file, you'll
   * never get get rid of the module. 
   */
  module_put(THIS_MODULE);

  return 0;
}

/* 
 * Called when a process, which already opened the dev file, attempts to
 * read from it.
 */
static ssize_t device_read(struct file *filp,	/* see include/linux/fs.h   */
         char *buffer,	/* buffer to fill with data */
         size_t length,	/* length of the buffer     */
         loff_t * offset)
{
  /*
   * Number of bytes actually written to the buffer 
   */
  int bytes_read = 0;

  /*
   * If we're at the end of the message, 
   * return 0 signifying end of file 
   */
  if (*msg_Ptr == 0)
    return 0;

  /* 
   * Actually put the data into the buffer 
   */
  while (length && *msg_Ptr) {

    /* 
     * The buffer is in the user data segment, not the kernel 
     * segment so "*" assignment won't work.  We have to use 
     * put_user which copies data from the kernel data segment to
     * the user data segment. 
     */
    put_user(*(msg_Ptr++), buffer++);

    length--;
    bytes_read++;
  }

  /* 
   * Most read functions return the number of bytes put into the buffer
   */
  return bytes_read;
}

/*  
 * Called when a process writes to dev file: echo "hi" > /dev/hello 
 */
static ssize_t
device_write(struct file *filp, const char *buff, size_t len, loff_t * off)
{
  printk(KERN_ALERT "Sorry, this operation isn't supported.\n");
  return -EINVAL;
}

 

Kaynak: http://www.tldp.org/LDP/lkmpg/2.6/html/x569.html

Yusuf

Yusuf

Bir Mühendis.

Önerilen makaleler

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Translate »