《Linux設備驅動開發詳解》第2版第6章 - 字符設備驅動

原文出處:

http://blog.csdn.net/21cnbao/article/details/7526159

宋老師的排版不太好,我整理了一下 ,轉載到自己的博客空間。


第六章 字符設備驅動

本章導讀

在整個Linux設備驅動的學習中,字符設備驅動較爲基礎。本章將展示Linux字符設備驅動程序的結構,並解釋其主要組成部分的編程方法。
6.1節講解了Linux字符設備驅動的關鍵數據結構cdev及file_operations結構體的操作方法,並分析了Linux字符設備的整體結構,給出了簡單的設計模板。
6.2節描述了本章及後續各章節所基於的globalmem虛擬字符設備,6~9章都將基於該虛擬設備實例進行字符設備驅動及併發控制等知識的講解。
6.3節依據6.1節的知識講解globalmem設備的驅動編寫方法,對讀寫函數、seek()函數和IO控制函數等進行了重點分析。該節的最後改造globalmem的驅動程序以利用文件私有數據。
6.4節給出了6.3的globalmem設備驅動在用戶空間的驗證。

6.1 Linux字符設備驅動結構

6.1.1 cdev結構體

在Linux 2.6內核中,使用 cdev 結構體描述一個字符設備,cdev 結構體的定義如代碼清單6.1。
代碼清單 6.1 cdev 結構體

struct cdev {
    struct kobject          kobj;          /* 內嵌的kobject對象 */
    struct module          *owner;         /* 所屬模塊 */
    struct file_operations *ops;           /* 文件操作結構體 */
    struct list_head        list;  
    dev_t                   dev;           /* 設備號 */
    unsigned int            count;  
};
cdev結構體的dev_t成員定義了設備號,爲32位,其中12位主設備號,20位次設備號。使用下列宏可以從dev_t獲得主設備號和次設備號:
MAJOR(dev_t dev)
MINOR(dev_t dev)
而使用下列宏則可以通過主設備號和設備號生成 dev_t:
MKDEV(int major, int minor)
cdev 結構體的另一個重要成員 file_operations 定義了字符設備驅動提供給虛擬文件系統的接口函數。
Linux 2.6 內核提供了一組函數用於操作 cdev 結構體:
void         cdev_init (struct cdev *, 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 *);
cdev_init() 函數用於初始化 cdev 的成員,並建立 cdev 和 file_operations 之間的連接,其源代碼如清單6-2。
代碼清單6.2 cdev_init() 函數
void cdev_init(struct cdev *cdev, struct file_operations *fops) {
    memset(cdev, 0, sizeof *cdev);  
    INIT_LIST_HEAD(&cdev->list);  
    kobject_init(&cdev->kobj, &ktype_cdev_default);
    cdev->ops = fops;         /* 將傳入的文件操作結構體指針賦值給 cdev 的 ops */  
}
cdev_alloc() 函數用於動態申請一個 cdev 內存,其源代碼如清單6-3。
代碼清單6.3 cdev_alloc() 函數
struct cdev *cdev_alloc(void) {
    struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);  
    if (p) {
        INIT_LIST_HEAD(&p->list);
        kobject_init(&p->kobj,&ktype_cdev_dynamic);
    }

    return p;  
}
cdev_add() 函數和 cdev_del() 函數分別向系統添加和刪除一個cdev,完成字符設備的註冊和註銷。
對 cdev_add() 的調用通常發生在字符設備驅動模塊加載函數中,而對 cdev_del() 函數的調用則通常發生在字符設備驅動模塊卸載函數中。

6.1.2分配和釋放設備號

在調用 cdev_add() 函數向系統註冊字符設備之前,應首先調用 register_chrdev_region() 或 alloc_chrdev_region() 函數向系統申請設備號,這兩個函數的原型爲:
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);
register_chrdev_region()函數用於已知起始設備的設備號的情況,而 alloc_chrdev_region() 用於設備號未知,向系統動態申請未被佔用的設備號的情況,
函數調用成功之後,會把得到的設備號放入第一個參數 dev 中。
alloc_chrdev_region() 與 register_chrdev_region() 對比的優點在於它會自動避開設備號重複的衝突。
相反地,在調用 cdev_del() 函數從系統註銷字符設備之後,unregister_chrdev_region() 應該被調用以釋放原先申請的設備號,這個函數的原型爲:
void unregister_chrdev_region(dev_t from, unsigned count);

6.1.3 file_operations結構體

