1. 程式人生 > >linux驅動之字元裝置驅動

linux驅動之字元裝置驅動

字元裝置驅動框架
這裡寫圖片描述

cdev結構體: 描述字元裝置;
dev_t:定義裝置號(分為主、次裝置號)以確定字元裝置的唯一性;
file_operations:定義字元裝置驅動提供給VFS的介面函式,如常見的open()、read()、write()等;

字元裝置驅動模型如下:
這裡寫圖片描述

結構體定義

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 *);
1)將整個結構體清零;
2) 初始化list成員使其指向自身;
3
) 初始化kobj成員; 4) 初始化ops成員;
struct cdev *cdev_alloc(void);
分配一個struct cdev結構,動態申請一個cdev記憶體
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
向核心註冊一個struct cdev結構
void cdev_del(struct cdev *p);
向核心登出一個struct cdev結構

裝置號
主裝置號和次裝置號(二者一起為裝置號):
一個字元裝置或塊裝置都有一個主裝置號和一個次裝置號。主裝置號用來標識與裝置檔案相連的驅動程式,用來反映裝置型別。次裝置號被驅動程式用來辨別操作的是哪個裝置,用來區分同類型的裝置。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <linux/semaphore.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/device.h>

#define MAXNUM 100
#define MAJOR_NUM 456 //主裝置號 ,沒有被使用

struct dev{
    struct cdev devm;           //字元裝置
    struct semaphore sem;
    wait_queue_head_t outq;     //等待佇列,實現阻塞操作
    int flag;                   //阻塞喚醒標誌
    char buffer[MAXNUM+1];      //字元緩衝區
    char *rd,*wr,*end;          //讀,寫,尾指標
} globalvar;

static struct class *my_class;
int major=MAJOR_NUM;

static ssize_t globalvar_read(struct file *,char *,size_t ,loff_t *);
static ssize_t globalvar_write(struct file *,const char *,size_t ,loff_t *);
static int globalvar_open(struct inode *inode,struct file *filp);
static int globalvar_release(struct inode *inode,struct file *filp);
/*
結構體file_operations在標頭檔案 linux/fs.h中定義,用來儲存驅動核心模組提供的對裝置進行各種操作的函式的指標。
該結構體的每個域都對應著驅動核心模組用來處理某個被請求的事務的函式的地址。
裝置"gobalvar"的基本入口點結構變數gobalvar_fops 
*/
struct file_operations globalvar_fops =
{
    /*
    標記化的初始化格式這種格式允許用名字對這類結構的欄位進行初始化,這就避免了因資料結構發生變化而帶來的麻煩。
    這種標記化的初始化處理並不是標準 C 的規範,而是對 GUN 編譯器的一種(有用的)特殊擴充套件
    */
    //用來從裝置中獲取資料. 在這個位置的一個空指標導致 read 系統呼叫以 -EINVAL("Invalid argument") 失敗. 一個非負返回值代表了成功讀取的位元組數( 返回值是一個 "signed size" 型別, 常常是目標平臺本地的整數型別).
    .read=globalvar_read,
    //傳送資料給裝置. 如果 NULL, -EINVAL 返回給呼叫 write 系統呼叫的程式. 如果非負, 返回值代表成功寫的位元組數.
    .write=globalvar_write,
    //儘管這常常是對裝置檔案進行的第一個操作, 不要求驅動宣告一個對應的方法. 如果這個項是 NULL, 裝置開啟一直成功, 但是你的驅動不會得到通知.
    .open=globalvar_open,
    //當最後一個開啟裝置的使用者程序執行close ()系統呼叫時,核心將呼叫驅動程式的release () 函式:release 函式的主要任務是清理未結束的輸入/輸出操作、釋放資源、使用者自定義排他標誌的復位等。
    .release=globalvar_release,
};

