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

张开发
2026/5/8 8:13:08 15 分钟阅读
Linux内核与驱动:9.Linux 驱动 API 封装
很多人在写完 Linux 字符设备驱动后应用层都是直接调用open()read()write()lseek()ioctl()close()。这样当然能跑但有两个问题很快会出现第一业务代码到处散落着 /dev/xxx、ioctl 命令号和各种系统调用。第二一旦驱动接口改了所有应用程序都要跟着改。更好的做法是在用户态再封装一层 API把驱动访问统一成几个清晰的函数。这篇文章基于一份已经写好的字符设备驱动支持 read/write支持 llseek支持 unlocked_ioctl设备节点是 /dev/cdev_test_deviceioctl 支持三个命令CMD_TIMER_OPENCMD_TIMER_CLOSECMD_TIMER_SET另外这个驱动内部还维护了一个内核定时器CMD_TIMER_SET 用于设置定时周期 CMD_TIMER_OPEN/CLOSE 用于开启和关闭定时器。1.为什么要封装用户态API如果不封装应用层一般会写成这样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命令直接暴露给上层错误处理不统一后续多个程序复用不方便所以更推荐再包一层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_opencdev_write_datacdev_timer_setcdev_closecdev_user.h内容#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#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实现#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); }测试程序#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 先打包成静态库再进行链接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_app3.2交叉编译到rk3568编译驱动模块的交叉编译我们在前文中讲过在此就不再赘述了。通过交叉编译将 cdev_api.c 先打包成静态库再进行链接的过程如下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

更多文章