file_operations 結構體中的成員函數是字符設備驅動程序設計的主體內容,這些函數實際會在應用程序進行 Linux 的 open()、write()、read()、close() 等系統調用時最終被調用。
file_operations 結構體目前已經比較龐大,它的定義如代碼清單6.4。
代碼清單6.4 file_operations 結構體
struct file_operations {
    struct module *owner;                       /* 擁有該結構的模塊的指針,一般爲THIS_MODULES */  

    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 *, char__user *, size_t, loff_t);                            /* 初始化一個異步的讀取操作*/  
    ssize_t      (*aio_write)        (struct kiocb *, constchar __user *, size_t, loff_t);                      /* 初始化一個異步的寫入操作 */  
    int          (*readdir)          (struct file *, void *,filldir_t);                                         /* 僅用於讀取目錄,對於設備文件,該字段爲 NULL */  
    unsigned int (*poll)             (struct file *, struct poll_table_struct *);                               /* 輪詢函數,判斷目前是否可以進行非阻塞的讀取或寫入 */  
    int          (*ioctl)            (struct inode *, struct file *,unsigned int, unsigned long);               /* 執行設備IO控制命令 */  
    long         (*unlocked_ioctl)   (struct file *, unsigned int, unsigned long);                              /* 不使用BLK的文件系統,將使用此種函數指針代替ioctl */  
    long         (*compat_ioctl)     (struct file *, unsignedint, unsigned long);                               /* 在64位系統上,32位的ioctl調用,將使用此函數指針代替 */  
    int          (*mmap)             (struct file *, structvm_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 *, intdatasync);                                             /* 異步fsync */  
    int          (*fasync)           (int, struct file *, int);                                                 /* 通知設備FASYNC標誌發生變化 */  
    int          (*lock)             (struct file *, int, structfile_lock*);  
    ssize_t      (*sendpage)         (struct file *, structpage *, int, size_t, loff_t *, int);                 /* 通常爲NULL */  
    unsigned long(*get_unmapped_area)(structfile *,unsigned long, unsigned long, unsigned long, unsigned long); /* 在當前進程地址空間找到一個未映射的內存段 */  
    int          (*check_flags)      (int);                                                                     /* 允許模塊檢查傳遞給fcntl(F_SETEL...)調用的標誌 */  
    int          (*dir_notify)       (struct file *filp, unsignedlong arg);                                     /* 對文件系統有效,驅動程序不必實現 */  
    int          (*flock)            (struct file *, int, structfile_lock *);  
    ssize_t      (*splice_write)     (struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); /* 由VFS調用,將管道數據粘接到文件 */  
    ssize_t      (*splice_read)      (struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); /* 由VFS調用,將文件數據粘接到管道 */  
    int          (*setlease)         (struct file *, long, struct file_lock **);  
};  
下面我們對 file_operations 結構體中的主要成員進行分析:
llseek() 函數用來修改一個文件的當前讀寫位置,並將新位置返回,在出錯時,這個函數返回一個負值。
read() 函數用來從設備中讀取數據,成功時函數返回讀取的字節數,出錯時返回一個負值。
write() 函數向設備發送數據,成功時該函數返回寫入的字節數。如果此函數未被實現,當用戶進行 write() 系統調用時,將得到 -EINVAL 返回值。
readdir() 函數僅用於目錄,設備節點不需要實現它。
ioctl() 提供設備相關控制命令的實現(既不是讀操作也不是寫操作),當調用成功時,返回給調用程序一個非負值。
mmap() 函數將設備內存映射到進程內存中,如果設備驅動未實現此函數,用戶進行 mmap() 系統調用時將獲得 -ENODEV 返回值。這個函數對於幀緩衝等設備特別有意義。
當用戶空間調用 Linux API 函數 open() 打開設備文件時,設備驅動的 open() 函數最終被調用。
驅動程序可以不實現這個函數,在這種情況下,設備的打開操作永遠成功。
與 open() 函數對應的是 release() 函數。
poll() 函數一般用於詢問設備是否可被非阻塞的立即讀寫。當詢問的條件未觸發時,用戶空間進行 select() 和 poll() 系統調用將引起進程的阻塞。
aio_read() 和 aio_write() 函數分別對與文件描述符對應的設備進行異步讀、寫操作。
設備實現這兩個函數後,用戶空間可以對該設備文件描述符調用 aio_read()、aio_write() 等系統調用進行讀寫。

6.1.4 Linux 字符設備驅動的組成

在 Linux 中,字符設備驅動由如下幾個部分組成:
字符設備驅動模塊加載與卸載函數
在字符設備驅動模塊加載函數中應該實現設備號的申請和cdev的註冊,而在卸載函數中應實現設備號的釋放和cdev的註銷。
工程師通常習慣爲設備定義一個設備相關的結構體,其包含該設備所涉及到的 cdev 、私有數據及信號量等信息。常見的設備結構體、模塊加載和卸載函數形式如代碼清單6.5。
代碼清單6.5 字符設備驅動模塊加載與卸載函數模板
/* 設備結構體 */
struct xxx_dev_t {
    struct cdev cdev;

    ...
} xxx_dev;

/* 設備驅動模塊加載函數
static int __init xxx_init(void)
{
    ...

    cdev_init(&xxx_dev.cdev, &xxx_fops);                  /* 初始化 cdev */
    xxx_dev.cdev.owner = THIS_MODULE;
    
    /* 獲取字符設備號 */
    if (xxx_major) {
        register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
    } else {
        alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
    }

    ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1);         /* 註冊設備 */

    ...
}

