(感谢周老师提供的资料,本文对此作了适当的修改及补充)
一.Linux设备驱动的分类
Linux设备驱动属于Linux内核模块范畴,运行在OS的内核空间,所以其生命周期受控于Linux内核。
Linux常规将设备驱动分为:字符设备、块设备和网络设备,它们区别如下表:
节点设备 ls /dev/ -l | c字符设备 | 因时间因素导致只能顺序访问数据的设备。 |
b块设备 | 可随机访问也可以顺序访问数据的设备。 | |
非节点设备 ifconfig | 网络设备 ethx和wlanx | 面向网络报文,使用socket和sk_buff访问的设备。 |
设备号由主设备号(设备类别)和次设备(该类别下的具体设备)号组成,在整个Linux系统中设备号是唯一的。
主设备号 | 次设备号 |
12bit | 20bit |
Linux内核提供了:
Dev_t MKDEV(int major,int minor)宏函数将输入主号和次号合成一个设备号。
int MAJOR(dev_t dev)从设备号中获取设备主号。
int MINOR(dev_t dev)从设备号中获取设备次号。
二.字符设备驱动的编写步骤
1.合成设备号
2.注册设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数:from第一个设备号(即次设备号为0的设备号)
count申请的连续设备的数量(>=1)
name自己定义的设备名称
返回值:0表示设备号注册成功,负数表示失败
由于每个设备的设备号唯一,在使用MKDEV手动合成的设备号有可能已经被占用了,因此若能动态的生成设备号则更加灵活,这有效地避开了设备号重复的冲突。
上述的1.合成设备号2.注册设备号可以使用以下的函数完成:
动态申请注册设备号
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数:dev分配到的设备号,函数调用成功会把得到的设备号放入第一个参数中
baseminor第一个次设备号(一般为0)
count申请的连续设备的数量
name自己定义的设备名称
在使用动态申请注册设备号的函数时,由于生成的设备的主次设备号未知,因此设备节点不能够事先手动创建,这就得添加自动创建和删除设备节点的代码了,具体函数后面会介绍。
3.初始化字符设备结构体
在Linux中使用cdev结构体表述一个字符设备,cdev结构体的定义如下
struct cdev {
struct kobject kobj;/*内嵌的kobject对象*/
struct module *owner;/*所属模块,通常为THIS_MODULE*/
struct file_operations *ops;/*文件操作结构体,为应用程序操作该模块提供了接口*/
struct list_head list;/*双向链表*/
dev_t dev;/*设备号*/
unsigned int count;/*模块使用计数,0则表示模块可被卸载*/
};
一个cdev一般有两种定义初始化方式:静态和动态。
静态内存定义初始化:cdev_init(struct cdev *cdev, struct file_operations *ops)函数用于静态初始化cdev的成员,并建立cdev和file_operations之间的连接。初始化代码如下:
struct cdev my_cdev;
cdev_init(&my_cdev,&fops);
my_cdev.owner = THIS_MODULES;
动态内存定义初始化:cdev_alloc(void)函数用于动态申请一个cdev内存。初始化代码如下:
struct cdev *my_cdev;
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULES;
两种使用方式的功能是一样的,只是使用内存区不一样,一般视实际的数据结构需求而定。下面贴出了两个函数的源代码,以具体看下它们之间的差异。
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;/*将传入的文件操作结构体指针赋值给cdev的ops*/
}
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;
}
由此可见,两个函数完成功能基本一致,只是cdev_init还多赋了一个cdev->ops的值。
4.向内核中添加字符设备及分配具体设备号
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数:p :cdev结构体
dev :字符设备号(第一个设备的设备号)
count :连续的次设备号的个数
返回值:0表示成功,非0表示失败
5.从内核中删除字符设备
void cdev_del(struct cdev *p)
参数: p :cdev结构体
6.从内核中注销设备号
void unregister_cdev_region(dev_t from, unsigned count)
参数:from :第一个设备号(即次设备号为0的设备号)
count :连续的次设备号的个数
三.应用程序接口file_operations
file_operations为指针函数集,我们暂时关注下面几个指针函数即变量成员:
owner,一般初始化为THIS_MODULE,让其归属与自己。
open(),它会自动被应用程序的open函数所调用。
release(),它会自动被应用程序的close函数所调用。
read(),从设备中读取数据。
write(),向设备中写入数据。
ioctl()(unlock_ioctl()),read和write等无法完成的东西都可以用ioctl()来完成。
3.1read()函数对copy_to_user()的使用
ssize_t read(int fd, void*buf, size_t count);
返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0。
从内核中读取数据到用户空间中。
第一个参数是文件块,
第二个参数是用户空间的数据地址。
第三个参数是
返回值为真实读到的字节数量。
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n);
copy_to_user函数可以实现read函数的需求。
第一参数用户空间的数据地址,
第二参数是内核空间数据的地址,
第三参数是复制的数量。
返回值为实际copy的字节数量。
下面给出一个例子:
/*
#define GLOBALMEM_SIZE 0x1000;/*全局内存最大4KB*/
struct globalmem_dev{
struct cdev cdev;/*cdev结构体*/
unsigned char mem[GLOBALMEM_SIZE];/*全局内存*/
}
*/
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;/*获得设备结构体指针*/
/*分析和获取有效长度*/
if(p >= GLOBALMEM_SIZE)
return 0;
if(count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;
/*内核空间->用户空间*/
if(copy_to_user ( buf, (void *) ( dev->mem + p ), count )) {
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
printf(KERN_INFO "read %u bytes(s) from %lu\n", count , p);
}
return ret;
}
3.2write()函数对copy_from_user()的使用
ssize_t write(int fd, const void *buf, size_t count);
返回值:成功返回写入的字节数,出错返回-1并设置errno写常规文件时,write的返回值通常等于请求写的字节数
count,而向终端设备或网络写则不一定。
下面给出一个例子:
/*
#define GLOBALMEM_SIZE 0x1000;/*全局内存最大4KB*/
struct globalmem_dev{
struct cdev cdev;/*cdev结构体*/
unsigned char mem[GLOBALMEM_SIZE];/*全局内存*/
}
*/
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;/*获得设备结构体指针*/
/*分析和获取有效写长度*/
if(p >= GLOBALMEM_SIZE)
return 0;
if(count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;
/*用户空间->内核空间*/
if(copy_from_user ( (void *) ( dev->mem + p ), buf, count ) ) {
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
printf(KERN_INFO "read %u bytes(s) from %lu\n", count , p);
}
return ret;
}
3.3ioctl中的cmd参数
ioctl函数中第三个整数参数作为命令参数。
设备类型(幻数) | 序列号(命令代码) | 方向 | 数据长度 |
8bit | 8bit | 2bit | 13/14bit |
设备类型字段为一个幻数,可以是0-0xff之间的值,内核中ioctl-number.txt给出了一些推荐的和已经被使用的幻数,新设备驱动定义幻数时要避免与其冲突。
方向字段为2位,该字段表示数据传送的方向,可能值是_IOC_NONE(无数据传输)、_IOC_READ(读)、_IOC_WRITE(写)、_IOC_READ|_IOC_WRITE(双向)。数据传送的方向是从应用程序的角度来看的。
内核中定义了_IO()、_IOR()、_IOW()、_IOWR()这4个宏来辅助生成命令。在linux/iotcl.h中有如下定义。
命令的合成宏函数如下:
_IO(type,nr)宏函数,type为幻数,nr为命令代码,不带数据。
_IOWR(type,nr,size)宏函数,type为幻数,nr为命令吗,size为数据长度,数据双向。
_IOW(type,nr,size) 宏函数,type为幻数,nr为命令吗,size为数据长度,数据写入操作。
_IOR(type,nr,size) 宏函数,type为幻数,nr为命令吗,size为数据长度,数据读取操作。
由此可见这几个宏的作用是根据传入的type(设备类型字段)、nr(序列号字段)、和size(数据长度字段)和宏名隐含的方向字段移位组合生成命令码。
从命令获取各个参数宏函数:
_IOC_DIR(),从命令中获取数据方向参数(_IOC_NONE,_IOC_READ或_IOC_WRITE)。
_IOC_TYPE(,,从命令中获取幻数。
_IO_NR(),从命令中获取序列号(命令码)。
_IO_SIZE(),从命令中获取数据大小参数。
同时内核中预定义了一些I/O控制命令,如果某设备驱动中包含了与预定义命令一样的命令码,这些命令会当做预定义命令被内核处理而不是设备驱动处理,预定义有如下4种:
1.FIOCLEX:即File IOctl Close on Exec,对文件设置专用标志,通知内核当exec()系统调用发生时自动关闭打开的文件。
2.FIONCLEX:即File IOctl Not Close on Exec,与FIOCLEX标志相反,清除由FIOCLEX命令设置的标志。
3.FIOQSIZE:获得一个文件或者目录的大小,当用于设备文件时,返回一个ENOTTY错误。
4.FIONBIO:即File IOctl Non-Blocking I/O,这个调用修改在filp->f_flags中的O_NONBLOCK标志。
宏定义如下:
#define FIONCLEX0x5450
#define FIOCLEX0x5451
#define FIOQSIZE0x5460
#define FIONBIO0x5421
用户的应用程序调用ioctl函数时,会自动调用模块fops的ioctl函数。
头文件:#include<sys/ioctl.h>
int ioctl(int handle, int cmd,[int *argdx, intargcx]);
返回值:成功为0,出错为-1
示例代码如下:
static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned in cmd, unsigned long arg)
{
struct globalmem_dev *dev = filp->private_data;/*获得设备结构体指针*/
switch(cmd){
case XXX_CMD:
......
break;
......
default:
return -EINVAL;
}
return 0;
}
四.自动创建/删除设备节点
设备节点中的设备标示符和设备号是一一对应的,应用程序通过打开设备标示符,内核系统会自动找到对应的设备号,最终使得应用程序打开了对应的设备。
在刚学习linux驱动时,一般都是用mknod手动创建设备节点,往后学习就知道,实际上Linux内核为我们提供了一组函数,可以用来在模块加载的时候自动在/dev目录下创建相应设备节点,并在卸载模块时删除该节点,当然前提条件是用户空间移植了udev。
在linux/device.h头文件中可知,内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应device_create(…)函数去/sysfs下寻找对应的类从而创建设备节点。
在2.6较早的内核版本中,device_create(…)函数的名称是class_device_create(…),所以在新的内核中编译以前的模块程序有时会报错,就是因为函数名称不同,而且里面的参数设置也有一些变化。
struct class和device_create(…) 以及device_create(…)都定义在/include/linux/device.h中,使用的时候一定要包含这个头文件,否则编译器会报错。
1、 alloc_register_region()函数向内核系统动态申请设备号。
2、 使用cdev_init()函数初始化字符设备控制块。
3、 cdev_add()函数向内核添加字符设备模块。
4、 使用class_create()在/sys/class中创建设备类型目录及相应一些配置。
5、 使用device_create()自动创建设备节点。
同时也在模块卸载程序中填入下面动作:
1、 device_destroy()删除自动创建爱的设备节点。
2、 class_destroy()删除自动创建的目录及一些配置。
3、 cdev_del()从内核中删除设备。
4、 unregister_chrdev_region()注销自动分配的设备号。
struct class *class_create(struct module *owner, const char *name)
class_create - create a struct class structure
@owner: pointer to the module that is to "own" this struct class
@name: pointer to a string for the name of this class.
This is used to create a struct class pointer that can then be used in calls to device_create().
Returns &struct class pointer on success, or ERR_PTR() on error.
在/sys/class/下创建设备类型目录
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
device_create - creates a device and registers it with sysfs
@class: pointer to the struct class that this device should be registered to
@parent: pointer to the parent struct device of this new device, if any
@devt: the dev_t for the char device to be added
@drvdata: the data to be added to the device for callbacks
@fmt: string for the device's name
void device_destroy(struct class *class, dev_t devt)
device_destroy - removes a device that was created with device_create()
@class: pointer to the struct class that this device was registered with
@devt: the dev_t of the device that was previously registered
This call unregisters and cleans up a device that was created with a call to device_create().
void class_destroy(struct class *cls)
class_destroy - destroys a struct class structure
@cls: pointer to the struct class that is to be destroyed
Note, the pointer to be destroyed must have been created with a call to class_create().
五.使用文件私有数据
六.参考代码
#include<linux/module.h>
#include<linux/cdev.h>
#include<linux/slab.h>
#include<linux/device.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/fs.h>
#include<linux/ioctl.h>
#include<linux/uaccess.h>
#define MEM_SIZE0x1000
static char*Device_Node_Name = "vcd";//the name is adevice node name ,will show in /dev/
module_param(Device_Node_Name,charp,S_IRUGO);
MODULE_PARM_DESC(Device_Node_Name,"Theparameter default value is vcd,");
struct vcd_dev{
#define DeviceName "vcd_device"//The name willshow in /proc/devices
#define DeviceClassName "vcd_class" //The name willshow in /sys/class/
#define nu_of_dev 3
#define bn_of_dev 1
struct class *my_class;
struct device *my_device;
struct cdevcdev;
dev_t dev;
unsigned charmem_data[MEM_SIZE];
};
#definevcd_dev_t struct vcd_dev
#definevcd_dev_p struct vcd_dev *
vcd_dev_p pVCD;
//open file APIfunction
static intapi_vcd_open(struct inode *i, struct file *f)
{
f->private_data= pVCD;
//Here add user init open code
printk(KERN_INFO "The vcd file wasopened by AP!\n");
return 0;
}
//To close fileAPI function
static intapi_vcd_close(struct inode *i, struct file *f)
{
//Here add user destroyed code
printk(KERN_INFO "The vcd file wasclosed by AP!\n");
return 0;
}
ssize_tapi_read(struct file *filp, char __user *buff, ssize_t n,loff_t *l)
{
vcd_dev_p fd =(vcd_dev_p)filp->private_data;
return copy_to_user(buff,fd->mem_data,n);
}
structfile_operations vcd_ops =
{
.owner = THIS_MODULE,
.open = api_vcd_open,
.release = api_vcd_close,
};
static int__init vcd_init(void)
{
int ret;
pVCD =kmalloc(sizeof(vcd_dev_t),GFP_ATOMIC); //Malloc the VCD_P space
ret=alloc_chrdev_region(&pVCD->dev,bn_of_dev,nu_of_dev,DeviceName);//The DeviceName will be show in/proc/devices's file
if(ret < 0) goto exit1;
cdev_init(&pVCD->cdev,&vcd_ops);
pVCD->cdev.owner = THIS_MODULE;
pVCD->cdev.ops = &vcd_ops;
ret =cdev_add(&pVCD->cdev,pVCD->dev,nu_of_dev); //Add device to the kernel
if(ret < 0) goto exit2;
pVCD->my_class =class_create(THIS_MODULE,DeviceClassName);//The DeviceClassName will be show in/sys/class's dir.
if(IS_ERR(pVCD->my_class)) gotoexit3;
pVCD->my_device =device_create(pVCD->my_class,NULL,pVCD->dev,NULL,Device_Node_Name);//The Device_Node_Name will create nodein /dev's dir.
if(IS_ERR(pVCD->my_device)) gotoexit4;
printk(KERN_INFO "The /dev/%s wascreated by the kernel,the dev value is %d %d!\n",Device_Node_Name,MAJOR(pVCD->dev),MINOR(pVCD->dev));
//Her add user other init code
goto exit0;
exit4:
class_destroy(pVCD->my_class);
exit3:
cdev_del(&pVCD->cdev);
exit2:
unregister_chrdev_region(pVCD->dev,nu_of_dev);
exit1:
kfree(pVCD);
// return -1;
exit0:
return ret;
}
static void__exit vcd_exit(void)
{
device_destroy(pVCD->my_class,pVCD->dev);
class_destroy(pVCD->my_class);
cdev_del(&pVCD->cdev);
unregister_chrdev_region(pVCD->dev,nu_of_dev);
kfree(pVCD);
printk(KERN_INFO "The /dev/%sdevice node was destroyed by the kernel!\n",Device_Node_Name);
}
MODULE_LICENSE("GPL");
module_init(vcd_init);
module_exit(vcd_exit);