kernel module编程(八):读取proc文件之seq_file

本文介绍Linux内核中使用seq_file替代read_proc的方法,包括创建/proc文件、关联操作及seq_file的具体处理过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文也即《Linux Device Drivers》,LDD3的第四章Debuging Techniques的读书笔记之三,但我们不限于此内容。

  在上次我们使用了read_proc的方式通过/proc文件读取kernel module的信息。作者给的例子他自己说是ugly。而我们在读取大量数据时发现,受到用户buffer大小的限制(page的大小),可能需要读取多 次,不仅需要记录上次读取的位置,而且由于每次读取我们申请了信号量,读取完释放,那么如果多次读取的间隔中,如果信号量被写所获取就好出现混乱。 linux kernel提供seq_file更好的方式来解决这个问题,除非我们确定读取的信息量非常少,能够在page中返回,我们应使用seq_file的方式 而不是read_proc 。

  LDD3中介绍的方式,我觉得是典型的西方人和中国人思维方式的不同。在seq_file的介绍中,LDD3先从每个操作具体将其,然后到如何 和proc文件联系,最后到如何创建proc文件,我喜欢反过来的方式,先创建proc,在一步步细化。老外是日月年,我们是年月日,嘿嘿。 seq_file的处理方式开看有点发展,步骤有些多,但是安全,是规范的处理方式。

步骤一:建立proc文件。

  通过一个struct proc_dir_entry的元素,在/proc中建立文件,如下:

struct proc_dir_entry * entry = create_proc_entry(“scullseq”,0,NULL)。参数的内容和read_proc,第一个参数表示文件名,第二个参数表示文 件属性,对于只读方式为0,第三个参数表示文件路径,NULL表示缺省路径,即/proc。

步骤二:关联proc的操作。

   需要对文件进行操作,见过文件和struct file_operations相关联,我们注意到这个数据结构也用于模块操作关联中。具体操作如下:

#ifdef SCULL_SEQ_FILE
/* 步骤二:2、定义proc文件所关联的文件操作数据 */
static struct file_operations scull_proc_ops = {
.owner = THIS_MODULE,
.open = scull_proc_open, //open通常这是我们唯一需要重新定义的函数,需要和特定的seq_file关联起来。
.read = seq_read, //采用系统的处理方式
.llseek = seq_lseek, //采用系统的处理方式
.release = seq_release, //采用系统的处理方式
};

/* 步骤二:4、在前面的步骤二1~3中我们创建了proc文件,关联了proc文件和file_operations,并进一步关联了seq_file,这里我们具体定义被关联的seq_file */
static struct seq_operations scull_seq_ops = {
.start = scull_seq_start,
.next = scull_seq_next,
.stop = scull_seq_stop,
.show = scull_seq_show,
};

static struct proc_dir_entry * entry;
#endif
… …
static int __init scull_init(void)
{
… …
#ifdef SCULL_SEQ_FILE
/* 步骤一:创建proc文件*/
entry = create_proc_entry ("scullseq", 0 ,NULL);
/* 步骤二:1、将proc文件和对应的文件操作关联起来*/
if(entry)
entry -> proc_fops = & scull_proc_ops ;
#endif
}
... ...

static void __exit scull_exit(void)
{
#ifdef SCULL_SEQ_FILE

/* 我们在模块中创建的proc文件,都应该模块cleanup模块的时候删除,以防影响系统,另外,我们应该在删除模块函数的开始执行这个操作,防止相关联的数据已经删除或者注销后再来处理,避免异常出现。 */
remove_proc_entry("scullseq",NULL );
#endif
if(is_get_dev < 0){
return ;
}else{
int i = 0;
for(i = 0; i < SCULL_DEV_NUM; i ++){
scull_trim(&mydev[i]);
cdev_del( & mydev[i].cdev );
}
unregister_chrdev_region(dev,SCULL_DEV_NUM);
WDEBUG(WEI_KERN_NOTICE,"Scull module exit\n");
}
}

/* 步骤二:3、具体实现proc文件的open操作,目的与seq_file相关联。*/
int scull_proc_open(struct inode * inode , struct file * file)
{
return seq_open(file, & scull_seq_ops );
}

步骤三:处理seq_file操作过程。

  seq_file操作定义了四个操作,格式如下:

void * start(struct seq_file * s, loff_t * v);
void * next (struct seq_file * s, void * v, loff_t * pos);
void stop (struct seq_file * s, void * v);
int show (struct seq_file * s, void * v);

  其中loff_t表示位置,这是由我们自己程序控制的,初始为0,在scull中我们依次读取scull0-3,因此使用该偏移量来表示我们所读取的设备的序号。

  我们利用void * v来记录设备的入口位置。start根据编译量,即我们的设备的序号,返回scullx的入口位置,无论下一操作是next,stop,还是show,这 个返回值会作为参数void *v输入。next表示下一查询,和start相似,只是多了void * v的输入,同样它的返回值也作为下一操作的参数void *v输入。show用于通过/proc文件输出。stop表示一次读取的结束。虽然在seq_file中和read_proc不一样,不需要考虑每次可以 输出的buff的大小,但是实际读取不会连续一片很大的数据输出,在例子后面,我们将讨论这些操作的执行。

  输出方式非常简单,一般可以使用seq_printf,另外还有seq_putc,seq_puts,seq_escape。例子如下:

#ifdef SCULL_SEQ_FILE
void * scull_seq_start (struct seq_file * s, loff_t * pos)
{
printk("==scull_seq_start() enter %p %p %lli\n", s , pos , * pos);
if( * pos >= SCULL_DEV_NUM)
return NULL;
else
return mydev + * pos;
}

void * scull_seq_next (struct seq_file * s, void * v, loff_t * pos)
{
printk("==scull_seq_next() enter %p %p %p %lli\n", s , v, pos , * pos);
(* pos ) ++;
return scull_seq_start(s, pos);
}

void scull_seq_stop (struct seq_file * s, void * v)
{
printk("==scull_seq_stop() enter\n");
return ;
}