/* 設備驅動模塊卸載函數 */
static void __exit xxx_exit(void)
{
    unregister_chrdev_region(xxx_dev_no, 1);              /* 釋放佔用的設備號 */
    cdev_del(&xxx_dev.cdev);                              /* 註銷設備 */

    ...
}
字符設備驅動的 file_operations 結構體中成員函數
file_operations 結構體中成員函數是字符設備驅動與內核的接口,是用戶空間對 Linux 進行系統調用最終的落實者。大多數字符設備驅動會實現 read()、write() 和 ioctl() 函數,常見的字符設備驅動這3個函數的形式如代碼清單6.6。
代碼清單6.6 字符設備驅動讀、寫、IO控制函數模板
/* 讀設備 */
ssize_t xxx_read(struct file *filp, char__user *buf, size_t count, loff_t *f_pos) {
    ...

    copy_to_user(buf, ..., ...);

    ...
}

/* 寫設備 */
ssize_t xxx_write(struct file *filp, const char__user *buf, size_t count, loff_t *f_pos) {
    ...

    copy_from_user(..., buf, ...);

    ...
}

/* ioctl函數 */
int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg) {
    ...

    switch(cmd) {
    case XXX_CMD1:
        ...
        break;
    caseXXX_CMD2:
        ...
        break;
    default:
        /* 不能支持的命令 */
        return  - ENOTTY;
    }

    return0;
}
設備驅動的寫函數中,filp 是文件結構體指針,buf 是用戶空間內存的地址,該地址在內核空間不能直接讀寫,count 是要寫的字節數,f_pos 是寫的位置相對於文件開頭的偏移。
由於內核空間與用戶空間的內存不能直接互訪,因此藉助了函數 copy_from_user() 完成用戶空間到內核空間的拷貝,以及 copy_to_user() 完成內核空間到用戶空間的拷貝,見代碼第6行和第14行。
完成內核空間和用戶空間內存拷貝的 copy_from_user() 和 copy_to_user() 的原型分別爲:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
上述函數均返回不能被複制的字節數,因此,如果完全複製成功,返回值爲0。
如果要複製的內存是簡單類型,如 char、int、long 等,則可以使用簡單的 put_user() 和 get_user() ,如:
intval;                    /* 內核空間整型變量 */
...
get_user(val, (int *)arg); /* 用戶->內核,arg 是用戶空間的地址 */
...
put_user(val, (int *)arg); /* 內核->用戶,arg是用戶空間的地址 */
讀和寫函數中的 __user 是一個宏,表明其後的指針指向用戶空間,這個宏定義爲:
#ifdef __CHECKER__
#define __user                 __attribute__((noderef, address_space(1)))
#else
#define __user
#endif
IO 控制函數的 cmd 參數爲事先定義的 IO 控制命令,而 arg 爲對應於該命令的參數。譬如對於串行設備,如果 SET_BAUDRATE 是一道設置波特率的命令,那後面的 arg 就應該是波特率值。
在字符設備驅動中,需要定義一個 file_operations 的實例,並將具體設備驅動的函數賦值給 file_operations 的成員,如代碼清單6.7。
代碼清單6.7 字符設備驅動文件操作結構體模板
struct file_operations xxx_fops = {
    .owner = THIS_MODULE,
    .read  = xxx_read,
    .write = xxx_write,
    .ioctl = xxx_ioctl,
    ...
};
上述 xxx_fops 在代碼清單6.5第10行的 cdev_init(&xxx_dev.cdev, &xxx_fops) 的語句中被建立與 cdev 的連接。
圖6.1描述了字符設備驅動的結構,字符設備驅動與字符設備以及字符設備驅動與用戶空間訪問該設備的程序之間的關係。

6.2 globalmem 虛擬設備實例描述

從本章開始,後續的數章都將基於虛擬的 globalmem 設備進行字符設備驅動的講解。globalmem 意味着“全局內存”,在 globalmem 字符設備驅動中會分配一片大小爲 GLOBALMEM_SIZE(4KB)的內存空間,並在驅動中提供針對該片內存的讀寫、控制和定位函數,以供用戶空間的進程能通過 Linux 系統調用訪問這片內存。
實際上,這個虛擬的 globalmem 設備幾乎沒有任何實用價值,僅僅是一種爲了講解問題的方便而憑空製造的設備。當然,它也並非百無一用,由於 globalmem 可被2個或2個以上的進程同時訪問,其中的全局內存可作爲用戶空間進程進行通信的一種蹩腳的手段。
本章下面的一節將給出 globalmem 設備驅動的雛形,而後續章節會在這個雛形的基礎上初步添加併發與同步控制等複雜功能。

6.3 globalmem設備驅動

6.3.1頭文件、宏及設備結構體

在 globalmem 字符設備驅動中,應包含它要使用的頭文件,並定義 globalmem 設備結構體及相關宏。
代碼清單6.8 globalmem 設備結構體和宏
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>

