使⽤cat读取和echo写内核⽂件节点的⼀些问题
平台:
busybox-1.24.2
Linux-4.10.17
Qemu+vexpress-ca9
概述:
在写驱动的时候,我们经常会向⽤户空间导出⼀些⽂件,然后⽤户空间使⽤cat命令去读取该节点,从⽽完成kernel跟user的通信。但是有时会发现,如果节点对应的read回调函数写的有问题的话,使⽤cat命令后,节点对应的read函数会被频繁调⽤,log直接刷屏,⽽我们只希望read被调⽤⼀次,echo也是⼀样的道理。背后的原因是什么呢?如何解决呢?下⾯我们以debugfs下的节点读写为例说明⼀下。
正⽂:
⼀、read和write的介绍:
1、系统调⽤read
ssize_t read(int fd, void *buf, size_t count);
这个函数会从fd表⽰的⽂件描述符中读取count个字节到buf缓冲区当中,返回值有下⾯⼏种:
如果返回值⼤于0,表⽰实际读到的字节数,返回0的话,表⽰读到了⽂件结尾,同时⽂件的file position也会被更新。实际读到的字节数可能会⽐count⼩。如果返回-1,表⽰读取失败,errno会被设置为相应的值。
2、系统调⽤write
ssize_t write(int fd, const void *buf, size_t count);
这个函数将以buf为⾸地址的缓冲区当中的count个字节写到⽂件描述符fd表⽰的⽂件当中,返回值:返回正整数,表⽰实际写⼊的字节数,返回0表⽰没有任何东西被写⼊,同时⽂件位置指针也会被更新返回-1,表⽰写失败,同时errno会被设置为相应的值。
3、LDD3上对驱动中实现的read回调函数的解释
原型:ssize_t (*read) (struct file *fp, char __user *user_buf, size_t count, loff_t *ppos);
fp:被打开的节点的⽂件描述符;
user_buf:表⽰的是⽤户空间的⼀段缓冲区的⾸地址,从kernel读取的数据需要存放该缓冲区当中
count:表⽰⽤户期望读取的字节数;
*ppos:表⽰当前当前⽂件位置指针的⼤⼩,这个值会需要驱动程序⾃⼰来更新,初始⼤⼩是0。
如果返回值等于传递给read系统调⽤的count参数,则说明所请求的字节数传输成功完成。这是最理想的情况。
如果返回值是正的,但是⽐count⼩,则说明只有部分数据传输成功。这种情况下因设备的不同可能有许多原因。⼤部分情况下,程序会再次读数据。例如,如果⽤fread函数读数据,这个库函数就会不断调⽤系统调⽤,直⾄所请求的数据传输完毕为⽌。
如果返回值为0,则表⽰已经达到了⽂件尾。
负值意味着发⽣了错误,该值指明了发⽣了什么错误,错误码在<linux/errno.h>中定义。⽐如这样的⼀些错误:-EINTR(系统调⽤被中断)或者-EFAULT(⽆效地址)。
4、LDD3上对驱动中实现的write回调函数的解释
原型:ssize_t (*write) (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos);
fp:被打开的要写的内核节点的⽂件描述符;
user_buf:表⽰的是⽤户空间的⼀段缓冲区的⾸地址,其中存放的是⽤户需要传递给kernel的数据;
count:⽤户期望写给kernel的字节数;
*ppos:⽂件位置指针,需要驱动程序⾃⼰更新。
如果返回值等于count,则完成了所请求数⽬的字节传输。
如果返回值为正的,但⼩于count,则这传输了部分数据。程序很可能再次试图写⼊余下的数据。
如果返回值为0,意味着什么也没有写⼊。这个结果不是错误,⽽且也没有理由返回⼀个错误码。再次重申,标准库会重复调⽤write。负值意味着发⽣了错误,与read相同,有效的错误码定义在<linux/errno.h>中。
上⾯加粗的红⾊字体引起驱动中的write或者read被反复调⽤的原因。
⼆、简略的分析⼀下read和write系统调⽤的实现
在⽤户空间调⽤read函数后,内核函数vfs_read会被调⽤:
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_READ))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_READ))
return -EINVAL;
if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
return -EFAULT;
ret = rw_verify_area(READ, file, pos, count);
if (!ret) {
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT;
ret = __vfs_read(file, buf, count, pos);
if (ret > 0) {
fsnotify_access(file);
add_rchar(current, ret);
}
inc_syscr(current);
}
return ret;
linux怎么读文件内容
}
下⾯是需要关注的:
第9⾏检查⽤户空间的buf缓冲区是否可以写⼊。
第14⾏检查count的⼤⼩,这⾥MAX_RW_COUNT被设置为1个页的⼤⼩,这⾥的值是4KB,也就是⼀次⽤户⼀次read最多获得4KB数据。
第16⾏调⽤__vfs_read,这个函数最终会调⽤到我们的驱动中的read函数,可以看到这个函数的参数跟驱动中的read函数⼀样,驱动中read返回的数字ret会返回给⽤户,这⾥并没有看到更新pos,所以需要在我们的驱动中⾃⼰去更新。
⽤户空间调⽤write函数后,内核函数vfs_write会被调⽤:
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_WRITE))
return -EINVAL;
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT;
ret = rw_verify_area(WRITE, file, pos, count);
if (!ret) {
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT;
file_start_write(file);
ret = __vfs_write(file, buf, count, pos);
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
}
return ret;
}
这⾥需要关注:
第9⾏,检查⽤户空间的缓冲区buf是否可以读。
第15⾏,限制⼀次写⼊的数据最多为1页,⽐如4KB。
第17⾏的_vfs_write的参数跟驱动中的write的参数⼀样,__vfs_write的返回值ret也就是⽤户调⽤write时的返回值,表⽰实际写⼊的字节数,这⾥也没有看到更新pos的代码,所以需要我们⾃⼰在驱动的write中实现。
三、简略分析cat和echo的实现
由于使⽤的根⽂件系统使⽤busybox做的,所以cat和echo的实现在busybox的源码中,如下:
coreutils/cat.c
coreutils/echo.c
CAT:
下⾯简略分析cat的实现,cat的默认实现采⽤了sendfile,采⽤sendfile可以减少不必要的内存拷贝,从⽽提⾼读写效率,这就是所谓的Linux的“零拷贝”。为了便于代码分析,可以关闭这个功能,然后cat就会调⽤read和write实现了:
Busybox Settings --->
General Configuration --->
[ ] Use sendfile system call
下⾯是cat的核⼼函数:
以cat xxx为例其中src_fd就是被打开的内核节点的⽂件描述符,dst_fd就是标准输出描述符,size是0。
static off_t bb_full_fd_action(int src_fd, int dst_fd, off_t size)
{
int status = -1;
off_t total = 0;
bool continue_on_write_error = 0;
ssize_t sendfile_sz;
char buffer[4 * 1024]; // ⽤户空间缓冲区,4KB⼤⼩
enum { buffer_size = sizeof(buffer) }; // 每次read期望获得的字节数
sendfile_sz = 0;
if (!size) {
size = (16 * 1024 *1024); // 刚开始,如传⼊的size是0,这⾥将size设置为16MB
status = 1; /* 表⽰⼀直读到⽂件结尾,也就是直到read返回0 */
}
while (1) {
ssize_t rd;
rd = safe_read(src_fd, buffer, buffer_size); // 这⾥调⽤的就是read, 读取4KB,rd是实际读到的字节数
if (rd < 0) {
bb_perror_msg(bb_msg_read_error);
break;
}
read_ok:
if (!rd) { /* 表⽰读到了⽂件结尾,那么结束循环 */
status = 0;
break;
}
/* 将读到的内容输出到dst_fd表⽰的⽂件描述符 */
if (dst_fd >= 0 && !sendfile_sz) {
ssize_t wr = full_write(dst_fd, buffer, rd);
if (wr < rd) {
if (!continue_on_write_error) {
bb_perror_msg(bb_msg_write_error);
break;
}
dst_fd = -1;
}
}
total += rd; // total记录的是读到的字节数的累计值
if (status < 0) { /* 如果传⼊的size不为0,那么status为-1,直到读到size个字节后,才会退出。如果size为0,这个条件不会满⾜ */
size -= rd;
if (!size) {
/* 'size' bytes copied - all done */
status = 0;
break;
}
}
}
out:
return status ? -1 : total; // 当读完毕,status为0,这⾥返回累计读到的字节数
}
从上⾯的分析我们知道如下信息:
使⽤cat xxx时,上⾯的函数传⼊的size为0,那么上⾯的while循环会⼀直进⾏read,直到出错或者read返回0,read返回0也就是读到⽂件结尾。最后如果出错,那么返回-1,否则的话,返回读到的累计的字节数。
到这⾥,应该就是知道为什么驱动中的read会被频繁调⽤了吧,也就是驱动中的read的返回值有问题。
ECHO:
echo的核⼼函数是full_write;
这⾥fd是要写的内核节点,buf缓冲区中存放的是要写⼊的内容,len是buf缓冲区中存放的字节数。
ssize_t FAST_FUNC full_write(int fd, const void *buf, size_t len)
{
ssize_t cc;
ssize_t total;
total = 0;
while (len) {
cc = safe_write(fd, buf, len);
if (cc < 0) {
if (total) {
/* we already wrote some! */
/* user can do another write to know the error code */
return total;
}
return cc; /* write() returns -1 on failure. */
}
total += cc;
buf = ((const char *)buf) + cc;
len -= cc;
}
return total;
}
上⾯的函数很简单,可以得到如下信息:
如果write的函数返回值cc⼩于len的话,会⼀直调⽤write,直到报错或者len个字节全部写完。⽽这⾥的cc对应的就是我们的驱动中write的返回值。最后,返回实际写⼊的字节数或者⼀个错误码。
到这⾥,应该也已经清除为什么调⽤⼀次echo后,驱动的write为什么会被频繁调⽤了吧,还是驱动中write的返回值的问题。
知道的上⾯的原因,下⾯我们结合⼀个简单的驱动看看。
四、实例分析
1、先看两个刷屏的例⼦
这个驱动在/sys/kernel/debug⽣成⼀个demo节点,⽀持读和写。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/debugfs.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
static struct dentry *demo_dir;
static ssize_t demo_read(struct file *fp, char __user *user_buf, size_t count, loff_t *ppos)
{
char kbuf[10];
int ret, wrinten;
printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n",
user_buf, count, *ppos);
wrinten = snprintf(kbuf, 10, "%s", "Hello");
ret = copy_to_user(user_buf, kbuf, wrinten+1);
if (ret != 0) {
printk(KERN_ERR "read error");
return -EIO;
}
*ppos += wrinten;
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论