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
相关推荐
li167090270几秒前
第二十七章:智能指针
c语言·数据结构·c++·visual studio
切糕师学AI16 分钟前
Ubuntu 下 Git 完全使用指南
linux·git·ubuntu
浪客灿心34 分钟前
Linux网络传输层协议
linux·运维·网络
王老师青少年编程42 分钟前
csp信奥赛C++高频考点专项训练之贪心算法 --【贪心与二分判定】:数列分段 Section II
c++·算法·贪心·csp·信奥赛·二分判定·数列分段 section ii
zh_xuan44 分钟前
libcurl调用https接口
c++·libcurl
就叫飞六吧1 小时前
QT写一个桌面程序exe并动态打包基本流程(c++)
开发语言·c++
蜡笔小马1 小时前
1.c++设计模式-工厂模式
c++
舟遥遥娓飘飘1 小时前
Nexus4CC 手机电脑同步claude code对话部署教程(基于linux系统)
linux·智能手机·电脑
何妨呀~1 小时前
Firewalld防火墙端口配置
linux
汉克老师1 小时前
GESP2025年3月认证C++五级( 第三部分编程题(2、原根判断))
c++·算法·模运算·gesp5级·gesp五级·原根·分解质因数