字符设备驱动——申请设备号、注册字符设备

版权声明:本文为博主原创文章,转载请注明出处,并附上原文链接。 https://blog.csdn.net/Windeal/article/details/9962337

1. 设备号

    主设备号:用来标识与设备文件相关的驱动程序,        ——反应设备类型
    次设备号:为内核所用,被驱动程序用来辨别操作那个设备文件    ——区分同类型的具体某个设备
 
1.1 设备号的内部表达
    在内核中,保存设备号(包括主设备号和此设备好)使用类型
    dev_t   (<linux/types.h>) 
    这是一个unsigned int  是一个32位的无符号整型。。
    主设备号——高12位
    此设备号——低12位
    我们可以使用宏来取一个设备号(dev)的主设备号和此设备号    
     定义在 <linux/kdev_t.h>
      MAJOR(dev_t dev)        取主设备号
      MINOR(dev_t dev)         取次设备号
    也可以将主次设备号合成一个完整的dev_t类型的设备号
    MKDEV(int major, int minor)       将主次设备号转换成dev_t
 
 1.2 分配主次设备号
    linux可以采用静态申请和动态申请两种方法来分配主次设备号
    
    静态申请
      1. 根据Documentation/devices.txt, 确定一个没有使用的主设备号
      2. 使用register_chrdev_region(dev_t first, unsigned int count, char *name)
       定义在<linux/fs.h>    
        count 为所请求的连续设备编号个数, 如果count过大,可以会各下一个主设备号重叠。
          name 为设备名, 注册后 出现在/proc/devices和sysfs中
 
    动态分配
    作为一个新的驱动程序,应该使用动态分配机制获取主设备号
    alloc_chrdev_region(dev_t *dev, unsigned int first, unsigned int count, char *name)
 
    不管用何种方法分配, 不用时都要释放掉
     void unregister_chrdev_region(dev_t first, unsigned int count);
 
    静态申请与动态申请的优缺点:
    静态申请——简单(优);  一旦驱动程序被广泛命使用, 随机选定的主设备号可以造成冲突,使驱动程序无法注册。(劣)
    动态申请——简单,易于驱动推广(优);无法在驱动安装前创建设备文件, 因为不能保证分配的主设备号始终一致。(劣)

 

2 创建设备文件

    设备文件的创建有    
     1. 使用mknod命令手工创建
     2. 自动创建
    两种方法。
    

    2.1 mknod手工创建

     mknod  用法:
    mknod  filename type  major  minor
 
        filename :  设备文件名
        type        :  设备文件类型
        major      :   主设备号
        minor      :   次设备号
    

2.2 自动创建

    如果我们在驱动里面动态创建的话需要这样做
    struct class *cdev_class; 
    cdev_class = class_create(owner,name)
    device_create(_cls,_parent,_devt,_device,_fmt)


 

2.3 模块退出时要销毁设备文件

    
    device_destroy(_cls,_device)
    class_destroy(struct class * cls)
 

 

3. 一些重要的结构体

    
        大部分的基础性的驱动操作包括 3 个重要的内核数据结构, 称为 file_operations, file, 和 inode.
  •   文件结构 struct file
             定义于<linux/fs.h>,   是一个内核结构, 不会出现在用户空间
   代表一个打开的文件。系统中每个找开的文件在内核空间一个关联的
   struct file, 它由内核在打开文件时创建, 在文件关闭后释放
重要成员
loff_t  f_ops     /* 文件读写位置 */
struct file_operations  *f_op    /*  文件关联的操作 */
mode_t  f_mode        /* 模式确定文件可读或者可写 */
unsigned int f_flags /* 文件标志,一般用来判断是否请求非阻塞操  作, 标志定义<linux/fcntl.h> */  
void *private_data; 

     open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.
  • 文件操作 struct file_operation
file_operation 结构是一个字符驱动如何建立这个连接. 这个结构, 定
义在 <linux/fs.h>, 是一个函数指针的集合. 每个打开文件(内部用一个 file 结构来代
表, 稍后我们会查看)与它自身的函数集合相关连( 通过包含一个称为 f_op 的成员, 它指
向一个 file_operations 结构). 这些操作大部分负责实现系统调用, 因此, 命名为 open,
read, 等等. 我们可以认为文件是一个"对象"并且其上的函数操作称为它的"方法", 使用
面向对象编程的术语来表示一个对象声明的用来操作对象的动作.
 
下面是一个file_operationd的声明:
        struct file_operations my_fops = {
        .owner =  THIS_MODULE, 
        .llseek =  my_llseek, 
        .read =  my_read, 
        .write =  my_write, 
        .ioctl =  my_ioctl, 
         .open =  my_open, 
         .release =  my_release,  
     };  


该声明使用标准的 C 标记式结构初始化语法. 这个语法是首选的, 因为它使驱动在结构定义的改变之间更加可移植  。
下面列出file_operationd部分成员的含义:(其他成员自行百度) 
struct module *owner 
     第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.
