Linux驅動程序開發 004- 字符設備驅動

序言
Linux下的大部分驅動程序都是字符設備驅動程序,在這一章我們就擴展我們的“Hello World”程序來支持用戶應用程序的讀寫操作。我們也會瞭解到字符設備是如何註冊到系統中的,應用程序是如何訪問驅動程序的數據的,及字符驅動程序是如何工作的。

設備號
通過前面的學習我們知道應用程序是通過設備節點來訪問驅動程序及設備的,其根本是通過設備節點的設備號(主設備號及從設備號)來關聯驅動程序及設備的,字符設備也不例外(其實字符設備只能這樣訪問)。這裏我們詳細討論Linux內部如何管理設備號的。
  • 設備號類型
Linux內核裏用“dev_t”來表示設備號,它是一個32位的無符號數,其高12位用來表示主設備號,低20位用來表示從設備號。它被定義在<linux/types.h>頭文件裏。內核裏提供了操作“dev_t”的函數,驅動程序中通過這些函數(其實是宏,定義在<linux/kdev_t.h>文件中)來操作設備號。

#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) - 1)
#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))


MAJOR(dev)用於獲取主設備號,MINOR(dev)用於獲取從設備號,而MKDEV(ma,mi)用於通過主設備號和從設備號構造"dev_t"數據。
另一點需要說明的是,dev_t數據類型支持2^12個主設備號,每個主設備號(通常是一個設備驅動)可以支持2^20個設備,目前來說這已經足夠大了,但誰又能說將來還能滿足要求呢?一個良好的編程習慣是不要依賴dev_t這個數據類型,切記必須使用內核提供的操作設備號的函數。
  • 字符設備號註冊
內核提供了字符設備號管理的函數接口,作爲一個良好的編程習慣,字符設備驅動程序應該通過這些函數向系統註冊或註銷字符設備號。

int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)


register_chrdev_region用於向內核註冊已知可用的設備號(次設備號通常是0)範圍。由於歷史的原因一些設備的設備號是固定的,你可以在內核源代碼樹的Documentation/devices.txt文件中找到這些靜態分配的設備號。
alloc_chrdev_region用於動態分配的設備號並註冊到內核中,分配的設備號通過dev參數返回。作爲一個良好的內核開發習慣,我們推薦你使用動態分配的方式來生成設備號。
unregister_chrdev_region用於註銷一個不用的設備號區域,通常這個函數在驅動程序卸載時被調用。

字符設備
Linux2.6內核使用“struct cdev”來記錄字符設備的信息,內核也提供了相關的函數來操作“struct cdev”對象,他們定義在<linux/cdev.h>頭文件中。可見字符設備及其操作函數接口定義的很簡單。

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

void cdev_init(struct cdev *, const struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);


對於Linux 2.6內核來說,struct cdev是內核字符設備的基礎結構用來表示一個字符設備,包含了字符設備需要的全部信息。
  • kobj:struct kobject對象數據,用來描述設備的引用計數,是Linux設備模型的基礎結構。我們在後面的“Linux設備模型”在做詳細的介紹。
  • owner:struct module對象數據,描述了模塊的屬主,指向擁有這個結構的模塊的指針,顯然它只有對編譯爲模塊方式的驅動才由意義。一般賦值位“THIS_MODULE”。
  • ops:struct file_operations對象數據,描述了字符設備的操作函數指針。對於設備驅動來說,這是一個很重要的數據成員,幾乎所有的驅動都要用到這個對象,我們會在下面做詳細介紹。
  • dev:dev_t對象數據,描述了字符設備的設備號。
內核提供了操作字符設備對象“struct cdev”的函數,我們只能通過這些函數來操作字符設備,例如:初始化、註冊、添加、移除字符設備。
  • cdev_alloc:用於動態分配一個新的字符設備 cdev 對象,並對其進行初始化。採用cdev_alloc分配的cdev對象需要顯示的初始化ownerops對象。

// 參考drivers/scsi/st.c:st_probe 函數
struct cdev *cdev = NULL;
cdev = cdev_alloc();
// Error Processing
cdev->owner = THIS_MODULE;
cdev->ops = &st_fops;


  • cdev_init:用於初始化一個靜態分配的cdev對象,一般這個對象會嵌入到其他的對象中。cdev_init會自動初始化ops數據,因此應用程序只需要顯示的給owner對象賦值。cdev_init的功能與cdev_alloc基本相同,唯一的區別是cdev_init初始化一個已經存在的cdev對象,並且這中初始化會影響到字符設備刪除函數(cdev_del)的行爲,請參考cdev_del函數。
  • cdev_add:向內核系統中添加一個新的字符設備cdev,並且使它立即可用。
  • cdev_del:從內核系統中移除cdev字符設備。如果字符設備是由cdev_alloc動態分配的,則會釋放分配的內存。
  • cdev_put:減少模塊的引用計數,一般很少會有驅動程序直接調用這個函數。
