Linux内核与驱动:9.Linux 驱动 API 封装

很多人在写完 Linux 字符设备驱动后,应用层都是直接调用:open(),read(),write(),lseek(),ioctl(),close()。这样当然能跑,但有两个问题很快会出现:

第一,业务代码到处散落着 /dev/xxx、ioctl 命令号和各种系统调用。

第二,一旦驱动接口改了,所有应用程序都要跟着改。

更好的做法是:

在用户态再封装一层 API,把驱动访问统一成几个清晰的函数。

这篇文章基于一份已经写好的字符设备驱动:

  • 支持 read/write
  • 支持 llseek
  • 支持 unlocked_ioctl
  • 设备节点是 /dev/cdev_test_device
  • ioctl 支持三个命令:
    • CMD_TIMER_OPEN
    • CMD_TIMER_CLOSE
    • CMD_TIMER_SET

另外,这个驱动内部还维护了一个内核定时器,CMD_TIMER_SET 用于设置定时周期 CMD_TIMER_OPEN/CLOSE 用于开启和关闭定时器。

1.为什么要封装用户态API?

如果不封装,应用层一般会写成这样:

cpp 复制代码
int fd = open("/dev/cdev_test_device", O_RDWR);
ioctl(fd, CMD_TIMER_OPEN);
ioctl(fd, CMD_TIMER_SET, 500);
write(fd, "hello", 5);
lseek(fd, 0, SEEK_SET);
read(fd, buf, 5);
close(fd);

功能没问题,但有几个缺点:

  • 设备节点写死在业务里
  • ioctl命令直接暴露给上层
  • 错误处理不统一
  • 后续多个程序复用不方便

所以更推荐再包一层:

cpp 复制代码
cdev_open();
cdev_timer_open();
cdev_timer_set();
cdev_write_data();
cdev_seek();
cdev_read_data();
cdev_close();

这样代码更清晰,也方便以后把这层封装做成静态库或动态库。

2.封装实战

封装时,我们推荐创建两个头文件:

第一个:共享协议头 cdev_user.h

这个头文件给驱动和用户态共同使用,里面只放:

  • 设备节点名
  • ioctl命令号

第二个:API 头文件 cdev_api.h

这个头文件只给应用程序使用,里面放:

  • cdev_open
  • cdev_write_data
  • cdev_timer_set
  • cdev_close

cdev_user.h内容:

cpp 复制代码
#ifndef CDEV_USER_H
#define CDEV_USER_H

#include <sys/ioctl.h>

#define CDEV_DEV_PATH "/dev/cdev_test_device"

#define CMD_TIMER_OPEN  _IO('L', 0)
#define CMD_TIMER_CLOSE _IO('L', 1)
#define CMD_TIMER_SET   _IOW('L', 2, int)

#endif

用户态 API 头文件:cdev_api.h

cpp 复制代码
#ifndef CDEV_API_H
#define CDEV_API_H

#include <sys/types.h>

int cdev_open(void);
int cdev_close(int fd);

ssize_t cdev_write_data(int fd, const void *buf, size_t count);
ssize_t cdev_read_data(int fd, void *buf, size_t count);
off_t cdev_seek(int fd, off_t offset, int whence);

int cdev_timer_open(int fd);
int cdev_timer_close(int fd);
int cdev_timer_set(int fd, int ms);

#endif

用户态API实现:

cpp 复制代码
#include "cdev_api.h"
#include "cdev_user.h"

#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>

int cdev_open(void)
{
    return open(CDEV_DEV_PATH, O_RDWR);
}

int cdev_close(int fd)
{
    return close(fd);
}

ssize_t cdev_write_data(int fd, const void *buf, size_t count)
{
    return write(fd, buf, count);
}

ssize_t cdev_read_data(int fd, void *buf, size_t count)
{
    return read(fd, buf, count);
}

off_t cdev_seek(int fd, off_t offset, int whence)
{
    return lseek(fd, offset, whence);
}

