字符设备驱动开发 Linux 设备号 字符设备驱动开发步骤 open 函数调用流程 设备号的组成 设备号的分配 Linux 应用程序对驱动程序的调用 字符设备注册与注销 实现设备的具体操作函数
字符设备驱动简介
字符设备是 Linux 最基本的设备驱动
字符设备就是一个一个字节,按字节流进行读写操作,读写数据分 先后顺序
字符设备驱动包括 点灯、按键、 IIC、 SPI,LCD 等
Linux 应用程序对驱动程序的调用
Linux 中一切皆为文件
驱动加载成功后,会在“/dev”目录下生成一个相应的文件
此文件就是驱动文件
open 打开 该驱动函数
close 关闭 该驱动函数
write 函数 向此驱动 写入数据
read 函数从驱动中读取相应的状态
应用程序 运行在 用户空间
驱动 运行在 内核空间
open 函数调用流程
// linux-5.5.4/include/linux/fs.h
/* Linux 内核驱动操作函数集合 */
struct file_operations
{
struct module *owner; // 该结构体的模块的指针
loff_t (* llseek) (struct file *, loft_t, int); // 修改文件当前的读写位置
/* 读取设备文件 */
ssize_t (* read) (struct file *, char __user *, size_t, loff_t *);
/* 向设备文件写入数据 */
ssize_t (* weite) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (* read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (* write_iter) (struct kiocb *, stuct iov_iter *);
int (* iterate) (struct file *, struct dir_context *);
/* 查询设备是否可以非阻塞的读写 */
unsigned int (* poll) (struct file *, struct poll_table_struct *);
/* 提供对设备的控制功能,32位系统,32位的应用程序调用 */
long (* unlocked_ioctl) (struct file *, unsigned int, unsigned long);
/* 提供对设备的控制功能,64位系统,32位的应用程序调用 */
long (* compat_ioctl) (struct file *, unsigned int, unsigned long);
/* 将设备的内存映射到进程空间(用户空间), 不用在用户空间和内核空间之间来回复制 */
int (* mmap) (struct file *, struct vm_area_struct *);
int (* mremap) (struct file *, struct vm_area_struct *);
int (* open) (struct inode *, struct file *); // 打开设备文件
int (* flush) (struct file *, fl_owner_t id);
int (* release)(struct inode *, struct file *); // 释放(关闭)设备文件
int (* fasync)(int, struct file*, int); //刷新待处理的数据, 将缓冲区的数据刷新到磁盘
// ...
}__randomize_layout;
字符设备驱动开发步骤
驱动模块的加载与卸载
Linux 驱动运行方式:
驱动编译进 Linux 内核,当 Linux 内核启动时,会运行驱动程序
驱动编译成模块( .ko ),Linux 内核启动后。
“ insmod ” 命令 加载驱动模块
“ rmmod ” 命令卸载具体驱动
调试驱动时一般将编译为模块,驱动编译为模块最大的好处就是方便开发
// linux-5.5.4/include/linux/module.h
#ifndef MODULE
/**
* module_init() - driver initialization entry point
* 驱动程序初始化函数
* @x: function to be run at kernel boot time or module insertion
* 在内核启动或模块插入时运行的函数
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module).
* There can only be one per module.
* 每个模块只能有一个
*/
#define module_init(x) __initcall(x);
/**
* module_exit() - driver exit entry point
* 驱动程序退出函数
* @x: function to be run when driver is removed
* 删除驱动程序时要运行的函数
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module.
* If the driver is statically compiled into the kernel,
* module_exit() has no effect.
* 如果驱动程序被静态编译到内核中,module_exit()无效
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);
#else /* MODULE */
/*
* In most cases loadable modules do not need custom
* initcall levels. There are still some valid cases where
* a driver may be needed early if built in, and does not
* matter when built as a loadable module. Like bus
* snooping debug drivers.
*/
/* Each module must use one module_init(). */
/* 每个模块必须使用一个module_init() */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
/* This is only required if you want to be unloadable. */
/* 仅当您要卸载时才需要 */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
#endif
字符设备驱动模块加载和卸载模板
// linux-5.5.4/arch/arm/common/locomo.c
/* 驱动入口函数 */
static int __init locomo_init(void)
{
/* 入口函数具体内容 */
int ret = bus_register(&locomo_bus_type);
if (ret == 0)
platform_driver_register(&locomo_device_driver);
return ret;
}
/* 驱动出口函数 */
static void __exit locomo_exit(void)
{
/* 出口函数具体内容 */
platform_driver_unregister(&locomo_device_driver);
bus_unregister(&locomo_bus_type);
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(locomo_init);
module_exit(locomo_exit);
驱动编译完成后,扩展名为.ko
insmod 用于加载指定的 .ko 模块,不能解决模块的依赖关系
modprobe 会 分析模块的依赖关系,将所有的依赖模块加载到内核
默认会去 /lib/modules/<kernel-version> 目录中查找模块, Linux kernel 为 版本号
modprobe -r 可卸载掉驱动模块所依赖的其他模块
字符设备注册与注销
驱动模块加载成功后,需要注册字符设备
卸载驱动模块时,需要注销掉字符设备
//linux-5.5.4/include/linux/fs.h
/* 注册字符设备
* major : 主设备号
* name :设备名字
* fops : 指向设备的操作函数集合变量
*/
static inline int register_chrdev(unsigned int major,
const char *name,
const struct file_operations *fops)
{
return __register_chardev(major, 0, 256, name, fops);
}
/* 注销字符设备
* major : 主设备号
* name :设备名字
*/
static inline void unregister_chrdev(unsigned int major,
const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
// linux-5.5.4/fs/char_dev.c
/**
* __register_chrdev() - create and register a cdev occupying a range of minors
* @major: major device number or 0 for dynamic allocation
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: name of this range of devices
* @fops: file operations associated with this devices
*
* If @major == 0 this functions will dynamically allocate a major and return
* its number.
*
* If @major > 0 this function will attempt to reserve a device with the given
* major number and will return zero on success.
*
* Returns a -ve errno on failure.
*
* The name of this device has nothing to do with the name of the device in
* /dev. It only helps to keep track of the different owners of devices. If
* your module name has only one type of devices it's ok to use e.g. the name
* of the module here.
*/
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
cd = __register_chrdev_region(major, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, baseminor, count));
return err;
}
/**
* __unregister_chrdev - unregister and destroy a cdev
* 注销并销毁 cdev
* @major: major device number
* 主设备号
* @baseminor: first of the range of minor numbers
* @count: the number of minor numbers this cdev is occupying
* @name: name of this range of devices
*
* Unregister and destroy the cdev occupying the region described by
* @major, @baseminor and @count. This function undoes what
* __register_chrdev() did.
*/
void __unregister_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name)
{
struct char_device_struct *cd;
cd = __unregister_chrdev_region(major, baseminor, count);
if (cd && cd->cdev)
cdev_del(cd->cdev);
kfree(cd);
}
输入命令“ cat /proc/devices ”可查看 当前已经被使用掉的设备号
// linux-5.5.4/arch/mips/sibyte/common/sb_tbprof.c
/* 主设备号为 240 */
#define SBPROF_TB_MAJOR 240
/* 设备名字为“sb_tbprof” */
#define DEVNAME "sb_tbprof"
/* 设备的操作函数集合 */
static const struct file_operations sbprof_tb_fops =
{
.owner = THIS_MODULE,
.open = sbprof_tb_open,
.release = sbprof_tb_release,
.read = sbprof_tb_read,
.unlocked_ioctl = sbprof_tb_ioctl,
.compat_ioctl = sbprof_tb_ioctl,
.mmap = NULL,
.llseek = default_llseek,
};
/* 驱动入口函数 */
static int __init sbprof_tb_init(void)
{
struct device *dev;
struct class *tbc;
int err;
/* 主设备号为 SBPROF_TB_MAJOR
* 设备名字为“DEVNAME”
*/
if (register_chrdev(SBPROF_TB_MAJOR, DEVNAME, &sbprof_tb_fops))
{
/* 字符设备注册失败,自行处理 */
printk(KERN_WARNING DEVNAME ": initialization failed (dev %d)\n",
SBPROF_TB_MAJOR);
return -EIO;
}
tbc = class_create(THIS_MODULE, "sb_tracebuffer");
if (IS_ERR(tbc))
{
err = PTR_ERR(tbc);
goto out_chrdev;
}
// ...
out_chrdev:
unregister_chrdev(SBPROF_TB_MAJOR, DEVNAME);
return err;
}
static void __exit sbprof_tb_cleanup(void)
{
device_destroy(tb_class, MKDEV(SBPROF_TB_MAJOR, 0));
/* 注销字符设备驱动 */
unregister_chrdev(SBPROF_TB_MAJOR, DEVNAME);
class_destroy(tb_class);
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(sbprof_tb_init);
module_exit(sbprof_tb_cleanup);
设备的具体操作函数
设备 进行读写操作框架
/* 打开设备 */
static int xxx_open(struct inode *inode, struct file *filp)
{
return 0;
}
/* 从设备读取 */
static ssize_t xxx_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/* 向设备写数据 */
static ssize_t xxx_write(struct file *file, const char __user *buf, size_t cnt, loff_t *offt)
{
/* 用户实现具体功能 */
return 0;
}
/* 关闭/释放设备 */
static int xxx_release(struct inode *inode, struct file *filp)
{
/* 用户实现具体功能 */
return 0;
}
static struct file_oprations xxx_fops =
{
.owner = THIS_MODULE,
.open = xxx_open,
.read = xxx_read,
.write = xxx_write,
.release = xxx.release,
};
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
int retvalue = 0;
/* 注册字符设备驱动 */
retvalue = register_chrdev(99, "xxx", &xxx_fops);
if(retvalue > 0)
{
/* 字符设备注册失败,自行处理 */
}
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(99,"xxx");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
/* LICENSE 采用 GPL 协议 */
MODULE_LICENSE("GPL");
/* 作者名字 */
MODULE_AUTHOR("CpuCode");
Linux 设备号
Linux 中每个设备都有一个设备号
设备号由主设备号和次设备号
主设备号:一个驱动,高 12 位为主设备号(0 ~ 4095)
次设备号:使用该驱动的各个设备,低 20 位为次设备号
// linux-5.5.4/inlcude/linux/types.h
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
// linux-5.5.4/include/uapi/asm-generic/int-ll64.h
typedef __u32 u32;
typedef unsigned int __u32;
// linux-5.5.4/include/linux/kdev_t.h
#define MINORBITS 20 //次设备号位数
#define MINORMASK ((1U << MINORBITS) - 1) //次设备号掩码
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // dev_t 中获取主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) // dev_t 中获取次设备号
/* 给定的主设备号和次设备号的值组合成 dev_t 类型的设备号 */
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
设备号的分配
静态分配设备号
cat /proc/devices // 查看系统使用的所有设备号
动态分配设备号
// linux-5.5.4/fs/char_dev.c
/**
* alloc_chrdev_region() - register a range of char device numbers
* 注册一系列字符设备号
* @dev: output parameter for first assigned number
* 第一个分配编号的输出参数
* @baseminor: first of the requested range of minor numbers
* 第一个请求的次设备数字
* @count: the number of minor numbers required
* 次设备的数量
* @name: the name of the associated device or driver
* 相关设备或驱动程序的名称
*
* Allocates a range of char device numbers.
* 分配一系列字符设备号
* The major number will be chosen dynamically, and returned
* (along with the first minor number)in @dev.
* Returns zero or a negative error code.
* 返回零或负错误代码
*/
int alloc_chrdev_region(det_t *dev, unsigned baseminor, unsigned count, const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if(IS_ERR(cd))
{
return PTR_ERR(cd);
}
*dev = MKDEV(cd -> major, cd -> baseminor);
return 0;
}
/**
* unregister_chrdev_region() - unregister a range of device numbers
* @from: the first in the range of numbers to unregister
* 注销的数字范围中的第一个
* @count: the number of device numbers to unregister
* 注销的设备号的数量
*
* This function will unregister a range of @count device numbers,starting with @from.
* 取消注册一系列@count设备数量,从@from开始
* The caller should normally be the one who allocated those numbers in the first place..
*
*/
void unregister_chrdev_region(dev_t from, unsigned count)
{
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next)
{
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
{
next = to;
}
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
}
添加头文件路径
按下“ Crtl+Shift+P ”打开 VSCode 的控制台,然后输入“C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件,
.vscode 目录下生成一个名为 c_cpp_properties.json 的文件
includePath 表示 头文件路径 ( 绝对路径 )
{
"configurations":
[
{
"name": "Linux",
"includePath":
[
"${workspaceFolder}/**",
"/home/.../linux-5.5.4/include",
"/home/.../linux-5.5.4/arch/arm/include",
"/home/.../linux-5.5.4/arch/arm/include/generated/"
],
"defines": [],
// ...
}
],
"version": 4
}
编写程序:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
// 主设备号
#define CHRDEVBASE_MAJOR 99
// 设备名
#define CHRDEVBASE_NAME "chrdevbase"
// 读缓冲区
static char readbuf[100];
// 写缓冲区
static char writebuf[100];
static char kerneldata[] = {"kernel data"};
/*
* 打开设备
* inode:传递驱动inode
* filp:设备文件
* return: 0:成功 其他:失败
*/
static int chrdevbase_open(struct inode*inode, struct file *filp)
{
// printf:用户态 printk:内核态 1.1
printk("chrdevbase_open \r\n");
return 0;
}
/*
* 读数据
* filp: 设备文件(文件描述符)
* buf: 返回给用户空间的数据缓冲区
* cnt: 数据长度
* offt:相当于文件首地址的偏移
* return: 成功:字节数 失败:负值
*/
static ssize_t chrdevbase_read(struct file *filp,
char __user *buf,
ssize_t cnt,
loff_t *offt)
{
int retvalue = 0;
printk("chrdevbase read!\r\n");
// kerneldata 数组中的数据拷贝到读缓冲区 readbuf
memcpy(readbuf, kerneldata, sizeof(kerneldata)):
/* readbuf 中的数据复制到参数 buf
* 内核空间不能直接操作用户空间的内存
*/
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0)
{
printk("kernel senddata ok \r\n");
}
else
{
printk("kernel senddata failed \r\n");
}
return 0;
}
/*
* 写数据
* filp:设备文件
* buf:写入数据
* cnt:数据长度
* offt:对于文件首地址的偏移量
* return:成功:字节数 失败:负数
*/
static ssize_t chrdevbase_write(struct file *filp,
const char __user *buf,
size_t cnt,
loff_t *offt)
{
int retvalue = 0;
printk("chrdevbase write \r\n");
/* buf 中的数据复制到写缓冲区 writebuf
* 用户空间内存不能直接访问内核空间的内存
*/
retvalue = copy_from_user(writebuf, buf, cnt);
if(retvalue == 0)
{
printk("kernel recevdata:%s \r\n", writebuf);
}
else
{
printk("kernel recevdata failed \r\n");
}
return 0;
}
/*
* 关闭设备
* filp:关闭设备文件
* return: 成功:0 失败:其他
*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
printk("chrdevbase release \r\n");
return 0;
}
// 设备操作函数结构体
static struct file_operations chrdevbase_fops =
{
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*
* 驱动入口
* return:成功:0 失败: 其他
*/
static int __init chrdevbase_init(void)
{
int retvalue =0;
printk("chrdevbase_init \r\n");
// 注册字符设备驱动
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
if(retvalue < 0)
{
printk("chrdevbase driver register failed \r\n");
}
return 0;
}
// 驱动出口
static void __exit chrdevbase_exit(void)
{
// 注销字符设备驱动
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase_exit() \r\n");
}
// 驱动入口 出口
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
// LICENSE 作者
MODULE_LICENSE("GPL");
MODULE_AUTHOR("CpuCode");
/* linux-5.5.4/linux/kern_levels.h */
/* 1.1
* 0 的优先级最高 7 的优先级最低
*
*/
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
// linux-5.5.4/include/linux/printk.h
/* printk's without a loglevel use this.. */
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT
/*
* Default used to be hard-coded at 7, quiet used to be hardcoded at 4,
* we're now allowing both to be set from kernel config.
*/
#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT
//linux-5.5.7/include/generated/autoconf.h
#define CONFIG_MESSAGE_LOGLEVEL_DEFAULT 4
#define CONFIG_CONSOLE_LOGLEVEL_DEFAULT 7
//linux-5.5.7/tools/virtio/linux/uaccess.h
static inline int copy_from_user(void *to, const void __user volatile *from,
unsigned long n)
{
__chk_user_ptr(from, n);
volatile_memcpy(to, from, n);
return 0;
}
static inline int copy_to_user(void __user volatile *to, const void *from,
unsigned long n)
{
__chk_user_ptr(to, n);
volatile_memcpy(to, from, n);
return 0;
}
编写应用程序
C 库文件操作基本函数
open 函数
/*
* pathname:打开的设备或 文件名
* return: 成功:文件描述符
*/
int open(const char *pathname, int flags)
flags: 文件打开模式
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
O_APPEND 每次写操作都写入文件的末尾
O_CREAT 如文件不存在,就创建该文件
O_EXCL 如文件存在,返回-1 并修改errno的值
O_TRUNC 如文件存在,并以只写/读写打开,清空文件全部内容
O_NOCTTY 如路径名指终端设备,不作控制终端
O_NONBLOCK
DSYNC
O_RSYNC
O_SYNC
read 函数
/*
* fd:文件描述符
* buf:数据读取到buf
* count:数据长度
* return:成功:字节数,文件名末尾返回0 失败:负值
*/
ssize_t read(int fd, void *buf, size_t count)
write 函数
/*
* fd: 文件描述符
* buf:写入的数据
* count:数据长度
* return:成功:字节数 没有数据返回0 失败:负值
*/
ssize_t write(int fd, const void *buf, size_t count)
close 函数
/*
* fd:文件描述符
* return:成功:0 失败:负值
*/
int close(int fd)
应用程序:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
// 测试数据
static char usrdata[] = {"usr data"};
/*
* argc: argv数组个数
* argv:命令行参数
* return:成功:0 失败:负值
*/
int main(int argc, char *argv[])
{
int fd;
int retalue;
char *filename;
char readbuf[100],writebuf[100];
// 判断参数是否为 3
if(argc != 3)
{
printf("Error Usage \r\n");
return -1;
}
// 设备名
filename = argv[1];
// 打开驱动文件
fd = open(filename, O_RDWR);
if(fd < 0)
{
printf("Can't open file %s \r\n", filename);
return -1;
}
// 读取数据,atoi 字符数转换为数字
if(atoi(argv[2]) == 1)
{
//驱动文件中读取数据
retvalue = read(fd, readbuf, 50);
if(retvalue < 0)
{
printf("read file %s failed \r\n", filename);
}
else
{
// 读取成功,打印数据
printf("read data %s \r\n", readbuf);
}
}
// 写数据
if(atoi(argv[2]) == 2)
{
// 写数据
memcpy(writebuf, usrdata, sizeof(usrdata));
retvalue = write(fd, writebuf, 50);
if(retvalue < 0)
{
printf("write file %s failed \r\n", filename);
}
}
// 关闭设备
retvalue = close(fd);
if(retvalue < 0)
{
printf("can't close file %s \r\n", filename);
return -1;
}
return 0;
}
Makefile:
# linux内核的绝对路径
KERNELDIR := /home/cpucode/linux-5.5.7
# 当前路径
CPRRENT_PATH := $(shell pwd)
#将文件编译为ko模块
obj-m := chrdevbase.o
build:kernel_modules
# modules :编译模块
# -C:当前目录切换到KERNELDIR
# M:模块源码目录
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
编译驱动程序:
make
编译应用程序:
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
#查看程序信息
file chrdevbaseApp
加载驱动文件
modprobe chrdevbase.ko
# 查看模块
lsmod
# 查看所有设备
cat /proc/devices
# mknod:创建节点
# chrdevbase:创建节点目录
# c:字符设备
# 99: 主设备
# 9:次数
mknod /dev/chrdevbase c 99 9
#卸载模块
rmmod chrdevbase.ko