208 lines
6.1 KiB
C
208 lines
6.1 KiB
C
/*
|
||
* chardev2.c - 创建一个输入/输出符的设备
|
||
*/
|
||
|
||
#include <linux/atomic.h>
|
||
#include <linux/cdev.h>
|
||
#include <linux/delay.h>
|
||
#include <linux/device.h>
|
||
#include <linux/fs.h>
|
||
#include <linux/init.h>
|
||
#include <linux/module.h>
|
||
#include <linux/printk.h>
|
||
#include <linux/types.h>
|
||
#include <linux/uaccess.h> /* 引入 get_user 和 put_user */
|
||
#include <linux/version.h>
|
||
|
||
#include <asm/errno.h>
|
||
|
||
#include "chardev.h"
|
||
#define SUCCESS 0
|
||
#define DEVICE_NAME "char_dev"
|
||
#define BUF_LEN 80
|
||
|
||
enum {
|
||
CDEV_NOT_USED = 0,
|
||
CDEV_EXCLUSIVE_OPEN = 1,
|
||
};
|
||
|
||
/* 设备当前是否已打开?用于防止对同一设备的并发访问 */
|
||
static atomic_t already_open = ATOMIC_INIT(CDEV_NOT_USED);
|
||
|
||
/* 设备在被请求时返回的消息 */
|
||
static char message[BUF_LEN + 1];
|
||
|
||
static struct class *cls;
|
||
|
||
/* 当进程尝试打开设备文件时调用此函数 */
|
||
static int device_open(struct inode *inode, struct file *file)
|
||
{
|
||
pr_info("[device_open]打开: (%p)\n", file);
|
||
|
||
try_module_get(THIS_MODULE);
|
||
return SUCCESS;
|
||
}
|
||
|
||
static int device_release(struct inode *inode, struct file *file)
|
||
{
|
||
pr_info("[device_release]释放: (%p,%p)\n", inode, file);
|
||
|
||
module_put(THIS_MODULE);
|
||
return SUCCESS;
|
||
}
|
||
|
||
/* 打开设备文件的进程尝试从中读取数据时调用此函数 */
|
||
static ssize_t device_read(struct file *file, /* 参见include/linux/fs.h */
|
||
char __user *buffer, /* 要填充的用户缓冲区 */
|
||
size_t length, /* 缓冲区的长度 */
|
||
loff_t *offset)
|
||
{
|
||
/* 实际写入到缓冲区的字节数 */
|
||
int bytes_read = 0;
|
||
/* 读取消息的进度指针,如果消息比缓冲区大,这很有用 */
|
||
const char *message_ptr = message;
|
||
|
||
if (!*(message_ptr + *offset)) { /* 到达消息末尾 */
|
||
*offset = 0; /* 重置偏移量 */
|
||
return 0; /* 表示文件结束 */
|
||
}
|
||
|
||
message_ptr += *offset;
|
||
/* 实际将数据写入缓冲区 */
|
||
while (length && *message_ptr) {
|
||
/* 由于缓冲区在用户数据段而非内核数据段,需要使用 put_user 将数据从内核复制到用户空间 */
|
||
put_user(*(message_ptr++), buffer++);
|
||
length--;
|
||
bytes_read++;
|
||
}
|
||
|
||
pr_info("读取 %d 字节, %ld 字节剩余\n", bytes_read, length);
|
||
|
||
*offset += bytes_read;
|
||
|
||
/* 读取函数应返回实际写入缓冲区的字节数 */
|
||
return bytes_read;
|
||
}
|
||
|
||
/* 当有人尝试向我们的设备文件写入数据时调用此函数 */
|
||
static ssize_t device_write(struct file *file, const char __user *buffer,
|
||
size_t length, loff_t *offset)
|
||
{
|
||
int i;
|
||
|
||
pr_info("[device_write]写入: %p,%p,%ld", file, buffer, length);
|
||
|
||
/* 将数据从用户空间缓冲区读取到内核空间的 message 中 */
|
||
for (i = 0; i < length && i < BUF_LEN; i++)
|
||
get_user(message[i], buffer + i);
|
||
|
||
/* 返回实际写入 message 中的字符数 */
|
||
return i;
|
||
}
|
||
|
||
/* 当进程尝试对设备文件执行 ioctl 操作时调用此函数。除了 inode 和 file 结构外,我们还接收到两个额外的参数:ioctl 的编号和传递给 ioctl 的参数。
|
||
* 如果 ioctl 是写操作或读/写操作(即返回输出给调用进程),ioctl 调用将返回此函数的输出。
|
||
*/
|
||
static long
|
||
device_ioctl(struct file *file,
|
||
unsigned int ioctl_num, /* ioctl 操作编号 */
|
||
unsigned long ioctl_param) /* ioctl 参数 */
|
||
{
|
||
int i;
|
||
long ret = SUCCESS;
|
||
|
||
/* 不允许同时与两个进程交互 */
|
||
if (atomic_cmpxchg(&already_open, CDEV_NOT_USED, CDEV_EXCLUSIVE_OPEN))
|
||
return -EBUSY;
|
||
|
||
/* 根据 ioctl 操作编号进行切换 */
|
||
switch (ioctl_num) {
|
||
case IOCTL_SET_MSG: {
|
||
/* 接收一个用户空间的消息指针,并将其设置为设备的消息 */
|
||
char __user *tmp = (char __user *)ioctl_param;
|
||
char ch;
|
||
|
||
/* 查找消息的长度 */
|
||
get_user(ch, tmp);
|
||
for (i = 0; ch && i < BUF_LEN; i++, tmp++)
|
||
get_user(ch, tmp);
|
||
|
||
device_write(file, (char __user *)ioctl_param, i, NULL);
|
||
break;
|
||
}
|
||
case IOCTL_GET_MSG: {
|
||
loff_t offset = 0;
|
||
|
||
/* 将当前消息返回给调用进程,参数是一个指针,将数据填充到该指针指向的缓冲区中 */
|
||
i = device_read(file, (char __user *)ioctl_param, 99, &offset);
|
||
|
||
/* 在缓冲区末尾添加一个终止符 */
|
||
put_user('\0', (char __user *)ioctl_param + i);
|
||
break;
|
||
}
|
||
case IOCTL_GET_NTH_BYTE:
|
||
/* 这个 ioctl 同时用于输入(ioctl_param)和输出(函数的返回值) */
|
||
ret = (long)message[ioctl_param];
|
||
break;
|
||
}
|
||
|
||
/* 准备好接待下一个调用者 */
|
||
atomic_set(&already_open, CDEV_NOT_USED);
|
||
|
||
return ret;
|
||
}
|
||
|
||
/* 模块声明 */
|
||
|
||
/* 这个结构体将保存当进程对我们创建的设备进行操作时调用的函数。
|
||
* 由于指向这个结构体的指针被保存在设备表中,它不能是 init_module 的局部变量。
|
||
* NULL 表示未实现的函数。
|
||
*/
|
||
static struct file_operations fops = {
|
||
.read = device_read,
|
||
.write = device_write,
|
||
.unlocked_ioctl = device_ioctl,
|
||
.open = device_open,
|
||
.release = device_release, /* 类似 关闭`close` 操作 */
|
||
};
|
||
|
||
/* 初始化模块-注册字符设备 */
|
||
static int __init chardev2_init(void)
|
||
{
|
||
/* 注册字符设备(尝试一下) */
|
||
int ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &fops);
|
||
|
||
/* 出现负值代表有个错误 */
|
||
if (ret_val < 0) {
|
||
pr_alert("注册字符设备 %d 失败\n", ret_val);
|
||
return ret_val;
|
||
}
|
||
|
||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
|
||
cls = class_create(DEVICE_FILE_NAME);
|
||
#else
|
||
cls = class_create(THIS_MODULE, DEVICE_FILE_NAME);
|
||
#endif
|
||
device_create(cls, NULL, MKDEV(MAJOR_NUM, 0), NULL, DEVICE_FILE_NAME);
|
||
|
||
pr_info("设备创建于 /dev/%s\n", DEVICE_FILE_NAME);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* 清理模块 - 从/proc注销移除相关文件 */
|
||
static void __exit chardev2_exit(void)
|
||
{
|
||
device_destroy(cls, MKDEV(MAJOR_NUM, 0));
|
||
class_destroy(cls);
|
||
|
||
/* 注销设备 */
|
||
unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
|
||
}
|
||
|
||
module_init(chardev2_init);
|
||
module_exit(chardev2_exit);
|
||
|
||
MODULE_LICENSE("GPL");
|
||
|