int cdev_timer_open(int fd)
{
    return ioctl(fd, CMD_TIMER_OPEN);
}

int cdev_timer_close(int fd)
{
    return ioctl(fd, CMD_TIMER_CLOSE);
}

int cdev_timer_set(int fd, int ms)
{
    return ioctl(fd, CMD_TIMER_SET, ms);
}

测试程序:

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "cdev_api.h"

int main(void)
{
    int fd;
    char wbuf[] = "hello_driver";
    char rbuf[32] = {0};

    fd = cdev_open();
    if (fd < 0) {
        perror("cdev_open");
        return -1;
    }

    if (cdev_timer_open(fd) < 0) {
        perror("cdev_timer_open");
        cdev_close(fd);
        return -1;
    }

    if (cdev_timer_set(fd, 1000) < 0) {
        perror("cdev_timer_set");
        cdev_close(fd);
        return -1;
    }

    if (cdev_write_data(fd, wbuf, strlen(wbuf)) < 0) {
        perror("cdev_write_data");
        cdev_close(fd);
        return -1;
    }

    if (cdev_seek(fd, 0, SEEK_SET) < 0) {
        perror("cdev_seek");
        cdev_close(fd);
        return -1;
    }

    if (cdev_read_data(fd, rbuf, strlen(wbuf)) < 0) {
        perror("cdev_read_data");
        cdev_close(fd);
        return -1;
    }

    printf("read back: %s\n", rbuf);

    sleep(3);

    if (cdev_timer_close(fd) < 0) {
        perror("cdev_timer_close");
    }

    cdev_close(fd);
    return 0;
}

3.编译与使用

我们分为Ubuntu本机编译(本机使用)和交叉编译到rk3568两种方式。

3.1Ubuntu本机编译

编译分两部分:

  • 编译驱动模块
  • 编译用户态 API 和测试程序

编译驱动模块很常规,写makefile然后make生成xxx.ko即可。

然后我们将 cdev_api.c 先打包成静态库,再进行链接:

cpp 复制代码
gcc -Wall -O2 -c cdev_api.c -o cdev_api.o
ar rcs libcdevapi.a cdev_api.o
gcc -Wall -O2 test_app.c -L. -lcdevapi -o test_app

3.2交叉编译到rk3568

编译驱动模块的交叉编译我们在前文中讲过,在此就不再赘述了。

通过交叉编译将 cdev_api.c 先打包成静态库,再进行链接的过程如下:

bash 复制代码
aarch64-linux-gnu-gcc -Wall -O2 -c cdev_api.c -o cdev_api.o
aarch64-linux-gnu-ar rcs libcdevapi.a cdev_api.o
aarch64-linux-gnu-gcc test_app.c -L. -lcdevapi -o test_app_arm64
相关推荐
吴梓穆2 小时前
UE5 c++ 常用方法
java·c++·ue5
Morwit2 小时前
【力扣hot100】 1. 两数之和
数据结构·c++·算法·leetcode·职场和发展
SpiderPex2 小时前
第十七届蓝桥杯 C++ B组-题目 (最新出炉 )
c++·职场和发展·蓝桥杯
炘爚2 小时前
C++ 右值引用与程序优化
开发语言·c++
si莉亚3 小时前
ROS2安装EVO工具包
linux·开发语言·c++·开源
Tingjct3 小时前
Linux常用指令
linux·运维·服务器
智者知已应修善业3 小时前
【51单片机单按键切换广告屏】2023-5-17
c++·经验分享·笔记·算法·51单片机
广州灵眸科技有限公司3 小时前
为RK3588注入澎湃算力:RK1820 AI加速卡完整适配与评测指南
linux·网络·人工智能·物联网·算法
IT界的老黄牛3 小时前
Linux 压缩命令实战:tar、gzip、bzip2、xz、zstd 怎么选?一篇讲清楚
linux·运维·服务器