linux設備驅動第三篇:寫一個簡單的字符設備驅動
在linux設備驅動第一篇:設備驅動程序簡介中簡單介紹了字符驅動,本篇簡單介紹如何寫一個簡單的字符設備驅動。本篇借鑒LDD中的源碼,實現一個與硬件設備無關的字符設備驅動,僅僅操作從內核中分配的一些內存。
下面就開始學習如何寫一個簡單的字符設備驅動。首先我們來分解一下字符設備驅動都有那些結構或者方法組成,也就是說實現一個可以使用的字符設備驅動我們必須做些什麽工作。
1、主設備號和次設備號
對於字符設備的訪問是通過文件系統中的設備名稱進行的。他們通常位於/dev目錄下。如下: [plain] view plain copy- [email protected]
1.1、設備編號的表達
1.2、分配和釋放設備編號
在構建一個字符設備之前,驅動程序首先要獲得一個或者多個設備編號,這類似一個營業執照,有了營業執照才在內核中正常工作營業。完成此工作的函數是: [cpp] view plain copy- int register_chrdev_region(dev_t first, unsigned int count, const char *name);
- int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
- void unregister_chrdev_region(dev_t dev, unsigned int count);
2、重要的數據結構
註冊設備編號僅僅是完成一個字符設備驅動的第一步。下面介紹大部分驅動都會包含的三個重要的內核的數據結構。2.1、文件操作file_operations
file_operations是第一個重要的結構,定義在 <linux/fs.h>, 是一個函數指針的集合,設備所能提供的功能大部分都由此結構提供。這些操作也是設備相關的系統調用的具體實現。此結構的具體實現如下所示: [cpp] view plain copy- struct file_operations {
- //它是一個指向擁有這個結構的模塊的指針. 這個成員用來在它的操作還在被使用時阻止模塊被卸載. 幾乎所有時間中, 它被簡單初始化為 THIS_MODULE
- 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);
- ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
- ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
- int (*iterate) (struct file *, struct dir_context *);
- unsigned int (*poll) (struct file *, struct poll_table_struct *);
- 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 *, loff_t, loff_t, 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 (*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 **);
- long (*fallocate)(struct file *file, int mode, loff_t offset,
- loff_t len);
- int (*show_fdinfo)(struct seq_file *m, struct file *f);
- };
2.2、文件結構struct file
struct file, 定義於 <linux/fs.h>, 是設備驅動中第二個最重要的數據結構。文件結構代表一個打開的文件. (它不特定給設備驅動; 系統中每個打開的文件有一個關聯的 struct file 在內核空間). 它由內核在 open 時創建, 並傳遞給在文件上操作的任何函數, 直到最後的關閉. 在文件的所有實例都關閉後, 內核釋放這個數據結構。file結構的詳細可參考fs.h,這裏列出來幾個重要的成員。- struct file_operations *f_op:就是上面剛剛介紹的文件操作的集合結構。
- mode_t f_mode:文件模式確定文件是可讀的或者是可寫的(或者都是), 通過位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函數中檢查這個成員的讀寫許可, 但是你不需要檢查讀寫許可, 因為內核在調用你的方法之前檢查. 當文件還沒有為那種存取而打開時讀或寫的企圖被拒絕, 驅動甚至不知道這個情況
- loff_t f_pos:當前讀寫位置. loff_t 在所有平臺都是 64 位。驅動可以讀這個值, 如果它需要知道文件中的當前位置, 但是正常地不應該改變它。
- unsigned int f_flags:這些是文件標誌, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驅動應當檢查 O_NONBLOCK 標誌來看是否是請求非阻塞操作。
- void *private_data:open 系統調用設置這個指針為 NULL, 在為驅動調用 open 方法之前. 你可自由使用這個成員或者忽略它; 你可以使用這個成員來指向分配的數據, 但是接著你必須記住在內核銷毀文件結構之前, 在 release 方法中釋放那個內存. private_data 是一個有用的資源, 在系統調用間保留狀態信息, 我們大部分例子模塊都使用它
2.3、inode 結構
inode 結構由內核在內部用來表示文件. 因此, 它和代表打開文件描述符的文件結構是不同的. 可能有代表單個文件的多個打開描述符的許多文件結構, 但是它們都指向一個單個 inode 結構。
inode 結構包含大量關於文件的信息。但對於驅動程序編寫來說一般不用關心,暫且不說。
3、字符設備的註冊
內核在內部使用類型 struct cdev 的結構來代表字符設備. 在內核調用你的設備操作前, 你編寫分配並註冊一個或幾個這些結構。有 2 種方法來分配和初始化一個這些結構. 如果你想在運行時獲得一個獨立的 cdev 結構, 你可以為此使用這樣的代碼:
[cpp] view plain copy- struct cdev *my_cdev = cdev_alloc();
- my_cdev->ops = &my_fops;
- void cdev_init(struct cdev *cdev, struct file_operations *fops);
- int cdev_add(struct cdev *dev, dev_t num, unsigned int count)
這裏, dev 是 cdev 結構, num 是這個設備響應的第一個設備號, count 是應當關聯到設備的設備號的數目. 常常 count 是 1。
從系統去除一個字符設備, 調用:
[cpp] view plain copy- void cdev_del(struct cdev *dev);
4、一個簡單的字符設備
上面大致介紹了實現一個字符設備所要做的工作,下面就來一個真實的例子來總結上面介紹的內容。源碼中的關鍵地方已經作了註釋。 [cpp] view plain copy- #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/uaccess.h>
- #include <linux/timer.h>
- #include <asm/atomic.h>
- #include <linux/slab.h>
- #include <linux/device.h>
- #define CDEVDEMO_MAJOR 255 /*預設cdevdemo的主設備號*/
- static int cdevdemo_major = CDEVDEMO_MAJOR;
- /*設備結構體,此結構體可以封裝設備相關的一些信息等
- 信號量等也可以封裝在此結構中,後續的設備模塊一般都
- 應該封裝一個這樣的結構體,但此結構體中必須包含某些
- 成員,對於字符設備來說,我們必須包含struct cdev cdev*/
- struct cdevdemo_dev
- {
- struct cdev cdev;
- };
- struct cdevdemo_dev *cdevdemo_devp; /*設備結構體指針*/
- /*文件打開函數,上層對此設備調用open時會執行*/
- int cdevdemo_open(struct inode *inode, struct file *filp)
- {
- printk(KERN_NOTICE "======== cdevdemo_open ");
- return 0;
- }
- /*文件釋放,上層對此設備調用close時會執行*/
- int cdevdemo_release(struct inode *inode, struct file *filp)
- {
- printk(KERN_NOTICE "======== cdevdemo_release ");
- return 0;
- }
- /*文件的讀操作,上層對此設備調用read時會執行*/
- static ssize_t cdevdemo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
- {
- printk(KERN_NOTICE "======== cdevdemo_read ");
- }
- /* 文件操作結構體,文中已經講過這個結構*/
- static const struct file_operations cdevdemo_fops =
- {
- .owner = THIS_MODULE,
- .open = cdevdemo_open,
- .release = cdevdemo_release,
- .read = cdevdemo_read,
- };
- /*初始化並註冊cdev*/
- static void cdevdemo_setup_cdev(struct cdevdemo_dev *dev, int index)
- {
- printk(KERN_NOTICE "======== cdevdemo_setup_cdev 1");
- int err, devno = MKDEV(cdevdemo_major, index);
- printk(KERN_NOTICE "======== cdevdemo_setup_cdev 2");
- /*初始化一個字符設備,設備所支持的操作在cdevdemo_fops中*/
- cdev_init(&dev->cdev, &cdevdemo_fops);
- printk(KERN_NOTICE "======== cdevdemo_setup_cdev 3");
- dev->cdev.owner = THIS_MODULE;
- dev->cdev.ops = &cdevdemo_fops;
- printk(KERN_NOTICE "======== cdevdemo_setup_cdev 4");
- err = cdev_add(&dev->cdev, devno, 1);
- printk(KERN_NOTICE "======== cdevdemo_setup_cdev 5");
- if(err)
- {
- printk(KERN_NOTICE "Error %d add cdevdemo %d", err, index);
- }
- }
- int cdevdemo_init(void)
- {
- printk(KERN_NOTICE "======== cdevdemo_init ");
- int ret;
- dev_t devno = MKDEV(cdevdemo_major, 0);
- struct class *cdevdemo_class;
- /*申請設備號,如果申請失敗采用動態申請方式*/
- if(cdevdemo_major)
- {
- printk(KERN_NOTICE "======== cdevdemo_init 1");
- ret = register_chrdev_region(devno, 1, "cdevdemo");
- }else
- {
- printk(KERN_NOTICE "======== cdevdemo_init 2");
- ret = alloc_chrdev_region(&devno,0,1,"cdevdemo");
- cdevdemo_major = MAJOR(devno);
- }
- if(ret < 0)
- {
- printk(KERN_NOTICE "======== cdevdemo_init 3");
- return ret;
- }
- /*動態申請設備結構體內存*/
- cdevdemo_devp = kmalloc(sizeof(struct cdevdemo_dev), GFP_KERNEL);
- if(!cdevdemo_devp) /*申請失敗*/
- {
- ret = -ENOMEM;
- printk(KERN_NOTICE "Error add cdevdemo");
- goto fail_malloc;
- }
- memset(cdevdemo_devp,0,sizeof(struct cdevdemo_dev));
- printk(KERN_NOTICE "======== cdevdemo_init 3");
- cdevdemo_setup_cdev(cdevdemo_devp, 0);
- /*下面兩行是創建了一個總線類型,會在/sys/class下生成cdevdemo目錄
- 這裏的還有一個主要作用是執行device_create後會在/dev/下自動生成
- cdevdemo設備節點。而如果不調用此函數,如果想通過設備節點訪問設備
- 需要手動mknod來創建設備節點後再訪問。*/
- cdevdemo_class = class_create(THIS_MODULE, "cdevdemo");
- device_create(cdevdemo_class, NULL, MKDEV(cdevdemo_major, 0), NULL, "cdevdemo");
- printk(KERN_NOTICE "======== cdevdemo_init 4");
- return 0;
- fail_malloc:
- unregister_chrdev_region(devno,1);
- }
- void cdevdemo_exit(void) /*模塊卸載*/
- {
- printk(KERN_NOTICE "End cdevdemo");
- cdev_del(&cdevdemo_devp->cdev); /*註銷cdev*/
- kfree(cdevdemo_devp); /*釋放設備結構體內存*/
- unregister_chrdev_region(MKDEV(cdevdemo_major,0),1); //釋放設備號
- }
- MODULE_LICENSE("Dual BSD/GPL");
- module_param(cdevdemo_major, int, S_IRUGO);
- module_init(cdevdemo_init);
- module_exit(cdevdemo_exit);
- ifneq ($(KERNELRELEASE),)
- obj-m := cdevdemo.o
- else
- KERNELDIR ?= /lib/modules/$(shell uname -r)/build
- PWD := $(shell pwd)
- default:
- $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
- endif
- clean:
- rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order Module.symvers
5、總結
本篇主要介紹了簡單字符設備的編寫與實現以及其中的關鍵點。下一篇會主要講解下驅動的一些常用的調試技巧。linux設備驅動第三篇:寫一個簡單的字符設備驅動