20 字符设备驱动相关的函数和参数及实现(虚拟文件)

字符设备驱动相关的函数和参数及实现(虚拟文件)


用户进程调用函数顺序:

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");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值