Linux字符设备驱动

前言

代码结构简单,旨在用最简单的原理理解最主要的框架逻辑,细节需要自行延伸。 -----------------学习的基础底层逻辑

基础步骤

开发linux内核驱动需要以下4个步骤:

  • 编写驱动代码
  • 编写makefile
  • 编译和加载驱动
  • 编写应用程序测试驱动

由于硬件设备各式各样,有了设备驱动程序,应用程序就可以不用在意设备的具体细节,而方便地与外部设备进行通信。从外部设备读取数据,或是将数据写入外部设备,即对设备进行控制。

设备驱动程序框架

  1. module_init 是 linux kernel 绝大多数模块的起始点。
  2. 我们所熟悉的应用程序都是从一个 main() 函数开始运行的,而与应用程序不同,内核模块的起始就是 module_init() 标记的函数 。
  3. module_init 是一个宏,它的参数就是模块自行定义的“起始函数”。这个函数使用 module_init 标记后,就会在内核初始化阶段,“自动”运行。
  4. 无论模块是编译进内核镜像,还是以ko的形式加载,都是从这里开始运行。
  5. 有开始就有结束,与 module_init 对应的就是 module_exit 。module_exit 负责进行一些和init反向的活动。

设备的种类繁多,所以设备的驱动程序也是各式各样的,SVR4是UNIX操作系统的一种内核标准。
规范有以下部分:

  • 驱动程序与内核的接口:是通过数据结构file_opration完成的
  • 驱动程序与设备的接口:描述了驱动程序如何与设备交互,这与具体的设备是密切相关的。
  • 驱动程序与系统引导的接口:驱动程序对设备进行初始化

接下来使用mknod命令创建设备文件结点,然后用chmod命令修改权限为777。此时设备就可以使用了。

/proc/devices与/dev的不同之处

  • 在/proc/devices下,显示的是驱动程序生成的设备及其主设备号。其中主设备号可用来让mknod作为参数。
  • 在/dev下的设备是mknod生成的设备,其中,用户通过使用/dev下的设备名来使用设备。

上层应用如何调用底层驱动

  1. 应用层的程序open(“/dev/xxx”,mode,flags)打开设备文件,进入内核中,即虚拟文件系统中。
  2. VFS层的设备文件有对应的struct inode,其中包含该设备对应的设备号,设备类型,返回的设备的结构体。
  3. 在驱动层中,根据设备类型和设备号就可以找到对应的设备驱动的结构体,用i_cdev保存。该结构体中有很重要的一个操作函数接口file_operations。
  4. 在打开设备文件时,会分配一个struct file,将操作函数接口的地址保存在该结构体中。
  5. VFS层 向应用层返回一个fd,fd是和struct file相对应,这样,应用层可以通过fd调用操作函数,即通过驱动层调用硬件设备了。

代码

起始函数

module_init(hello_init);

下面的函数的主要工作是:

  • 注册驱动
  • 申请资源
  • 节点创建
int hello_init(void)//三件事情
{
    
    devNum = MKDEV(reg_major, reg_minor);
    if(OK == register_chrdev_region(devNum, subDevNum, "helloworld"))//驱动注册
    {
        printk(KERN_EMERG"register_chrdev_region ok \n"); 
    }else {
    printk(KERN_EMERG"register_chrdev_region error n");
        return ERROR;
    }
    printk(KERN_EMERG" hello driver init \n");
    gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);//资源申请
    gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);//资源申请
    gFile->open = hello_open;
    gFile->read = hello_read;
    gFile->write = hello_write;
    gFile->owner = THIS_MODULE;
    cdev_init(gDev, gFile);
    cdev_add(gDev, devNum, 3);//把它添加到系统中去
    return 0;
}

驱动注册
将驱动信息注册完毕后,如果有匹配的设备接入,本驱动的 probe 就会被调用。
原生代码里有很多进一步对 module_init 封装的宏,其实做的也就是注册驱动这件事。

申请资源&创建节点

  • 并非所有的模块都是驱动模块,又有一些纯软件功能的模块,例如class、bus管理等。

  • 这类模块在其实函数中,则不需要注册驱动,而是按照本模块的需求,申请资源,创建节点等。

  • 需要注意的是,所有在 module_init 里做的动作,都需要在 module_exit 中做反向操作,避免资源泄漏。

  • MKDEV 功能:将主设备号和次设备号转换成dev_t类型

  • register_chrdev_region(devNum, subDevNum, “helloworld”):驱动注册,第一个参数表示设备号,第二个参数表示注册的此设备数目,第三个表示设备名称。

  • kzalloc:资源申请

  • 钩子函数挂钩:gFile->open = hello_open

  • cdev_init(gDev, gFile);//初始化,建立cdev和file_operation 之间的连接

  • cdev_add(gDev, devNum, 3);//把它添加到系统中去,注册设备,通常发生在驱动模块的加载函数中

