Linux驱动入门[一]

linux驱动主要作用就是初始化硬件设备,并给硬件接口提供上层应用程序调用。

1. 驱动的分类

linux系统将驱动分为三类:字符设备驱动、块设备驱动、网络设备驱动

  1. 字符设备:是指只能一个字节一个字节进行读写的设备,读取数据需要按照前后顺序读取,不能随机读取内存中的某一数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台等。

  1. 块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备主要由硬盘、磁盘、U盘等。

  1. 网络设备:网络设备是指可以硬件设备,如网卡;也可以是软件设备,如回环接口(lo),通过软件实现虚拟网络接口。

2. 驱动的认知

linux系统结构如图所示:

Linux驱动入门[一]_第1张图片

用户空间

指用户在进行程序编写和运行的层面,用户在进行开发时需要C语言和C库来进行。C库其实是就是C libary,它提供程序调用内核的接口,如open、read、write、fork等命令是在C库中进行封装实现。

内核空间

用户程序在使用某个硬件设备时,需要调用内核空间的设备驱动程序,从而驱动硬件设备。由于linux中一切皆文件,各类设备都是文件,因此可以通过文件操作函数来操作这些设备。所以的linux设备都是存放在/dev目录下,为了管理这些设备,系统为设备进行编号,每个设备分为主设备和次设备。主设备是用来区分设备的类型,次设备号是用来区分同一个类型的多种设备。并且每一种硬件设备都对应着不同的驱动。对于一些常用的设备,linux会约定俗成编号,如硬盘设备的编号为3。

Linux驱动入门[一]_第2张图片

在文件权限前面的字母表示着不同含义:

(1)(‘-’,regular file)表示普通文件,分为二进制文件和文本文件。

(2) ('d',directory file)表示文件夹文件,一般需要库函数将其打开。

(3) ('l',link file)表示链接文件。

(4) ('p',piple file)管道文件,用于进程通信

(5) ('c',character file)字符设备文件,为虚拟文件,本身不存在与硬盘中,由fs创建,不能直接读写,需要使用API调用

(6) ('b',block file)块设备文件,也是虚拟文件,是要API调用。

(7) ('s',socket file)套接字文件,用于网络中。

驱动链表:用于管理所有设备的驱动,添加或查找。添加驱动程序是在写完驱动程序后,加载到内核。驱动插入到链表的顺序是由设备号检索,通过设备号将驱动程序加载到链表的某个位置。

3. 字符设备驱动原理

Linux驱动入门[一]_第3张图片
  1. 在linux文件系统中,每个文件都有一个struct inode结构体来描述,这个结构体存放着这个文件的所有信息,如文件类型、访问权限等。当使用open函数打开设备文件时,可根据设备文件对应的struct inode结构体里描述的信息,可知道操作的设备类型(字符设备还是块设备),并且会分配一个struct file结构体。

  1. 根据struct inode结构体中记录的设备号,可以找到对应的驱动程序。每个字符设备都有一个struct cdev结构体。该结构体中记录了字符设备的操作函数接口等关键信息。

  1. 在找到struct cdev结构体后,linux内核会将struct cdev结构体所在的空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中记录的函数操作接口地址记录在struct file结构体的f_opts成员中。

  1. 任务完成后,VFS层会给应用返回一个文件描述符(fd)。fd与struct file结构体相对应,上层应用程序可以通过fd找到struct file,然后在struct file找到操作字符设备的函数接口file_operation。

4.字符设备驱动结构

字符设备驱动结构如图:

Linux驱动入门[一]_第4张图片

在linux内核中,使用cdev结构体来描述字符设备,使用dev_t定义设备号(分主、次设备号)来确定字符设备的唯一性。通过成员file_operation定义字符设备驱动提供给VFS的接口函数,例如open()、read()、write()等。

在linux字符设备驱动中,模块加载函数通过register_chrdev_region()或alloc_chrdev_region来静态或动态获取设备号,使用cdev_init()建cdev与file_operations之间的连接,通过cdev_add()向系统添加一个cdev完成注册。模块卸载函数通过cdev_del()来注销cdev,通过unregister_chrdev_region()来释放设备号。

5.字符驱动程序开发流程

5.1 字符设备的注册和注销

对于字符设备驱动,需要在驱动模块加载成功后注册字符设备,同样在卸载驱动模块时注销字符设备。字符设备的注册和注销函数如下:

static int __init hello_driver_init(void)
{
    int ret;
    printk("hello_driver_init\r\n");
    ret = register_chrdev(CHRDEVBASE_MAJOR,"hello_driver",&hello_world_fops);
 
    return 0;
}
 
