在Linux内核中,使用一个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;
};
其中dev_t成员定义了设备号,为32位,其中12位为主设备号,20位为次设备号;file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数
使用以下宏可以从dev_t 获得主设备号和次设备号
MAJOR(dev_t dev)
MINOR(dev_t dev)
使用以下宏可以通过主设备号和次设备号生成dev_t
MKDEV(int major, int minor)
Linux内核提供了一组函数以用于操作cdev结构体
/*用于初始化cdev成员,并建立cdev和file_operations之间的连接*/
void cdev_init(struct cdev *dev, struct file_operations *fops);
{
memset(cdev, 0, sizeof(cdev));
INIT_LIST_HEAD(&head->list); //初始化双向链表的宏,并且会将双向链表头部的指针指向自己
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops //建立连接
}
/*cdev_alloc函数用于动态的申请一个cdev内存*/
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;
}
}
void cdev_put(struct cdev *p);
/*分别向系统添加和删除一个cdev,完成字符设备的注册和注销,分别出现在模块加载函数和模块卸载函数*/
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
在使用 cdev_add()
函数向系统注册字符设备之前,应首先调用 register_chrdev_region()
或者是 alloc_chrdev_region()
向系统申请设备号。
其中register表示已知起始设备的设备号的情况,而alloc表示设备号未知的情况向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把设备号放入第一个参数dev中。
相反,在使用 cdev_del()
函数时,应该先将设备号主动释放使用 unregister_chrdev_region()
应该被调用以释放原先申请的设备号。
file_operation结构体中的成员函数是字符设备驱动设计的主体内容,这次函数会在应用程序对文件操作open()/close()/write()/read()时最终被调用,结构体被存放在 include/linux/fs.h
在字符设备驱动模块加载函数中应该实现对设备号的申请和cdev的注册,在模块卸载函数中应该实现对设备号的释放和对cdev的注销。
#define test DEV_NAME
#define 1 MAJOR
/*cdev 设备结构体
structe test_dev {
struct cdev cdev;
...
} test_dev;
*/
//设备模块加载函数
static int __init dev_init(void)
{
//初始化cdev
cdev_init(&test_dev.cdev, test_fops);
test_dev.cdev.owner = THIS_MODULE;
//注册设备号
//获取字符设备号
if(test_major){
register_chrdev_region(test_dev_no, 1, DEV_NAME);
}
else{
alloc_chrdev_region(&test_dev_no, 0, 1, DEV_NAME);
}
//注册设备
cdev_add(&test_dev.cdev, test_dev_no, 1);
}
/*模块卸载函数*/
static void __exit test_exit(void)
{
//释放设备号
unregister_chrdev_region(test_dev_no, 1);
//注销设备
cdev_del(&test_dev_cdev);
}
字符设备驱动读、写、I/O控制函数模板:
驱动程序的write()方法包括从用户空间读取到内核空间,然后在内核中处理这些数据使用
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
read()方法则相反,从内核空间读取到用户空间使用
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
其中带__user的参数的都表示用户空间内存
虽然内核空间可以访问用户空间的缓存区,但是在访问前需要检查其的合法性通过 access_ok(type, add, size)
进行判断,以确定传入的缓存区属于用户空间。
如果要复制单个简单变量如 char、int这样,内核会提供专用的宏:
x表示复制到的值,ptr表示目标空间的地址,成功时返回0,失败时返回-EFAULT
put_user(x, ptr)
get_user(x, ptr)
还有__put_user()这种与上面不一样的是不会进行access_ok()的检查
file_operations的write函数:
ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *pos);
返回值是写入的字节数(长度)
*buf是来自用户的数据缓存区
count是请求传输的数据长度
*pos表示数据在文件中应该写入的起始位置
file_operations的read函数:
#define 文件内存大小 filesize
ssize_t eeprom_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
//检查来自用户空间的错误请求
//只有在设备提供内存(电可擦编程只读存储器、I/O内存等)时才有意义
if(*pos >= filesize)
return -EINVAL;
//针对剩余字节数调整count,以便不超过文件大小
if(*pos + count > filesize)
count = filesize - *pos;
//找到能够开始写入的位置
void *from = pos_to_address(*pos)
//从用户空间拷贝内容到内核
if(copy_from_user(dev->buff, buff, count) != 0){
retval = -EFAULT;
goto out;
}
//再从内核空间写入到设备中去
write_error = write_device(dev->buff, count);
if(write_error){
return -EFAULT;
}
//根据写入的字节数设置文件光标的位置,返回复制的字节数
*pos += count;
return count;
}
ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *pos);
返回值是读取的数据量
count请求传输数据的大小
#define 文件内存大小 filesize
ssize_t eeprom_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
//防止读操作超出文件大小,并返回文件结束
if(*pos >= filesize){
return 0; //0 means EOF
}
//读取的字节数不能超过文件大小
if(*pos + count > filesize){
count = filesize - *pos;
}
//找到读取的起始位置
void *from = pos_to_address(*pos);
//读取数据
if(copy_to_user(buff, from, count)){
return -EFAULT;
}
//移动光标的位置
*pos += count;
return count;
}
open是每次打开设备文件时都会调用,通常在open中执行设备和数据结构的初始化
第一个参数struct inode结构内的i_cdev字段通常指向在__init函数中分配出来的cdev
int (*open)(struct inode *inode, struct file *filp);
struct pcf2127 {
struct cdev cdev;
unsigned char *sram_data;
struct i2c_client *client;
int sram_size;
...
};
static unsigned int sram_major = 0;
static struct class *sram_class = NULL;
static int sram_open(struct inode *inode, struct file *filp)
{
//获取主次设备号
unsigned int maj = imajor(inode);
unsigned int min = iminor(inode);
if(maj != sram_major || min <0){
pr_err("device not found\n"); //这个宏会将指定的错误信息输出到系统日志中,以便开发人员进行故障排除。
return -ENODEV;
}
//获取特定成员的结构体的指针
struct pcf2127 *pcf = NULL;
pcf = container_of(inode->i_cdev, struct pcf2127, cdev); //container_of用的好
pcf->sram_size = SRAM_SIZE;
//如果是第一次打开,准备缓冲
if(pcf->sram_data == NULL){
pcf->data = kzalloc(pcf->sram_size, GFP_KERNEL);
if(pcf->sram_data == NULL){
pr_err("memory allocation failed\n");
return -ENOMEM;
}
}
~~~~filp->private_data = pcf;//文件私有数据
return 0;
}
文件私有数据
static int sram_release(struct inode *inode, struct file *filp);
与open相反,release需要在设备关闭的时候调用,必须撤销在open任务中已经执行的所有操作。
释放在open阶段所有的私有内存
关闭设备
static int sram_release(struct inode *inode, struct file *filp)
{
struct pcf2127 *pcf = NULL;
pcf = container_of(inode->i_cdev, struct pcf2127, cdev);
mutex_lock(&device_list_lock);
filp->private_data = NULL;
//关闭设备
pcf->users--;
if(!pcf->users--){
kfree(tx_buffer);
kfree(rx_buffer);
tx_buffer = NULL;
rx_buffer = NULL;
if(any_global_struct)
kfree(any_global_struct);
}
mutex_unlock(&device_list_lock);
return 0;
}
在文件中移动光标位置时会调用llseek函数
loff_t(*llseek) (structfile *filp, loff_t offset, int whence);
返回值是文件中的新位置
offset是相对与当前位置的文件的偏移量,也就是位置将改变多少
whence定义从哪里开始找{SEEK_SET SEEK_CUR SEEK_END}
switch(whence){
case SEEK_SET:
newpos += offset;
break;
case SEEK_CUR:
newpos = file->f_pos + offset;
break;
case SEEK_END:
newpos = filesize + offset;
break;
default:
return -EINVAL;
}
//检查newpos是否有效
if(newpos < 0){
return -EINVAL;
}
//将文件光标重置
file->f_pos = newpos;
//返回新文件指针的位置
return newpos;
long ioctl(struct file *f, unsigned int cmd, unsigned long arg);
有时候设备需要进行未被定义的系统调用的命令,特别是文件和设备文件相关的命令,用ioctl可以向设备发送特殊命令(重置,关机,配置等)如果驱动程序没有定义这个方法,则内核向ioctl系统调用返回-ENOTTY错误。
Linux提供了四个帮助宏来创建ioctl标识符,选用取决于是否有数据传输和传输的方向:
_IO (MAGIC, SEQ_NO):ioctl不需要传输数据
_IOW (MAGIC, SEQ_NO, type):ioctl需要写入参数
_IOR (MAGIC, SEQ_NO, type):ioctl需要读取参数
_IOWR (MAGIC, SEQ_NO, type):ioctl需要写入和读取参数
参数的含义(8为魔数[0~255],8为序列号或命令ID,数据类型)
/*生成ioctl命令*/
//定义一个eep_ioctl.h文件,在用户和内核空间中都要调用
"#include "
#ifndef PACKT_IOCTL_H
#define PACKT_IOCTL_H
//需要为驱动选择一个数字,以及每个驱动选择一个序列号
#define EEP_MAGIC 'E'
//擦除
#define ERASE_SEQ_NO 0x01
//重命名
#define RENAME_SEQ_NO 0x02
//清除字节
#define CLEAN_BYTE_SEQ_NO 0x03
//获取大小
#define GET_SIZE_SEQ_NO 0x04
//定义最大分区大小
#define MAX_PART 32
//定义ioctl命令
#define EEP_ERASE _IO(EEP_MAGIC, ERASE_SEQ_NO)
#define EEP_RENAME_PART _IOW(EEP_MAGIC, RENAME_SEQ_NO, unsigned long)
#define EEP_GET_SIZE _IOR(EEP_MAGIC, GET_SIZE_SEQ_NO, int *)
#endif
//内核空间代码
#include "epp_ioctl.h"
static long ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
int part;
char *buf = NULL;
int size = 1300;
switch (cmd){
case EEP_ERASE:
erase_eeprom();
break;
case EEP_RENAME_PART:
buf = kzalloc(MAX_PART, GFP_KERNEL);
copy_from_user(buf, (char *)arg, MAX_PART);
rename_part(buf);
break;
case EEP_GET_SIZE:
copy_to_user((int *)arg, &size, sizeof(int));//最后的长度为指针的大小
break;
default:
break;
}
return 0;
}
//用户空间程序代码 my_main()
#include "eep_ioctl.h"
int main()
{
int size = 0;
int fd;
char *newname = "new_part";//小于MAX_PART
fd = open("/dev/eep_mem1", O_RDWR);
if(fd<0){
printf("Eorror while opening the eeprom\n");
return -1;
}
ioctl(fd, EEP_ERASE);
ioctl(fd, EEP_RENAME_PART, newname);
ioctl(fd, EEP_GET_SIZE, &size);
close(fd);
return 0;
}
globalmem为一块虚拟的存储空间,大小为4KB内存空间,并在驱动中提供针对该片内存的读写,控制,和定位函数,以供用户空间的进程能通过Linux系统调用获取或设置这片内存的内容
/*
* a simple char device driver : globalmem without mutex
* Licensed under GPLv2 or later
* 按照上面的模块写的,头文件我就不一一列举出来了
*/
#define MEM_SIZE 0x1000 //4KB
//打开设备文件
//释放设备文件
//控制设备
//读取设备
//写入设备
//设备文件定位
//回调file_operations
//设置设备
//加载模块
//卸载模块
//模块信息
假如我要加载 N 个globalmem的驱动代码
首先结构框架:
#define MAX_MEM 0x1000
#define GLOBAL_MAJOR 230
#define MEM_CLEAN 0x1
#define GLOBAL_NUM N
static int globalmem_major = GLOBAL_MAJOR
module_param(globalmem_major, int, S_IRUGO);
//设备文件
static struct globalmem{
unsigned char mem[MAX_MEM];
struct cdev cdev;
};
struct globalmem *dev_p;
//设置设备
static void globalmem_setup_dev(struct globalmem *dev, int index) //其中dev指代的就是我的设备
{
int error;
dev_t devno = MKDEV(globalmem_major, index);
//初始化设备
cdev_init(&dev->cdev, &globalmem_fops);
//添加设备
error = cdev_add(&dev->cdev, devno, 1);
if(error){
printk("add device failed\n");
return error;
}
}
//打开文件
static int globalmem_open(struct inode *inode, struct file *filp)
{
//可以使用 container_of 先通过结构体某一成员的访问找到这个结构体的首地址,
//inode中包含一个i_cdev
//然后就可以访问其它成员变量了
struct globalmem *dev = container_of(inode->i_cdev, struct globalmem, cdev)
//第一个参数为结构体指针,第二个参数为结构体类型,第三个参数是要访问的结构体成员
filp->private_data = dev_p;
return 0;
}
//释放文件
static void globalmem_release(struct inode *inode, struct file *filp)
{
return 0;
}
//读取文件
static int globalmem_read(struct file *filp, char __buf *buf, size_t size, loff_t *pos)
{
int ret;
unsigned int count = size;
unsigned long p = *pos;
struct globalmem *dev = file->private_data;
//检查是否光标已经到了末尾
if(p >= MAX_MEM){
printk("the location of pos is exceed\n");
return -EINVAL;
}
//检查读取的数量是否不够文件大小
if(p + count > MAX_MEM){
count = MAX_MEM - p;
}
//读取
ret = copy_to_user(buf, dev->mem+p, count);
if(ret < 0){
printk("read failed\n");
return -EFAULT;
}
else{
*pos += count;//光标开始移动
ret = count;
printk("has been read %u bytes dara\n", count);
}
return ret;
}
//写入文件
static int globalmem_write(struct file *filp, char __buf *buf, size_t size, loff_t *pos)
{
int ret;
unsigned int count = size;
unsigned long p = *pos;
struct globalmem *dev = filp->private_data;
//检查是否已经写到文件末尾
if(p >= MAX_MEM){
return 0;
}
//检查一次写入的量是否超出了文件大小
if(p + count > MAX_MEM){
count = MAX_MEM - p;
}
//写入数据
ret = copy_from_user(dev->mem + p, buf, count);
if(ret < 0){
printk("write failed\n");
return -EFAULT;
}
else{
*pos += count;
ret = count;
printk("has been writen %u bytes data\n", count);
}
return ret;
}
//文件的控制
static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct globalmem *dev = filp->private_data;
switch(cmd){
case MEM_CLEAN:
memset(dev->mem, 0 , MAX_MEM);
printk("KERN INFO:the globalmem is set to zero\n");
break;
default:
return -EINVAL;
}
}
//文件偏移
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t ret;
switch(whence){
case 0://f_pos在文件开头
//如果文件指针偏移大于文件大小
if((unsigned int)offset > MAX_MEM){
return -EINVAL;
break;
}
//如果文件指针偏移为负数
if(offset < 0){
return -EINVAL;
break;
}
filp->f_pos += (unsigned int)offset;
ret = file->f_pos;
return ret;
break;
case 1://文件指针f_pos不在文件开头
//如果文件指针移动大于文件大小
if(file->f_pos + (unsigned int)offset > MAX_MEM){
return -EINVAL;
break;
}
//如果文件指针移动小于0
if(file->f_pos + (unsigned int)offset < 0){
return -EINVAL;
break;
}
file->f_pos += (unsigned int)offset;
ret = file->f_pos;
return ret;
break;
}
}
//文件操作函数重载,用在初始化设备当中
const struct file_operations globalmem_fops{
.open = globalmem_open,
.release = globalmem_release,
.write = globalmem_write,
.read = globalmem_read,
.ioctl = globalmem_ioctl,
.llseek = globalmem_llseek,
.owner = THIS_MODULE,
};
//添加模块
static int __init globalmem_init(void)
{
int ret;
//获取设备号
dev_t devno = MKDEV(globalmem_major, 0);
//注册设备
if(globalmem_major){
ret = register_chrdev_region(devno, GLOBAL_NUM, "globalmem");
if(ret){
printk("register failed\n");
return ret;
}
}
else{
alloc_chrdev_region(devno, 0, GLOBAL_NUM, "globalmem");
globalmem_major = MAJOR(devno);
}
//设置设备
dev_p = kzalloc(sizeof(struct globalmem) * GLOBAL_NUM, GFP_KERNEL);
if(!dev_p){
return -ENOMEM;
goto fail_malloc;
}
for(int i = 0, i<GLOBAL_NUM, i++)
globalmem_setup_dev(dev_p+i, i);
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return ret;
}
//卸载模块
static void __exit globalmem_exit(void)
{
int i;
for(i=0, i<GLOBAL_NUM, i++){
//删除设备
cdev_del(&(dev_p+i)->cdev);
//释放空间
kfree(dev_p)
dev_p = NULL;
}
//注销设备
unregister_chrdev_region(MKDEV(globalmem_major, 0), GLOBAL_NUM);
}
//模块信息
module_init(globalmem_init);
module_exit(globalmem_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("GuduMemories")