目录
一、前言
之前的章节已经实现了最基本的字符设备,目前我们的字符设备拥有自动生成设备节点、基
本读写等功能。本章节是对基本读写功能的扩展,使得我们的字符设备的读写功能更加具体。
二、读写功能相关的基本函数
2.1 struct inode
表示文件的结构体,主要利用其成员"i_cdev","i_cdev"保存了字符设备结构的地址。
2.2 struct file
表示打开的文件描述符,主要利用其成员"private_data"。
2.3 container_of()
通过结构体成员地址得到整个结构体变量的首地址,我们用在open()函数中。其函数原型如下
所示:
#define offsetof(TYPE, MEMBER) ((size_t) & ((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
输入参数:
1)ptr: 结构体内成员member的地址;
2)type: 结构体类型;
3)member: 结构体内成员;
返回值:
结构体的首地址。
我们将字符设备结构定义为:
struct hello_char_dev{
struct cdev cdev;
char *c; /* 保存字符串 */
int n; /* 保存字符个数 */
};
我们在注册字符设备结构时,只是将"cdev"注册给了内核,内核只知道"cdev"的信息和地址,
并将其保存在"inode->i_cdev"中,但如果我们想要操作字符设备,必须要知道结构体地址,所以要
通过"container_of()"来获取结构体地址,大致流程如下图所示。
代码举例:
int hc_open(struct inode *inode, struct file *filp)
{
struct hello_char_dev *hc_dev;
/* 获取设备结构体的地址 */
hc_dev = container_of(inode->i_cdev, struct hello_char_dev, cdev);
/* 将设备结构地址放到文件描述符结构的私有数据中 */
filp->private_data = hc_dev;
return 0;
}
2.4 copy_to_user()
将内核空间的数据拷贝到用户空间,我们在read()函数中使用,其定义如下。
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
输入参数:
1)to: 用户空间的指针;
2)from是内核空间的指针;
3)n表示从内核空间向用户空间拷贝数据的字节数;
返回值:
若数据拷贝成功,则返回0,否则返回没有拷贝成功的字节数。
代码举例:
ssize_t hc_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
ssize_t retval = 0;
/* 获得字符设备的地址 ,保存在hc_dev中 */
struct hello_char_dev *hc_dev = filp->private_data;
/* 数据的偏移量是否大于数据的总量 */
if (*f_pos >= hc_dev->n) {
goto out;
}
/* 所需要读取个数与要读取数据之和是否大于数据总量*/
if (*f_pos + count > hc_dev->n) {
count = hc_dev->n - *f_pos;
}
if (copy_to_user(buf, hc_dev->c, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
return count;
out:
return retval;
}
2.5 copy_from_user()
将用户空间的数据拷贝到内核空间, 我们将它用在write()函数中,其使用如下。
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
输入参数:
1)to表示内核空间的数据目标地址指针;
2)from表示用户空间的数据元地址指针;
3)n表示数据的长度;
返回值:
若拷贝成功返回0,否则返回没有拷贝成功的数据字节数。
ssize_t hc_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
struct hello_char_dev *hc_dev = filp->private_data;
int retval = -ENOMEM, i;
/* 清空之前的数据 */
kfree(hc_dev->c);
hc_dev->c = NULL;
hc_dev->n = 0;
hc_dev->c = kzalloc(count, GFP_KERNEL);
if (!hc_dev->c) {
goto out;
}
if (copy_from_user(hc_dev->c, buf, count)) {
retval = -EFAULT;
goto fail_copy;
}
hc_dev->n = count;
return count;
fail_copy:
kfree(hc_dev->c);
out:
return retval;
}
三、编译运行
step1: 编译
step2: 插入内核
step3: 查看设备节点
step4: 读写测试
1)读取字符设备(此时字符设备为空)
2)向空字符设备写数据并查看
3)再次读取设备(此时设备中已写入内容)
【注】我们发现进行一次cat操作,却调用了两次read(),这是因为cat是通过文件结束符来结束读
取操作,第一次是读取了所有数据,此时并不知道已经到了文件末尾,第二次读到的长度为0,此
时才知道读到了文件末尾,cat退出。
四、小结
本章节实现了字符设备具体的读写功能,但仍然存在不足。比如在write()函数中,多个进程同
时写,会造成内存泄露等问题,这些问题在接下来的章节会得到解决。