#define GLOBALMEM_SIZE  0x1000            /* 全局內存大小:4K字節 */
#define MEM_CLEAR       0x1               /* 清0全局內存 */
#define GLOBALMEM_MAJOR 250               /* 預設的 globalmem 的主設備號 */

static int globalmem_major = GLOBALMEM_MAJOR;

/* globalmem 設備結構體 */
struct globalmem_dev {                                                        
    struct cdev   cdev;                   /* cdev 結構體 */                      
    unsigned char mem[GLOBALMEM_SIZE];    /* 全局內存 */       
};

struct globalmem_dev dev;                 /* 設備結構體實例 */
從第19~22行代碼可以看出,定義的 globalmem_dev 設備結構體包含了對應於 globalmem 字符設備的 cdev、使用的內存 mem[GLOBALMEM_SIZE]。當然,程序中並不一定要把 mem[GLOBALMEM_SIZE] 和 cdev 包含在一個設備結構體中,但這樣定義的好處在於,它借用了面向對象程序設計中“封裝”的思想,體現了一種良好的編程習慣。

6.3.2加載與卸載設備驅動

globalmem 設備驅動的模塊加載和卸載函數遵循代碼清單6.5的類似模板,其實現的工作與代碼清單6.5完全一致,如代碼清單6.9。
代碼清單6.9 globalmem 設備驅動模塊加載與卸載函數
/* globalmem 設備驅動模塊加載函數 */
int globalmem_init(void)
{
    int result;
    dev_t devno = MKDEV(globalmem_major, 0);

    /* 申請字符設備驅動區域 */
    if (globalmem_major)
        result = register_chrdev_region(devno, 1,"globalmem");
    else {
    /* 動態獲得主設備號 */
        result = alloc_chrdev_region(&devno,0, 1, "globalmem");
        globalmem_major = MAJOR(devno);
    }
    if (result < 0)
        return result;

    globalmem_setup_cdev();
    return 0;
}

/* globalmem 設備驅動模塊卸載函數 */
void globalmem_exit(void)
{
    cdev_del(&dev.cdev);                                    /* 刪除 cdev 結構 */
    unregister_chrdev_region(MKDEV(globalmem_major, 0), 1); /* 註銷設備區域 */
}
第18行調用的 globalmem_setup_cdev() 函數完成 cdev 的初始化和添加,如代碼清單6.10。
代碼清單6.10 初始化並添加 cdev 結構體
/*初始化並添加 cdev 結構體*/
static void globalmem_setup_cdev(void) {
    int err, devno = MKDEV(globalmem_major, 0);

    cdev_init(&dev.cdev, &globalmem_fops);
    dev.cdev.owner = THIS_MODULE;
    err = cdev_add(&dev.cdev, devno, 1);
    if (err)
        printk(KERN_NOTICE "Error %d addingglobalmem", err);
}
在 cdev_init() 函數中,與 globalmem 的 cdev 關聯的 file_operations 結構體如代碼清單6.11。
代碼清單6.11 globalmem 設備驅動文件操作結構體
static const struct file_operations globalmem_fops = {
    .owner  = THIS_MODULE,
    .llseek = globalmem_llseek,
    .read   = globalmem_read,
    .write  = globalmem_write,
    .ioctl  = globalmem_ioctl,
};

6.3.3讀寫函數

globalmem 設備驅動的讀寫函數主要是讓設備結構體的 mem[] 數組與用戶空間交互數據,並隨着訪問的字節數變更返回給用戶的文件讀寫偏移位置。讀和寫函數的實現分別如代碼清單6.12和6.13。
代碼清單6.12 globalmem 設備驅動讀函數
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret = 0;
 
    /* 分析和獲取有效的讀長度 */
    if (p >= GLOBALMEM_SIZE)             /* 要讀的偏移位置越界 */
        return 0;
    if (count > GLOBALMEM_SIZE - p)      /* 要讀的字節數太大 */
        count = GLOBALMEM_SIZE - p;

    /* 內核空間->用戶空間 */
    if (copy_to_user(buf, (void *)(dev.mem + p), count))
        ret = - EFAULT;
    else {
        *ppos += count;
        ret = count;

        printk(KERN_INFO "read %d bytes(s)from %d\n", count, p);
    }

    return ret;
}
代碼清單6.13 globalmem設備驅動寫函數
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret = 0;

    /* 分析和獲取有效的寫長度 */
    if (p >= GLOBALMEM_SIZE)   /* 要寫的偏移位置越界 */
        return 0;
    if (count > GLOBALMEM_SIZE - p) /* 要寫的字節數太多
        count = GLOBALMEM_SIZE - p;

    /*用戶空間->內核空間*/
    if (copy_from_user(dev.mem + p, buf, count))
        ret = - EFAULT;
    else {
        *ppos += count;
        ret= count;

        printk(KERN_INFO "written %d bytes(s)from %d\n", count, p);
    }

    return ret;
}

