Linux驱动开发基础(Hello驱动)

所学内容来自百问网

目录

1. 文件在内核中的表示

2. 打开字符设备节点时,内核中也有对应的struct file

3. 编写驱动程序步骤

4. 相关知识点

4.1 module_init/module_exit的实现

4.2 register_chrdev的内部实现

4.3 class_destroy/device_create 浅析

5. 示例代码

5.1 驱动代码

5.2 应用代码

5.3 Makefile

5.4 效果


1. 文件在内核中的表示

APP 打开文件时,可以得到一个整数,这个整数被称为文件句柄。对于APP 的每一个文件句柄,在内核里面都有一个“struct file”与之对应。

使用open打开文件时,传入的flags、mode等参数会被记录在内核中对应的struct file结构体里(f_flags、f_mode):

int open(const char *pathname, int flags, mode_t mode); 

去读写文件时,文件的当前偏移地址也会保存在 struct file 结构体的 f_pos 成员里。

2. 打开字符设备节点时,内核中也有对应的struct file

注意这个结构体中的结构体:struct file_operations *f_op,这是由驱 动程序提供的。

结构体struct file_operations的定义如下:

3. 编写驱动程序步骤

1.确定主设备号,也可以让内核分配

2.定义自己的file_operations结构体

3.实现对应的drv_open/drv_read/drv_write 等函数,填入file_operations结构体

4.把file_operations 结构体告诉内核:register_chrdev

5.得有一个入口函数:安装驱动程序时,就会去调用这个入口函数

6.有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev

7.其他完善:提供设备信息,自动创建设备节点:class_create, device_create

4. 相关知识点

4.1 module_init/module_exit的实现

一个驱动程序有入口函数、出口函数,代码如下:

module_init(hello_init); 
module_exit(hello_exit); 

驱动程序可以被编进内核里,也可以被编译为ko文件后手工加载。对于这 两种形式,“module_init/module_exit”这2个宏是不一样的。在内核文件 “include\linux\module.h”中可以看到这2个宏:

module_init(initfn) 宏用于声明模块初始化函数。当模块被加载到内核时,initfn函数会被自动调用。这个函数通常用于执行模块所需的任何初始化任务,比如注册设备、分配内存、初始化数据结构等。

module_exit(exitfn)宏用于声明模块退出函数。当模块从内核中卸载时,exitfn函数会被自动调用。这个函数通常用于执行模块卸载前的任何清理任务,比如注销设备、释放内存、清理数据结构等。

4.2 register_chrdev的内部实现

register_chrdev函数源码如下:

static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) 
{ 
    return __register_chrdev(major, 0, 256, name, fops); 
} 

它调用__register_chrdev函数,这个函数的代码精简如下:

01 int __register_chrdev(unsigned int major, unsigned int baseminor, 
02                       unsigned int count, const char *name, 
03                       const struct file_operations *fops) 
04 { 
05     struct char_device_struct *cd; 
06     struct cdev *cdev; 
07     int err = -ENOMEM; 
08  
09     cd = __register_chrdev_region(major, baseminor, count, name); 
10  
11     cdev = cdev_alloc(); 
12  
13     cdev->owner = fops->owner; 
14     cdev->ops = fops; 
15     kobject_set_name(&cdev->kobj, "%s", name); 
16  
17     err = cdev_add(cdev, MKDEV(cd->major, baseminor), count); 
18 } 

这个函数主要的代码是第09行、第11~15行、第17行。

第09行,调用__register_chrdev_region函数来“注册字符设备的区域”, 它仅仅是查看设备号(major, baseminor)到(major, baseminor+count-1) 有没有被占用,如果未被占用的话,就使用这块区域。

内核中存在着一个chrdevs数组:

