這個程序主要參考ldd3的第三章來寫,這一章主要通過介紹字符設備scull(Simple Character Utility for Loading Localities,區域裝載的簡單字符工具)的驅動程序編寫,來學習Linux設備驅動的基本知識。scull可以爲真正的設備驅動程序提供樣板。
下面這個驅動程序用於驅動字符設備mychar,參考scull源碼。
廢話少說,直接上代碼,後面再來慢慢解釋:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <asm/uaccess.h>
MODULE_LICENSE("Dual BSD/GPL");
#define MYCHAR_MAJOR 0 //主設備號
#define MYCHAR_MINOR 0 //次設備號
#define MYCHAR_COUNT 1 //請求的設備個數
#define MYCHAR_QUANTUM 4000 //每個量子大小
#define MYCHAR_QSET 1000 //每個量子集含量子數量
/* 允許參數傳遞,缺省爲以下默認值 */
static int mychar_major = MYCHAR_MAJOR, mychar_minor = MYCHAR_MINOR, count = MYCHAR_COUNT, quantum = MYCHAR_QUANTUM, qset = MYCHAR_QSET;
module_param(mychar_major, int, S_IRUGO);
module_param(mychar_minor,int,S_IRUGO);
module_param(count,int, S_IRUGO);
module_param(quantum, int, S_IRUGO);
module_param(qset, int, S_IRUGO);
struct mychar_dev* dev;
/* 爲設備建立特定的設備結構 */
struct mychar_dev
{
struct mychar_qset *data;
int quantum;
int qset;
unsigned long size;
struct semaphore sem;
struct cdev cdev;
};
/* 定義鏈表項結構 */
struct mychar_qset
{
void **data;
struct mychar_qset *next;
};
/* 當設備文件以只寫方式打開,釋放設備結構內存 */
int mychar_trim(struct mychar_dev *dev)
{
struct mychar_qset *next, *qptr;
int i;
int qset = dev->qset;
/* 遍歷所有鏈表項 */
for (qptr = dev->data; qptr; qptr = next)
{
if (qptr->data)
{
for (i = 0; i < qset; i++)
{
kfree(qptr->data[i]); //釋放每個量子
}
kfree(qptr->data); //釋放每個量子集
qptr->data = NULL;
}
next = qptr->next;
kfree(qptr);
}
/* 把設備結構設定爲初始化值 */
dev->quantum = MYCHAR_QUANTUM;
dev->qset = MYCHAR_QSET;
dev->size = 0;
dev->data = NULL;
return 0;
}
/* 找到第item個鏈表項 */
struct mychar_qset* mychar_follow(struct mychar_dev* dev, int item)
{
int i;
struct mychar_qset* dptr = dev->data;
/* 分配內存給第0個鏈表項 */
if (!dptr)
{
dptr = dev->data = kmalloc(sizeof(struct mychar_qset), GFP_KERNEL);
if(!dptr)
{
return NULL;
}
memset(dptr, 0, sizeof(struct mychar_qset)); //把鏈表項內存清零
}
/* 遍歷鏈表並分配內存,直到找到第item個鏈表項
注意不要先把next賦給dptr,否則會把一個隨機的地址賦給dptr,可能導致出錯,必須先分配內存,然後把內存的地址賦給next後,纔可以把next賦給dptr */
for (i = 0; i < item; i++)
{
if (!dptr->next)
{
dptr->next = kmalloc(sizeof(struct mychar_qset), GFP_KERNEL);
if (!dptr->next)
{
return NULL;
}
memset(dptr->next, 0, sizeof(struct mychar_qset));
}
dptr = dptr->next;
}
return dptr;
}
/* 分配設備號 */
int alloc_mychar_dev(int major, int minor,unsigned int count)
{
int result;
dev_t devno;
if (major)
{
devno = MKDEV(major, minor);
result = register_chrdev_region(devno, count, "mychar"); //major大於0時,靜態分配設備號
}
else
{
result = alloc_chrdev_region(&devno, 0, count, "mychar"); //major爲0時,動態分配設備號
major = MAJOR(devno);
minor = MINOR(devno);
}
mychar_major = major;
mychar_minor = minor;
if (result)
{
printk(KERN_WARNING "mychar: can't get major %d",major);
}
return result;
}
/* 打開設備 */
int mychar_open(struct inode *inode, struct file *filp)
{
/* 打開設備文件,把設備結構與file結構關聯起來,初始化file結構中某些值 */
struct mychar_dev *dev;
dev = container_of(inode->i_cdev, struct mychar_dev, cdev); //container_of宏返回的是結構體mychar_dev的地址
filp->private_data = dev;
/* 當設備文件以write-only方式打開時,把設備文件長度截斷爲0 */
if ((filp->f_flags & O_ACCMODE) == O_WRONLY)
{
mychar_trim(dev);
}
return 0;
}
/* 釋放設備 */
int mychar_release(struct inode* inode, struct file* filp)
{
return 0;
}
/* 讀設備操作 */
ssize_t mychar_read(struct file* filp, __user char* buf, size_t count, loff_t *f_pos)
{
struct mychar_dev *dev = filp->private_data;
struct mychar_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = 0;
if (down_interruptible(&dev->sem))
{
return -ERESTARTSYS;
}
/* 判斷偏移量 */
if (*f_pos >= dev->size)
{
goto out;
}
if (*f_pos + count >= dev->size)
{
count = dev->size - *f_pos;
}
/* 在量子集中尋找鏈表項、qset索引以及偏移量 */
item = (long)*f_pos / itemsize; //第item鏈表項
rest = (long)*f_pos % itemsize; //在鏈表項中的第rest個字節數
s_pos = rest / quantum; //在該量子集中的第s_pos個量子
q_pos = rest % quantum; //在該量子中的第q_pos個字節
/* 沿該鏈表前行,直到正確的位置 */
dptr = mychar_follow(dev, item);
/* 讀取該量子的數據直到結尾 */
if (count >= quantum - q_pos)
{
count = quantum -q_pos;
}
if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count))
{
retval = -EFAULT;
goto out;
}
*f_pos += count; //修改文件當前位置
retval = count;
out:
up(&dev->sem);
return retval;
}
/* 寫設備操作 */
ssize_t mychar_write(struct file* filp, const char __user *buf, size_t count, loff_t *f_pos)
{
struct mychar_dev* dev = filp->private_data;
struct mychar_qset* dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = qset * quantum;
int item, s_pos, q_pos, rest;
size_t retval = -ENOMEM;
if (down_interruptible(&dev->sem))
{
return -ERESTARTSYS;
}
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;
dptr = mychar_follow(dev, item);
if (dptr == NULL)
{
goto out;
}
/* 爲量子集分配內存 */
if (!dptr->data)
{
dptr->data = kmalloc(qset * sizeof(char*), GFP_KERNEL);
if (!dptr->data)
{
printk(KERN_INFO "Error scull_write qs->data = kmalloc.");
goto out;
}
memset(dptr->data, 0, qset * sizeof(char*));
}
/* 爲量子分配內存 */
if (!dptr->data[s_pos])
{
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
{
printk(KERN_INFO "Error scull_write qs->data[s_pos] = kmalloc.");
goto out;
}
memset(dptr->data[s_pos], 0, quantum);
}
if (count > quantum - q_pos)
{
count = quantum - q_pos;
}
if (copy_from_user(dptr->data[s_pos] + q_pos, buf, count))
{
retval = -EFAULT;
goto out;
}
retval = count;
*f_pos += count;
/* 修改設備文件大小 */
if (dev->size < *f_pos)
{
dev->size = *f_pos;
}
out:
up(&dev->sem);
return retval;
}
/* file_operations結構初始化 */
struct file_operations mychar_fops =
{
.owner = THIS_MODULE,
.open = mychar_open,
.release = mychar_release,
.write = mychar_write,
.read = mychar_read,
};
/* 利用系統分配的設備號註冊字符設備 */
void mychar_setup_dev(struct mychar_dev *dev, int index)
{
int err;
dev_t devno = MKDEV(mychar_major, mychar_minor + index);
/* 初始化cdev結構 */
cdev_init(&dev->cdev, &mychar_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &mychar_fops;
/* 註冊字符設備 */
err = cdev_add(&dev->cdev, devno, 1);
if (err)
{
printk(KERN_NOTICE "Error %d adding mychar%d", err, index);
}
}
/* 初始化設備 */
static __init int mychar_init(void)
{
int i;
/* 分配設備號 */
if (!alloc_mychar_dev(mychar_major, mychar_minor,count))
{
printk(KERN_ALERT "major:%d, minor: %d, count: %d\n", mychar_major, mychar_minor, count);
}
/* 爲設備結構分配內存並把內存區清零 */
dev = kmalloc(count * sizeof(struct mychar_dev), GFP_KERNEL);
if (!dev)
{
printk(KERN_ALERT "kmalloc\n");
}
memset(dev, 0, sizeof(struct mychar_dev) * count);
for (i = 0; i< count; i++)
{
init_MUTEX(&dev[i].sem);
mychar_setup_dev(&dev[i], i); //註冊字符設備
dev[i].qset = qset;
dev[i].quantum = quantum;
dev[i].size = 0;
}
return 0;
}
/* 卸載設備 */
static __exit void mychar_exit(void)
{
int i;
dev_t devno = MKDEV(mychar_major, mychar_minor);
/* 卸載字符設備 */
for (i = 0; i < count; i++)
{
cdev_del(&dev[i].cdev);
}
kfree(dev);
unregister_chrdev_region(devno, count);
}
module_init(mychar_init);
module_exit(mychar_exit);
代碼有點長啊不好意思,但是貌似每個部分都不可或缺的說,下面挑些必須要知道的知識來說,下面所說的大部分是ldd3上的內容,用scull設備作講解:
一、主設備號和次設備號
可以通過命令查看系統設備:
#ls /dev -l
ls -l命令可在設備文件項的最後修改日期前看到2個數此位置通常指文件長度,而設備文件卻是2個數,這兩個數就是相應設備的主設備號和次設備號。可通過次設備號獲得一個指向內核設備的直接指針,也可將其當做設備本地數組的索引。
主設備號表示設備對應的驅動程序;次設備號由內核使用,用於正確確定設備文件所指的設備。
內核用dev_t類型(</usr/src/kernels/2.6.18-92.el5-i686/include/linux/types.h>)來保存設備編號,dev_t是一個32位的數,12位表示主設備號,20爲表示次設備號。
在實際使用中,是通過<linux/kdev_t.h>中定義的宏來轉換格式。
(dev_t)-->主設備號、次設備號 | MAJOR(dev_t dev) MINOR(dev_t dev) |
主設備號、次設備號-->(dev_t) | MKDEV(int major,int minor) |
建立一個字符設備之前,驅動程序首先要做的事情就是獲得設備編號。其這主要函數在<linux/fs.h>中聲明:
int register_chrdev_region(dev_t first, unsigned int count,char *name);//指定設備編號 int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,unsigned int count, char *name);//動態生成設備編號 void unregister_chrdev_region(dev_t first, unsigned int count); //釋放設備編號 |
對於一個新的驅動程序,我們強烈建議讀者不要隨便選擇一個當前未使用的設備號作爲主設備號,而應該使用動態分配機制獲取主設備號。
換句話說,驅動程序應該始終使用alloc_chrdev_region而不是register_chrdev_region函數。
分配主設備號的最佳方式是:默認採用動態分配,同時保留在加載甚至是編譯時指定主設備號的餘地。
以下是在scull.c中用來獲取主設備好的代碼:
if (scull_major) { dev = MKDEV(scull_major, scull_minor); } else { result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,"scull"); scull_major = MAJOR(dev); }if (result < 0) { printk(KERN_WARNING "scull: can't get major %d\n", scull_major); |
在這部分中,比較重要的是在用函數獲取設備編號後,其中的參數name是和該編號範圍關聯的設備名稱,它將出現在/proc/devices和sysfs中。
大部分基本的驅動程序操作涉及及到三個重要的內核數據結構,分別是file_operations、file和inode,它們的定義都在<linux/fs.h>。
file_operations結構就是用來將驅動程序操作連接到設備編號;
// 文件操作,設備操作,將驅動程序操作鏈接到設備編號
struct file_operations scull_fops =
{
.owner = THIS_MODULE,
.read = scull_read, //用來從設備讀取數據
.write = scull_write, //向設備發送數據
.open = scull_open,
.release = scull_release,
};
file結構代表一個打開的文件(它並不僅僅限定於設備驅動程序,系統中每個打開的文件在內核空間都有一個對應的file結構)。inode結構,內核用inode結構在內部表示文件。inode結構中包含了大量有關文件的信息。作爲常規,只有下面兩個字段對編寫驅動程序有用。
dev_t i_rdev ;//對錶示設備文件的inode結構,該字段包含了真正的設備編號。
struct cdev *i_cdev ;//struct cdev是表示字符設備的內核的內部結構
內核內部使用struct cdev結構來表示字符設備。在內核調用設備的操作之前,必須分配並註冊一個或多個struct cdev。代碼應包含<linux/cdev.h>,它定義了struct cdev以及與其相關的一些輔助函數。
註冊一個獨立的cdev設備的基本過程如下:
1、爲struct cdev 分配空間(如果已經將struct cdev 嵌入到自己的設備的特定結構體中,並分配了空間,這步略過!)
struct cdev *my_cdev = cdev_alloc();
2、初始化struct cdev
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
3、初始化cdev.owner
cdev.owner = THIS_MODULE;
4、cdev設置完成,通知內核struct cdev的信息(在執行這步之前必須確定你對struct cdev的以上設置已經完成!)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
從系統中移除一個字符設備:void cdev_del(struct cdev *p);(此函數後不能再訪問cdev結構了)
以下是scull中的初始化代碼(之前已經爲struct scull_dev 分配了空間):
|
以下是scull模型的結構體:
|
scull驅動程序引入了兩個Linux內核中用於內存管理的核心函數,它們的定義都在<linux/slab.h>:
|
以下是scull模塊中的一個釋放整個數據區的函數(類似清零),將在scull以寫方式打開和scull_cleanup_module中被調用:
|
五、open和release
5.1 open方法提供給驅動程序以初始化的能力,爲以後的操作作準備。應完成的工作如下:
(1)檢查設備特定的錯誤(如設備未就緒或硬件問題);
(2)如果設備是首次打開,則對其進行初始化;
(3)如有必要,更新f_op指針;
(4)分配並填寫置於filp->private_data裏的數據結構。
而根據scull的實際情況,他的open函數只要完成第四步(將初始化過的struct scull_dev dev的指針傳遞到filp->private_data裏,以備後用)就好了,所以open函數很簡單。但是其中用到了定義在<linux/kernel.h>中的container_of宏,源碼如下:
|
其實從源碼可以看出,其作用就是:通過指針ptr,獲得包含ptr所指向數據(是member結構體)的type結構體的指針。即是用指針得到另外一個指針。
5.2 release方法提供釋放內存,關閉設備的功能。應完成的工作如下:
(1)釋放由open分配的、保存在file->private_data中的所有內容;
(2)在最後一次關閉操作時關閉設備。
由於前面定義了scull是一個全局且持久的內存區,所以他的release什麼都不做。
六、read和write
read和write方法的主要作用就是實現內核與用戶空間之間的數據拷貝。因爲Linux的內核空間和用戶空間隔離的,所以要實現數據拷貝就必須使用在<asm/uaccess.h>中定義的:
|
而值得一提的是以上兩個函數和
|
之間的關係:通過源碼可知,前者調用後者,但前者在調用前對用戶空間指針進行了檢查。
至於read和write 的具體函數比較簡單,就在實驗中驗證好了。
七、模塊實驗
測試程序在PC上開發,交叉編譯後在arm上運行。
1、 加載驅動模塊,建立設備節點
主設備號和次設備號我在初始化函數中分配以後打印出來,若不想打印的話也可用 cat /proc/devices命令找出自己設備的主設備號:
2、測試驅動程序
先貼上測試程序:
#include <stdio.h>
#include <linux/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define MYDEVICE "/dev/mychar0"
int main()
{
int fd, i, len_w, len_r, len, len_buf;
char buf_write[] = "abcdefghijklmnopqrst";
char buf_read[100];
char *tmp_buf;
for (i = 0, len = 0; buf_write[i] != '\0'; i++, len++);
if ((fd = open(MYDEVICE, O_RDWR)) < 0)
{
perror("Open");
}
len_buf = len;
tmp_buf = buf_write;
for (i = 0; i < len; i+=len_w)
{
len_w = write(fd, tmp_buf, len_buf);
printf("write %d bytes!\n", len_w);
tmp_buf += len_w;
len_buf -= len_w;
}
close(fd);
if ((fd = open(MYDEVICE, O_RDWR)) < 0)
{
perror("Open");
}
len_buf = len;
tmp_buf = buf_read;
for (i = 0; i < len; i+=len_r)
{
len_r = read(fd, tmp_buf, len_buf);
printf("read %d bytes!\n", len_r);
tmp_buf += len_r;
len_buf -= len_r;
}
for (i = 0; i < len; i++)
{
printf("[%d]: %d\n", i, buf_read[i]);
}
close(fd);
return 0;
}
編譯運行:
97~116 在ASCII碼上對應‘a’~‘t’,看來讀寫能力測試是成功了。
下面換種情況,把量子大小改爲6,量子集大小改爲2
測試量子讀寫也成功了!
在命令行上其實可以直接用cat命令查看mychar0的內容:
實驗不僅測試了模塊的讀寫能力,還測試了量子讀寫是否有效。
參考日誌:http://myswirl.blog.163.com/blog/static/51318642201092751938393/