6.3.4 seek 函數

seek() 函數對文件定位的起始地址可以是文件開頭(SEEK_SET, 0)、當前位置 (SEEK_CUR, 1) 和文件尾 (SEEK_END, 2) ,globalmem 支持從文件開頭和當前位置相對偏移。
在定位的時候,應該檢查用戶請求的合法性,若不合法,函數返回 - EINVAL,合法時返回文件的當前位置,如代碼清單6.14。
代碼清單6.14 globalmem 設備驅動 seek() 函數
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) {
    loff_t ret;
    
    switch (orig) {
    case 0:                         /* 從文件開頭開始偏移 */
        if (offset < 0) {
            ret = - EINVAL;
            break;
        }
        if ((unsigned int)offset > GLOBALMEM_SIZE) {
            ret = - EINVAL;
            break;
        }
        filp->f_pos = (unsigned int)offset;
        ret = filp->f_pos;
        break;
    case 1:                         /* 從當前位置開始偏移 */
        if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
            ret = - EINVAL;
            break;
        }
        if ((filp->f_pos + offset) < 0) {
            ret = - EINVAL;
            break;
        }
        filp->f_pos += offset;
        ret = filp->f_pos;
        break;
    default:
        ret = - EINVAL;
    }

    return ret;
}

6.3.5 ioctl 函數

1、globalmem 的 ioctl() 函數
globalmem 的 ioctl() 函數接受 MEM_CLEAR 命令,這個命令會將全局內存的有效數據長度清0,對於設備不支持的命令,ioctl()函數應該返回- EINVAL,如代碼清單6.15。
代碼清單6.15 globalmem 設備驅動 IO 控制函數
static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned int cmd, unsigned long arg) {
    switch (cmd) {
    case MEM_CLEAR:
        /* 清除全局內存 */
        memset(dev->mem, 0, GLOBALMEM_SIZE);
        printk(KERN_INFO "globalmem is set tozero\n");
        break;

    default:
        return - EINVAL; /* 其他不支持的命令 */
    }

    return 0;
}
在上述程序中,MEM_CLEAR 被宏定義爲0x01,實際上並不是一種值得推薦的方法,簡單地對命令定義爲0x0、0x1、0x2等類似值會導致不同的設備驅動擁有相同的命令號。如果設備A、B都支持0x0、0x1、0x2這樣的命令,假設用戶本身希望給A發0x1命令,可是不經意間發給了B,這個時候B因爲支持該命令,它就會執行該命令。因此,Linux內核推薦採用一套統一的ioctl()命令生成方式。
2、ioctl() 命令
Linux 建議以如圖6.2所示的方式定義 ioctl() 的命令。
圖6.2 IO控制命令的組成
設備類型 序列號 方向 數據尺寸
8 bit 8 bit 2 bit 13/14 bit
命令碼的設備類型字段爲一個“幻數”,可以是0~0xff之間的值,內核中的ioctl-number.txt給出了一些推薦的和已經被使用的“幻數”,新設備驅動定義“幻數”的時候要避免與其衝突。
命令碼的序列號也是8位寬。
命令碼的方向字段爲2位,該字段表示數據傳送的方向,可能的值是_IOC_NONE(無數據傳輸)、 _IOC_READ(讀)、_IOC_WRITE(寫)和_IOC_READ|_IOC_WRITE(雙向)。數據傳送的方向是從應用程序的角度來看的。
命令碼的數據長度字段表示涉及到的用戶數據的大小,這個成員的寬度依賴於體系結構,通常是13 或者14 位。
內核還定義了_IO()、_IOR()、_IOW()和_IOWR()這4個宏來輔助生成命令,這4個宏的通用定義如代碼清單6.16。
代碼清單6.16 _IO()、_IOR()、_IOW()和_IOWR()宏定義
#define _IO(type, nr)                 _IOC(_IOC_NONE, (type), (nr), 0)
#define _IOR(type, nr, size)          _IOC(_IOC_READ, (type), (nr), (_IOC_TYPECHECK(size)))
#define _IOW(type, nr, size)          _IOC(_IOC_WRITE, (type), (nr), (_IOC_TYPECHECK(size)))
#define _IOWR(type, nr, size)         _IOC(_IOC_READ|_IOC_WRITE, (type), (nr), (_IOC_TYPECHECK(size)))
/* _IO、_IOR等使用的_IOC宏 */
#define _IOC(dir,type,nr,size)        (((dir) << _IOC_DIRSHIFT) | ((type) << _IOC_TYPESHIFT) | ((nr)  << _IOC_NRSHIFT) | ((size) << _IOC_SIZESHIFT))
由此可見,這幾個宏的作用是根據傳入的type(設備類型字段)、nr(序列號字段)和size(數據長度字段)和宏名隱含的方向字段移位組合生成命令碼。
由於 globalmem 的 MEM_CLEAR 命令不涉及數據傳輸,因此它可以定義爲:
#define GLOBALMEM_MAGIC    ...
#define MEM_CLEAR          _IO(GLOBALMEM_MAGIC, 0)
3、預定義命令
內核中預定義了一些 IO 控制命令,如果某設備驅動中包含了與預定義命令一樣的命令碼,這些命令會被當作預定義命令被內核處理而不是被設備驅動處理,預定義命令包括:
FIOCLEX : 即 File IOctl Close on Exec,對文件設置專用標誌,通知內核當 exec() 系統調用發生時自動關閉打開的文件。
FIONCLEX : 即 File IOctl Not CLose on Exec,與 FIOCLEX 標誌相反,清除由 FIOCLEX 命令設置的標誌。
FIOQSIZE : 獲得一個文件或者目錄的大小,當用於設備文件時,返回一個 ENOTTY 錯誤。
FIONBIO : 即 File IOctl Non-Blocking I/O,這個調用修改在 filp->f_flags 中的 O_NONBLOCK 標誌。
FIOCLEX、FIONCLEX、FIOQSIZE 和 FIONBIO 這些宏的定義爲:
#define FIONCLEX            0x5450
#define FIOCLEX             0x5451
#define FIOQSIZE            0x5460
#define FIONBIO             0x5421