文件操作對象
Linux中的所有設備都是文件,內核中用“struct file”結構來表示一個文件。儘管我們的驅動不會直接使用這個結構中的大部分對象,其中的一些數據成員還是很重要的,我們有必要在這裏做一些介紹,具體的內容請參考內核源代碼樹<linux/fs.h>頭文件。

// struct file 中的一些重要數據成員
const struct file_operations    *f_op;
unsigned int         f_flags;
mode_t            f_mode;
loff_t            f_pos;
struct address_space    *f_mapping;


這裏我們不對struct file做過多的介紹,在後面的Linux虛擬文件系統中我們在做詳細介紹。這個結構中的f_ops成員是我們的驅動所關心的,它是一個struct file_operations結構。Linux裏的struct file_operations結構描述了一個文件操作需要的所有函數,它定義在<linux/fs.h>頭文件中。

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 (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, 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);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    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 (*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);
    int (*check_flags)(int);
    int (*dir_notify)(struct file *filp, unsigned long arg);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
};


這是一個很大的結構,包含了所有的設備操作函數指針。當然,對於一個驅動,不是所有的接口都需要來實現的。對於一個字符設備來說,一般實現open、release、read、write、mmap、ioctl這幾個函數就足夠了。
這裏需要指出的是,open和release函數的第一個參數是一個struct inode對象。這個一個內核文件系統索引節點對象,它包含了內核在操作文件或目錄是需要的全部信息。對於字符設備驅動來說,我們關心的是從struct inode對象中獲取設備號(inode的i_rdev成員)內核提供了兩個函數來做這件事。

static inline unsigned iminor(const struct inode *inode)
{
    return MINOR(inode->i_rdev);
}
static inline unsigned imajor(const struct inode *inode)
{
    return MAJOR(inode->i_rdev);
}


儘管我們可以直接從inode->i_rdev獲取設備號,但是儘量不要這樣做。我們推薦你調用內核提供的函數來獲取設備號,這樣即使將來inode->i_rdev有所變化,我們的程序也會工作的很好。

新的驅動
我們已經瞭解了Linux字符驅動程序的知識點,下面我們就用上面學到的知識來構建一個字符驅動程序。前面我們的“Hello World”程序僅僅是在驅動註冊和註銷的時候打印“Hello world!”消息,這裏我們將通過用戶應用程序來讀到這個消息,甚至我們可以修改從驅動讀到的信息。這個驅動的讀寫邏輯是很簡單的,幾乎沒有什麼邏輯,但它足以滿主現在的需要了。首先還是來瀏覽我們新的驅動,並編譯運行這個驅動看看運行效果,最後我們對這個驅動做個詳細的介紹。

// hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>

#define DEFAULT_MSG "Hello World!\n"
#define MAXBUF 20
static unsigned char hello_buf[MAXBUF];

static int hello_open (struct inode *inode, struct file *filp);
static int hello_release (struct inode *inode, struct file *filp);
static ssize_t hello_read (struct file *filp, char __user *buf,
                           size_t count, loff_t *pos);
static ssize_t hello_write (struct file *filp, const char __user *buf,
                            size_t count, loff_t *pos);

static int hello_open (struct inode *inode, struct file *filp)
{
    return 0;
}

static int hello_release (struct inode *inode, struct file *filp)
{
    return 0;
}

static ssize_t hello_read (struct file *filp, char __user *buf,
                           size_t count, loff_t *pos)
{
    int size = count < MAXBUF ? count : MAXBUF;
    printk("hello: Read Hello World !\n");
    if (copy_to_user(buf, hello_buf, size))
        return -ENOMEM;
    return size;
}
static ssize_t hello_write (struct file *filp, const char __user *buf,
                            size_t count, loff_t *pos)
{
    int size = count < MAXBUF ? count : MAXBUF;
    printk("hello: Write Hello World !\n");
    memset(hello_buf, 0, sizeof(hello_buf));
    if (copy_from_user(hello_buf, buf, size))
        return -ENOMEM;
    return size;
}

static struct file_operations hello_fops = {
        .read = hello_read,
        .write = hello_write,
        .open = hello_open,
        .release = hello_release,
};

static struct cdev *hello_cdev;
static int __init hello_init(void)
{
    dev_t dev;
    int error;

    error = alloc_chrdev_region(&dev, 0, 2, "hello");
    if (error)
    {
        printk("hello: alloc_chardev_region failed!\n");
        return error;
    }
    hello_cdev = cdev_alloc();
    if (hello_cdev == NULL)
    {
        printk("hello: alloc cdev failed!\n");
        unregister_chrdev_region(hello_cdev->dev, 2);
        return -ENOMEM;
    }

   

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章