static struct char_device_struct { 
    struct char_device_struct *next;  
    unsigned int major; 
    unsigned int baseminor; 
    int minorct; 
    char name[64]; 
    struct cdev *cdev;      /* will die */ 
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; 

去访问它的时候,并不是直接使用主设备号major来确定数组项,而是使用如下函数来确定数组项:

/* index in the above */ 
static inline int major_to_index(unsigned major) 
{ 
    return major % CHRDEV_MAJOR_HASH_SIZE; 
} 

上述代码中,CHRDEV_MAJOR_HASH_SIZE等于255。比如主设备号1、256, 都会使用chardevs[1]。chardevs[1]是一个链表,链表里有多个 char_device_struct结构体,某个结构体表示主设备号为1的设备,某个结构 体表示主设备号为256的设备。

chardevs的结构图如图1.6所示:

由此可见:

1.chrdevs[i]数组项是一个链表头

链表里每一个元素都是一个char_device_struct结构体,每个元素表示 一个驱动程序。

char_device_struct结构体内容如下:

struct char_device_struct { 
    struct char_device_struct *next; 
    unsigned int major; 
    unsigned int baseminor; 
    int minorct; 
    char name[64]; 
    struct cdev *cdev;      /* will die */ 
} 

它指定了主设备号major、次设备号baseminor、个数minorct,在cdev 中含有file_operations结构体。

char_device_struct结构体的含义是:主次设备号为(major, baseminor)、(major, baseminor+1)、(major, baseminor+2)、(major, baseminor+ minorct-1)的这些设备,都使用同一个file_operations来操作。

2.在图1.6中,chardevs[1]中有3个驱动程序

第1个char_device_struct结构体对应主次设备号(1, 0)、(1, 1),这 是第1个驱动程序。

第2个char_device_struct结构体对应主次设备号(1, 2)、(1, 2)、……、 (1, 11),这是第2个驱动程序。

第3个char_device_struct结构体对应主次设备号(256, 0),这是第3 个驱动程序。

第11~15行分配一个cdev结构体,并设置它:它含有file_operations 结构体。

第17行调用cdev_add把cdev结构体注册进内核里,cdev_add函数代码 如下:

01 int cdev_add(struct cdev *p, dev_t dev, unsigned count) 
02 { 
03     int error; 
04  
05     p->dev = dev; 
06     p->count = count; 
07  
08     error = kobj_map(cdev_map, dev, count, NULL, 
09              exact_match, exact_lock, p); 
10     if (error)  
11         return error; 
12  
13     kobject_get(p->kobj.parent); 
14  
15     return 0; 
16 } 

这个函数涉及kobj的操作,这是一个通用的链表操作函数。它的作用是: 把cdev结构体放入cdev_map链表中,对应的索引值是“dev”到“dev+count 1”。以后可以从cdev_map链表中快速地使用索引值取出对应的cdev。

比如执行以下代码:

err = cdev_add(cdev, MKDEV(1, 2), 10);

其中的MKDEV(1,2)构造出一个整数“1<<8 | 2”,即0x102;上述代码将 cdev放入cdev_map链表中,对应的索引值是0x102到0x10c(即0x102+10)。 以后根据这10个数值(0x102、0x103、0x104、……、0x10c)中任意一个,都 可以快速地从cdev_map链表中取出cdev结构体。

APP打开某个字符设备节点时,进入内核。在内核里根据字符设备节点的主、 次设备号,计算出一个数值(major<<8 | minor,即inode->i_rdev),然后使 用这个数值从cdev_map中快速得到cdev,再从cdev中得到file_operations 结构体。

关键函数如下:

在打开文件的过程中,可以看到并未涉及chrdevs,都是使用cdev_map。 所以可以看到在chrdevs的定义中看到如下注释:

4.3 class_destroy/device_create 浅析

驱动程序的核心是 file_operations 结构体:分配、设置、注册它。 “class_destroy/device_create”函数知识起一些辅助作用:在/sys目录下 创建一些目录、文件,这样Linux系统中的APP(比如udev、mdev)就可以根据 这些目录或文件来创建设备节点。

以下代码将会在“/sys/class”目录下创建一个子目录“hello_class”

hello_class = class_create(THIS_MODULE, "hello_class"); 

以下代码将会在“/sys/class/hello_class”目录下创建一个文件 “hello”:

device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); 

5. 示例代码

5.1 驱动代码

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.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>
​
//1.确定主设备号,也可以让内核分配
static int major = 0;
static char kernel_buf[1024];
static struct class *hello_class;
#define MIN(a,b) (a < b ? a : b)
//3.实现对应的drv_open/drv_read/drv_write 等函数,填入file_operations结构体
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    int ret;
    printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
    ret = copy_to_user(buf, kernel_buf, MIN(1024,size));
    return MIN(1024,size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    int ret;
    printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
    ret = copy_from_user(kernel_buf, buf, MIN(1024,size));
    return MIN(1024,size);
}
static int hello_drv_open (struct inode *node, struct file *file)
{
    printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
    return 0;
}
static int hello_drv_close (struct inode *node, struct file *file)
{
    printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE__);
    return 0;
}
​
//2.定义自己的file_operations结构体
static struct file_operations hello_drv = {
    .owner = THIS_MODULE,
    .open = hello_drv_open,
    .read = hello_drv_read,
    .write = hello_drv_write,
    .release = hello_drv_close,
};
​
//4.把file_operations 结构体告诉内核:register_chrdev
//5.得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
static int __init hello_init(void)
{
    int err;
    major = register_chrdev(0,"hello",&hello_drv);
​
    hello_class = class_create(THIS_MODULE, "hello_class");
    err = PTR_ERR(hello_class);
    if(IS_ERR(hello_class))
    {
        unregister_chrdev(major,"hello");
        return -1;
    }
​
    device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
    return 0;
}
//6.有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev
static void __exit hello_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    device_destroy(hello_class, MKDEV(major, 0));
    class_destroy(hello_class);
    unregister_chrdev(major, "hello");
}
​
//7.其他完善:提供设备信息,自动创建设备节点:class_create,  device_create
module_init(hello_init);
module_exit(hello_exit);
// 声明许可证类型
MODULE_LICENSE("GPL");

5.2 应用代码

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
​
/*
    ./hello_drv_test -w abc
    ./hello_drv_test -r
*/
int main(int argc,char **argv)
{
    int fd;
    char buf[1024];
    int len;
​
    // 判断参数
    if(argc < 2)
    {
        printf("Usage: %s -w <string>\n",argv[0]);
        printf("       %s -r\n",argv[0]);
        return -1;
    }
​
    // 打开文件
    fd = open("/dev/hello",O_RDWR);
    if(fd == -1)
    {
        printf("can not open file /dev/hello\n");
        return -1;
    }
​
    // 写文件或读文件
    if((strcmp(argv[1],"-w") == 0) && (argc == 3))
    {
        len = strlen(argv[2]) + 1;
        len = len < 1024 ? len : 1024;
        write(fd,argv[2],len);
    }else
    {
        len = read(fd,buf,1024);
        buf[1023] = '\0';
        printf("APP read : %s\n",buf);
    }
​
    close(fd);
    return 0;
}

5.3 Makefile

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin 
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
#       请参考各开发板的高级用户使用手册
​
// 此处使用的是IMX6U_6ULL开发板
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
​
all:
    make -C $(KERN_DIR) M=`pwd` modules 
    $(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c 
​
clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -rf modules.order
    rm -f hello_drv_test
​
obj-m   += hello_drv.o

5.4 效果

linux:

开发板:

装载驱动

查看驱动是否转载成功

执行程序

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值