Linux 驅動學習筆記05--字符驅動實例,實現一個共享內存設備的驅動

斷斷續續學驅動,好不容易有空,作了段字符驅動的例子。主要仍是跟書上學習在此記錄下來,之後說不定能回過頭來溫故知新。node

首先上驅動源碼 gmem.clinux

/************************************************************************* > File Name: gmem.c > Author: hailin.ma > Mail: mhl2018@126.com > Created Time: Fri 18 Dec 2015 05:08:51 PM CST ************************************************************************/ #include <linux/cdev.h> #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/mm.h> #include <linux/types.h> #include <linux/sched.h> #include <asm/io.h> #include <asm/system.h> #include <asm/uaccess.h> #define GMEM_SIZE 0X1000 #define GMEM_CLEAR 0XFF #define GMEN_MAJOR 250 static int gmem_major = GMEN_MAJOR; typedef struct gmem_dev{ struct cdev cdev; unsigned char mem[GMEM_SIZE]; }*GMEM_DEVP; GMEM_DEVP gmem_devp = NULL; static ssize_t gmem_read(struct file *filp,char __user *buf,size_t count,loff_t *ppos) { unsigned long p = *ppos; int ret = 0; GMEM_DEVP dev = filp->private_data; if(p >= GMEM_SIZE) return 0; if(count > GMEM_SIZE - p) count = GMEM_SIZE - p; if(copy_to_user(buf,(void*)(dev->mem + p),count)) ret = -EFAULT; else{ *ppos += count; ret = count; printk(KERN_INFO"read %d byte(s) from %d\n",count,p); } return ret; } static ssize_t gmem_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos) { unsigned long p = *ppos; int ret = 0; GMEM_DEVP dev = filp->private_data; if(p >= GMEM_SIZE) return 0; if(p > GMEM_SIZE - p) count = GMEM_SIZE - p; if(copy_from_user(dev->mem + p,buf,count)) ret = -EFAULT; else{ *ppos += count; ret = count; printk(KERN_INFO"written %d byte(s) from %d\n",count,p); } } static loff_t gmem_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 > GMEM_SIZE){ ret = -EINVAL; break; } filp->f_pos = (unsigned int)offset; ret = filp->f_pos; break; case 1: if((filp->f_pos + offset) > GMEM_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; } static int gmem_ioctl(struct inode *inodep,struct file *filp,unsigned int cmd,unsigned long arg) { GMEM_DEVP dev = filp->private_data; switch(cmd){ case GMEM_CLEAR: memset(dev->mem,0,GMEM_SIZE); printk(KERN_INFO"gmem is clear!\n"); break; default: return -EINVAL; } return 0; } int gmem_open(struct inode *inode,struct file *filep) { filep->private_data = gmem_devp; return 0; } static const struct file_operations gmem_fops = { .owner = THIS_MODULE, .open = gmem_open, .llseek = gmem_llseek, .read = gmem_read, .write = gmem_write, .ioctl = gmem_ioctl, }; void gmem_setup_cdev() { int err; dev_t devno = MKDEV(gmem_major,0); cdev_init(&gmem_devp->cdev,&gmem_fops); gmem_devp->cdev.owner = THIS_MODULE; err = cdev_add(&gmem_devp->cdev,devno,1); if(err){ printk(KERN_NOTICE"Error %d adding gmem",err); } } static int __init gmem_init(void) { int result; printk(KERN_INFO"Init gmem\n"); dev_t devno = MKDEV(gmem_major,0); //經過MKDEV宏生成設備號 if(gmem_major){ result = register_chrdev_region(devno,1,"gmem"); } else{ result = alloc_chrdev_region(&devno,0,1,"gmem"); gmem_major = MAJOR(devno); } if(result < 0){ return result; } gmem_devp = kmalloc(sizeof(struct gmem_dev),GFP_KERNEL); if(!gmem_devp){ result = - ENOMEM; goto fail_malloc; } memset(gmem_devp,0,sizeof(struct gmem_dev)); gmem_setup_cdev(); return 0; fail_malloc: unregister_chrdev_region(devno,0); return result; } static void __exit gmem_exit(void) { printk(KERN_INFO"exit gmem\n"); } module_init(gmem_init); module_exit(gmem_exit); module_param(gmem_major,int,S_IRUGO); MODULE_AUTHOR("malth <malth0988@163.com>"); MODULE_LICENSE("Dual BSD/GPL"); MODULE_DESCRIPTION("globle memory module"); MODULE_ALIAS("globle memory");

程序不算長,200行左右,下面進行具體分析。shell

在 Linux 2.6 內核中,使用 cdev 結構體描述一個字符設備, cdev 結構體的定義:app

 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 *);
async

cdev_init()函數用於初始化 cdev 的成員,並創建 cdev 和 file_operations 之間的鏈接,其源代
碼:函數

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 內存,其源代碼:學習

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()函數的調用則
一般發生在字符設備驅動模塊卸載函數中。ui

在調用 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);    //用於設備號未知,向系統動態申請未被佔用的設備號的狀況
spa

