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
相关推荐
一叶龙洲1 小时前
wslg打开Ubuntu24.04默认打开图形界面
linux·服务器·数据库·ubuntu
敖行客 Allthinker3 小时前
Parallels Ubuntu虚拟机项目如何让手机访问?完整解决方案
linux·运维·ubuntu
王维同学3 小时前
[自学][Windows C++]RunOnceEx 注册表分组键的安全遍历
c++·windows·安全·开源
keyipatience4 小时前
线程栈与TLS和线程互斥
java·linux·服务器·开发语言·ubuntu
j7~4 小时前
【Linux】二十二.《Linux 信号机制完全指南:表示、捕捉与处理》
linux·信号处理·volatile·可入重函数·保存信号
Scott9999HH4 小时前
告别流量波动玄学!从法拉第电磁定律到底层 C/C++ 流量累积算法,深度解密工业级电磁流量计选型与开发
c语言·c++·算法
脱胎换骨-军哥5 小时前
C++/Rust无缝互操作:混合系统新常态
开发语言·c++·rust
观山岳五楼5 小时前
Ubuntu 24 怎么使用Ubuntu 20 的镜像源
linux·运维·ubuntu
m0_715646766 小时前
20260720
linux·arm
寒晓星6 小时前
[linux]线程及多线程
linux·运维