字符设备驱动相关的函数和参数及实现(虚拟文件)
用户进程调用函数顺序:
open ---> kernel ---> cdev.ops->open(..)
read ---> kernel ---> cdev.ops->read(..)
ioctl ---> kernel ---> cdev.ops->unlocked_ioctl(..)
设备驱动实现好功能后,基本上由用户进程通过系统调用后进来调用的。
如需要循环操作硬件,则应是在用户进程里循环。
用户进程操作设备驱动,网络通信等与普通的文本文件操作的编程接口基本一样(open, read, write, ioctl …),这套接口就是所谓的VFS(虚拟文件系统)。
当用户进程open设备文件时,内核会根据打开的设备文件的设备号找到对应的cdev对象,检查cdev.ops->open,如果不为空,则调用驱动里的open函数,如果为空,内核直接返回fd。
注意:用户进程打开设备文件得到文件描述符不是由设备驱动里指定的,设备驱动里的open函数仅仅是告诉内核是否已正常打开。
在linux内核,用一个inode节点对象描述一个要操作的文件/设备文件,包括权限,设备号等信息。
一个文件可以打开很多次,但都是共用一个inode对象来描述属性的。
文件描述符属于一个进程的资源,不同进程里有可能相同的文件描述符。
struct inode {
...
dev_t i_rdev; //设备文件对应的设备号,驱动里即可通过区分次设备号来区别不同的具体硬件
struct cdev *i_cdev; //指向对应的字符设备驱动cdev对象的地址
...
};
在用户进程里用一个int类型来表示文件描述符,但文件描述符里有还存有对文件位置的偏移,打开标志等信息,用一个int数无法记录下来的,所在每个文件描述符的信息都是由内核里用file对象描述文件描述符,在文件打开时创建,关闭时销毁。
struct file {
...
struct path f_path;
const struct file_operations *f_op; //对应的文件操作对象的地址
unsigned int f_flags; //文件打开的标志
fmode_t f_mode; //权限
loff_t f_pos; //文件描述符的偏移
struct fown_struct f_owner; //属于哪个进程
unsigned int f_uid, f_gid;
void *private_data; //给驱动程序员使用
...
};
如果打开设备文件,那么得到的file对象:
file对象里的成员f_path.dentry->d_inode->i_rdev可以获取到设备文件的设备号
file对象里的成员f_path.dentry->d_inode可以获取到设备文件的inode对象的地址
注意:一个文件只有一个inode节点对象,但是可以打开多次,得到不同的文件描述符对象(也就是多个struct file对象)。
虚拟文件系统实现相关函数:
struct file_operations里函数参数:
int (*open) (struct inode *, struct file *);
//inode表示应用程序打开的文件的节点对象,file表示打开文件获取到的文件描述符
//返回值0表示成功打开,负数表示打开失败
//内核根据open函数的返回值来确定是否给调用的用户进程分配文件描述符
//在驱动可以不实现此函数,如不实现,则表示每次打开都是成功的
ssize_t (*read) (struct file *fl, char __user *buf, size_t len, loff_t *off);
//buf指向用户进程里的缓冲区,len表示buf的大小(由用户调用read时传进来的)
//off表示fl文件描述符的操作偏移,返回值为实际给用户的数据字节数
//注意:必须通过off指针来改变文件描述符的偏移(*off += 操作字节数),不可以直接通过"fl->f_pos"来设置
ssize_t (*write) (struct file *, const char __user *buf, size_t len, loff_t *off);
//用户进程把数据给驱动,也就是让驱动存放用户进程传进来的数据
//buf指向用户进程里的缓冲区,len表示buf的大小(由用户调用write时传进来的)
//off表示fl文件描述符的操作偏移,返回值为实际给驱动的数据字节数
//注意:必须通过off指针来改变文件描述符的偏移(*off += 操作字节数),不可以直接通过"fl->f_pos"来设置
long (*unlocked_ioctl) (struct file *fl, unsigned int cmd, unsigned long arg);
//cmd表示用户进程调用ioctl时的第二个参数,arg表示第三个参数(可选)
//返回值为0表示ioctl成功,返回负数表示失败
loff_t (*llseek) (struct file *fl, loff_t offset, int whence);
//如:lseek(fd, 54, SEEK_SET)
在驱动里操作用户数据缓冲区的函数:
#include <asm/uaccess.h>
extern inline long copy_to_user(void __user *to, const void *from, long n);
//返回值为还有多少字节没有复制成功,正常情况下返回0
//to指用户进程的缓冲区,from指驱动里装数据的缓冲区,n要复制多少字节
extern inline long copy_from_user(void *to, const void __user *from, long n);
//返回值为还有多少字节没有复制成功,正常情况下返回0
//to指驱驱动里装数据的缓冲区,from指用户进程的缓冲区,n指多少字节
put_user(x, p);
//x为值,p为地址
//如果与用户进程交互的数据是1, 2, 4, 8字节的话,可用
get_user(x,p);
//如果从用户进程获取1, 2, 4字节的话,可用
驱动里动态申请缓冲区的函数:
#include <linux/slab.h>
void *kmalloc(size_t size, gfp_t flags);//申请后要内存要清零
void *kzalloc(size_t size, gfp_t flags);//申请出来的内存已清零
void kfree(const void *objp);//回收kmalloc/kzalloc的内存
void *vmalloc(unsigned long size);//申请大内存空间
void vfree(const void *addr);//回收vmalloc的内存
//kmalloc申请出来的内存是物理地址连续的,vmalloc不一定是连续的
//动态申请内存,并清零,size为申请多大(不要超过128K)
//flags为标志(常为GFP_KERNEL),成功返回地址,失败返回NULL
//GFP_ATOMIC,使用系统的内存紧急池
在驱动里用数据缓冲区代替文本文件的数据存取
虚拟文件系统的实现(test.c):
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#define MYMA 1234
#define MYMI 5500
#define COUNT 1
dev_t devid; //设备号id
struct cdev mycdev; //设备对象
u8 *data; //驱动数据缓冲区
int data_len = 26; //驱动数据缓冲区长度
//当用户进程读时,应把驱动里的缓冲区数据复制给用户进程
//fl->f_pos表示当前文件描述符对文件的偏移, len表示用户进程想要读的大小
ssize_t myread(struct file *fl, char __user *buf, size_t len, loff_t *off)
{
int copy_len, ret;
if((fl->f_pos + len) > strlen(data)) //如果剩下没读的数据长度少于len,则只复制出剩下没读部分
copy_len = strlen(data) - fl->f_pos;
else
copy_len = len; //如果剩下的数据长度超出len,则本次复制len字节
ret = copy_to_user(buf, data+fl->f_pos, copy_len); //读取驱动数据缓冲区内容
//内容复制后,需要改变文件描述符的位置偏移
*off += copy_len - ret; //在read/write函数里必须通过off来改变
return copy_len - ret;
}
//当用户进程write时,驱动里应把用户进程的数据存放起来
ssize_t mywrite(struct file *fl, const char __user *buf, size_t len, loff_t *off)
{
int copy_len, ret;
if((fl->f_pos + len) > data_len) //如果要复制的内容超出数据缓冲区的大小
copy_len = data_len - fl->f_pos; //只复制剩下空间大小的内容
else
copy_len = len;
ret = copy_from_user(data+ fl->f_pos, buf, copy_len); //将数据写入驱动数据缓冲区
*off += len_copy - ret;
return len_copy - ret;
}
//当用户进程lseek(fd, 54, SEEK_SET)时触发此函数
loff_t myllseek(struct file *fl, loff_t offset, int whence)
{
//除了read/write函数,可以通过fl->f_pos来改变文件描述符的偏移
u8 *tmp;
int len;
switch(whence)
{
case SEEK_SET:
if(offset < data_len)
fl->f_pos = offset;
else
return -1;
break;
case SEEK_CUR:
if((fl->f_pos + offset) < data_len)
fl->f_pos += offset;
else
return -1;
break;
case SEEK_END:
if(0 == offset)
fl->f_pos = data_len - 1; //数据缓冲区的最后位置
else if(offset > 0)
{
//表示需要把缓冲区变为(原大小+offset)
len = data_len;
data_len += offset;
tmp = kzalloc(data_len, GFP_KERNEL); //分配出新大小的缓冲区
memcpy(tmp, data, len); //把原缓冲区上的数据复制到新缓冲区
kfree(data); //回收原空间
data = tmp; //取代原缓冲区
fl->f_pos = data_len - 1;
}
else
return -1;
break;
}
return fl->f_pos;
}
struct file_operations fops = {
.owner = THIS_MODULE,
.read = myread,
.write = mywrite,
.llseek = myllseek,
};
static int __init test_init(void)
{
int ret, i;
devid = MKDEV(MYMA, MYMI); //生成设备号
ret = register_chrdev_region(devid, COUNT, "mydev"); //申请设备号
if(ret < 0)
goto err0;
cdev_init(&mycdev, &fops); //初始化设备对象
mycdev.owner = THIS_MODULE;
ret = cdev_add(&mycdev, devid, COUNT); //将设备对象加到内核
if (ret < 0)
goto err1;
data = kzalloc(dlen, GFP_KERNEL); //申请数据缓冲区, 此缓冲区用于与用户进程数据交互.
if (NULL == data)
goto err2;
//初始化驱动缓冲区内容
for (i = 0; i < 26; i++)
data[i] = 'A' + i;
printk("init cdev success\n");
return 0;
err2:
cdev_del(&mycdev); //从内核中移除设备对象
err1:
unregister_chrdev_region(devid, COUNT); //回收设备号
err0:
return ret;
}
static void __exit test_exit(void)
{
unregister_chrdev_region(devid, COUNT); //回收设备号
cdev_del(&mycdev); //从内核中移除设备对象
kfree(data); //释放内核驱动缓冲区
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
用户功能实现(app_test.c):
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
int fd;
int ret;
char ch;
char buf[100];
fd = open("/dev/mydev", O_RDWR);
if(fd < 0)
{
printf("open file failed\n");
return -1;
}
ret = read(fd, buf, 100); //调用内核驱动的myread函数
if(ret < 0)
{
perror("read");
return -1;
}
printf("%s\n", buf);
lseek(fd, 0, SEEK_SET);//调用内核驱动的myllseek函数
ret = write(fd, "aaaaaaaaaaaat", 13);//调用内核驱动的mywrite函数
if(ret < 0)
{
perror("write");
return -1;
}
lseek(fd, 0, SEEK_SET);//调用内核驱动的myllseek函数
while(1)
{
ret = read(fd, &ch, 1);//调用内核驱动的myread函数
if(ret < 0)
{
perror("read");
break;
}
else if (ret == 0)
break;
printf("%c\n", ch);
}
close(fd);
return 0;
}
Makefile文件:
obj-m += test.o
KSRC := /目录路径/orangepi_sdk/source/linux-3.4.112/
export ARCH := arm
export CROSS_COMPILE := arm-linux-gnueabihf-
all :
make -C $(KSRC) modules M=`pwd`
.PHONY : clean
clean :
make -C $(KSRC) modules clean M=`pwd`
写好test.c和Makefile文件后,进行测试步骤:
1.编译内核驱动test.ko模块:
make
2.交叉编译生成用户功能文件app:
arm-linux-gnueabifh-gcc app.c -o app
3.加载驱动模块:
insmod test.ko
4.运行功能文件:
./app
5.卸载驱动模块:
rmmod test 或 rmmod test.ko
一个驱动支持多个虚拟文件的实现(test.c):
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#define MYMA 1234
#define MYMI 7788
#define COUNT 3 //三个设备号,每个设备号对应一个设备文件和一个数据缓冲区
dev_t devid;
struct cdev mycdev;
u8 *data[COUNT]; //驱动数据缓冲区
int dlen = 1024; //驱动数据缓冲区长度
ssize_t myread(struct file *fl, char __user *buf, size_t len, loff_t *off)
{
int mi = MINOR(fl->f_path.dentry->d_inode->i_rdev);
int len_copy, ret, n;
n = mi - MYMI;
if (n >= COUNT)
return -ENODEV;
if ((fl->f_pos + len) > strlen(data[n]))
len_copy = strlen(data[n]) - fl->f_pos;
else
len_copy = len;
ret = copy_to_user(buf, data[n]+fl->f_pos, len_copy);
*off += len_copy - ret;
return len_copy - ret;
}
ssize_t mywrite(struct file *fl, const char __user *buf, size_t len, loff_t *off)
{
int mi = MINOR(fl->f_path.dentry->d_inode->i_rdev);
int len_copy, ret, n;
n = mi - MYMI;
if (n >= COUNT)
return -ENODEV;
if ((fl->f_pos + len) > dlen)
len_copy = dlen - fl->f_pos;
else
len_copy = len;
ret = copy_from_user(data[n] + fl->f_pos, buf, len_copy);
*off += len_copy - ret;
return len_copy - ret;
}
struct file_operations fops = {
.owner = THIS_MODULE,
.read = myread,
.write = mywrite,
};
static int __init test_init(void)
{
int ret, i, j;
devid = MKDEV(MYMA, MYMI);
ret = register_chrdev_region(devid, COUNT, "mydev");
if (ret < 0)
goto err0;
cdev_init(&mycdev, &fops);
mycdev.owner = THIS_MODULE;
ret = cdev_add(&mycdev, devid, COUNT);
if (ret < 0)
goto err1;
for (i = 0; i < COUNT; i++)
{
data[i] = kzalloc(dlen, GFP_KERNEL);
for (j = 0; j < 26; j++)
data[i][j] = 'A' + j;
}
return 0;
err1:
unregister_chrdev_region(devid, COUNT);
err0:
return ret;
}
static void __exit test_exit(void)
{
int i;
unregister_chrdev_region(devid, COUNT);
cdev_del(&mycdev);
for (i = 0; i < COUNT; i++)
kfree(data[i]);
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");