函數調用成功以後,會把獲得的設備號放入第一個參數 dev 中。 alloc_chrdev_region()與 register_chrdev_region()對比的優勢在於它會自動
避開設備號重複的衝突。
相反地,在調用 cdev_del()函數從系統註銷字符設備以後, unregister_chrdev_region()應該被調
用以釋放原先申請的設備號,這個函數的原型爲:
void unregister_chrdev_region(dev_t from, unsigned count);

 file_operations 結構體中的成員函數是字符設備驅動程序設計的主體內容,這些函數實際會在
應用程序進行 Linux 的 open()、 write()、 read()、 close()等系統調用時最終被調用。 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 *, const char _ _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);
    /* 執行設備 I/O 控制命令*/
    long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
    /* 不使用 BLK 的文件系統,將使用此種函數指針代替 ioctl */
    long(*compat_ioctl)(struct file *, unsigned int, unsigned long);
    /* 在 64 位系統上, 32 位的 ioctl 調用,將使用此函數指針代替*/
    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);
    /* 異步 fsync */
    int(*fasync)(int, struct file *, int);
    /* 通知設備 FASYNC 標誌發生變化*/
    int(*lock)(struct file *, int, struct file_lock*);
    ssize_t(*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int);
    /* 一般爲 NULL */
    unsigned long(*get_unmapped_area)(struct file *,unsigned long, nsigned long,
    unsigned long, unsigned long);
    /* 在當前進程地址空間找到一個未映射的內存段 */
    int(*check_flags)(int);
    /* 容許模塊檢查傳遞給 fcntl(F_SETEL...)調用的標誌 */
    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); /* 由 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()等系統調用進行讀寫。

Linux字符設備驅動的組成

1.字符設備驅動模塊加載與卸載函數
在字符設備驅動模塊加載函數中應該實現設備號的申請和 cdev 的註冊,而在卸載函數中應實現設備號的釋放和 cdev 的註銷。

 

/* 設備結構體*/
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); /* 註銷設備*/
    ...
}

 

 

 

2.字符設備驅動的 file_operations 結構體中成員函數
file_operations 結構體中成員函數是字符設備驅動與內核的接口,是用戶空間對 Linux 進行系
統調用最終的落實者。大多數字符設備驅動會實現 read()、 write()和 ioctl()函數,常見的字符設備
驅動的這 3 個函數的形式如

/* 讀設備*/
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;
    case XXX_CMD2:
        ...
        break;
    default:
        /* 不能支持的命令 */
        return - ENOTTY;
    }
    return 0;
}    

 

設備驅動的讀函數中, filp 是文件結構體指針, buf 是用戶空間內存的地址,該地址在內核空間不能直接讀寫, count 是要讀的字節數, f_pos 是讀的位置相對於文件開頭的偏移。
設備驅動的寫函數中, filp 是文件結構體指針, buf 是用戶空間內存的地址,該地址在內核空間不能直接讀寫, count 是要寫的字節數, f_pos 是寫的位置相對於文件開頭的偏移。
因爲內核空間與用戶空間的內存不能直接互訪,所以藉助了函數 copy_from_user()完成用戶空間到內核空間的拷貝,以及 copy_to_user()完成內核空間到用戶空間的拷貝。
完成內核空間和用戶空間內存拷貝的 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(),如:

int val; // 內核空間整型變量
...
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

 

I/O 控制函數的 cmd 參數爲事先定義的 I/O 控制命令,而 arg 爲對應於該命令的參數。例如對於串行設備,若是 SET_BAUDRATE 是一道設置波特率的命令,那後面的 arg 就應該是
波特率值。
在字符設備驅動中,須要定義一個 file_operations 的實例,並將具體設備驅動的函數賦值給file_operations 的成員,如代碼所示。

struct file_operations xxx_fops = {
    .owner = THIS_MODULE,
    .read = xxx_read,
    .write = xxx_write,
    .ioctl = xxx_ioctl,
    ...
};

 xxx_fops 在 cdev_init(&xxx_dev.cdev, &xxx_fops)的語句中被創建與 cdev 的鏈接。

分析完了基本的函數,下面貼上Makefile文件:

 

#CFLAGS = -g
KVERS = $(shell uname -r)

obj-m += gmem.o
mem-objs := gmem.o


all:
    make -C /lib/modules/$(KVERS)/build M=$(PWD) modules
    @rm *.o

clean:
    make -C /lib/modules/$(KVERS)/build M=$(PWD) clean
    @rm -f *.markers *.order

 

make以後生成gmem.ko文件,insmod gmem.ko gmem_major=303 加載驅動,303是指定主設備號, mknod /dev/gmem c 303 0 建立字符設備,主設備號303,次設備號0。

echo hello gmem! > /dev/gmem

cat /devgmem

經過上面兩條命令能夠向gmem寫入數據和查看數據,這個方法比較方便,另外就是用open,read來查看了:

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>


int main()
{
    int fd;
    char* pathname = "/dev/gmem";

    fd = open(pathname,O_RDWR);
    if(fd == -1)
    {
        printf("open error");
    }

    char wrbuf[] = "gmem is good!\n";
    write(fd,wrbuf,sizeof(wrbuf));

    close(fd);

    return 0;
}

上面代碼簡單的用文件來打開設備,而後向其中寫入數據,程序能夠繼續拓展 read,ioctl之類的,時間緣由,本文就更新到這裏,後續可能會補充完善。

相關文章
相關標籤/搜索