6.3.6 使用文件私有數據

6.3.1~6.3.5節給出的代碼完整地實現了預期的 globalmem 雛形,在其代碼中,爲 globalmem 設備結構體 globalmem_dev 定義了全局實例 dev(見代碼清單6.7第25行),而 globalmem 的驅動中 read()、write()、ioctl()、llseek() 函數都針對dev進行操作。
實際上,大多數 Linux 驅動工程師遵循一個“潛規則”,那就是將文件的私有數據 private_data 指向設備結構體,在 read()、write()、ioctl()、llseek() 等函數通過 private_data 訪問設備結構體。
這個時候,我們要將各函數進行少量的修改,爲了讓讀者朋友建立字符設備驅動的全貌視圖,代碼清單6.17列出了完整的使用文件私有數據的 globalmem 的設備驅動,本程序位於虛擬機/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6目錄。
代碼清單6.17 使用文件私有數據的 globalmem 的設備驅動
#include <linux/module.h>  
#include <linux/types.h>  
#include <linux/fs.h>  
#include <linux/errno.h>  
#include <linux/mm.h>  
#include <linux/sched.h>  
#include <linux/init.h>  
#include <linux/cdev.h>  
#include <asm/io.h>  
#include <asm/system.h>  
#include <asm/uaccess.h>  

#define GLOBALMEM_SIZE  0x1000            /* 全局內存最大4K字節 */  
#define MEM_CLEAR       0x1               /* 清0全局內存 */  
#define GLOBALMEM_MAJOR 250               /* 預設的globalmem的主設備號 */  

static int globalmem_major = GLOBALMEM_MAJOR;  

/*globalmem設備結構體*/  
struct globalmem_dev {
    structcdev cdev;                      /* cdev 結構體 */  
    unsignedchar mem[GLOBALMEM_SIZE];     /* 全局內存 */  
};

struct globalmem_dev *globalmem_devp;     /*設備結構體指針*/  

/*文件打開函數*/  
int globalmem_open(struct inode *inode, struct file *filp) {  
    /*將設備結構體指針賦值給文件私有數據指針*/  
    filp->private_data =globalmem_devp;  
    
    return0;  
}  

/*文件釋放函數*/  
int globalmem_release(struct inode *inode, structfile *filp)  
{  
    return0;  
}  

/* ioctl設備控制函數 */  
static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned intcmd, unsigned long arg) {  
    struct globalmem_dev *dev = filp->private_data;             /* 獲得設備結構體指針 */  
    
    switch(cmd) {  
        caseMEM_CLEAR:  
            memset(dev->mem,0, GLOBALMEM_SIZE);  
            printk(KERN_INFO"globalmem is set to zero\n");  
            break;  
        default:  
            return  - EINVAL;  
    }  

    return0;  
}  

/* 讀函數 */  
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) {  
    unsignedlong p = *ppos;  
    unsignedint count = size;  
    intret = 0;  
    struct globalmem_dev *dev = filp->private_data;             /* 獲得設備結構體指針 */  

    /*分析和獲取有效的寫長度*/  
    if (p >= GLOBALMEM_SIZE)  
        return0;  
    
    if (count > GLOBALMEM_SIZE - p)  
        count= GLOBALMEM_SIZE - p;  

    /*內核空間->用戶空間*/  
    if (copy_to_user(buf, (void *)(dev->mem + p), count)) {  
        ret=  - EFAULT;  
    } else {  
        *ppos += count;  
        ret = count;  
        printk(KERN_INFO"read %u bytes(s) from %lu\n", count, p);  
    }  

    return ret;  
}  

