Linux字符設備驅動框架

字符設備是Linux三大設備之一(另外兩種是塊設備,網絡設備),字符設備就是字節流形式通信的I/O設備,絕大部分設備都是字符設備,常見的字符設備包括鼠標、鍵盤、顯示器、串口等等,當咱們執行ls -l /dev的時候,就能看到大量的設備文件,c就是字符設備,b就是塊設備,網絡設備沒有對應的設備文件。編寫一個外部模塊的字符設備驅動,除了要實現編寫一個模塊所須要的代碼以外,還須要編寫做爲一個字符設備的代碼。node

驅動模型

Linux一切皆文件,那麼做爲一個設備文件,它的操做方法接口封裝在struct file_operations,當咱們寫一個驅動的時候,必定要實現相應的接口,這樣才能使這個驅動可用,Linux的內核中大量使用"註冊+回調"機制進行驅動程序的編寫,所謂註冊回調,簡單的理解,就是當咱們open一個設備文件的時候,實際上是經過VFS找到相應的inode,並執行此前建立這個設備文件時註冊在inode中的open函數,其餘函數也是如此,因此,爲了讓咱們寫的驅動可以正常的被應用程序操做,首先要作的就是實現相應的方法,而後再建立相應的設備文件。linux

#include <linux/cdev.h> //for struct cdev
#include <linux/fs.h>   //for struct file
#include <asm-generic/uaccess.h>    //for copy_to_user
#include <linux/errno.h>            //for error number

static int ma = 0;
static int mi = 0;
const int count = 3;

/* 準備操做方法集 */
/* 
struct file_operations {
    struct module *owner;   //THIS_MODULE
    
    //讀設備
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    //寫設備
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

    //映射內核空間到用戶空間
    int (*mmap) (struct file *, struct vm_area_struct *);

    //讀寫設備參數、讀設備狀態、控制設備
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

    //打開設備
    int (*open) (struct inode *, struct file *);
    //關閉設備
    int (*release) (struct inode *, struct file *);

    //刷新設備
    int (*flush) (struct file *, fl_owner_t id);

    //文件定位
    loff_t (*llseek) (struct file *, loff_t, int);

    //異步通知
    int (*fasync) (int, struct file *, int);
    //POLL機制
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    。。。
};
*/

ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset)
{
    return 0;
}

struct file fops = {
    .owner = THIS_MODULE,
    .read = myread,
    ...
};

/* 字符設備對象類型 
struct cdev {
    struct kobject kobj;     
    struct module *owner;        //模塊全部者(THIS_MODULE),用於模塊計數
    const struct file_operations *ops;    //操做方法集(分工:打開、關閉、讀/寫、...)
    struct list_head list;
    dev_t dev;                            //設備號(第一個)
    unsigned int count;            //設備數量
};
*/

static int __init chrdev_init(void)
{
    ...
    /* 構造cdev設備對象 */
    struct cdev *cdev_alloc(void);

    /* 初始化cdev設備對象 */
    void cdev_init(struct cdev*, const struct file_opeartions*);

    /* 申請設備號,靜態or動態*/
    /* 爲字符設備靜態申請第一個設備號 */
    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);

    ma = MAJOR(dev)     //從dev_t數據中獲得主設備號
    mi = MINOR(dev)     //從dev_t數據中獲得次設備號
    MKDEV(ma,1) //將主設備號和次設備號組合成設備號,多用於批量建立/刪除設備文件

    /* 註冊字符設備對象cdev到內核 */
    int cdev_add(struct cdev* , dev_t, unsigned);
    ...
}

static void __exit chrdev_exit(void)
{
    ...
    /* cdev_del()、cdev_put()二選一 */
    /* 從內核註銷cdev設備對象 */
    void cdev_del(struct cdev* );

    /* 從內核註銷cdev設備對象 */
    void cdev_put(stuct cdev *);

    /* 回收設備號 */
    void unregister_chrdev_region(dev_t from, unsigned count);
    ...
}