int scull_seq_show (struct seq_file * s, void * v)
{
struct scull_dev * dev = (struct scull_dev *) v;
struct scull_qset * qs = NULL;
int j = 0;
printk("==scull_seq_show() enter\n");

if(down_interruptible(&dev->sem))
return -ERESTARTSYS;

seq_printf (s, "\n Device Scull%d: qset %i, q %i sz %li\n", (int) (dev - mydev),dev->qset, dev->quantum, dev->size);
printk("\n Device Scull%d: qset %i, q %i sz %li\n", (int) (dev - mydev),dev->qset, dev->quantum, dev->size);

for(qs = dev->data ; qs ; qs=qs->next){
seq_printf ( s," item at %p, qset at %p\n", qs, qs->data);
printk(" item at %p, qset at %p\n", qs, qs->data);
if(qs->data ){
for(; j < dev->qset /* && qs->data[ j ] */ ;j++){
seq_printf (s ,"\t%4i:%8p\n", j,qs->data[ j ]);
printk("\t%4i:%8p\n", j,qs->data[ j ]);
}
}
}

up(&dev->sem);
return 0;
}

  我们描述一下处理的过程:当我们读取proc文件,例如cat /proc/scullseq时,我们假设scull0和scull1都有较多信息输入。

  一开始调用start,偏移量为0,返回scull0的入口,接着调用show,scull0的入口作为参数输入,在show中,我们可以遍历 scull0的数据结构,通过seq_printf输出。完成show后,由于输出信息多,进入stop,在例子中stop没有实际操作,我们只是用来跟 踪处理的流程。

   再次调用start,偏移量步进1,即1,返回scull0的入口,接着调用show,scull1的入口作为参数输入,在show中,我们可以遍历scull1的数据结构,通过seq_printf输出。完成show后,由于输出信息多,进入stop。

  再次调用start,偏移量步进1,即2,返回scull2的入口,接着调用show,scull2的入口作为参数输入,在show中,我们可 以遍历scull2的数据结构,通过seq_printf输出。完成show后,由于输出信息非常少,kernel认为可以继续进行操作,而不需要 stop,调用next(),在next参数中输入的参数loff_t为2,next将其加一,为3,返回scull3的入口。接着调用 show,scull3的入口作为参数输入,在show中,我们可以遍历scull3的数据结构,通过seq_printf输出。完成show后,由于输 出信息非常少,kernel认为可以继续进行操作,而不需要stop,调用next()。由于已经全部信息返回,在next中发现没有数据,返回 NULL。系统再次调用start,返回NULL,系统调用stop,结束这次输出。

  再次调用start,返回NULL,表示已经没有数据输出,调用stop,结束所有的输出。

  值得注意 seq_file 代码在调用 start 和 stop 之间不睡眠或者进行其他非原子性任务. 你也肯定会看到在调用 start 后马上有一个 stop 调用. 因此, 对你的 start 方法来说请求信号量或自旋锁是安全的. 只要你的其他 seq_file 方法是原子的, 调用的整个序列是原子的.(http://www.deansys.com/doc/ldd3/ch04s03.html)

#include <linux/module.h> #include <linux/kernel.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> #include <linux/ip.h> #include <linux/icmp.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> #include <linux/slab.h> #include <linux/spinlock.h> #include <linux/timekeeping.h> #include <linux/version.h> #define MAX_ENTRIES 1000 // 最大统计条目数 #define PROC_NAME “ping_stat” // proc文件名 // 统计数据结构 struct ping_stat_entry { struct timespec64 timestamp; // 时间戳 __be32 src_ip; // 源IP地址 struct list_head list; // 链表指针 }; // 全局变量 static struct list_head stat_list; // 统计链表 static DEFINE_SPINLOCK(stat_lock); // 保护链表的自旋锁 static unsigned int param_n = 100; // 默认数据大小 static unsigned int packet_count = 0; // 总包计数 static struct proc_dir_entry *proc_entry; // proc文件入口 // 模块参数声明 module_param(param_n, uint, 0); MODULE_PARM_DESC(param_n, “ICMP data size to filter (100-1000)); // Netfilter钩子函数 static unsigned int hook_func(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct iphdr *ip_header; struct icmphdr *icmp_header; unsigned int data_size; struct timespec64 now; struct ping_stat_entry *new_entry; // 获取IP头 ip_header = ip_hdr(skb); if (!ip_header || ip_header->version != 4) return NF_ACCEPT; // 检查是否为ICMP协议 if (ip_header->protocol != IPPROTO_ICMP) return NF_ACCEPT; // 获取ICMP头 icmp_header = (struct icmphdr *)((char *)ip_header + (ip_header->ihl * 4)); if (!icmp_header) return NF_ACCEPT; // 检查是否为回显请求(ping) if (icmp_header->type != ICMP_ECHO) return NF_ACCEPT; // 计算数据部分大小 data_size = ntohs(ip_header->tot_len) - (ip_header->ihl * 4) - sizeof(struct icmphdr); // 检查数据大小是否符合要求 if (data_size != param_n) return NF_ACCEPT; // 获取当前时间 ktime_get_real_ts64(&now); // 创建新条目 new_entry = kmalloc(sizeof(*new_entry), GFP_ATOMIC); if (!new_entry) return NF_ACCEPT; // 填充数据 new_entry->timestamp = now; new_entry->src_ip = ip_header->saddr; // 加锁保护链表 spin_lock(&stat_lock); // 更新统计信息 packet_count++; // 维护链表大小 if (packet_count > MAX_ENTRIES) { struct ping_stat_entry *old_entry; old_entry = list_first_entry(&stat_list, struct ping_stat_entry, list); list_del(&old_entry->list); kfree(old_entry); } // 添加到链表尾部 list_add_tail(&new_entry->list, &stat_list); spin_unlock(&stat_lock); return NF_ACCEPT; } // Netfilter钩子配置 static struct nf_hook_ops nfho = { .hook = hook_func, .pf = NFPROTO_IPV4, .hooknum = NF_INET_PRE_ROUTING, .priority = NF_IP_PRI_FIRST, }; // seq_file操作:开始迭代 static void *ping_seq_start(struct seq_file *s, loff_t *pos) { spin_lock(&stat_lock); return seq_list_start(&stat_list, *pos); } // seq_file操作:下一个元素 static void *ping_seq_next(struct seq_file *s, void *v, loff_t *pos) { return seq_list_next(v, &stat_list, pos); } // seq_file操作:结束迭代 static void ping_seq_stop(struct seq_file *s, void *v) { spin_unlock(&stat_lock); } // seq_file操作:显示元素 static int ping_seq_show(struct seq_file *s, void *v) { struct ping_stat_entry *entry = list_entry(v, struct ping_stat_entry, list); char ip_str[16]; // 格式化IP地址 snprintf(ip_str, sizeof(ip_str), "%pI4", &entry->src_ip); // 输出统计信息 seq_printf(s, "Time: %lld.%09ld | Source: %s\n", (long long)entry->timestamp.tv_sec, entry->timestamp.tv_nsec, ip_str); return 0; } // seq_file操作结构 static const struct seq_operations ping_seq_ops = { .start = ping_seq_start, .next = ping_seq_next, .stop = ping_seq_stop, .show = ping_seq_show, }; // 打开proc文件 static int ping_proc_open(struct inode *inode, struct file *file) { return seq_open(file, &ping_seq_ops); } // proc文件操作 static const struct proc_ops ping_proc_ops = { .proc_open = ping_proc_open, .proc_read = seq_read, .proc_lseek = seq_lseek, .proc_release = seq_release, }; // 模块初始化 static int __init ping_stat_init(void) { // 检查参数范围 if (param_n < 100 || param_n > 1000) { printk(KERN_ERR “Invalid param_n value (%d), must be 100-1000\n”, param_n); return -EINVAL; } // 初始化链表和锁 INIT_LIST_HEAD(&stat_list); // 注册Netfilter钩子 if (nf_register_net_hook(&init_net, &nfho)) { printk(KERN_ERR "Failed to register netfilter hook\n"); return -ENODEV; } // 创建proc文件 proc_entry = proc_create(PROC_NAME, 0, NULL, &ping_proc_ops); if (!proc_entry) { nf_unregister_net_hook(&init_net, &nfho); printk(KERN_ERR "Failed to create proc entry\n"); return -ENOMEM; } printk(KERN_INFO "Ping stat module loaded, filtering %d byte packets\n", param_n); return 0; } // 模块退出 static void __exit ping_stat_exit(void) { struct ping_stat_entry *entry, *tmp; // 移除proc文件 if (proc_entry) proc_remove(proc_entry); // 注销Netfilter钩子 nf_unregister_net_hook(&init_net, &nfho); // 清理链表内存 spin_lock(&stat_lock); list_for_each_entry_safe(entry, tmp, &stat_list, list) { list_del(&entry->list); kfree(entry); } spin_unlock(&stat_lock); printk(KERN_INFO "Ping stat module unloaded\n"); } module_init(ping_stat_init); module_exit(ping_stat_exit); MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Your Name”); MODULE_DESCRIPTION(“ICMP Ping Packet Statistics Module);我希望在ping_seq_show函数中加入显示报文数目,包字节大小,时间用年月日时分秒的格式显示
最新发布
08-05
#include <linux/module.h> #include <linux/kernel.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> #include <linux/ip.h> #include <linux/icmp.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> #include <linux/slab.h> #include <linux/spinlock.h> #include <linux/timekeeping.h> #include <linux/version.h> #define MAX_ENTRIES 1000 // 最大统计条目数 #define PROC_NAME "ping_stat" // proc文件名 // 统计数据结构 struct ping_stat_entry { struct timespec64 timestamp; // 时间戳 __be32 src_ip; // 源IP地址 struct list_head list; // 链表指针 }; // 全局变量 static struct list_head stat_list; // 统计链表 static DEFINE_SPINLOCK(stat_lock); // 保护链表的自旋锁 static unsigned int param_n = 100; // 默认数据大小 static unsigned int packet_count = 0; // 总包计数 static struct proc_dir_entry *proc_entry; // proc文件入口 // 模块参数声明 module_param(param_n, uint, 0); MODULE_PARM_DESC(param_n, "ICMP data size to filter (100-1000)"); // Netfilter钩子函数 static unsigned int hook_func(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct iphdr *ip_header; struct icmphdr *icmp_header; unsigned int data_size; struct timespec64 now; struct ping_stat_entry *new_entry; // 获取IP头 ip_header = ip_hdr(skb); if (!ip_header || ip_header->version != 4) return NF_ACCEPT; // 检查是否为ICMP协议 if (ip_header->protocol != IPPROTO_ICMP) return NF_ACCEPT; // 获取ICMP头 icmp_header = (struct icmphdr *)((char *)ip_header + (ip_header->ihl * 4)); if (!icmp_header) return NF_ACCEPT; // 检查是否为回显请求(ping) if (icmp_header->type != ICMP_ECHO) return NF_ACCEPT; // 计算数据部分大小 data_size = ntohs(ip_header->tot_len) - (ip_header->ihl * 4) - sizeof(struct icmphdr); // 检查数据大小是否符合要求 if (data_size != param_n) return NF_ACCEPT; // 获取当前时间 ktime_get_real_ts64(&now); // 创建新条目 new_entry = kmalloc(sizeof(*new_entry), GFP_ATOMIC); if (!new_entry) return NF_ACCEPT; // 填充数据 new_entry->timestamp = now; new_entry->src_ip = ip_header->saddr; // 加锁保护链表 spin_lock(&stat_lock); // 更新统计信息 packet_count++; // 维护链表大小 if (packet_count > MAX_ENTRIES) { struct ping_stat_entry *old_entry; old_entry = list_first_entry(&stat_list, struct ping_stat_entry, list); list_del(&old_entry->list); kfree(old_entry); } // 添加到链表尾部 list_add_tail(&new_entry->list, &stat_list); spin_unlock(&stat_lock); return NF_ACCEPT; } // Netfilter钩子配置 static struct nf_hook_ops nfho = { .hook = hook_func, .pf = NFPROTO_IPV4, .hooknum = NF_INET_PRE_ROUTING, .priority = NF_IP_PRI_FIRST, }; // seq_file操作:开始迭代 static void *ping_seq_start(struct seq_file *s, loff_t *pos) { spin_lock(&stat_lock); return seq_list_start(&stat_list, *pos); } // seq_file操作:下一个元素 static void *ping_seq_next(struct seq_file *s, void *v, loff_t *pos) { return seq_list_next(v, &stat_list, pos); } // seq_file操作:结束迭代 static void ping_seq_stop(struct seq_file *s, void *v) { spin_unlock(&stat_lock); } // seq_file操作:显示元素 static int ping_seq_show(struct seq_file *s, void *v) { struct ping_stat_entry *entry = list_entry(v, struct ping_stat_entry, list); char ip_str[16]; // 格式化IP地址 snprintf(ip_str, sizeof(ip_str), "%pI4", &entry->src_ip); // 输出统计信息 seq_printf(s, "Time: %lld.%09ld | Source: %s\n", (long long)entry->timestamp.tv_sec, entry->timestamp.tv_nsec, ip_str); return 0; } // seq_file操作结构 static const struct seq_operations ping_seq_ops = { .start = ping_seq_start, .next = ping_seq_next, .stop = ping_seq_stop, .show = ping_seq_show, }; // 打开proc文件 static int ping_proc_open(struct inode *inode, struct file *file) { return seq_open(file, &ping_seq_ops); } // proc文件操作 static const struct proc_ops ping_proc_ops = { .proc_open = ping_proc_open, .proc_read = seq_read, .proc_lseek = seq_lseek, .proc_release = seq_release, }; // 模块初始化 static int __init ping_stat_init(void) { // 检查参数范围 if (param_n < 100 || param_n > 1000) { printk(KERN_ERR "Invalid param_n value (%d), must be 100-1000\n", param_n); return -EINVAL; } // 初始化链表和锁 INIT_LIST_HEAD(&stat_list); // 注册Netfilter钩子 if (nf_register_net_hook(&init_net, &nfho)) { printk(KERN_ERR "Failed to register netfilter hook\n"); return -ENODEV; } // 创建proc文件 proc_entry = proc_create(PROC_NAME, 0, NULL, &ping_proc_ops); if (!proc_entry) { nf_unregister_net_hook(&init_net, &nfho); printk(KERN_ERR "Failed to create proc entry\n"); return -ENOMEM; } printk(KERN_INFO "Ping stat module loaded, filtering %d byte packets\n", param_n); return 0; } // 模块退出 static void __exit ping_stat_exit(void) { struct ping_stat_entry *entry, *tmp; // 移除proc文件 if (proc_entry) proc_remove(proc_entry); // 注销Netfilter钩子 nf_unregister_net_hook(&init_net, &nfho); // 清理链表内存 spin_lock(&stat_lock); list_for_each_entry_safe(entry, tmp, &stat_list, list) { list_del(&entry->list); kfree(entry); } spin_unlock(&stat_lock); printk(KERN_INFO "Ping stat module unloaded\n"); } module_init(ping_stat_init); module_exit(ping_stat_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("ICMP Ping Packet Statistics Module");改进代码,我需要在ping_seq_show里显示报文数目
08-05
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值