字符设备2

  • 驱动开发流程图
  • 3.5字符设备的注册
  • 3.5.1Linux内核驱动和系统调用之间的联系
  • 3.6 字符设备驱动开发步骤
  • 3.7字符设备源码
  • 3.8 编写字符测试 APP
  • 3.9在ARM开发板上测试



驱动开发流程图

Linux设备驱动开发(字符设备2)_驱动开发

3.5字符设备的注册

注册一个字符设备的早期方法:

int register_chrdev(unsigned int major,  const char *name, 
			struct file_operations *fops);
参数:
	major 是给定的主设备号。为0代表自动分配设备号
	name 是驱动的名字(将出现在 /proc/devices), 
	fops 是设备驱动的file_operations 结构。
	register_chrdev 将给设备分配 0-255 的次设备号, 并且为每一个建立一个缺省的 cdev 结构

从系统中卸载字符设备的函数:

int unregister_chrdev(unsigned int major, const char *name);

3.5.1Linux内核驱动和系统调用之间的联系

Linux内核驱动和系统调用之间有着紧密的联系,这是因为系统调用是用户空间程序与内核交互的接口,而驱动程序则是内核中与硬件交互的接口。

当用户空间程序需要访问硬件设备或执行特权操作时,需要调用相应的系统调用。系统调用会通过中断或陷阱的方式进入内核,进而调用相应的驱动程序来实现对硬件设备的访问或特权操作的执行。因此,驱动程序是系统调用的实际执行者。

此外,系统调用和驱动程序还需要通过一些数据结构进行交互,比如文件描述符、缓冲区和进程上下文等。这些数据结构也是内核中的重要组成部分,驱动程序需要与这些数据结构进行交互以完成相应的操作。

因此,Linux内核驱动和系统调用之间密不可分,它们共同构成了Linux操作系统的基础,为用户提供了访问硬件和实现特权操作的接口。

驱动和内核之间的交互分为以下四个步骤:
1 硬件发起一个中断(interrupt)。
2 内核接收中断,并调用相应的驱动程序。
3 驱动程序向硬件设备发送请求,并等待操作完成。
4 驱动程序向内核返回数据结果,并将数据结果提供给应用程序。

3.6 字符设备驱动开发步骤

①相应的设备硬件初始化
②分配主次设备号,这里即支持静态指定,也支持动态申请
③分配cdev结构体,我们这里使用动态申请的方式
④绑定主次设备号、fops到cdev结构体中,并注册给Linux内核

  • 字符设备模块入口函数执行流程:
  • 字符设备模块出口函数的执行流程:

3.7字符设备源码

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/major.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

/*确定主设备号 */
//#define DEV_MAJOR 79
#ifndef DEV_MAJOR 
#define DEV_MAJOR 0
#endif
int dev_major = DEV_MAJOR; //主设备号

#define DEV_NAME "chrdev" //设备名

static struct cdev *chrtest_cdev; //cdec结构体

//static struct class *chrdev_class; //定义一个class结构体用于自动创建类

static char kernel_buf[1024];

#define  MIN(a, b)       ( a > b ? a : b)

/* 实现对应的open/read/write函数,填入到file_operations结构体 */
static ssize_t chrtest_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
        int err;
        printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
    err = copy_to_user(buf, kernel_buf, MIN(1024,size)); /* 内核空间的数据到用户空间的复制 */
        return MIN(1024,size);
}

static ssize_t chrtest_drv_write(struct file *file,const char __user *buf, size_t size, loff_t *offset)
{
        int err;
        printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
    err = copy_from_user(kernel_buf,buf, MIN(1024,size)); /* 将buf中的数据复制到写缓冲区域的kernel_buf中 */
        return MIN(1024,size);
}

static int chrtest_drv_open(struct inode *node, struct file *file)
{
        printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
        return 0;
}

static int chrtest_drv_close(struct inode *node,struct file *file)
{
        printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
        return 0;
}


/*定义自己的file_operations结构体*/
static struct file_operations chrtest_fops = {
        .owner   = THIS_MODULE,
        .open    = chrtest_drv_open,
        .read    = chrtest_drv_read,
        .write   = chrtest_drv_write, 
        .release = chrtest_drv_close,
};

