在linux設備驅動第一篇:設備驅動程序簡介中簡單介紹了字符驅動,本篇簡單介紹如何寫一個簡單的字符設備驅動。本篇借鑑LDD中的源碼,實現一個與硬件設備無關的字符設備驅動,僅僅操做從內核中分配的一些內存。node
下面就開始學習如何寫一個簡單的字符設備驅動。首先咱們來分解一下字符設備驅動
都有那些結構或者方法組成,也就是說實現一個可使用的字符設備驅動咱們必須作些什麼工做。linux
對於字符設備的訪問是經過文件系統中的設備名稱進行的。他們一般位於/dev目錄下。以下:程序員
[cpp] view plaincopyshell
xxx@ubuntu :~$ ls -l /dev/ ubuntu
total 0 微信
brw-rw---- 1 root disk 7, 0 3月 25 10:34 loop0 數據結構
brw-rw---- 1 root disk 7, 1 3月 25 10:34 loop1 app
brw-rw---- 1 root disk 7, 2 3月 25 10:34 loop2 微信公衆平臺
crw-rw-rw- 1 root tty 5, 0 3月 25 12:48 tty async
crw--w---- 1 root tty 4, 0 3月 25 10:34 tty0
crw-rw---- 1 root tty 4, 1 3月 25 10:34 tty1
crw--w---- 1 root tty 4, 10 3月 25 10:34 tty10
其中b表明塊設備,c表明字符設備。對於普通文件來講,ls -l會列出文件的長度,而對於設備文件來講,上面的7,5,4等表明的是對應設備的主設備號,然後面的0,1,2,10等則是對應設備的次設備號。那麼主設備號和次設備號分別表明什麼意義呢?通常狀況下,能夠這樣理解,主設備號標識設備對應的驅動程序,也就是說1個主設備號對應一個驅動程序。固然,如今也有多個驅動程序共享主設備號的狀況。而次設備號有內核使用,用於肯定/dev下的設備文件對應的具體設備。舉一個例子,虛擬控制檯和串口終端有驅動程序4管理,而不一樣的終端分別有不一樣的次設備號。
在內核中,dev_t用來保存設備編號,包括主設備號和次設備號。在2.6的內核版本種,dev_t是一個32位的數,其中12位用來表示主設備號,其他20位用來標識次設備號。
經過dev_t獲取主設備號和次設備號使用下面的宏:
MAJOR(dev_t dev);
MINOR(dev_t dev);
相反,經過主設備號和次設備號轉換爲dev_t類型使用:
MKDEV(int major, int minor);
在構建一個字符設備以前,驅動程序首先要得到一個或者多個設備編號,這相似一個營業執照,有了營業執照纔在內核中正常工做營業。完成此工做的函數是:
[cpp] view plaincopy
int register_chrdev_region(dev_t first, unsigned int count, const char *name);
first是要分配的設備編號範圍的起始值。count是連續設備的編號的個數。name是和該設備編號範圍關聯的設備名稱,他將出如今/proc/devices和sysfs中。此函數成功返回0,失敗返回負的錯誤碼。此函數是在已知主設備號的狀況下使用,在未知主設備號的狀況下,咱們使用下面的函數:
[cpp] view plaincopy
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
dev用於輸出申請到的設備編號,firstminor要使用的第一個此設備編號。
在不使用時須要釋放這些設備編號,已提供其餘設備程序使用:
[cpp] view plaincopy
void unregister_chrdev_region(dev_t dev, unsigned int count);
函數多在模塊的清除函數中調用。
分配到設備編號以後,咱們只是拿到了營業執照,雖然說如今已經準備的差很少了,可是咱們只是從內核中申請到了設備號,應用程序仍是不能對此設備做任何事情,咱們須要一個簡單的函數來把設備編號和此設備能實現的功能鏈接起來,這樣咱們的模塊才能提供具體的功能.這個操做很簡單,稍後就會提到,在此以前先介紹幾個重要的數據結構。
註冊設備編號僅僅是完成一個字符設備驅動的第一步。下面介紹大部分驅動都會包含的三個重要的內核的數據結構。
file_operations是第一個重要的結構,定義在 <linux/fs.h>, 是一個函數指針的集合,設備所能提供的功能大部分都由此結構提供。這些操做也是設備相關的系統調用的具體實現。此結構的具體實現以下所示:
[cpp] view plaincopy
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);
};
須要說明的是這裏面的函數在驅動中不用所有實現,不支持的操做留置爲NULL。
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 是一個有用的資源, 在系統調用間保留狀態信息, 咱們大部分例子模塊都使用它
inode 結構由內核在內部用來表示文件. 所以, 它和表明打開文件描述符的文件結構是不一樣的. 可能有表明單個文件的多個打開描述符的許多文件結構, 可是它們都指向一個單個 inode 結構。
inode 結構包含大量關於文件的信息。但對於驅動程序編寫來講通常不用關心,暫且不說。
內核在內部使用類型 struct cdev 的結構來表明字符設備. 在內核調用你的設備操做前, 你編寫分配並註冊一個或幾個這些結構。
有 2 種方法來分配和初始化一個這些結構. 若是你想在運行時得到一個獨立的 cdev 結構, 你能夠爲此使用這樣的代碼:
[cpp] view plaincopy
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
更多的狀況是把cdv結構嵌入到你本身封裝的設備結構中,這時須要使用下面的方法來分配和初始化:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
後面的例子程序就是這麼作的。一旦 cdev 結構創建, 最後的步驟是把它告訴內核:
[cpp] view plaincopy
int cdev_add(struct cdev *dev, dev_t num, unsigned int count)
<span style="font-family: Simsun;">這裏, dev 是 cdev 結構, num 是這個設備響應的第一個設備號, count 是應當關聯到設備的設備號的數目. 經常 count 是 1。</span>
<span style="font-family: Simsun;"></span><p style="font-family: Simsun;">從系統去除一個字符設備, 調用:</p>
[cpp] view plaincopy
void cdev_del(struct cdev *dev);
<h3 style="margin: 0px; padding: 0px;">四、一個簡單的字符設備</h3><div>上面大體介紹了實現一個字符設備所要作的工做,下面就來一個真實的例子來總結上面介紹的內容。源碼中的關鍵地方已經做了註釋。</div>
[cpp] view plaincopy
#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);
Makefile文件以下:
[cpp] view plaincopy
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
五、<strong>總結</strong>
本篇主要介紹了簡單字符設備的編寫與實現以及其中的關鍵點。下一篇會主要講解下驅動的一些經常使用的調試技巧。
第一時間得到博客更新提醒,以及更多技術信息分享,歡迎關注我的微信公衆平臺:程序員互動聯盟(coder_online)
1.直接幫你解答linux設備驅動疑問點
2.第一時間得到業內十多個領域技術文章
3.針對文章內疑點提出問題,第一時間回覆你,幫你耐心解答
4.讓你和原創做者成爲很好的朋友,拓展本身的人脈資源
掃一掃下方二維碼或搜索微信號coder_online便可關注,咱們能夠在線交流。