今天在调试driver时,当查看一个proc文件时,碰到一个问题, 之后发现是因为现实内容查过了4Kb(也就是一个page的大小), 所以,理所当然使用的kernel提供的seq_file的方式, 不过,经过修改之后问题依旧,最后通过查看src,发现原来是自己对seq_file理解还不够深刻。
问题首先出现在网上找到的一片关于seq_file使用的文章,其中主要的逻辑图如下:
这里,有几点没有涉及:
- 这里并没有说明show在哪里被调用。
- 如果内容大于一个page时,处理逻辑又是怎样的。
下面就结合代码(linux-2.6.32-220.el6),一一解释。
Essential
首先是关键的数据结构:
struct seq_file {
char *buf;
size_t size;
size_t from;
size_t count;
loff_t index;
loff_t read_pos;
u64 version;
struct mutex lock;
const struct seq_operations *op;
void *private;
};
- size: 申请的buf的大小。
- from: 实际内容在buf中offset。
- count: 实际内容的大小。
- read_pos: 当前在文件的offset。
其中buf初始大小为PAGE_SIZE
m->buf = kmalloc(m->size = PAGE_SIZE, GFP_KERNEL);
seq_operations定义如下:
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
- start: 开始迭代的callback,返回序列中第一个元素(user definition)
- stop: 暂停迭代的callback
- next: 返回序列中的下一个元素,如果没有则返回NULL
- show: 将元素写入写入到buf中
其中seq_file提供了show的方法,拿seq_printf为例:
int seq_printf(struct seq_file *m, const char *f, ...)
{
va_list args;
int len;
if (m->count < m->size) {
va_start(args, f);
len = vsnprintf(m->buf + m->count, m->size - m->count, f, args);
va_end(args);
if (m->count + len < m->size) {
m->count += len;
return 0;
}
}
m->count = m->size;
return -1;
}
可以看出,如果buf中的内容超过buf的大小,则返回错误(-1).
知道了这些,我们可以看下seq_read的逻辑。
Normal case
首先看看一般正常情形下的处理逻辑:
- start开始迭代
- show将第一个元素写入buf
- 如果buf为空,调用next返回下一个元素,返回步骤2
- 如果buf不为空,且user buffer中还有空间,调用next返回下一个元素继续show。 知道buf满了为止。
- 暂停迭代,调用stop callback。将buf中的内容copy到user buffer中, 更新count,from,read_pos
这里牵涉到2个buffer:
每次show只是将序列中的一个元素copy到kernel buffer中, 而一次seq_read会将kernel buffer中的内容copy到user buffer中。 这里为了提高性能,如果user buffer中空闲的大小大于kernel buffer中内容的大小,会尽可能填充kernel buffer。
Corner case
下面来看2个异常的case
- 如果一次show写入的内容超过buf size怎么办? 答案是重新申请一个更大的buf,再试一次。
...
m->op->stop(m, p);
kfree(m->buf);
m->buf = kmalloc(m->size <<= 1, GFP_KERNEL);
if (!m->buf)
goto Enomem;
m->count = 0;
m->version = 0;
pos = m->index;
p = m->op->start(m, &pos);
...
- 由于seq_file支持seek,这就read offset就有可能会改变, 那么如果read offset和seq_file中记录的offset(即read_pos字段)不一致怎么办? 答案是从头开始,模拟一次读写过程,就好象之前已经读到该offset位置。
...
p = m->op->start(m, &index);
while (p) {
error = PTR_ERR(p);
if (IS_ERR(p))
break;
error = m->op->show(m, p);
if (error < 0)
break;
if (unlikely(error)) {
error = 0;
m->count = 0;
}
if (m->count == m->size)
goto Eoverflow;
if (pos + m->count > offset) {
m->from = offset - pos;
m->count -= m->from;
m->index = index;
break;
}
pos += m->count;
m->count = 0;
if (pos == offset) {
index++;
m->index = index;
break;
}
p = m->op->next(m, p, &index);
}
m->op->stop(m, p);
m->index = index;
...
其中offset为经过修改的read offset。
Conclusion
进过上述的分析,给出一个经过修改的逻辑图:
其中红色为异常情况,绿色为一般正常流程,数字表示执行顺序。
FIN