nhmk/示例/3-ioctl/chardev2.c

208 lines
6.1 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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");