linux驱动主要作用就是初始化硬件设备,并给硬件接口提供上层应用程序调用。
linux系统将驱动分为三类:字符设备驱动、块设备驱动、网络设备驱动
字符设备:是指只能一个字节一个字节进行读写的设备,读取数据需要按照前后顺序读取,不能随机读取内存中的某一数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台等。
块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备主要由硬盘、磁盘、U盘等。
网络设备:网络设备是指可以硬件设备,如网卡;也可以是软件设备,如回环接口(lo),通过软件实现虚拟网络接口。
linux系统结构如图所示:
用户空间:
指用户在进行程序编写和运行的层面,用户在进行开发时需要C语言和C库来进行。C库其实是就是C libary,它提供程序调用内核的接口,如open、read、write、fork等命令是在C库中进行封装实现。
内核空间:
用户程序在使用某个硬件设备时,需要调用内核空间的设备驱动程序,从而驱动硬件设备。由于linux中一切皆文件,各类设备都是文件,因此可以通过文件操作函数来操作这些设备。所以的linux设备都是存放在/dev目录下,为了管理这些设备,系统为设备进行编号,每个设备分为主设备和次设备。主设备是用来区分设备的类型,次设备号是用来区分同一个类型的多种设备。并且每一种硬件设备都对应着不同的驱动。对于一些常用的设备,linux会约定俗成编号,如硬盘设备的编号为3。
在文件权限前面的字母表示着不同含义:
(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)套接字文件,用于网络中。
驱动链表:用于管理所有设备的驱动,添加或查找。添加驱动程序是在写完驱动程序后,加载到内核。驱动插入到链表的顺序是由设备号检索,通过设备号将驱动程序加载到链表的某个位置。
在linux文件系统中,每个文件都有一个struct inode结构体来描述,这个结构体存放着这个文件的所有信息,如文件类型、访问权限等。当使用open函数打开设备文件时,可根据设备文件对应的struct inode结构体里描述的信息,可知道操作的设备类型(字符设备还是块设备),并且会分配一个struct file结构体。
根据struct inode结构体中记录的设备号,可以找到对应的驱动程序。每个字符设备都有一个struct cdev结构体。该结构体中记录了字符设备的操作函数接口等关键信息。
在找到struct cdev结构体后,linux内核会将struct cdev结构体所在的空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中记录的函数操作接口地址记录在struct file结构体的f_opts成员中。
任务完成后,VFS层会给应用返回一个文件描述符(fd)。fd与struct file结构体相对应,上层应用程序可以通过fd找到struct file,然后在struct file找到操作字符设备的函数接口file_operation。
字符设备驱动结构如图:
在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()来释放设备号。
对于字符设备驱动,需要在驱动模块加载成功后注册字符设备,同样在卸载驱动模块时注销字符设备。字符设备的注册和注销函数如下:
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函数用于注销字符设备,函数只有有两个参数。
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);将用户空间的数据复制到内核空间。
linux驱动有两种运行方式,一种是将驱动编译到linux内核中,当linux内核启动的时候会自动运行驱动程序。第二种是将驱动编译成模块(扩展名为.ko),在linux内核中使用“insmod”命令加载模块。
模块有加载和卸载两种操作,在编写驱动时需要注册这两种操作函数,模块的加载和卸载注册函数为:
module_init(hello_driver_init);
module_exit(hello_driver_cleanup);
测试程序如下:
#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);
}
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
makefile文件编写好后直接使用make命令进行编译
make编译完成后发现文件中存在.ko文件,使用“insmod”命令记载模块,并使用“lsmod”查看
使用cat /proc/devices可以查看设备号
使用mknod创建节点
编译测试文件gcc -o test_app test_app.c
应用程序读取数据:sudo .test/test_app /dev/chardev read ,并使用dmesg查看
应用程序写数据:sudo .test/test_app /dev/chardev write 1234567890,并使用dmesg查看
使用sudo rmmod hello_drive.ko,lsmod查看模型信息,模块已删除。