seq文件传输
一、PROC读写
在进行proc
接口创建的时候,我们需要制定对应的文件操作函数,即创建的proc
文件被读、写的时候要调用的函数,也就是我们的回调函数。其结构体定义如下:
struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
......
} __randomize_layout;
这个结构体也是VFS
通用的文件操作函数设置的结构体,由于procfs
相对比较简单,因此里面大部分的字段都是用不到的,这里只列出常用的几个。
open
:打开文件时会被调用read
:读取文件时调用,其中参数包括要读取到的用户态buf
、要读取的长度和开始的位置write
:对于某些proc
接口,可以支持写操作llseek
:定位当前读取文件到指定位置。大部分proc
接口都是顺序读的,但是有些接口也支持随机访问,比如/proc/<pid>/mem
从这里我们可以看出来,当用户顺序读取proc
接口时,我们的实现是很不方便的。例如,用户要读取/proc/<pid>/status
,read方法会被调用。此时我们会把进程的状态信息写到用户态传递下来的buf
,但是由于该buf
可能不够大,因此我们只能写一部分进去。之后,用户会再次调用read
,想从刚才的地方pos
继续往下读,这时我们就不好实现了。
二、SEQ文件
为了方便这种序列化的读取,内核封装了struct seq_file
,即序列化读取数据,增强了proc
对于大数据的支持。下面简单说一下其实现,其过程可以大致分为以下几步:
- 定义open函数,在open函数中创建并初始化
struct seq_file
,一般直接调用seq_open(file, op)
即可,该函数会创建个seq_file
实例,并添加到file->private_data
。创建的时候,还需要指定seq_file
的操作函数,这个下面分析; - 将
read
函数设置为seq_read
,这是一个内核已经封装好的基于seq
机制的读接口; - 将
write
函数设置为seq_write
,这是一个内核已经封装好的基于seq
机制的写接口; - 将
release
设置为seq_release
,该函数用于完事后释放资源。
经过上述设置,在用户读取proc
的时候,seq_read
就会被调用。我们简单来看一下其具体实现。这个函数的参数我们上面已经分析过了,值得注意的是*ppos
为要读取的位置。
ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
struct seq_file *m = file->private_data;
size_t copied = 0;
size_t n;
void *p;
int err = 0;
/* 常规的加锁保护,防止并发read */
mutex_lock(&m->lock);
m->version = file->f_version;
/*
* 如果是从开头读取的话,那么将数据进行初始化设置
*/
if (*ppos == 0) {
m->index = 0;
m->version = 0;
m->count = 0;
}
/* 一般上一次读到的位置会保存在m->read_pos里,但是也有可能用户不进行顺序读取。
* 这个比较少见,这里还是要预防一下。
*/
if (unlikely(*ppos != m->read_pos)) {
while ((err = traverse(m, *ppos)) == -EAGAIN)
;
if (err) {
/* With prejudice... */
m->read_pos = 0;
m->version = 0;
m->index = 0;
m->count = 0;
goto Done;
} else {
m->read_pos = *ppos;
}
}
/* seq_file有个自带的buf缓冲区,其大小为一个页。因此使用seq_file,我们的数据并不是存储到用户态buf,
* 而是先存储到seq_file的缓冲区
*/
if (!m->buf) {
m->buf = seq_buf_alloc(m->size = PAGE_SIZE);
if (!m->buf)
goto Enomem;
}
/* count里存储的是当前seq_file缓冲区中数据的长度。如果里面还有数据,那就先把这部分数据给用户。 */
if (m->count) {
n = min(m->count, size);
err = copy_to_user(buf, m->buf + m->from, n);
if (err)
goto Efault;
m->count -= n;
m->from += n;
size -= n;
buf += n;
copied += n;
if (!size)
goto Done;
}
/* 走到这里,说明缓冲区是空的,需要读取数据了 */
m->from = 0;
/* 下面就开始调用seq_file上面的操作函数了,注意调用顺序,分别是:start、show、next和stop */
/* start主要完成一次读取前的初始化,包括根据当前index计算出对应的元素,这个取决于具体的实现。
* 当返回值为NULL或者ERR的时候,会停止读取,即认为已经读取完毕。
* 注意:一次读取只会分配一次buf,调用一次start,但是可能会调用多次show和next,直到把buf填满
*/
p = m->op->start(m, &m->index);
while (1) {
err = PTR_ERR(p);
if (!p || IS_ERR(p))
break;
/* 这个是核心函数,用于进行数据的输出,即将被读取的数据使用对应的seq帮助函数输出到缓冲区,
* 帮助函数有seq_printf、seq_putc等,很方便。
*/
err = m->op->show(m, p);
if (err < 0)
break;
if (unlikely(err))
m->count = 0;
/* 如果没有读取到数据的话,那么调用next,将index移动到下一个元素,这个也是根据具体实现的。 */
if (unlikely(!m->count)) {
p = m->op->next(m, p, &m->index);
continue;
}
/* 如果读取到的数据没有把缓冲区填满,那么跳转到Fill */
if (m->count < m->size)
goto Fill;
/* 走到这里,说明缓冲区溢出了,那么把缓冲区释放,并分配一个更大的缓冲区,重新读取一次 */
m->op->stop(m, p);
kvfree(m->buf);
m->count = 0;
m->buf = seq_buf_alloc(m->size <<= 1);
if (!m->buf)
goto Enomem;
m->version = 0;
p = m->op->start(m, &m->index);
}
m->op->stop(m, p);
m->count = 0;
goto Done;
Fill:
/* they want more? let's try to get some more */
while (1) {
size_t offs = m->count;
loff_t pos = m->index;
/* 前面调用过了show,这里要调用一次next,移动到下一个元素 */
p = m->op->next(m, p, &m->index);
if (pos == m->index)
/* Buggy ->next function */
m->index++;
if (!p || IS_ERR(p)) {
err = PTR_ERR(p);
break;
}
if (m->count >= size)
break;
/* 继续读取 */
err = m->op->show(m, p);
/* 如果这次读取溢出了,那么这次的读取就不算数,并且结束读取过程。 */
if (seq_has_overflowed(m) || err) {
m->count = offs;
if (likely(err <= 0))
break;
}
}
m->op->stop(m, p);
n = min(m->count, size);
/* 拷贝到用户态 */
err = copy_to_user(buf, m->buf, n);
if (err)
goto Efault;
copied += n;
m->count -= n;
m->from = n;
Done:
if (!copied)
copied = err;
else {
/* 更新对应的位置信息 */
*ppos += copied;
m->read_pos += copied;
}
file->f_version = m->version;
mutex_unlock(&m->lock);
return copied;
Enomem:
err = -ENOMEM;
goto Done;
Efault:
err = -EFAULT;
goto Done;
}
整体的流程如下图所示: