【Linux设备驱动】最简单的 字符设备驱动编写

虚拟字符设备编写及测试

了解如何编写最基础的驱动后,这一篇对最简单的字符设备驱动,做一个概述;并对代码中涉及的知识点进行一个罗列,方便大家的学习

char.c文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/cdev.h>

#include "include/common.h"

static struct cdev *chr_dev;//定义一个字符设备对象
static dev_t ndev;//字符设备节点设备号


static int chr_open(struct inode *nd, struct file *filp)
{
    int major = 0;
    int minor = 0;

    major = MAJOR(nd->i_rdev);
    minor = MINOR(nd->i_rdev);

    printk("chr_open:major = %d, minor = %d\n", major, minor);
    return 0;
}

static int chr_read(struct file *pFile, char __user *u, size_t size, loff_t *off)
{
    printk("chr_read success!\n");

    return 0;
}

struct file_operations chr_ops = {
    .owner = THIS_MODULE,
    .open = chr_open,
    .read = chr_read
};

static __init int char_init(void)
{
    int ret = 0;

    ret = alloc_chrdev_region(&ndev, 0, 1, "chr_dev");//动态分配设备号
    if(ret < 0){
        printk("alloc_chrdev_region failed!\n");
        return ret;
    }

    printk("init():major = %d, minor = %d\n", MAJOR(ndev), MINOR(ndev));//打印主设备号和次设备号

    chr_dev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if(NULL == chr_dev){
		printk("kzalloc cdev failed!\n");
		goto err_alloc_cdev;
	}

    cdev_init(chr_dev, &chr_ops);//初始化字符设备对象,[加入系统前必须初始化]
    
    ret = cdev_add(chr_dev, ndev, 1);//将字符设备对象插入chr_dev注册进系统
    if(ret < 0){
        printk("cdev_add error!\n");
        goto err_cdev_add;//注册失败记得要销毁刚才分配的设备号
    }

    printk("create char driver success!\n");
    return 0;

err_cdev_add:
	kfree(chr_dev);

err_alloc_cdev:
	unregister_chrdev_region(ndev, 1);
    
    printk("create char driver failed!\n");
	return -1;

}

static __exit void char_exit(void)
{
    unregister_chrdev_region(ndev, 1);//释放分配的设备号
    cdev_del(chr_dev);//将字符设备对象chr_dev从系统中注销
    kfree(chr_dev);
	printk("exit char driver success!\n");
}


module_init(char_init);
module_exit(char_exit);

MODULE_AUTHOR("zywang");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("char driver demo");

Makefile:
#交叉编译器配置
CROSS_COMPILE ?= XXX-linux-gnu-

ENV_KERNEL_DIR ?= $(PWD)/../../kernel
KDIR := ${ENV_KERNEL_DIR}

MODULE_NAME := char_demo

all: modules

.PHONY: modules clean
    
EXTRA_CFLAGS += -I$(PWD)/include

obj-m := $(MODULE_NAME).o
$(MODULE_NAME)-objs := char.o

modules:
    @$(MAKE) -C $(KDIR) M=$(shell pwd) $@

clean:
    @find . -name '*.o' -o -name '*~' -o -name '.depend' -o -name '.*.cmd' \
    -o -name '*.mod.c' -o -name '.tmp_versions' -o -name '*.ko' \
    -o -name '*.symvers' -o -name 'modules.order' | xargs rm -rf

应用程序
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

#define CHR_DEV_NAME "/dev/chr_dev"

int main()
{

    int ret = 0;
    char buf[32];
    int fd = 0;

    fd = open(CHR_DEV_NAME, O_RDONLY);
    if(fd < 0)
    {   
        printf("open file '%s' failed!\n", CHR_DEV_NAME);
        return -1; 
    }   

    read(fd, buf, 32);
    close(fd);

    return 0;
}
编译app上层应用程序
   XX-linux-gcc app_char.c -o app_char
测试程序
  • 加载驱动

    insmod char_demo.ko
    

    ==>界面会打印majorminor值,例如他们的值为:250, 0

  • 创建设备节点

    mknod /dev/chr_dev c 250 0
    
  • 执行应用程序

    ./app_char
    