/* 寫函數 */  
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_tsize, loff_t *ppos) {  
    unsigned long p =  *ppos;  
    unsigned int count = size;  
    int ret = 0;  
    struct globalmem_dev *dev = filp->private_data;             /* 獲得設備結構體指針 */  

    /*分析和獲取有效的寫長度*/  
    if (p >= GLOBALMEM_SIZE)  
        return 0;  

    if (count > GLOBALMEM_SIZE - p)  
        count= GLOBALMEM_SIZE - p;  

    /* 用戶空間->內核空間 */  
    if (copy_from_user(dev->mem +p, buf, count))  
        ret =  - EFAULT;  
    else {  
        *ppos += count;  
        ret = count;  
        printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);  
    }  

    return ret;  
}  

/* seek 文件定位函數 */  
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) {  
    loff_t ret = 0;  
    switch (orig) {  
    case 0:                  /*相對文件開始位置偏移*/  
        if (offset < 0) {  
            ret=  - EINVAL;  
            break;  
        }  
        if ((unsigned int)offset > GLOBALMEM_SIZE) {  
            ret=  - EINVAL;  
            break;  
        }  
        filp->f_pos =(unsigned int)offset;  
        ret = filp->f_pos;  
        break;  
    case 1:                  /*相對文件當前位置偏移*/  
        if ((filp->f_pos+ offset) > GLOBALMEM_SIZE) {  
            ret=  - EINVAL;  
            break;  
        }  
        if ((filp->f_pos+ offset) < 0) {  
            ret=  - EINVAL;  
            break;  
        }  
        filp->f_pos += offset;  
        ret = filp->f_pos;  
        break;  
    default:  
        ret =  - EINVAL;  
        break;  
    }  

    return ret;  
}  

/* 文件操作結構體 */  
static const struct file_operations globalmem_fops = {  
    .owner   = THIS_MODULE,  
    .llseek  = globalmem_llseek,  
    .read    = globalmem_read,  
    .write   = globalmem_write,  
    .ioctl   = globalmem_ioctl,  
    .open    = globalmem_open,  
    .release = globalmem_release,  
};  

/* 初始化並註冊 cdev */  
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index) {  
    int err, devno = MKDEV(globalmem_major, index);  

    cdev_init(&dev->cdev,&globalmem_fops);  
    dev->cdev.owner =THIS_MODULE;  
    err = cdev_add(&dev->cdev, devno, 1);  
    if (err)  
        printk(KERN_NOTICE"Error %d adding globalmem %d", err, index);  
}  

/* 設備驅動模塊加載函數 */  
int globalmem_init(void) {  
    int result;  

    dev_t devno = MKDEV(globalmem_major, 0);  

    /* 申請設備號 */  
    if (globalmem_major)  
        result = register_chrdev_region(devno, 1, "globalmem");  
    else { 
        /* 動態申請設備號 */  
        result =alloc_chrdev_region(&devno, 0, 1, "globalmem");  
        globalmem_major = MAJOR(devno);  
    }  
    if (result < 0)  
        return result;  

    /* 動態申請設備結構體的內存 */  
    globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL);  
    /* 申請失敗 */
    if (!globalmem_devp) {      
        result =  - ENOMEM;  
        goto fail_malloc;  
    }  
    memset(globalmem_devp, 0,sizeof(struct globalmem_dev));  
    globalmem_setup_cdev(globalmem_devp,0);  

    return 0;  

fail_malloc:  
    unregister_chrdev_region(devno,1);  
    
    return result;  
}  

/* 模塊卸載函數 */  
void globalmem_exit(void) {  
    cdev_del(&globalmem_devp->cdev);                        /* 註銷 cdev */  
    kfree(globalmem_devp);                                  /* 釋放設備結構體內存 */  
    unregister_chrdev_region(MKDEV(globalmem_major,0), 1);  /* 釋放設備號 */  
}  

MODULE_AUTHOR("Barry Song <[email protected]>");  
MODULE_LICENSE("Dual BSD/GPL");  

module_param(globalmem_major, int, S_IRUGO);  

module_init(globalmem_init);  
module_exit(globalmem_exit);  
除了在 globalmem_open() 函數中通過 filp->private_data = globalmem_devp 語句(見第29行)將設備結構體指針賦值給文件私有數據指針並在 globalmem_read() 、globalmem_write()、globalmem_llseek()和globalmem_ioctl()函數中通過struct globalmem_dev *dev = filp->private_data語句獲得設備結構體指針並使用該指針操作設備結構體外,代碼清單6.17與代碼清單6.7~6.15的程序並無二致。
讀者朋友們,這個時候,請您翻回到本書的第1章,再次閱讀代碼清單1.4,即 Linux下LED 的設備驅動,是否豁然開朗?
代碼清單6.17僅僅作爲使用 private_data 的範例,實際上,在這個程序中使用 private_data 沒有任何意義,直接訪問全局變量 globalmem_devp 來的更加結構清晰。如果 globalmem 不只包括1個設備,而是同時包括2個或2個以上的設備,採用private_data的優勢就會集中顯現出來。
在不對代碼清單6.17中的 globalmem_read()、globalmem_write()、globalmem_ioctl()等重要函數及 globalmem_fops 結構體等數據結構進行任何修改的前提下,只是簡單的修改 globalmem_init()、globalmem_exit()和globalmem_open(),就可以輕鬆地讓globalmem驅動中包含2個同樣的設備(次設備號分別爲0和1),如代碼清單6.18。
代碼清單6.18 支持2個 globalmem 設備的 globalmem 驅動
/* 文件打開函數 */  
int globalmem_open(struct inode *inode, struct file *filp) {  
    /* 將設備結構體指針賦值給文件私有數據指針 */  
    struct globalmem_dev *dev;  

    dev = container_of(inode->i_cdev, structglobalmem_dev, cdev);   
    filp->private_data = dev;   

    return 0;  
}  
  