loff_t (*llseek) (struct file *, loff_t, int); 
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32 位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).  
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 
     用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -
    EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 
    发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
           int (*open) (struct inode *, struct file *); 
      尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知
int (*release) (struct inode *, struct file *); 
     在文件结构被释放时引用这个操作. 相当于close
  • struct inode
        由内核在内部用来表示文件。因些,它和代表打开文件的file结构是不同的。一个文件可以对应多个file结构, 但只有一个inode结构
        
重要成员
dev_t   i_rdev:  / * 对于代表设备文件的节点, 这个成员包含实际的设备编号 */    
struct cdev *i_cdev;  /* struct cdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这个结构, 当节点指的是一个字符设备文件时.*/
 
 

4. 字符设备的注册

        Linux2.6内核中,字符设备使用struct  cdev来描述字符设备驱动的注册。
        字符设备驱动的注册主要有三个步骤
        (1) 分配cdev
        (2)初始化cdev
        (3)添加cdev
        
分配
    struct cdev *my_cdev = cdev_alloc();
初始化
    void cdev_init(struct  cdev *cdev,  const struct file_operations *fops);
    cdev:  待初始化的cdev结构
    fops:  设备对应的操作函数集
注册, 告诉内核
    int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
    dev: 添加到内核的字符设备结构
    num: 设备响应的第一个设备号
    count: 关联到设备的设备号数目,通常为1
去除字符设备
    void cdev_del(struct cdev *dev);
 
 
使用 cdev_add 是有几个重要事情要记住
    1.第一个是这个调用可能失败. 如果它返回一个负的错误码, 你的设备没有增加到系统中.
 
    2. cdev_add 一返回成功, 你的设备就是"活的"并且内核可以调用它的操作.
        所以除非你的驱动完全准备好处理设备上的操作, 你不应当调用 cdev_add.
 
 
 

5 注册字符设备的一个例子

 
1. 还是线上源代码:
//memdev.h
#ifndef _MEMDEV_H_
#define _MEMDEV_H_
 
#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 200
#endif
 
#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2
#endif
 
#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif
 
struct mem_dev{
    char* data;
    unsigned long size;
 
};
 
#endif

//memdev.c  
# include < linux / module.h >
# include < linux / types.h >
# include < linux / fs.h >
# include < linux / errno.h >
# include < linux / mm.h >
# include < linux / sched.h >
# include < linux / init.h >
# include < linux / cdev.h >
# include < asm / io.h >
# include < asm / system.h >
# include < asm / uaccess.h >
# include < linux / wait.h >
# include < linux / completion.h >
 
# include "memdev.h"
 
MODULE_LICENSE( "Dual BSD/GPL" );
 
static int   mem_major = MEMDEV_MAJOR;
 
struct mem_dev * mem_devp; /*设备结构体指针*/
 
struct cdev cdev;
 
/*文件打开函数*/
int mem_open( struct inode * inode, struct file * filp)
{
printk( "open own file\n" );
     return 0 ;
}
 
/*文件操作结构体*/
static const struct file_operations mem_fops =
{
  .owner = THIS_MODULE,
  .open = mem_open,
};
 
/*设备驱动模块加载函数*/
static int memdev_init( void )
{
   int result;
   int i;
 
  dev_t devno = MKDEV(mem_major, 0 );
 
   /* 静态申请设备号*/
    result = register_chrdev_region(devno, 2 , "memdev" );
   if (result < 0 )
     return result;
 
   /*初始化cdev结构*/
  cdev_init( & cdev, & mem_fops);
 
   /* 注册字符设备 */
  cdev_add( & cdev, MKDEV(mem_major, 0 ), MEMDEV_NR_DEVS);
 
   return result;
}
 
/*模块卸载函数*/
static void memdev_exit( void )
{
  cdev_del( & cdev);    /*注销设备*/
  unregister_chrdev_region(MKDEV(mem_major, 0 ), 2 ); /*释放设备号*/
}
 
module_init(memdev_init);
module_exit(memdev_exit);
 
#Makefile
ifneq ($(KERNELRELEASE),)
    obj-m := memdev.o
else
    KERNELDIR ?= /lib/modules/$(shell uname -r)/build
    PWD = $(shell pwd)
default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
    rm memdev.mod*  module* memdev.o memdev.ko Module.*
endif
 
 
2. 测试
    首先先make下,生成memdev.ko
    然后insmod memdev.ko生成memdev模块
    创建设备节点:sudo mknod /dev/memdev_t c 200 0
    接下开使用设备文件
    下面是一个测试程序

  // memusr.c
#include <stdio.h>
#include <string.h>
 
int main()
{
    FILE *fp0;
    /*打开设备文件*/
    fp0 = fopen("/dev/memdev_t","r+");
    if (fp0 == NULL) {
        printf("Open Memdev0 Error!\n");
        return -1;
    }
}


 
编译运行,然后使用dmesg可以看到日志文件里输出
[38439.741816] Hello World!
[38657.654345] Goodbye
[40393.039520] open own file


 
记得要使用sudo 运行memusr   否则会显示设备打开失败。

展开阅读全文

没有更多推荐了,返回首页