struct file_operateions
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);//修改文件当前的读写位置
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);//轮询函数,用于查询设备是否可以进行非阻塞的读写
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//提供对于设备的控制功能,与应用程序中的ioctl 函数对应
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);//与unlocked_ioctl 函数功能一样,区别在于在64 位系统上,32 位的应用程序调用将会使用此函数。在32 位的系统上运行32 位的应用程序调用的是unlocked_ioctl
    int (*mmap) (struct file *, struct vm_area_struct *);//mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id); 
    int (*release) (struct inode *, struct file *);//释放(关闭)设备文件,与应用程序中的close 函数对应
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);//与fasync 函数的功能类似,只是aio_fsync 是异步刷新待处理的数据
    int (*fasync) (int, struct file *, int);//刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
};

上述程序,你也许能发现,现实中的字符设备驱动程序的编写,其实基本上是围绕如何实现 struct file_operation 中的那些函数指针成员而展开的.

通过内核文件系统在其间穿针引线,应用程序对文件类函数–函数指针的调用,如read(),将最终被转接到 struct file_operations 中对应函数指针的具体实现上.

该结构体中唯一非函数指针成员 owner , 标识当前 struct file_operateions 对象所属的内核模块,几乎所有的设备驱动程序都会用 THIS_MODULE 宏给 owner赋值

#include <linux/module.h>
#define THIS_MODULE (&__this_module)

__this_module 是内核模块的编译工具链为当前模块产生的 struct module的类型对象,所以 THIS_MODULE 实际上是当前模块对象的指针, file_operateion 中的owner 成员可以避免 file_operateions中的函数正在被调用时,其所属的模块被系统中卸载掉.若被编译进内核, THIS_MODULE 被赋值NULL,无任何作用.

字符设备的内核抽象
struct cdev {
    struct kobject kobj;//内核对象
    struct module *owner;//字符设备驱动程序所在的内核模块对象指针
    const struct file_operations *ops;
    struct list_head list;//系统中字符设备形成的链表
    dev_t dev;//设备号,包含主设备号和次设备号
    unsigned int count;//隶属于同一主设备号的次设备号的个数,用于标识由当前设备驱动程序控制的同类设备的数量
};
设备号构成

linux系统中一个设备号由主设备号和次设备号构成,Linux 内核用主设备号来顶为对应的设备驱动程序 , 而次设备号则由驱动程序使用,用来标识它所管理的若干同类设备.因此,从这个角度而言,设备号作为设备资源需要严格管理,防止因设备号的混乱所导致系统里设备的混乱.

linux 用 dev_t 类型变量来标识一个设备号,它是 unsigned int类型(无符号整型):

<include/linux/types.h>
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t  dev_t;

在内核2.6.39中,dev_t 的低20位表示次设备号,高12位代表主设备号.

<include/linux/kdev_t.h>
#define MAJOR(dev)	((unsigned int)((dev)>>MINORBITS))//提取主设备号
#define MINOR(dev)	((unsigned int)((dev) & MINIRMASK))//提取次设备号
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))//合并主次设备号
设备号分配管理
<linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, char *name);
//first 要分配的起始设备号,他的次设备号一般是从0开始
//count 申请次设备号的数量
//name	连接到这个编号范围的设备的名字

register_chrdev_region 函数第三个参数**name它会出现在 /proc/devicessysfs中**.

如果你确实事先知道你需要哪个设备编号, register_chrdev_region 工作得好. 然而, 你常常不会知道你的设备使用哪个主编号; 在 Linux 内核开发社团中一直努力使用动态分配设备编号. 内核会乐于动态为你分配一个主编号, 但是你必须使用一个不同的函数来请求这个分配.

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

不管你任何分配你的设备编号, 你应当在不再使用它们时释放它. 设备编号的释放使用:

void unregister_chrdev_region(dev_t first, unsigned int count);

调用 unregister_chrdev_region 的地方常常是你的模块的 cleanup 函数.

主设备号的动态分配

略,详见<linux设备驱动程序>P35

字符设备的注册

把一个字符设备加入到系统中所需调用的函数为cdev_add.

设备文件节点的生成

Linux系统所有的设备文件都位于 /dev 目录下.当Linux内核在挂载完根文件系统后,会在这个根文件系统/dev目录上重新挂载一个芯的文件系统 devtmpfs.

/dev目录下用 mknod命令来创建一个新的设备文件节点 demodev , 对应的驱动程序使用的设备主设备号为2, 次设备号为0,命令如下:

mknod /dev/demodev c 2 0

这篇文章只是简单的对字符设备进行叙述,设备节点都是手动挂载,那如何自动挂载呢,请看这篇【Linux设备驱动】使用class_create()和device_create()自动创建设备节点


以上实例代码已打包,有需要的读者可以自行下载,下载资源无需积分:Linux设备驱动程序+字符驱动sample程序

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Van.Ghylivan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值