一、字符設備基礎知識1、設備驅動分類 linux系統將設備分為3類:
字符設備、塊設備、網絡設備。使用驅動程序:
字符設備:是指只能一個字節一個字節讀寫的設備,不能隨機讀取設備內存中的某一數據,讀取數據需要按照先後數據。字符設備是面向流的設備,常見的字符設備有鼠標、鍵盤、串口、控制台和LED設備等。
塊設備:是指可以從設備的任意位置讀取一定長度數據的設備。塊設備包括硬盤、磁盤、U盤和SD卡等。
每一個字符設備或塊設備
都在/dev目錄下對應一個設備文件。
linux用戶程序通過設備文件(或稱設備節點)來使用驅動程序操作字符設備和塊設備。
2、字符設備、字符設備驅動與用戶空間訪問該設備的程序三者之間的關系 如圖,在Linux內核中:
a -- 使用cdev結構體來描述字符設備;
b -- 通過其成員dev_t來定義設備號(分為主、次設備號)以確定字符設備的唯一性;
c -- 通過其成員file_operations來定義字符設備驅動提供給VFS的接口函數,如常見的open()、read()、write()等;
在Linux字符設備驅動中:
a -- 模塊加載函數通過 register_chrdev_region( ) 或 alloc_chrdev_region( )來靜態或者動態獲取設備號;
b -- 通過 cdev_init( ) 建立cdev與 file_operations之間的連接,通過 cdev_add( ) 向系統添加一個cdev以完成注冊;
c -- 模塊卸載函數通過cdev_del( )來注銷cdev,通過 unregister_chrdev_region( )來釋放設備號;
用戶空間訪問該設備的程序:
a -- 通過Linux系統調用,如open( )、read( )、write( ),來“調用”file_operations來定義字符設備驅動提供給VFS的接口函數;
3、字符設備驅動模型二、cdev 結構體解析 在Linux內核中,
使用cdev結構體來描述一個字符設備,cdev結構體的定義如下:
[cpp] view
plain copy
<include/linux/cdev.h>
struct cdev {
struct kobject kobj; //內嵌的內核對象.
struct module *owner; //該字符設備所在的內核模塊的對象指針.
const struct file_operations *ops; //該結構描述了字符設備所能實現的方法,是極為關鍵的一個結構體.
struct list_head list; //用來將已經向內核注冊的所有字符設備形成鏈表.
dev_t dev; //字符設備的設備號,由主設備號和次設備號構成.
unsigned int count; //隸屬於同一主設備號的次設備號的個數.
};
內核給出的操作struct cdev結構的接口主要有以下幾個:
a -- void cdev_init(struct cdev *, const struct file_operations *);其源代碼如代碼清單如下:
[cpp] view
plain copy
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
該函數主要對struct cdev結構體做初始化,最重要的就是建立cdev
和 file_operations之間的連接:
(1) 將整個結構體清零;
(2) 初始化list成員使其指向自身;
(3) 初始化kobj成員;
(4) 初始化ops成員;
b --struct cdev *cdev_alloc(void); 該函數主要分配一個struct cdev結構,
動態申請一個cdev內存,並做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在調用cdev_alloc後,顯式的做初始化即: .ops=xxx_ops).
其源代碼清單如下:
[cpp] view
plain copy
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;
}
在上面的兩個初始化的函數中,我們沒有看到關於owner成員、dev成員、count成員的初始化;其實,owner成員的存在體現了驅動程序與內核模塊間的親密關系,struct module是內核對於一個模塊的抽象,該成員在字符設備中可以體現該設備隸屬於哪個模塊,在驅動程序的編寫中一般由用戶顯式的初始化 .owner = THIS_MODULE, 該成員可以防止設備的方法正在被使用時,設備所在模塊被卸載。而dev成員和count成員則在cdev_add中才會賦上有效的值。
c -- int cdev_add(struct cdev *p, dev_t dev, unsigned count); 該函數向內核注冊一個struct cdev結構,即正式通知內核由struct cdev *p代表的字符設備已經可以使用了。
當然這裡還需提供兩個參數:
(1)第一個設備號 dev,
(2)和該設備關聯的設備編號的數量。
這兩個參數直接賦值給struct cdev 的dev成員和count成員。
d -- void cdev_del(struct cdev *p); 該函數向內核注銷一個struct cdev結構,即正式通知內核由struct cdev *p代表的字符設備已經不可以使用了。
從上述的接口討論中,我們發現對於struct cdev的初始化和注冊的過程中,我們需要提供幾個東西
(1) struct file_operations結構指針;
(2) dev設備號;
(3) count次設備號個數。
但是我們依舊不明白這幾個值到底代表著什麼,而我們又該如何去構造這些值!
三、設備號相應操作1 -- 主設備號和次設備號(二者一起為設備號): 一個字符設備或塊設備都有一個主設備號和一個次設備號。主設備號用來標識與設備文件相連的驅動程序,用來反映設備類型。次設備號被驅動程序用來辨別操作的是哪個設備,用來區分同類型的設備。
linux內核中,設備號用dev_t來描述,2.6.28中定義如下:
typedef u_long dev_t;
在32位機中是4個字節,
高12位表示主設備號,低20位表示次設備號。內核也為我們提供了幾個方便操作的宏實現dev_t:
1) -- 從設備號中提取major和minorMAJOR(dev_t dev);
MINOR(dev_t dev);
2) -- 通過major和minor構建設備號MKDEV(int major,int minor);
注:這只是構建設備號。並未注冊,需要調用 register_chrdev_region 靜態申請;
[cpp] view
plain copy
//宏定義:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))</span>
2、分配設備號(兩種方法):
a -- 靜態申請:
int register_chrdev_region(dev_t from, unsigned count, const char *name);其源代碼清單如下:
[cpp] view
plain copy
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
b -- 動態分配:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);其源代碼清單如下:
[cpp] view
plain copy
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
可以看到二者都是調用了__register_chrdev_region 函數,其源代碼如下:
[cpp] view
plain copy
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
struct char_device_struct *cd, **cp;
int ret = 0;
int i;
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
/* temporary */
if (major == 0) {
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
if (chrdevs[i] == NULL)
break;
}
if (i == 0) {
ret = -EBUSY;
goto out;
}
major = i;
ret = major;
}
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
strlcpy(cd->name, name, sizeof(cd->name));
i = major_to_index(major);
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
/* Check for overlapping minor ranges. */
if (*cp && (*cp)->major == major) {
int old_min = (*cp)->baseminor;
int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
int new_min = baseminor;
int new_max = baseminor + minorct - 1;
/* New driver overlaps from the left. */
if (new_max >= old_min && new_max <= old_max) {
ret = -EBUSY;
goto out;
}
/* New driver overlaps from the right. */
if (new_min <= old_max && new_min >= old_min) {
ret = -EBUSY;
goto out;
}
}
cd->next = *cp;
*cp = cd;
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
通過這個函數可以看出
register_chrdev_region和
alloc_chrdev_region 的區別,
register_chrdev_region直接將Major
注冊進入,而 alloc_chrdev_region從Major = 0 開始,逐個查找設備號,直到找到一個閒置的設備號,並將其注冊進去;
二者應用可以簡單總結如下:
register_chrdev_region [b] alloc_chrdev_region [/b]
devno = MKDEV(major,minor);
ret = register_chrdev_region(devno, 1, "hello");
cdev_init(&cdev,&hello_ops);
ret = cdev_add(&cdev,devno,1);
alloc_chrdev_region(&devno, minor, 1, "hello");
major = MAJOR(devno);
cdev_init(&cdev,&hello_ops);
ret = cdev_add(&cdev,devno,1)
register_chrdev(major,"hello",&hello可以看到,除了前面兩個函數,還加了一個register_chrdev 函數,可以發現這個函數的應用非常簡單,只要一句就可以搞定前面函數所做之事;
下面分析一下
register_chrdev 函數,其源代碼定義如下:
[cpp] view
plain copy
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
調用了 __register_chrdev(major, 0, 256, name, fops) 函數:
[cpp] view
plain copy
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
cd = __register_chrdev_region(major, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, baseminor, count));
return err;
}
可以看到這個函數不只幫我們注冊了設備號,還幫我們做了cdev 的初始化以及cdev 的注冊;
3、注銷設備號:
void unregister_chrdev_region(dev_t from, unsigned count);
4、創建設備文件: 利用
cat /proc/devices查看申請到的設備名,設備號。
1)使用mknod手工創建:mknod filename type major minor
2)自動創建設備節點: 利用udev(mdev)來實現設備文件的自動創建,首先應保證支持udev(mdev),由busybox配置。在驅動初始化代碼裡調用class_create為該設備創建一個class,再為每個設備調用device_create創建對應的設備。
詳細解析見:Linux 字符設備驅動開發 (二)—— 自動創建設備節點
下面看一個實例,練習一下上面的操作:
hello.c
[cpp] view
plain copy
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
static int major = 250;
static int minor = 0;
static dev_t devno;
static struct cdev cdev;
static int hello_open (struct inode *inode, struct file *filep)
{
printk("hello_open \n");
return 0;
}
static struct file_operations hello_ops=
{
.open = hello_open,
};
static int hello_init(void)
{
int ret;
printk("hello_init");
devno = MKDEV(major,minor);
ret = register_chrdev_region(devno, 1, "hello");
if(ret < 0)
{
printk("register_chrdev_region fail \n");
return ret;
}
cdev_init(&cdev,&hello_ops);
ret = cdev_add(&cdev,devno,1);
if(ret < 0)
{
printk("cdev_add fail \n");
return ret;
}
return 0;
}
static void hello_exit(void)
{
cdev_del(&cdev);
unregister_chrdev_region(devno,1);
printk("hello_exit \n");
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);
測試程序 test.c
[cpp] view
plain copy
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
main()
{
int fd;
fd = open("/dev/hello",O_RDWR);
if(fd<0)
{
perror("open fail \n");
return ;
}
close(fd);
}
makefile:
[cpp] view
plain copy
ifneq ($(KERNELRELEASE),)
obj-m:=hello.o
$(info "2nd")
else
KDIR := /lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
all:
$(info "1st")
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order
endif
編譯成功後,使用 insmod 命令加載:
然後用cat /proc/devices 查看,會發現設備號已經申請成功;