羅嗦一句,若是使用靜態申請設備號,那麼最大的問題就是不要與已知的設備號相沖突,內核在文檔"Documentation/devices.txt"中已經註明了哪些主設備號被使用了,從中能夠看出,在2^12個主設備號中,咱們可以使用的範圍是240-255以及261-2^12-1的部分,這也能夠解釋爲何咱們動態申請的時候,設備號常常是250的緣由。此外,經過這個文件,咱們也能夠看出,"主設備號表徵一類設備",可是字符/塊設備自己就能夠被分爲好多類,因此內核給他們每一類都分配了主設備號。
網絡

實現read,write

Linux下各個進程都有本身獨立的進程空間,即便是將內核的數據映射到用戶進程,該數據的PID也會自動轉變爲該用戶進程的PID,因爲這種機制的存在,咱們不能直接將數據從內核空間和用戶空間進行拷貝,而須要專門的拷貝數據函數/宏:異步

long copy_from_user(void *to, const void __user * from, unsigned long n)

long copy_to_user(void __user *to, const void *from, unsigned long n)

這兩個函數能夠將內核空間的數據拷貝到回調該函數的用戶進程的用戶進程空間,有了這兩個函數,內核中的read,write就能夠實現內核空間和用戶空間的數據拷貝。async

ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset)
{
    long ret = 0;
    size = size > MAX_KBUF?MAX_KBUF:size;
    if(copy_to_user(user_buf, kbuf,size)
        return -EAGAIN;
    }
    return 0;
}

實現ioctl

ioctl是Linux專門爲用戶層控制設備設計的系統調用接口,這個接口具備極大的靈活性,咱們的設備打算讓用戶經過哪些命令實現哪些功能,均可以經過它來實現,ioctl在操做方法集中對應的函數指針是long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);,其中的命令和參數徹底由驅動指定,一般命令會寫在一個頭文件中以供應用層和驅動層遵照一樣的通訊協議,Linux建議如圖所示的方式定義ioctl()命令函數

設備類型    序列號     方向      數據尺寸
8bit        8bit    2bit    13/14bit

設備類型字段爲一個幻數,能夠是0~0xff之間的數,內核中的"ioctl-number.txt"給出了一個推薦的和已經被使用的幻數(可是已經很久沒人維護了),新設備驅動定義幻數的時候要避免與其衝突。
序列號字段表示當前命令是整個ioctl命令中的第幾個,從1開始計數。
方向字段爲2bit,表示數據的傳輸方向,可能的值是:_IOC_NONE_IOC_READ_IOC_WRITE_IOC_READ|_IOC_WRITE
數據尺寸字段表示涉及的用戶數據的大小,這個成員的寬度依賴於體系結構,一般是13或14位。測試

內核還定義了_IO()_IOR()_IOW()_IOWR()這4個宏來輔助生成這種格式的命令。這幾個宏的做用是根據傳入的type(設備類型字段),nr(序列號字段)和size(數據長度字段)和方向字段移位組合生成命令碼。設計

內核中還預約義了一些I/O控制命令,若是某設備驅動中包含了與預約義命令同樣的命令碼,這些命令會被當作預約義命令被內核處理而不是被設備驅動處理,有以下4種:指針

  • FIOCLEX:即file ioctl close on exec 對文件設置專用的標誌,通知內核當exec()系統帶哦用發生時自動關閉打開的文件
  • FIONCLEX:即file ioctl not close on exec,清除由FIOCLEX設置的標誌
  • FIOQSIZE:得到一個文件或目錄的大小,當用於設備文件時,返回一個ENOTTY錯誤
  • FIONBIO:即file ioctl non-blocking I/O 這個調用修改flip->f_flags中的O_NONBLOCK標誌

實例