钩子函数的几个实现:没有实现任何功能,只是为了让框架更明显

 int hello_open(struct inode *p, struct file *f)
{
    printk(KERN_EMERG"hello_open\r\n");
    return 0;
}
ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
    printk(KERN_EMERG"hello_write\r\n");
    return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
    printk(KERN_EMERG"hello_read\r\n");      
    return 0;
}

几个重要的数据结构:
内核中每个字符设备都对应一个 cdev 结构的变量:
struct cdev

struct cdev {
struct kobject kobj;          // 每个cdev 都是一个 kobject
struct module *owner;       // 指向实现驱动的模块
const struct file_operations *ops;   // 操纵这个字符设备文件的方法
struct list_head list;       // 与cdev 对应的字符设备文件的 inode->i_devices 的链表头
dev_t dev;                  // 起始设备编号
unsigned int count;       // 设备范围号大小
};

struct file_operations

struct file_operations { 
    struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES 
    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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作 
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作 
    int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL 
    unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入 
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令 
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl 
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替 
    int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间
    int (*open) (struct inode *, struct file *); //打开 
    int (*flush) (struct file *, fl_owner_t id); 
    int (*release) (struct inode *, struct file *); //关闭 
    int (*fsync) (struct file *, struct dentry *, int datasync); //刷新待处理的数据 
    int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据 
    int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化 
    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 **); 
};

完整代码

驱动代码:

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/cdev.h>
#include <linux/fs.h>//file_operations结构体
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <linux/slab.h>

#define BUFFER_MAX    (10)
#define OK            (0)
#define ERROR         (-1)

struct cdev *gDev;//代表字符设备的数据结构
struct file_operations *gFile;//struct file_operations是一个字符设备把驱动的操作和设备号联系在一起的纽带.该驱动程序的核心。它给出了对文件操作函数的定义。当然,具体的实现函数是留给驱动程序编写的
dev_t  devNum;//设备文件的设备号
unsigned int subDevNum = 1;
int reg_major  =  232;    
int reg_minor =   0;
char *buffer;
int flag = 0;
int hello_open(struct inode *p, struct file *f)
{
    printk(KERN_EMERG"hello_open\r\n");
    return 0;
}

ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
    printk(KERN_EMERG"hello_write\r\n");
    return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
    printk(KERN_EMERG"hello_read\r\n");      
    return 0;
}
int hello_init(void)//三件事情注册驱动,申请资源,节点创建
{
    
    devNum = MKDEV(reg_major, reg_minor);//将主设备号和次设备号转换成dev_t类型
    if(OK == register_chrdev_region(devNum, subDevNum, "helloworld")){
        printk(KERN_EMERG"register_chrdev_region ok \n"); 
    }else {
    printk(KERN_EMERG"register_chrdev_region error n");
        return ERROR;
    }
    printk(KERN_EMERG" hello driver init \n");
    gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
    gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
    gFile->open = hello_open;
    gFile->read = hello_read;
    gFile->write = hello_write;
    gFile->owner = THIS_MODULE;
    cdev_init(gDev, gFile);//初始化,建立cdev和file_operation 之间的连接
    cdev_add(gDev, devNum, 3);//把它添加到系统中去,注册设备,通常发生在驱动模块的加载函数中
    return 0;
}

void __exit hello_exit(void)
{
    cdev_del(gDev);
    unregister_chrdev_region(devNum, subDevNum);
    kfree(gDev);
    kfree(gFile);
    return;
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

编译驱动的Makefile:

ifneq ($(KERNELRELEASE),)
obj-m := helloDev.o
else
PWD := $(shell pwd)
#KDIR:= /lib/modules/4.4.0-31-generic/build
KDIR := /lib/modules/`uname -r`/build
all:
	make -C $(KDIR) M=$(PWD)
clean:	
	rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~
endif

使用make命令编译结果:
在这里插入图片描述
ls -l 如下:
在这里插入图片描述

加载进内核:
在这里插入图片描述
想要控制驱动就需要添加设备节点:mknod /dev/hello c 232 0
在这里插入图片描述

应用层代码:

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>

#define DATA_NUM    (64)
int main(int argc, char *argv[])
{
    int fd, i;
    int r_len, w_len;
    fd_set fdset;
    char buf[DATA_NUM]="hello world";
    memset(buf,0,DATA_NUM);
    fd = open("/dev/hello", O_RDWR);
	printf("%d\r\n",fd);
    if(-1 == fd) {
      	perror("open file error\r\n");
		return -1;
    }	
	else {
		printf("open successe\r\n");
	}
    w_len = write(fd,buf, DATA_NUM);
    r_len = read(fd, buf, DATA_NUM);
    printf("%d %d\r\n", w_len, r_len);
    printf("%s\r\n",buf);
    return 0;
}

编译:

gcc test.c -o test.o

结果:
在这里插入图片描述
执行test测试驱动:
在这里插入图片描述
再次执行dmesg查看驱动输出,发现驱动里的hell_open, hello_write, hello_read被依次调用了:
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值