Linux字符驅動框架相比初學還是比較難記的,在學了一陣子字符驅動的開發後對於框架的搭建總結出了幾個字 。
對於框架來講主要要完成兩步。
申請設備號,注冊字符驅動
其關鍵代碼就兩句
~
int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);//動態申請設備號
int cdev_add(struct cdev *, dev_t, unsigned); //注冊字符驅動
~
執行完次就可以將我們的驅動程序加載到內核裡了
首先我們搭建主程序,字符驅動的名字就叫做"main"
首先先寫下將要用到的頭文件,以及一個宏定義,指明了我們驅動的名稱,當然名稱可以任意這裡就取"main" 作為名字
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/coda.h>
#include <linux/slab.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define MUDULE_NAME "main"
驅動由於需要加載到內核裡,所以我們需要聲明一下我們驅動所遵循的協議,如果沒有申明,那麼加載內核的時候系統會提示一段信息。我們按照內核的風格來,就加一個GPL協議吧
MODULE_LICENSE("GPL");
我們要想將我們的驅動注冊到內核裡,就必須將我們的驅動本身作為一個抽象,抽象成一個struct cdev的結構體。因為我們系統內部有許多中字符驅動,為了將這些不同種類的驅動都能使用同一個函數進行注冊,內核聲明了一個結構體,不同的驅動通過這個結構體--變成了一個抽象的驅動供系統調用。這段有點羅嗦,我們來看一下cdev這個結構體吧。
//這段不屬於主程序
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
這個結構體就包含了一個驅動所應有的東西其中 kobj 不需要管它,我也沒有仔細研究,owner指向模塊的所有者,常常使用THIS_MODULE這個宏來賦值,ops是我們主要做的工作,其中定義了各種操作的接口。
下面我們定義了我們程序的抽象體mydev,以及他所需要的接口
struct cdev mydev;
struct file_operations ops;
struct file_operations這個結構有點龐大。
//不屬於本程序
struct file_operations {
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);
int (*readdir) (struct file *, void *, filldir_t);
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);
};
上面看到,這個結構內部都是一些函數指針,相當與這個結構本身就是一個接口,在c語言中沒有接口這個概念,使用這種方式來定義也是一種巧妙的用法。不過有所不同的是我們可以不完全實現其中的接口。
應用程序在使用驅動的時候常常需要open,write,read,close這幾種操作,也就對應了file_operations結構中的open,write,read,release這幾個函數指針。下面我們開始實現我們自己的函數體。注意:我們自己實現的函數必須滿足接口函數所定義的形式。
static int main_open(struct inode* inode,struct file* filp)
{
return 0;
}
這個教程裡面的程序,我們就讓驅動只能往裡面寫一個字符為例,讀取也是只能讀取一個字符。
我們定義一個靜態的字符類型的變量來當作我們的存儲空間,通過copy_from_user來將用戶空間的數據拷貝到我們驅動 所在的內核空間。原型是:
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
類似地,我們使用copy_to_user來完成內核空間到用戶空間的數據拷貝。
static inline long copy_to_user(void __user *to,const void *from, unsigned long n)
static char main_buffer;
static ssize_t main_write(struct file* filp,const char __user* buffer,size_t length,loff_t * l)
{
if(length!=1) return -1;
copy_from_user(&main_buffer,buffer,length);
return 1;
}
下面是讀的實現
static ssize_t main_read(struct file* filp,char __user * buffer,size_t length,loff_t*l)
{
if(length!=1) return -1;
copy_to_user(buffer,&main_buffer,length);
return 1;
}
再稍稍實現一下close
static int main_close(struct inode* inode,struct file* filp)
{
return 0;
}
我們所需要的內容都已經填寫完畢,我們在驅動初始化的時候調用cdev_add驅動注冊到系統就行了,不過在注冊之前我們要申請設備號。
static dev_t dev;
static int __init main_init(void)
{
首先我們使用動態申請的方式申請設備號
int result=alloc_chrdev_region(&dev,0,1,MODULE_NAME);
dev就是我們申請的設備號,其中dev_t其實就是一個無符號的long型,通過調用alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *) 將申請到的設備號寫入到dev中,第二個參數是子設備號從幾開始,第三個參數是申請幾個設備號,因為申請多個設備號是連續的,所以我們只需要知道第一個就行了。第四個參數代表我們驅動的名稱。
返回值如果小於0則表示我們申請失敗,通過printk打印錯誤信息。我測試的在動態加載的時候printk都不能打印其信息,如果在Ubuntu下可以查看/var/log/kern.log,如果是CentOS下可以查看/var/log/mssages來查看printk打印的信息,一般查看後10條就能足夠了。
if(result<0)
{
printk(KERN_ALERT"device load error");
return -1;
}
然後我們再構造一下我們的接口結構體
ops.owner=THIS_MODULE;
ops.open=main_open;
ops.release=main_close;
ops.write=main_write;
ops.read=main_read;
構造完之後,我們就只剩下我們最重要的一步了,就是向系統注冊我們的驅動。
不過,先別急,我們注冊前得先把我們的抽象驅動mydev給構造了,mydev的定義在最上面。
cdev_init(&mydev,&ops);
mydev.owner=THIS_MODULE;
這樣,我們就使用了我們的ops構造了我們的抽象驅動,當我們把這個驅動添加到我們的內核裡面的時候,假如內核想對這個驅動進行寫的操作,就會從mydev->ops->main_write這樣找到我們自己實現的寫函數了。
接下來就注冊我們的驅動。
cdev_add(&mydev,dev,1);
printk(KERN_ALERT"device load success\n");
return 0;
}
至此,我們的驅動就算完成了,不過有一點,就是我們的驅動有了初始化函數了就一定還有一個清理的函數了,該函數主要在我們卸載驅動的時候會調用,module_init及module_exit主要用於聲明這個驅動的入口與出口,是必須要做的一步。
static void __exit main_exit(void)
{
unregister_chrdev_region(dev,1);
cdev_del(&mydev);
}
module_init(main_init);
module_exit(main_exit);
我的Makefile是下面這樣
ifeq ($(KERNELRELEASE),)
KERNELDIR?=/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules -Wall
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install -Wall
clean:
rm -rf *.0 *~ core .depend .*.cmd
sudo rmmod main
sudo rm /dev/main
install:
sudo insmod main.ko
sudo mknod /dev/main c 250 0
message:
tail -n 5 /var/log/kern.log
.PHONY: modules modules_install clean
else
obj-m :=main.o
endif
因為我的機器是使用ubuntu,所以在message標簽下是tail -n 5 /var/log/kern.log 如果的/var/log目錄下沒有kern.log,那麼就替換成/var/log/messages
編譯
$make
因為這個Makefile的install 都是針對我自己電腦而寫的,所以並不能在你電腦上保證執行make install 的正確性。還是在命令行中敲吧
sudo insmod main.ko
安裝模塊,然後查看/proc/devices裡面main這個模塊對應的設備號是多少,我的是250,所以創建設備節點時這樣創建
sudo mknod /dev/main c 250 0
這樣就成功把我們的驅動安裝到內核裡了
執行make message可以看到如下信息
tail -n 5 /var/log/kern.log
Sep 17 20:05:57 quanweiC kernel: [23536.688371] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=2558 DF PROTO=UDP SPT=11818 DPT=26724 LEN=43
Sep 17 20:06:02 quanweiC kernel: [23541.691748] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=3335 DF PROTO=UDP SPT=11818 DPT=26724 LEN=43
Sep 17 20:06:51 quanweiC kernel: [23590.610275] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=10708 PROTO=UDP SPT=11818 DPT=10948 LEN=43
Sep 17 20:07:04 quanweiC kernel: [23603.815562] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=12523 PROTO=UDP SPT=11818 DPT=10104 LEN=43
Sep 17 20:07:04 quanweiC kernel: [23603.930248] device load success
最後一行顯示我們加載驅動成功了。這樣一個簡單的字符驅動就寫成功了。
Linux字符驅動中動態分配設備號與動態生成設備節點 http://www.linuxidc.com/Linux/2014-03/97438.htm
字符驅動設計----mini2440 LED驅動設計之路 http://www.linuxidc.com/Linux/2012-08/68706.htm
Linux 設備驅動 ====> 字符驅動 http://www.linuxidc.com/Linux/2012-03/57581.htm
如何編寫Ubuntu字符驅動 http://www.linuxidc.com/Linux/2010-05/25887.htm
2.4下內核linux字符驅動模板 http://www.linuxidc.com/Linux/2007-06/5338.htm