/* 設備驅動模塊加載函數 */  
int globalmem_init(void) {  
    int result;  
  
    dev_t devno = MKDEV(globalmem_major, 0);  

    /* 申請設備號 */  
    if (globalmem_major)  
        result = register_chrdev_region(devno, 2, "globalmem");  
    else { 
        /* 動態申請設備號 */  
        result = alloc_chrdev_region(&devno, 0, 2, "globalmem");  
        globalmem_major = MAJOR(devno);  
    }   
  
    if (result < 0)  
        return result;  
  
    /* 動態申請2個設備結構體的內存 */  
    globalmem_devp = kmalloc(2*sizeof(structglobalmem_dev), GFP_KERNEL);  
    if (!globalmem_devp) { 
        /*申請失敗*/  
        result = - ENOMEM;  
  
        goto fail_malloc;  
    }  
  
    memset(globalmem_devp, 0, 2*sizeof(structglobalmem_dev));  
  
    globalmem_setup_cdev(&globalmem_devp[0], 0);  
  
    globalmem_setup_cdev(&globalmem_devp[1], 1);  
  
    return 0;  
  
fail_malloc: 
    unregister_chrdev_region(devno, 1);  
  
    return result;  
}  
  
/* 模塊卸載函數 */  
void globalmem_exit(void) {  
    cdev_del(&(globalmem_devp[0].cdev));  
  
    cdev_del(&(globalmem_devp[1].cdev));                    /* 註銷cdev */  
  
    kfree(globalmem_devp);                                  /* 釋放設備結構體內存 */  
  
    unregister_chrdev_region(MKDEV(globalmem_major, 0), 2); /* 釋放設備號 */  
}  
  
/* 其它代碼同清單6.16 */  

6.4 globalmem 驅動在用戶空間的驗證

在對應目錄通過 make 命令編譯 globalmem 的驅動,得到 globalmem.ko 文件。運行:
$sudo su
#insmod globalmem.ko
命令加載模塊,通過 lsmod 命令,發現 globalmem 模塊已被加載。再通過 cat /proc/devices 命令察看,發現多出了主設備號爲250的 globalmem 字符設備驅動:
#cat /proc/devices
Characterdevices:
  1 mem
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  6 lp
  7 vcs
 10 misc
 13 input
 14 sound
 21 sg
 29 fb
 99 ppdev
108ppp
116alsa
128ptm
136pts
180usb
188ttyUSB
189usb_device
216rfcomm
226drm
250globalmem
接下來,通過命令
#mknod /dev/globalmem c 250 0
創建 /dev/globalmem 設備節點,並通過 echo 'hello world' > /dev/globalmem 命令和 cat/dev/globalmem 命令分別驗證設備的寫和讀,結果證明 hello world 字符串被正確地寫入 globalmem 字符設備:
#echo "hello world" > /dev/globalmem
#cat /dev/globalmem
helloword
如果啓用了 sysfs 文件系統,將發現多出了 /sys/module/globalmem 目錄,該目錄下的樹型結構爲:
|--refcnt
`--sections
    |--.bss
    |-- .data
    |-- .gnu.linkonce.this_module
    |-- .rodata
    |-- .rodata.str1.1
    |-- .strtab
    |-- .symtab
    |-- .text
    `-- __versions
refcnt 記錄了 globalmem 模塊的引用計數,sections 下包含的數個文件則給出了 globalmem 所包含的 BSS、數據段和代碼段等的地址及其它信息。
對於代碼清單6.18給出的支持2個 globalmem 設備的驅動,在加載模塊後需創建2個設備節點,/dev/globalmem0 對應主設備號 globalmem_major,次設備號0,/dev/globalmem1 對應主設備號 globalmem_major,次設備號1。分別讀寫/dev/globalmem0和/dev/globalmem1,發現都讀寫到了正確的對應的設備。

6.5 總結

字符設備是三大類設備(字符設備、塊設備和網絡設備)中較簡單的一類設備,其驅動程序中完成的主要工作是初始化、添加和刪除cdev結構體,申請和釋放設備號,以及填充 file_operations 結構體中的操作函數,實現 file_operations 結構體中的 read()、write() 和 ioctl() 等函數是驅動設計的主體工作。




















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