/* 把file_operations结构体告诉内核:register_chrdev */
/* 注册驱动函数,写入口函数,安装驱动程序时会调用这个入口函数 */
static int __init chrdev_init(void)
{
        int result;
        dev_t devno;/* 定义一个dev_t的变量来表示设备号 */

        printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);

        /* 字符设备驱动注册流程第二步:分配主次设备号,这里既支持静态分配,也支持动态分配 */
        if( 0 != dev_major ) //static
        {
                devno = MKDEV(dev_major,0);
                result = register_chrdev_region(devno, 1, DEV_NAME); /*  /proc/devices/chrdev */
        }
        else
        {
                result = alloc_chrdev_region(&devno, 0 , 1,DEV_NAME);
                dev_major = MAJOR(devno); //获得主设备号
        }

        /* 自动分配设备号失败 */
        if( result < 0 )
        {
                printk(KERN_ERR " %s driver can't use major %d\n",DEV_NAME, dev_major );
                return -ENODEV;
        }
        printk(KERN_DEBUG " %s driver use major %d\n",DEV_NAME,dev_major );

        /* 字符设备驱动注册流程第三步:分配cdev结构体,我们这里使用动态申请的方式 */
        if(NULL ==(chrtest_cdev = cdev_alloc()))
        {
                printk(KERN_ERR " %s driver can't alloc for the cdev\n",DEV_NAME);
                unregister_chrdev_region(devno,1);
                return -ENOMEM;
        }

        /* 字符设备驱动注册流程第四步:分配cdev结构体,绑定主次设备号、fops到cdev结构体中,并注册给Linux内核 */
        chrtest_cdev->owner = THIS_MODULE;  /* .owner表示拥有你这个驱动程序 */
        cdev_init(chrtest_cdev, &chrtest_fops); /* 初始化设备 */
        result = cdev_add(chrtest_cdev, devno , 1); /* 将字符设备注册进内核 */

        if( 0 != result )
        {
                printk(KERN_INFO " %s driver can't register cdev:result=%d\n",DEV_NAME,result);
                goto ERROR;
        }
        printk(KERN_INFO " %s driver can register cdev:result=%d\n",DEV_NAME, result);

        /* 自动创建设备类型、/dev设备节点 */
#if 0
        chrdev_class = class_create(THIS_MODULE, DEV_NAME); /* 创建设备类型 sys/class/chrdev */
        if (IS_ERR(chrdev_class))
        {
                result = PTR_ERR(chrdev_class);
                goto ERROR;
        }
        device_create(chrdev_class, NULL, MKDEV(dev_major, 0), NULL, DEV_NEME); /*  /dev/chrdev 注册这个设备节点 */
#endif

        return 0;

ERROR:
        printk(KERN_ERR " %s driver installed failure .\n",DEV_NAME );
        cdev_del(chrtest_cdev);
        unregister_chrdev_region(devno, 1);
        return result;

}

/*  有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数 */
static void __exit chrdev_exit(void)
{
        printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);

        /* 注销设备类型,/dev设备节点 */
#if 0
        device_destory(chrdev_class, MKDEV(dev_major, 0)); /* 注销这个设节点 */
        class_destory(chrdev_class);    /* 删除这个设备类型 */
#endif

        cdev_del(chrtest_cdev); /* 注销字符设备 */
        unregister_chrdev_region(MKDEV(dev_major,0), 1); /* 释放设备号 */

        printk(KERN_ERR " %s driver version 1.0 removed !\n",DEV_NAME);
        return;
}

/*  其他完善:提供设备信息,自动创建设备节点 */
module_init(chrdev_init);
module_exit(chrdev_exit);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("LinXinCheng <1481155734@qq.com>");

编译的过程出现如下错误:

Linux设备驱动开发(字符设备2)_字符设备_02

把static ssize_t chrtest_drv_write(struct file *file, char __user *buf, size_t size, loff_t *offset)改成static ssize_t chrtest_drv_write(struct file *file,const char __user *buf, size_t size, loff_t *offset)

原因是指针的不安全性,使用const在一定程度上可以提高程序的安全性和可靠性,具体参考const的用法

其中,由于内核空间与用户空间的内存不能直接互访,因此借助函数copy_to_user()完成内核空间到用户空间的复制,函数copy_from_user()完成用户空间到内核空间的复制。

unsigned long copy_to_user(void *to, const void __user *from, 
usigned long n);
参数:
	to 目标地址,这个地址是用户空间的地址;
	from 源地址,这个地址是内核空间的地址;
	n 将要拷贝的数据的字节数。
unsigned long copy_from_user(void __user *to, const void *from, 
usigned long n);
参数:
    to 目标地址,这个地址是内核空间的地址;
    from 源地址,这个地址是用户空间的地址;
    n 将要拷贝的数据的字节数。

3.8 编写字符测试 APP

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc,char **argv)
{
        int  fd;
        char buf[1024];
        int  len;

        /* 1.判断参数 */
        if( argc <2 )
        {
                printf("Usage :%s -w <string>\n",argv[0]);
                printf("           %s -r\n",argv[0]);
                return -1;
        }

        /* 2.打开文件 */
        fd = open("/dev/chrdev",O_RDWR);
        if( fd == -1 )
        {
                printf("can't open file /dev/hello\n");
                return -1;
        }

        /* 3.写文件或者读文件 */
        if(( 0 == strcmp(argv[1], "-w")) && (argc == 3))
        {
                len = strlen(argv[2]) + 1;
                len = len < 1024 ? len : 1024;
                write(fd, argv[2], len);
        }
		....  省略  ....
		
        close(fd);
        return 0;
}

3.9在ARM开发板上测试

Linux设备驱动开发(字符设备2)_字符设备_03

测试成功。