//mycmd.h
...
#include <asm/ioctl.h>
#define CMDT 'A'
#define KARG_SIZE 36
struct karg{
    int kval;
    char kbuf[KARG_SIZE];
};
#define CMD_OFF _IO(CMDT,0)
#define CMD_ON  _IO(CMDT,1)
#define CMD_R   _IOR(CMDT,2,struct karg)
#define CMD_W   _IOW(CMDT,3,struct karg)
...
//chrdev.c
static long demo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    static struct karg karg = {
        .kval = 0,
        .kbuf = {0},
    };
    struct karg *usr_arg;

    switch(cmd){
    case CMD_ON:
        /* 開燈 */
        break;
    case CMD_OFF:
        /* 關燈 */
        break;
    case CMD_R:
        if(_IOC_SIZE(cmd) != sizeof(karg)){
            return -EINVAL;
        }
        usr_arg = (struct karg *)arg;
        if(copy_to_user(usr_arg, &karg, sizeof(karg))){
            return -EAGAIN;
        }
        break;
    case CMD_W:     
        if(_IOC_SIZE(cmd) != sizeof(karg)){
            return -EINVAL;
        }
        usr_arg = (struct karg *)arg;
        if(copy_from_user(&karg, usr_arg, sizeof(karg))){
            return -EAGAIN;
        }
        break;
    default:
        ;
    };
    return 0;
}

建立設備文件

插入的設備模塊,咱們就可使用cat /proc/devices命令查看當前系統註冊的設備,可是咱們尚未建立相應的設備文件,用戶也就不能經過文件訪問這個設備。設備文件的inode應該是包含了這個設備的設備號,操做方法集指針等信息,這樣咱們就能夠經過設備文件找到相應的inode進而訪問設備。建立設備文件的方法有兩種,手動建立自動建立手動建立設備文件就是使用mknod /dev/xxx 設備類型 主設備號 次設備號的命令建立,因此首先須要使用cat /proc/devices查看設備的主設備號並經過源碼找到設備的次設備號,須要注意的是,理論上設備文件能夠放置在任何文件加夾,可是放到"/dev"才符合Linux的設備管理機制,這裏面的devtmpfs是專門設計用來管理設備文件的文件系統。設備文件建立好以後就會和建立時指定的設備綁定,即便設備已經被卸載了,如要刪除設備文件,只須要像刪除普通文件同樣rm便可。理論上模塊名(lsmod),設備名(/proc/devices),設備文件名(/dev)並無什麼關係,徹底能夠不同,可是原則上仍是建議將三者進行統一,便於管理。code

除了使用蹩腳的手動建立設備節點的方式,咱們還能夠在設備源碼中使用相應的措施使設備一旦被加載就自動建立設備文件,自動建立設備文件須要咱們在編譯內核的時候或製做根文件系統的時候就好相應的配置:

Device Drivers --->
        Generic Driver Options --->
            [*]Maintain a devtmpfs filesystem to mount at /dev
            [*] Automount devtmpfs at /dev,after the kernel mounted the rootfs

OR
製做根文件系統的啓動腳本寫入

mount -t sysfs none sysfs /sys
mdev -s //udev也行

有了這些準備,只須要導出相應的設備信息到"/sys"就能夠按照咱們的要求自動建立設備文件。內核給咱們提供了相關的API

class_create(owner,name);
struct device *device_create_vargs(struct class *cls, struct device *parent,dev_t devt, void *drvdata,const char *fmt, va_list vargs);

void class_destroy(struct class *cls);   
void device_destroy(struct class *cls, dev_t devt);

有了這幾個函數,咱們就能夠在設備的xxx_init()xxx_exit()中分別填寫如下的代碼就能夠實現自動的建立刪除設備文件

/* 在/sys中導出設備類信息 */
    cls = class_create(THIS_MODULE,DEV_NAME);

    /* 在cls指向的類中建立一組(個)設備文件 */
    for(i= minor;i<(minor+cnt);i++){
        devp = device_create(cls,NULL,MKDEV(major,i),NULL,"%s%d",DEV_NAME,i);
    }
/* 在cls指向的類中刪除一組(個)設備文件 */
    for(i= minor;i<(minor+cnt);i++){
        device_destroy(cls,MKDEV(major,i));
    }

    /* 在/sys中刪除設備類信息 */
    class_destroy(cls);             //必定要先卸載device再卸載class

完成了這些工做,一個簡單的字符設備驅動就搭建完成了,如今就能夠寫一個用戶程序進行測試了^ - ^

相關文章
相關標籤/搜索