【学习记录】从0开始的Linux学习之旅——字符型设备驱动及应用
一、概述
Linux操作系统通常是基于Linux内核,并结合GNU项目中的工具和应用程序而成。Linux操作系统支持多用户、多任务和多线程,具有强大的网络功能和良好的兼容性。基于前面应用与驱动的开发学习,本文主要讲述如何在linux系统上把应用与驱动的链路打通,即在应用中使用新增的驱动接口。
二、概念及原理
应用程序通过系统调用与内核进行交互,而驱动程序则提供了硬件设备的访问接口,内核本身则提供了系统调用、驱动框架等基础设施。
驱动开发:Linux 驱动开发是指为 Linux 内核开发各种设备驱动程序,用于控制和管理硬件设备。驱动程序运行在内核空间,直接与硬件进行交互。Linux 内核提供了丰富的接口和框架,开发者可以编写各种类型的设备驱动,包括网络设备、存储设备、输入设备等。驱动程序通过内核提供的接口与用户空间的应用程序进行通信。
应用开发:Linux 应用开发是指在 Linux 系统上开发各种类型的应用程序,包括命令行工具、图形界面应用、服务器端应用等。Linux 提供了丰富的开发环境和工具链,开发者可以使用各种编程语言和开发工具进行应用开发。应用程序运行在用户空间,通过系统调用与操作系统内核进行交互,执行各种任务和功能。
内核开发:Linux 内核开发是指对 Linux 内核本身进行开发和维护。Linux 内核是操作系统的核心,负责管理系统资源、调度任务、提供系统调用等功能。内核开发包括对内核功能的添加和修改,修复内核漏洞,优化性能等工作。内核开发人员通常会编写和维护内核的各种子系统和模块,包括调度器、文件系统、网络协议栈等。
设备类型:Linux 中的设备类型主要分为字符设备和块设备两种,它们分别适用于以字符为单位和以数据块为单位进行输入输出的场景。
用户空间与内核空间:用户空间和内核空间分别代表了操作系统中不同的内存空间和权限级别,它们共同构成了操作系统的运行环境,保证了系统的稳定性、安全性和可管理性。用户与内核的交互只能通过特定的系统接口,这样就保证了内核的稳定性。
三、准备工作
- 安装虚拟机VMware
- 安装ubuntu 22.04
- 安装vim、vscode等工具
sudo apt update
sudo apt install vim code
另外需要先熟悉单独的驱动开发及应用开发,具体可参考最底下相关文章链接。
四、代码实现
4.1 驱动代码实现
除了原本的模块加载和卸载接口,这里我们还要实现一些接口可供应用层使用。众所周知,Linux中万物皆文件,那我们就来实现操作文件所需的最常用的几个接口:open、close、write、read。
首先新建一个driver.c的文件,在文件中添加如下代码。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#define DEVICE_NAME "myfirstdev"
#define CLASS_NAME "myfirstdev_class"
/****************************模块的文件操作接口***************************/
static int majorNumber;
static struct class* charClass = NULL;
static struct device* charDevice = NULL;
/* 用来存储应用层传下来数据 */
static char DevMsg[100];
/* 打开接口 */
static int dev_open(struct inode *inodep,
struct file *f){
printk(KERN_INFO "打开设备!\n");
return 0;
}
/* 关闭接口 */
static int dev_release(struct inode *inodep,
struct file *f){
printk(KERN_INFO "关闭设备!\n");
return 0;
}
/* 读接口 */
static ssize_t dev_read(struct file *f,
char *buffer,
size_t len,
loff_t *offset){
int error_count = 0;
/* 可以传一些数据到应用层 */
if (len > sizeof(DevMsg)){
printk(KERN_INFO "读取数据字节数过长,共获取了%d字节\n", len);
return 0;
}
/* 把模块内的数据缓存拷贝到用户缓存中 */
error_count = copy_to_user(buffer, DevMsg, len);
if (error_count == 0){
printk(KERN_INFO "成功发送%d个字节数据给到用户\n", len);
return 0;
}
else{
printk(KERN_INFO "发送失败\n");
return -EFAULT;
}
}
/* 写接口 */
static ssize_t dev_write(struct file *f,
const char *buffer,
size_t len,
loff_t *offset){
int error_count = 0;
/* 可以保存一些应用层传下来的数据 */
if (len > sizeof(DevMsg)){
printk(KERN_INFO "写入数据字节数过长,需要写入%d字节\n", len);
return 0;
}
/* 内核空间与用户空间的数据交互必须通过这个接口 */
copy_from_user(DevMsg, buffer, len);
printk(KERN_INFO "写入数据成功,共写入%d个字节\n", len);
return len;
}
/* 文件接口挂接 */
static struct file_operations fops = {
.open = dev_open,
.release = dev_release,
.read = dev_read,
.write = dev_write,
};
/****************************模块的加载和卸载接口****************************/
/* 定义模块的初始化函数 */
static int Driver_Init(void)
{
/* 先注册字符型设备 */
majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
printk(KERN_INFO "注册的设备名为:%s\n", DEVICE_NAME);
/* 创建设备类 */
charClass = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(charClass)){
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_ALERT "注册设备失败\n");
return PTR_ERR(charClass);
}
/* 创建设备驱动 */
charDevice = device_create(charClass,
NULL,
MKDEV(majorNumber, 0),
NULL,
DEVICE_NAME);
if (IS_ERR(charDevice)){
class_destroy(charClass);
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_ALERT "创建设备失败\n");
return PTR_ERR(charDevice);
}
printk(KERN_INFO "字符型设备驱动加载成功!\n");
return 0;
}
/* 定义模块的退出函数 */
static void Driver_Exit(void)
{
/* 先销毁设备驱动 */
device_destroy(charClass, MKDEV(majorNumber, 0));
class_unregister(charClass);
class_destroy(charClass);
/* 再注销字符型设备 */
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_INFO "字符型设备驱动卸载成功!\n");
}
/* 注册模块的初始化和退出函数,这个是给内核识别的 */
module_init(Driver_Init);
module_exit(Driver_Exit);
/* 声明该模块符合GPL协议——必须加,不然编译会出错 */
MODULE_LICENSE("GPL");
/* 下面是声明作者姓名、设备类型和版本号,可加可不加 */
MODULE_AUTHOR("Chewie");
MODULE_DESCRIPTION("A simple char driver");
MODULE_VERSION("0.1");
再新增一个Makefile文件,添加如下内容。
obj-m := driver.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
编译模块
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
编译成功无告警后会生成xx.ko
文件,加载.ko
模块,在应用使用期间都需要保持加载状态。
sudo insmod driver.ko
如不需要使用此模块时,需要使用rmmod
卸载模块。
sudo rmmod driver
4.2 应用代码实现
同样的,即然驱动实现了对应的文件接口,那应用层就可以直接打开对应的驱动文件进行操作。新建一个app.c的文件,添加如下代码。
#include <stdio.h>
#include <fcntl.h>
#define DEVICE_NODE "/dev/myfirstdev"
int main(){
int file_desc;
int ret;
char msg[100];
char write_msg[] = "hello";
/* 打开刚才的设备驱动文件 */
file_desc = open(DEVICE_NODE, O_RDWR);
if (file_desc < 0){
printf("无法打开设备文件\n");
return -1;
}
/* 从设备中写入数据 */
ret = write(file_desc, write_msg, strlen(write_msg));
printf("写入的数据为:%s\n", write_msg);
if (ret < 0){
printf("写入数据失败\n");
close(file_desc);
return -1;
}
/* 从设备中读取数据 */
ret = read(file_desc, msg, sizeof(msg));
printf("读出的数据为:%s\n", msg);
if (ret < 0){
printf("读取数据失败\n");
close(file_desc);
return -1;
}
printf("读出写入的数据为:%s\n", msg);
/* 关闭设备 */
close(file_desc);
return 0;
}
再新增一个Makefile文件,添加如下内容。
# 定义编译器和编译选项
CC = gcc
CFLAGS = -Wall -g
# 定义目标文件和源文件
TARGET = app
SRCS = app.c
OBJS = $(SRCS:.c=.o)
# 默认构建规则
all: $(TARGET)
# 生成目标可执行文件
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
# 生成目标文件
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# 清理生成的文件
clean:
rm -f $(OBJS) $(TARGET)
在当前目录下,使用make
命令编译应用程序。编译无错误后会生成app
文件,执行以下命令运行程序,因为这里程序里调用了内核驱动,所以需要sudo
权限。
sudo ./app
这里可以打开系统日志看下整个过程。
sudo tail -f /var/log/kern.log
五、相关链接
【学习记录】从0开始的Linux学习之旅——驱动模块编译与加载
【学习记录】从0开始的Linux学习之旅——应用开发(helloworld)