static void __exit hello_driver_cleanup(void)
{
    unregister_chrdev(CHRDEVBASE_MAJOR,"hello_driver");
    printk("hello_driver_cleanup\r\n");
}

register_chrdev函数用于注册字符设备,其中CHRDEVBASE_MAJOR表示主设备号;"hello_driver"表示设备名称;hello_world_fops指向结构体file_operations类型指针。

unregister_chrdev函数用于注销字符设备,函数只有有两个参数。

5.2 字符设备方法构成

static int hello_world_open(struct inode * inode, struct file * file)
{
    printk("hello_world_open\r\n");
    return 0;
}
 
static int hello_world_release (struct inode * inode, struct file * file)
{
    printk("hello_world_release\r\n");
    return 0;
}
 
static ssize_t hello_world_read (struct file * file, char __user * buffer, size_t size, loff_t * ppos)
{
    printk("hello_world_read size:%ld\r\n",size);
    copy_to_user(buffer,kernel_buffer,size);
    return size;
}
 
static ssize_t hello_world_write(struct file * file, const char __user * buffer, size_t size, loff_t *ppos)
{
    printk("hello_world_write size:%ld\r\n",size);
    copy_from_user(kernel_buffer,buffer,size);
    return size;
}
 
 
static const struct file_operations hello_world_fops = {
    .owner        = THIS_MODULE,
    .open        = hello_world_open,
    .release = hello_world_release,
    .read        = hello_world_read,
    .write    = hello_world_write,
};

用户空间的函数需要在file_operations结构体中对应,才能使用户空间实现对内核的操作。其中两个函数是用于内核与用户空间交换数据的:

copy_to_user(buffer,kernel_buffer,size);将内核空间的数据到用户空间复制。buffer是目标地址,kernel_buffer表示源地址,size表示复制的数据长度。

copy_from_user(kernel_buffer,buffer,size);将用户空间的数据复制到内核空间。

5.3 驱动的加载和卸载

linux驱动有两种运行方式,一种是将驱动编译到linux内核中,当linux内核启动的时候会自动运行驱动程序。第二种是将驱动编译成模块(扩展名为.ko),在linux内核中使用“insmod”命令加载模块。

模块有加载和卸载两种操作,在编写驱动时需要注册这两种操作函数,模块的加载和卸载注册函数为:

module_init(hello_driver_init);
module_exit(hello_driver_cleanup);

5.4 编写测试程序

测试程序如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
uint8_t buffer[512] = {0};
int main(int argc, char *argv[])
{
    int fd;
    int ret;
    fd  = open(argv[1], O_RDWR);
    if(!strcmp("read",argv[2]))
    {
        printf("read data from kernel\r\n");
        ret = read(fd,buffer,sizeof(buffer));
        printf("ret len:%d data:%s\r\n",ret,buffer);
    }
 
    if(!strcmp("write",argv[2]))
    {
        printf("write data to kernel %s len:%d\r\n",argv[3],strlen(argv[3]));
        ret = write(fd,argv[3],strlen(argv[3]));
        printf("ret len:%d\r\n",ret);
    }  
    close(fd);    
}

5.5 Makefile文件

kERNELDIR := /lib/modules/$(shell uname -r)/build
PWD        := $(shell pwd)
obj-m := hello_drive.o
all:
    make -C /lib/modules/$$(uname -r)/build/ M=$(PWD) modules

clean:
    make -C /lib/modules/$$(uname -r)/build/ M=$(PWD) clean

6 代码编译

6.1 make编译

makefile文件编写好后直接使用make命令进行编译

Linux驱动入门[一]_第5张图片

6.2 insmod加载模块

make编译完成后发现文件中存在.ko文件,使用“insmod”命令记载模块,并使用“lsmod”查看

Linux驱动入门[一]_第6张图片

6.3 程序验证

使用cat /proc/devices可以查看设备号

Linux驱动入门[一]_第7张图片

使用mknod创建节点

Linux驱动入门[一]_第8张图片

编译测试文件gcc -o test_app test_app.c

应用程序读取数据:sudo .test/test_app /dev/chardev read ,并使用dmesg查看

应用程序写数据:sudo .test/test_app /dev/chardev write 1234567890,并使用dmesg查看

6.4 卸载模块

使用sudo rmmod hello_drive.ko,lsmod查看模型信息,模块已删除。

Linux驱动入门[一]_第9张图片

你可能感兴趣的:(linux,ubuntu)