//核心模組的初始化
static int globalvar_init(void)
{

    /*
    int register_chrdev(unsigned int major, unsigned int baseminor,unsigned int count, const char *name,const struct file_operations *fops)
    返回值提示操作成功還是失敗。負的返回值表示錯誤;0 或正的返回值表明操作成功。
    major引數是被請求的主裝置號,name 是裝置的名稱,該名稱將出現在 /proc/devices 中, 
    fops是指向函式指標陣列的指標,這些函式是呼叫驅動程式的入口點,
    在 2.6 的核心之後,新增了一個 register_chrdev_region 函式,
    它支援將同一個主裝置號下的次裝置號進行分段,每一段供給一個字元裝置驅動程式使用,使得資源利用率大大提升,
    */    
    int result = 0;
    int err = 0;
    /*
    巨集定義:#define MKDEV(major,minor) (((major) << MINORBITS) | (minor))
    成功執行返回dev_t型別的裝置編號,dev_t型別是unsigned int 型別,32位,用於在驅動程式中定義裝置編號,
    高12位為主裝置號,低20位為次裝置號,可以通過MAJOR和MINOR來獲得主裝置號和次裝置號。
    在module_init巨集呼叫的函式中去註冊字元裝置驅動
    major傳0進去表示要讓核心幫我們自動分配一個合適的空白的沒被使用的主裝置號
    核心如果成功分配就會返回分配的主裝置號;如果分配失敗會返回負數
    */
    dev_t dev = MKDEV(major, 0);
    if(major)
    {
        //靜態申請裝置編號
        result = register_chrdev_region(dev, 1, "charmem");
    }
    else
    {
        //動態分配裝置號
        result = alloc_chrdev_region(&dev, 0, 1, "charmem");
        major = MAJOR(dev);
    }
    if(result < 0)
        return result;
    /*
    file_operations這個結構體變數,讓cdev中的ops成員的值為file_operations結構體變數的值。
    這個結構體會被cdev_add函式想核心註冊cdev結構體,可以用很多函式來操作他。
    如:
    cdev_alloc:讓核心為這個結構體分配記憶體的
    cdev_init:將struct cdev型別的結構體變數和file_operations結構體進行繫結的
    cdev_add:向核心裡面新增一個驅動,註冊驅動
    cdev_del:從核心中登出掉一個驅動。登出驅動
    */
    //註冊字元裝置驅動,裝置號和file_operations結構體進行繫結
    cdev_init(&globalvar.devm, &globalvar_fops);
    /*
    #define THIS_MODULE (&__this_module)是一個struct module變數,代表當前模組,
    與那個著名的current有幾分相似,可以通過THIS_MODULE巨集來引用模組的struct module結構,
    比如使用THIS_MODULE->state可以獲得當前模組的狀態。
    現在你應該明白為啥在那個歲月裡,你需要毫不猶豫毫不遲疑的將struct usb_driver結構裡的owner設定為THIS_MODULE了吧,
    這個owner指標指向的就是你的模組自己。
    那現在owner咋就說沒就沒了那?這個說來可就話長了,咱就長話短說吧。
    不知道那個時候你有沒有忘記過初始化owner,
    反正是很多人都會忘記,
    於是在2006年的春節前夕,在咱們都無心工作無心學習等著過春節的時候,Greg堅守一線,去掉了 owner,
    於是千千萬萬個寫usb驅動的人再也不用去時刻謹記初始化owner了。
    咱們是不用設定owner了,可core裡不能不設定,
    struct usb_driver結構裡不是沒有owner了麼,
    可它裡面嵌的那個struct device_driver結構裡還有啊,設定了它就可以了。
    於是Greg同時又增加了usb_register_driver()這麼一層,
    usb_register()可以通過將引數指定為THIS_MODULE去呼叫它,所有的事情都挪到它裡面去做。
    反正usb_register() 也是內聯的,並不會增加呼叫的開銷。
    */
    globalvar.devm.owner = THIS_MODULE;
    err = cdev_add(&globalvar.devm, dev, 1);
    if(err)
        printk(KERN_INFO "Error %d adding char_mem device", err);
    else
    {
        printk("globalvar register success\n");
        sema_init(&globalvar.sem,1);            //初始化訊號量
        init_waitqueue_head(&globalvar.outq);   //初始化等待佇列
        globalvar.rd = globalvar.buffer; //讀指標
        globalvar.wr = globalvar.buffer; //寫指標
        globalvar.end = globalvar.buffer + MAXNUM;//緩衝區尾指標
        globalvar.flag = 0; // 阻塞喚醒標誌置 0
    }
    /*
    定義在/include/linux/device.h
    建立class並將class註冊到核心中,返回值為class結構指標
    在驅動初始化的程式碼裡呼叫class_create為該裝置建立一個class,再為每個裝置呼叫device_create建立對應的裝置。
    省去了利用mknod命令手動建立裝置節點
    */
    my_class = class_create(THIS_MODULE, "chardev0");
    device_create(my_class, NULL, dev, NULL, "chardev0");
    return 0;
}
/*
在大部分驅動程式中,open 應完成如下工作:
● 遞增使用計數。--為了老版本的可移植性
● 檢查裝置特定的錯誤(諸如裝置未就緒或類似的硬體問題)。
● 如果裝置是首次開啟,則對其進行初始化。
● 識別次裝置號,並且如果有必要,更新 f_op 指標。
● 分配並填寫被置於 filp->private_data 裡的資料結構。
*/
static int globalvar_open(struct inode *inode,struct file *filp)
{
    try_module_get(THIS_MODULE);//模組計數加一
    printk("This chrdev is in open\n");
    return(0);
}
/*
release都應該完成下面的任務:
● 釋放由 open 分配的、儲存在 filp->private_data 中的所有內容。
● 在最後一次關閉操作時關閉裝置。字元裝置驅動程式
● 使用計數減 1。
如果使用計數不歸0,核心就無法解除安裝模組。
並不是每個 close 系統呼叫都會引起對 release 方法的呼叫。
僅僅是那些真正釋放裝置資料結構的 close 呼叫才會呼叫這個方法,
因此名字是 release 而不是 close。核心維護一個 file 結構被使用多少次的計數器。
無論是 fork 還是 dup 都不建立新的資料結構(僅由 open 建立),它們只是增加已有結構中的計數。
*/
static int globalvar_release(struct inode *inode,struct file *filp)
{
    module_put(THIS_MODULE); //模組計數減一
    printk("This chrdev is in release\n");
    return(0);
}
static void globalvar_exit(void)
{
    device_destroy(my_class, MKDEV(major, 0));
    class_destroy(my_class);
    cdev_del(&globalvar.devm);
    /*
    引數列表包括要釋放的主裝置號和相應的裝置名。
    引數中的這個裝置名會被核心用來和主裝置號引數所對應的已註冊裝置名進行比較,如果不同,則返回 -EINVAL。
    如果主裝置號超出了所允許的範圍,則核心同樣返回 -EINVAL。
    */
    unregister_chrdev_region(MKDEV(major, 0), 1);//登出裝置
}
/*
ssize_t read(struct file *filp, char *buff,size_t count, loff_t *offp);
引數 filp 是檔案指標,引數 count 是請求傳輸的資料長度。
引數 buff 是指向使用者空間的緩衝區,這個緩衝區或者儲存將寫入的資料,或者是一個存放新讀入資料的空緩衝區。
最後的 offp 是一個指向“long offset type(長偏移量型別)”物件的指標,這個物件指明使用者在檔案中進行存取操作的位置。
返回值是“signed size type(有符號的尺寸型別)”

主要問題是,需要在核心地址空間和使用者地址空間之間傳輸資料。
不能用通常的辦法利用指標或 memcpy來完成這樣的操作。由於許多原因,不能在核心空間中直接使用使用者空間地址。
核心空間地址與使用者空間地址之間很大的一個差異就是,使用者空間的記憶體是可被換出的。
當核心訪問使用者空間指標時,相對應的頁面可能已不在記憶體中了,這樣的話就會產生一個頁面失效
*/
static ssize_t globalvar_read(struct file *filp,char *buf,size_t len,loff_t *off)
{
    if(wait_event_interruptible(globalvar.outq,globalvar.flag!=0)) //不可讀時 阻塞讀程序
    {
        return -ERESTARTSYS;
    }
    /*
    down_interruptible 可以由一個訊號中斷,但 down 不允許有訊號傳送到程序。
    大多數情況下都希望訊號起作用;否則,就有可能建立一個無法殺掉的程序,併產生其他不可預期的結果。
    但是,允許訊號中斷將使得訊號量的處理複雜化,因為我們總要去檢查函式(這裡是 down_interruptible)是否已被中斷。
    一般來說,當該函式返回 0 時表示成功,返回非 0 時則表示出錯。
    如果這個處理過程被中斷,它就不會獲得訊號量 , 因此,也就不能呼叫 up 函數了。
    因此,對訊號量的典型呼叫通常是下面的這種形式:
    if (down_interruptible (&sem))
        return -ERESTARTSYS;
    返回值 -ERESTARTSYS通知系統操作被訊號中斷。
    呼叫這個裝置方法的核心函式或者重新嘗試,或者返回 -EINTR 給應用程式,這取決於應用程式是如何設定訊號處理函式的。
    當然,如果是以這種方式中斷操作的話,那麼程式碼應在返回前完成清理工作。

    使用down_interruptible來獲取訊號量的程式碼不應呼叫其他也試圖獲得該訊號量的函式,否則就會陷入死鎖。
    如果驅動程式中的某段程式對其持有的訊號量釋放失敗的話(可能就是一次出錯返回的結果),
    那麼其他任何獲取該訊號量的嘗試都將阻塞在那裡。
    */
    if(down_interruptible(&globalvar.sem)) //P 操作
    {
        return -ERESTARTSYS;
    }
    globalvar.flag = 0;
    printk("into the read function\n");
    printk("the rd is %c\n",*globalvar.rd); //讀指標
    if(globalvar.rd < globalvar.wr)
        len = min(len,(size_t)(globalvar.wr - globalvar.rd)); //更新讀寫長度
    else
        len = min(len,(size_t)(globalvar.end - globalvar.rd));
    printk("the len is %d\n",len);
    /*
    read 和 write 程式碼要做的工作,就是在使用者地址空間和核心地址空間之間進行整段資料的拷貝。
    這種能力是由下面的核心函式提供的,它們用於拷貝任意的一段位元組序列,這也是每個 read 和 write 方法實現的核心部分:
    unsigned long copy_to_user(void *to, const void *from,unsigned long count);
    unsigned long copy_from_user(void *to, const void *from,unsigned long count);
    雖然這些函式的行為很像通常的 memcpy 函式,但當在核心空間內執行的程式碼訪問使用者空間時,則要多加小心。
    被定址的使用者空間的頁面可能當前並不在記憶體,於是處理頁面失效的程式會使訪問程序轉入睡眠,直到該頁面被傳送至期望的位置。
    例如,當頁面必須從交換空間取回時,這樣的情況就會發生。對於驅動程式編寫人員來說,
    結果就是訪問使用者空間的任何函式都必須是可重入的,並且必須能和其他驅動程式函式併發執行。
    這就是我們使用訊號量來控制併發訪問的原因.
    這兩個函式的作用並不限於在核心空間和使用者空間之間拷貝資料,它們還檢查使用者空間的指標是否有效。
    如果指標無效,就不會進行拷貝;另一方面,如果在拷貝過程中遇到無效地址,則僅僅會複製部分資料。
    在這兩種情況下,返回值是還未拷貝完的記憶體的數量值。
    如果發現這樣的錯誤返回,就會在返回值不為 0 時,返回 -EFAULT 給使用者。
    負值意味著發生了錯誤,該值指明發生了什麼錯誤,錯誤碼在<linux/errno.h>中定義。
    比如這樣的一些錯誤:-EINTR(系統呼叫被中斷)或 -EFAULT (無效地址)。
    */
    if(copy_to_user(buf,globalvar.rd,len))
    {
        printk(KERN_ALERT"copy failed\n");
        /*
        up遞增訊號量的值,並喚醒所有正在等待訊號量轉為可用狀態的程序。
        必須小心使用訊號量。被訊號量保護的資料必須是定義清晰的,並且存取這些資料的所有程式碼都必須首先獲得訊號量。
        */
        up(&globalvar.sem);
        return -EFAULT;
    }
    printk("the read buffer is %s\n",globalvar.buffer);
    globalvar.rd = globalvar.rd + len;
    if(globalvar.rd == globalvar.end)
        globalvar.rd = globalvar.buffer; //字元緩衝區迴圈
    up(&globalvar.sem); //V 操作
    return len;
}
static ssize_t globalvar_write(struct file *filp,const char *buf,size_t len,loff_t *off)
{
    if(down_interruptible(&globalvar.sem)) //P 操作
    {
        return -ERESTARTSYS;
    }
    if(globalvar.rd <= globalvar.wr)
        len = min(len,(size_t)(globalvar.end - globalvar.wr));
    else
        len = min(len,(size_t)(globalvar.rd-globalvar.wr-1));
    printk("the write len is %d\n",len);
    if(copy_from_user(globalvar.wr,buf,len))
    {
        up(&globalvar.sem); //V 操作
        return -EFAULT;
    }
    printk("the write buffer is %s\n",globalvar.buffer);
    printk("the len of buffer is %d\n",strlen(globalvar.buffer));
    globalvar.wr = globalvar.wr + len;
    if(globalvar.wr == globalvar.end)
    globalvar.wr = globalvar.buffer; //迴圈
    up(&globalvar.sem);
    //V 操作
    globalvar.flag=1; //條件成立,可以喚醒讀程序
    wake_up_interruptible(&globalvar.outq); //喚醒讀程序
    return len;
}
module_init(globalvar_init);
module_exit(globalvar_exit);
MODULE_LICENSE("GPL");

寫一個Makefile,編譯成ko,然後用insmod載入
cat /proc/devices可以看到具體的裝置 456 charmem

Character devices:
  1 mem
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
 10 misc
 13 input
 29 fb
 81 video4linux
 89 i2c
 90 mtd
108 ppp
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
456 charmem
226 drm
240 cmem
241 pvrsrvkm
242 rpmsg_rpc
243 roccat
244 hidraw
245 uio
246 oases
247 bsg
248 watchdog
249 tee
250 iio
251 ptp
252 pps
253 media
254 rtc