- Navigation (`・ω・´)
- house主页
- 作品
- 暮光小猿图片管理器2
- av_timerSC 科学创造
- 记录
- videocam没事瞎折腾系列
- devices隔壁老王210
- devicesLinux嵌入式
- 关于
- archive更新日志
- restore历史版本
什么是字符设备
- 字符设备:字符设备是能够像字节流一样被访问的设备,当对字符设备发出读写请求,
相应的I/O 操作立即发生。Linux 系统中很多设备都是字符设备,如字符终端、串口、键盘、
鼠标等。在嵌入式Linux 开发中,接触最多的就是字符设备以及驱动。
(摘自《嵌入式Linux开发教程(下册)-周立功.pdf》)
- 字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。
相反,此类设备支持按字节/字符来读写数据。
举例来说,键盘、串口、调制解调器都是典型的字符设备。
(摘自百度知道linux中什么是块设备和字符设备)
实现字节流操作
为了方便解释 现在先不建工程.
这次把实现操作的文件和模块主文件分开写.
[kuresaru@linux ~]$ mkdir testDev
[kuresaru@linux ~]$ cd testDev/
[kuresaru@linux testDev]$ vim devops.h
[kuresaru@linux testDev]$ vim devops.c
devops.h
#ifndef __CDT_FOPS_H
#define __CDT_FOPS_H
#include <linux/fs.h> // 结构体 file_operations 在这里边定义
extern struct file_operations testdev_fops;
#endif
devops.c
#include "devops.h"
#include <linux/module.h> // 下边的 THIS_MODULE 在这个里边定义
#include <linux/version.h> // 下边的内核版本判断在这里边定义
// 在Linux中一切都是文件 设备访问点也是文件 所以需要实现文件的打开关闭方法
static int testdev_open(struct inode *inode, struct file *file)
{
return 0;
}
static int testdev_release(struct inode *inode, struct file *file)
{
return 0;
}
// 实现读取操作
static ssize_t testdev_read(struct file *file, char *buf, size_t count, loff_t *f_pos)
{
// 返回结果 读取了0个字节
return 0;
}
// 实现写入操作
static ssize_t testdev_write(struct file *file, const char *buf, size_t count, loff_t *f_pos)
{
// 返回结果 写的所有字节全部处理完成了
return count;
}
// 实现设备特有的操作 比如光驱设备实现弹出
static int testdev_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
return 0;
}
// 定义头文件里声明的结构体变量
struct file_operations testdev_fops = {
.owner = THIS_MODULE,
.read = testdev_read,
.write = testdev_write,
.open = testdev_open,
.release = testdev_release,
// 不同版本内核 这个叫法不一样 所以根据内核版本编译不同的程序
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 36)
.unlocked_ioctl = testdev_ioctl
#else
.ioctl = testdev_ioctl
#endif
};
编写模块主程序
main.c
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include "devops.h"
#define DEV_NAME "testdev"
static int major; // 设备的主设备号
static dev_t devno; // 设备的主设备号
struct cdev *testdev; // 设备
static struct class *testdev_class; //设备类
static int __init testdev_init(void)
{
// 动态分配一个主设备号
int ret = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);
if (ret < 0)
goto error;
// 从分配到的设备信息里取出主设备号
major = MAJOR(devno);
// 分配一个新的字符设备
testdev = cdev_alloc();
if (testdev == NULL)
goto error;
// 给新分配到的字符设备设置刚才实现好的操作方法
cdev_init(testdev, &testdev_fops);
testdev->owner = THIS_MODULE;
// 把初始化好的字符设备增加到系统中 1代表一个设备
if (cdev_add(testdev, devno, 1) != 0)
goto error;
// 创建设备类
testdev_class = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(testdev_class))
goto error;
// 现在执行这个才真正的把设备加入到了/dev/testdev
device_create(testdev_class, NULL, devno, NULL, DEV_NAME);
return 0;
error:
return -1;
}
static void __exit testdev_exit(void)
{
// 释放资源
// 删除/dev/testdev
device_destroy(testdev_class, devno);
// 删除设备类
class_destroy(testdev_class);
// 删除设备
cdev_del(testdev);
// 删除设备号
unregister_chrdev_region(devno, 1);
}
module_init(testdev_init);
module_exit(testdev_exit);
MODULE_LICENSE("GPL");
编译测试
复制前面的Makefile到本项目目录, 修改模块名和源文件
# 修改模块名字
NAME = testdev
KDIR := /usr/src/kernels/$(shell uname -r)
obj-m := $(NAME).o
# 修改源文件 因为有main.c和devops.c 所以这需要写两个文件 后缀是.o
$(NAME)-objs := main.o devops.o
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
-rm -rf *.o *.ko *.order $(NAME).mod.c Module.symvers
load:
insmod $(NAME).ko
unload:
rmmod $(NAME)
编译并安装模块 可以看到/dev/testdev这个设备
编译过程中有警告 是因为设备实现里没有完全实现所有功能
[kuresaru@linux testDev]$ make
make -C /usr/src/kernels/3.10.0-957.21.3.el7.x86_64 M=/home/kuresaru/testDev modules
make[1]: 进入目录“/usr/src/kernels/3.10.0-957.21.3.el7.x86_64”
CC [M] /home/kuresaru/testDev/main.o
CC [M] /home/kuresaru/testDev/devops.o
/home/kuresaru/testDev/devops.c:49:1: 警告:从不兼容的指针类型初始化 [默认启用]
};
^
/home/kuresaru/testDev/devops.c:49:1: 警告:(在‘testdev_fops.unlocked_ioctl’的初始化附近) [默认启用]
LD [M] /home/kuresaru/testDev/testdev.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/kuresaru/testDev/testdev.mod.o
LD [M] /home/kuresaru/testDev/testdev.ko
make[1]: 离开目录“/usr/src/kernels/3.10.0-957.21.3.el7.x86_64”
[kuresaru@linux testDev]$ sudo make load
insmod testdev.ko
[kuresaru@linux testDev]$ ls -l /dev/testdev
crw------- 1 root root 244, 0 9月 8 15:11 /dev/testdev
[kuresaru@linux testDev]$ sudo make unload
rmmod testdev
[kuresaru@linux testDev]$ ls -l /dev/testdev
ls: 无法访问/dev/testdev: 没有那个文件或目录
实现设备的读写
编辑devops.c, 增加一个静态全局变量字符数组
static char buffer[64];
因为用户不能直接访问内核的内存空间 内核也不能直接访问用户的内存空间
所以需要"间接"访问, 加入头文件
后面有字符串操作, 加入头文件
#include <asm/uaccess.h> // 提供copy_to_user和copy_from_user
#include <linux/string.h> // 提供strlen和memset</code></pre>
修改read和write函数 为了防止出错我把整个文件放出来
devops.c
#include "devops.h"
#include <linux/module.h> // 下边的 THIS_MODULE 在这个里边定义
#include <linux/version.h> // 下边的内核版本判断在这里边定义
#include <asm/uaccess.h> // 提供copy_to_user和copy_from_user
#include <linux/string.h> // 提供strlen和memset
static char buffer[64];
// 在Linux中一切都是文件 设备访问点也是文件 所以需要实现文件的打开关闭方法
static int testdev_open(struct inode *inode, struct file *file)
{
return 0;
}
static int testdev_release(struct inode *inode, struct file *file)
{
return 0;
}
// 实现读取操作
static ssize_t testdev_read(struct file *file, char *buf, size_t count, loff_t *f_pos)
{
// 取buffer里字符串的长度
size_t len = strlen(buffer);
if (len > 0) {
// 把内核空间buffer里的数据复制到用户空间buf里
if (copy_to_user(buf, buffer, len)) {
// 复制出错
printk(KERN_ERR "Write error\n");
return -EFAULT;
}
// 读取完成(写buf)后清空buffer
memset(buffer, 0, 64);
//返回读取到的字节数
return len;
}
return 0;
}
// 实现写入操作
static ssize_t testdev_write(struct file *file, const char *buf, size_t count, loff_t *f_pos)
{
// 把用户空间buf里的数据复制到内核空间buffer里
if (copy_from_user(buffer, buf, count)) {
printk(KERN_ERR "Write error\n");
return -EFAULT;
}
return count;
}
// 实现设备特有的操作 比如光驱设备实现弹出
static int testdev_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
return 0;
}
// 定义头文件里声明的结构休变量
struct file_operations testdev_fops = {
.owner = THIS_MODULE,
.read = testdev_read,
.write = testdev_write,
.open = testdev_open,
.release = testdev_release,
// 不同版本内核 这个叫法不一样 所以根据内核版本编译不同的程序
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 36)
.unlocked_ioctl = testdev_ioctl
#else
.ioctl = testdev_ioctl
#endif
};
最终测试
# 卸载之前的模块, 清空编译好的文件, 重新编译, 加载
[kuresaru@linux testDev]$ sudo make unload
[kuresaru@linux testDev]$ make clean
[kuresaru@linux testDev]$ make
[kuresaru@linux testDev]$ sudo make load
# 默认只有root有读写权限, 更改权限把读写给所有人
[kuresaru@linux testDev]$ ls -l /dev/testdev
crw------- 1 root root 244, 0 9月 8 15:43 /dev/testdev
[kuresaru@linux testDev]$ sudo chmod 666 /dev/testdev
[kuresaru@linux testDev]$ ls -l /dev/testdev
crw-rw-rw- 1 root root 244, 0 9月 8 15:43 /dev/testdev
# 写入数据给设备, 会把数据放到前边定义的那64字节的数组里
[kuresaru@linux testDev]$ echo "Hello World! 暮光小猿wzt" > /dev/testdev
# 从设备读取数据, 会读出64字节数组里的数据
[kuresaru@linux testDev]$ cat /dev/testdev
Hello World! 暮光小猿wzt
# 因为上次读取的时候已经清空了数组, 所以再读取不会有数据
[kuresaru@linux testDev]$ cat /dev/testdev