Linux内核procfs读写接口:proc_create实现完全指南

Linux内核procfs读写接口:proc_create实现完全指南

【免费下载链接】linux Linux kernel source tree 【免费下载链接】linux 项目地址: https://gitcode.com/GitHub_Trending/li/linux

引言:你还在为procfs接口开发头疼吗?

在Linux内核开发中,procfs(Process File System,进程文件系统)是用户空间与内核空间通信的重要桥梁。通过/proc目录下的虚拟文件,用户可以轻松获取系统状态、配置内核参数,甚至实现复杂的交互逻辑。然而,大多数开发者在实现procfs接口时,仍面临以下痛点:

  • 不清楚proc_create系列函数的选型策略
  • 读写处理逻辑与内核安全规范冲突
  • 无法高效调试接口性能问题
  • 不理解proc_dir_entry结构体的生命周期管理

本文将系统讲解proc_create接口的实现原理,通过10+实战案例、5个核心结构体解析和3种高级应用场景,帮助你彻底掌握procfs接口开发。读完本文你将获得

  • 精准选择proc_create/proc_create_single/proc_create_seq的能力
  • 编写安全高效的procfs读写处理函数的方法
  • 排查常见内存泄漏和并发访问问题的技巧
  • 实现复杂状态展示和配置接口的完整方案

一、procfs核心架构与proc_create函数族

1.1 procfs的内核架构

procfs采用虚拟文件系统架构,所有文件操作最终映射到内核函数调用。其核心数据流向如下:

mermaid

关键结构体关系:

mermaid

1.2 proc_create函数族选型指南

Linux内核提供了多组procfs创建函数,适用于不同场景:

函数原型适用场景核心优势典型案例
proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct proc_ops *proc_ops)通用读写接口全功能自定义,支持复杂交互/proc/stat/proc/cmdline
proc_create_single_data(const char *name, umode_t mode, struct proc_dir_entry *parent, int (*show)(struct seq_file *, void *), void *data)只读简单数据简化实现,自动处理seq_file/proc/meminfo/proc/cpuinfo
proc_create_seq_private(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct seq_operations *ops, unsigned int state_size, void *data)大型列表数据支持分页和迭代,适合大输出/proc/slabinfo/proc/timer_list
proc_create_data(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct proc_ops *proc_ops, void *data)需要私有数据直接传递自定义数据指针设备驱动状态接口

选型决策树

mermaid

二、proc_create实现原理深度解析

2.1 proc_create函数源码剖析

proc_create函数定义于fs/proc/generic.c,是创建procfs文件的基础接口:

struct proc_dir_entry *proc_create(const char *name, umode_t mode,
				   struct proc_dir_entry *parent,
				   const struct proc_ops *proc_ops)
{
	return proc_create_data(name, mode, parent, proc_ops, NULL);
}
EXPORT_SYMBOL(proc_create);

struct proc_dir_entry *proc_create_data(const char *name, umode_t mode,
		struct proc_dir_entry *parent,
		const struct proc_ops *proc_ops, void *data)
{
	struct proc_dir_entry *p;

	p = proc_create_reg(name, mode, &parent, data);
	if (!p)
		return NULL;
	p->proc_ops = proc_ops;
	return proc_register(parent, p);
}
EXPORT_SYMBOL(proc_create_data);

其核心工作流程分为三步:

  1. 路径解析与父目录定位:通过__xlate_proc_name解析路径,定位父目录proc_dir_entry
  2. 目录项创建:调用proc_create_reg分配并初始化proc_dir_entry结构体
  3. 注册与链接:通过proc_register将新目录项插入父目录的红黑树结构

2.2 proc_dir_entry结构体详解

proc_dir_entry是procfs的核心元数据结构,定义于fs/proc/internal.h

struct proc_dir_entry {
	/*
	 * number of callers into module in progress;
	 * negative -> it's going away RSN
	 */
	atomic_t in_use;
	refcount_t refcnt;
	struct list_head pde_openers;	/* who did ->open, but not ->release */
	/* protects ->pde_openers and all struct file->private_data in this dir */
	spinlock_t pde_unload_lock;
	struct completion *pde_completion;
	const char *name;		/* Name of this entry */
	mode_t mode;			/* File mode */
	nlink_t nlink;			/* Number of links */
	kuid_t uid;			/* File uid */
	kgid_t gid;			/* File gid */
	loff_t size;			/* Size of this file */
	struct file_operations *proc_fops;	/* File operations */
	const struct inode_operations *proc_iops;	/* Inode operations */
	const struct proc_ops *proc_ops;	/* Proc operations */
	const struct seq_operations *seq_ops;	/* Seq operations */
	struct proc_dir_entry *pde_parent;	/* Parent directory */
	struct rb_node subdir_node;	/* subdir rb tree node */
	struct rb_root subdir;		/* subdirectories */
	void *data;			/* User data */
	u32 namelen;			/* Length of name */
	char inline_name[];		/* Inline name storage */
	/* If low_ino is set, the inode number is fixed; otherwise, it's dynamic */
	unsigned int low_ino;
	unsigned int namelen;
	/* PDE flags, see below */
	unsigned int flags;
	/* For procfs internal use */
	struct proc_dir_entry *parent;
};

关键成员解析:

  • name/namelen:文件名及长度,短名称使用inline_name优化存储
  • mode:文件权限位,如S_IRUGO(0444)表示只读,S_IWUSR(0200)表示用户可写
  • proc_ops:核心操作函数集,连接内核实现与用户空间调用
  • data:私有数据指针,用于在操作函数间传递上下文
  • subdir/subdir_node:红黑树结构,管理子目录项
  • low_ino:inode编号,动态分配自PROC_DYNAMIC_FIRST(0xF0000000)开始的范围

2.3 proc_ops操作集定义

proc_ops结构体定义了procfs文件的所有操作,替代了旧版的file_operations

struct proc_ops {
	int	(*proc_open)(struct inode *, struct file *);
	ssize_t	(*proc_read)(struct file *, char __user *, size_t, loff_t *);
	ssize_t	(*proc_write)(struct file *, const char __user *, size_t, loff_t *);
	loff_t	(*proc_lseek)(struct file *, loff_t, int);
	int	(*proc_release)(struct inode *, struct file *);
	__poll_t (*proc_poll)(struct file *, struct poll_table_struct *);
	long	(*proc_ioctl)(struct file *, unsigned int, unsigned long);
#ifdef CONFIG_COMPAT
	long	(*proc_compat_ioctl)(struct file *, unsigned int, unsigned long);
#endif
	int	(*proc_mmap)(struct file *, struct vm_area_struct *);
	unsigned long (*proc_get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	ssize_t	(*proc_read_iter)(struct kiocb *, struct iov_iter *);
	ssize_t	(*proc_write_iter)(struct kiocb *, struct iov_iter *);
	unsigned int proc_flags;
};

现代内核推荐使用proc_read_iterproc_write_iter替代传统的proc_read/proc_write,以支持更高效的IO向量操作。

三、实战开发:从基础到高级

3.1 基础只读接口:系统运行时间统计

需求:创建/proc/uptime_stats接口,展示系统运行时间和空闲时间。

实现步骤

  1. 定义proc_ops结构体
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/sched.h>
#include <linux/time.h>

static int uptime_stats_show(struct seq_file *m, void *v)
{
	struct timespec64 uptime, idle;
	
	ktime_get_boottime_ts64(&uptime);
	idle = ktime_to_timespec64(idle_time);
	
	seq_printf(m, "uptime: %lld.%02lld seconds\n",
		   uptime.tv_sec, (uptime.tv_nsec / 10000000));
	seq_printf(m, "idle: %lld.%02lld seconds\n",
		   idle.tv_sec, (idle.tv_nsec / 10000000));
	return 0;
}

static int uptime_stats_open(struct inode *inode, struct file *file)
{
	return single_open(file, uptime_stats_show, NULL);
}

static const struct proc_ops uptime_stats_proc_ops = {
	.proc_open	= uptime_stats_open,
	.proc_read	= seq_read,
	.proc_lseek	= seq_lseek,
	.proc_release	= single_release,
};
  1. 注册procfs接口
static int __init uptime_stats_init(void)
{
	proc_create("uptime_stats", 0444, NULL, &uptime_stats_proc_ops);
	return 0;
}

static void __exit uptime_stats_exit(void)
{
	remove_proc_entry("uptime_stats", NULL);
}

module_init(uptime_stats_init);
module_exit(uptime_stats_exit);
MODULE_LICENSE("GPL");

关键技术点

  • 使用single_open/single_release简化seq_file操作
  • seq_printf确保内核数据安全格式化输出
  • 权限设置为0444(S_IRUGO),只允许读取

3.2 读写接口:运行时配置参数

需求:创建/proc/max_connections接口,支持动态配置最大连接数。

实现代码

#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/sched.h>
#include <linux/string.h>
#include <linux/uaccess.h>

static int max_connections = 1024;
static DEFINE_MUTEX(connections_mutex);

static int max_connections_show(struct seq_file *m, void *v)
{
	mutex_lock(&connections_mutex);
	seq_printf(m, "%d\n", max_connections);
	mutex_unlock(&connections_mutex);
	return 0;
}

static ssize_t max_connections_write(struct file *file, const char __user *buffer,
				     size_t count, loff_t *ppos)
{
	char buf[32];
	int val, ret;
	
	if (count >= sizeof(buf))
		return -EINVAL;
	
	if (copy_from_user(buf, buffer, count))
		return -EFAULT;
	
	buf[count] = '\0';
	ret = kstrtoint(buf, 10, &val);
	if (ret < 0)
		return ret;
	
	if (val < 1 || val > 65535)
		return -EINVAL;
	
	mutex_lock(&connections_mutex);
	max_connections = val;
	mutex_unlock(&connections_mutex);
	
	return count;
}

static int max_connections_open(struct inode *inode, struct file *file)
{
	return single_open(file, max_connections_show, NULL);
}

static const struct proc_ops max_connections_proc_ops = {
	.proc_open	= max_connections_open,
	.proc_read	= seq_read,
	.proc_write	= max_connections_write,
	.proc_lseek	= seq_lseek,
	.proc_release	= single_release,
};

static int __init max_connections_init(void)
{
	proc_create("max_connections", 0644, NULL, &max_connections_proc_ops);
	return 0;
}

static void __exit max_connections_exit(void)
{
	remove_proc_entry("max_connections", NULL);
}

module_init(max_connections_init);
module_exit(max_connections_exit);
MODULE_LICENSE("GPL");

安全最佳实践

  • 使用mutex保护共享变量,防止并发写冲突
  • 严格验证用户输入范围(1-65535),防止非法配置
  • 限制输入缓冲区大小(32字节),防止栈溢出
  • 使用kstrtoint替代atoi,提供更好的错误处理

3.3 高级应用:进程状态监控接口

需求:创建/proc/process_monitor接口,展示系统中占用CPU最高的5个进程。

实现代码

#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/sched.h>
#include <linux/sched/stat.h>
#include <linux/sched/task.h>
#include <linux/list.h>
#include <linux/sort.h>
#include <linux/spinlock.h>

#define MAX_PROCS 5

struct proc_info {
	pid_t pid;
	char comm[TASK_COMM_LEN];
	unsigned long cpu_time;
};

static struct proc_info top_procs[MAX_PROCS];

static int compare_proc_info(const void *a, const void *b)
{
	const struct proc_info *p1 = a;
	const struct proc_info *p2 = b;
	return p2->cpu_time - p1->cpu_time;
}

static void collect_top_procs(void)
{
	struct proc_info procs[1024];
	struct task_struct *p;
	int count = 0;
	unsigned long flags;
	
	read_lock_irqsave(&tasklist_lock, flags);
	for_each_process(p) {
		if (count >= ARRAY_SIZE(procs))
			break;
		
		procs[count].pid = p->pid;
		strncpy(procs[count].comm, p->comm, TASK_COMM_LEN);
		procs[count].cpu_time = p->utime + p->stime;
		count++;
	}
	read_unlock_irqrestore(&tasklist_lock, flags);
	
	sort(procs, count, sizeof(struct proc_info), compare_proc_info, NULL);
	memcpy(top_procs, procs, min(count, MAX_PROCS) * sizeof(struct proc_info));
}

static int process_monitor_show(struct seq_file *m, void *v)
{
	int i;
	
	collect_top_procs();
	
	seq_printf(m, "Top %d CPU-consuming processes:\n", MAX_PROCS);
	seq_printf(m, "%-6s %-16s %s\n", "PID", "COMM", "CPU TIME(us)");
	
	for (i = 0; i < MAX_PROCS && top_procs[i].pid != 0; i++) {
		seq_printf(m, "%-6d %-16s %lu\n",
			   top_procs[i].pid,
			   top_procs[i].comm,
			   top_procs[i].cpu_time);
	}
	
	return 0;
}

static int process_monitor_open(struct inode *inode, struct file *file)
{
	return single_open(file, process_monitor_show, NULL);
}

static const struct proc_ops process_monitor_proc_ops = {
	.proc_open	= process_monitor_open,
	.proc_read	= seq_read,
	.proc_lseek	= seq_lseek,
	.proc_release	= single_release,
};

static int __init process_monitor_init(void)
{
	proc_create("process_monitor", 0444, NULL, &process_monitor_proc_ops);
	return 0;
}

static void __exit process_monitor_exit(void)
{
	remove_proc_entry("process_monitor", NULL);
}

module_init(process_monitor_init);
module_exit(process_monitor_exit);
MODULE_LICENSE("GPL");

高级技术点

  • 使用tasklist_lock安全遍历进程列表
  • 采用sort函数实现进程CPU时间排序
  • 通过seq_file分页输出大结果集
  • TASK_COMM_LEN确保进程名缓冲区安全

四、内核procfs最佳实践与性能优化

4.1 内存安全与泄漏预防

procfs开发中常见内存问题及解决方案:

问题类型预防措施检测方法
proc_dir_entry泄漏使用remove_proc_entry正确清理slabinfo监控proc_dir_entry_cache
未释放data指针模块退出时显式释放kmemleak工具检测
缓冲区溢出使用seq_printf/scnprintfKASAN编译选项
用户数据拷贝错误检查copy_from_user返回值lockdep跟踪锁定状态

内存安全代码示例

// 正确的procfs清理流程
static struct proc_dir_entry *my_proc_entry;

static int __init my_proc_init(void) {
    my_proc_entry = proc_create("my_proc", 0644, NULL, &my_proc_ops);
    if (!my_proc_entry)
        return -ENOMEM;
    
    my_proc_entry->data = kmalloc(sizeof(my_data), GFP_KERNEL);
    if (!my_proc_entry->data) {
        remove_proc_entry("my_proc", NULL);
        return -ENOMEM;
    }
    return 0;
}

static void __exit my_proc_exit(void) {
    kfree(my_proc_entry->data);  // 先释放data
    remove_proc_entry("my_proc", NULL);  // 再移除目录项
}

4.2 并发控制与同步机制

procfs接口必须处理多进程并发访问,常用同步机制比较:

同步机制适用场景性能特点实现复杂度
mutex读写互斥中等简单
rw_semaphore多读单写高读性能中等
spinlock短临界区最高性能复杂
seqlock频繁读,少写读无锁

rw_semaphore实现示例

static struct rw_semaphore stats_rwsem;

static int stats_show(struct seq_file *m, void *v) {
    down_read(&stats_rwsem);
    // 读取统计数据
    up_read(&stats_rwsem);
    return 0;
}

static ssize_t stats_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    down_write(&stats_rwsem);
    // 更新统计数据
    up_write(&stats_rwsem);
    return count;
}

4.3 性能优化策略

procfs接口性能优化技巧:

  1. 数据缓存:对计算密集型数据结果进行缓存,设置合理的过期策略
static unsigned long cached_uptime;
static unsigned long cache_timestamp;

static int uptime_show(struct seq_file *m, void *v) {
    unsigned long now = jiffies;
    
    // 每1秒更新一次缓存
    if (time_after(now, cache_timestamp + HZ)) {
        cached_uptime = get_uptime_seconds();
        cache_timestamp = now;
    }
    
    seq_printf(m, "%lu\n", cached_uptime);
    return 0;
}
  1. 分页输出:使用seq_file实现大数据集分页,避免单次输出过大
static void *seq_start(struct seq_file *m, loff_t *pos) {
    if (*pos >= MAX_ENTRIES)
        return NULL;
    return &entries[*pos];
}

static void *seq_next(struct seq_file *m, void *v, loff_t *pos) {
    (*pos)++;
    if (*pos >= MAX_ENTRIES)
        return NULL;
    return &entries[*pos];
}

static void seq_stop(struct seq_file *m, void *v) {
    // 清理工作
}

static int seq_show(struct seq_file *m, void *v) {
    struct entry *e = v;
    seq_printf(m, "%s: %d\n", e->name, e->value);
    return 0;
}

static const struct seq_operations my_seq_ops = {
    .start = seq_start,
    .next  = seq_next,
    .stop  = seq_stop,
    .show  = seq_show
};
  1. 延迟计算:仅在有读取操作时才计算数据,避免后台资源消耗

五、内核源码中的proc_create实战案例

5.1 /proc/stat实现分析

/proc/stat是系统统计信息的核心接口,其实现位于fs/proc/stat.c

static int stat_proc_show(struct seq_file *m, void *v)
{
	struct sysinfo si;
	cputime64_t user, nice, system, idle, iowait, irq, softirq, steal;
	cputime64_t guest, guest_nice;
	unsigned int per_cpu_nr;
	int j;

	sysinfo(&si);

	/*
	 * output the cpu times as user, nice, system, idle, iowait,
	 * irq, softirq, steal, guest, guest_nice
	 */
	for_each_possible_cpu(j) {
		user = kcpustat_cpu(j).cpustat.user;
		nice = kcpustat_cpu(j).cpustat.nice;
		system = kcpustat_cpu(j).cpustat.system;
		idle = kcpustat_cpu(j).cpustat.idle;
		iowait = kcpustat_cpu(j).cpustat.iowait;
		irq = kcpustat_cpu(j).cpustat.irq;
		softirq = kcpustat_cpu(j).cpustat.softirq;
		steal = kcpustat_cpu(j).cpustat.steal;
		guest = kcpustat_cpu(j).cpustat.guest;
		guest_nice = kcpustat_cpu(j).cpustat.guest_nice;

		/*
		 * We need to subtract guest time from user time,
		 * because it has already been accounted for in user time.
		 */
		user -= guest;
		nice -= guest_nice;

		seq_printf(m, "cpu%d %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu\n",
			   j,
			   (unsigned long long)cputime64_to_clock_t(user),
			   (unsigned long long)cputime64_to_clock_t(nice),
			   (unsigned long long)cputime64_to_clock_t(system),
			   (unsigned long long)cputime64_to_clock_t(idle),
			   (unsigned long long)cputime64_to_clock_t(iowait),
			   (unsigned long long)cputime64_to_clock_t(irq),
			   (unsigned long long)cputime64_to_clock_t(softirq),
			   (unsigned long long)cputime64_to_clock_t(steal),
			   (unsigned long long)cputime64_to_clock_t(guest),
			   (unsigned long long)cputime64_to_clock_t(guest_nice));
	}

	// 后续输出磁盘、内存、进程等统计信息...
	return 0;
}

static const struct proc_ops stat_proc_ops = {
	.proc_open	= stat_proc_open,
	.proc_read	= seq_read,
	.proc_lseek	= seq_lseek,
	.proc_release	= single_release,
};

static int __init proc_stat_init(void)
{
	proc_create("stat", 0, NULL, &stat_proc_ops);
	return 0;
}
fs_initcall(proc_stat_init);

核心技术点

  • 使用for_each_possible_cpu遍历CPU统计数据
  • cputime64_to_clock_t转换时间单位为时钟滴答
  • sysinfo获取系统整体信息
  • fs_initcall确保在文件系统初始化阶段注册

5.2 /proc/schedstat实现分析

schedstat接口提供调度器详细统计,实现位于kernel/sched/stat.c

static int schedstat_show(struct seq_file *m, void *v)
{
	unsigned int cpu;

	seq_puts(m, "cpu#\t  runqlen \t active \t load \t\t nr_switches \t nr_running \t nr_uninterruptible\n");
	for_each_online_cpu(cpu) {
		struct rq *rq = cpu_rq(cpu);
		unsigned int i, runqlen = rq->nr_running;

		seq_printf(m, "cpu%d\t", cpu);

		/* runqlen */
		seq_printf(m, "  %3u \t", runqlen);

		/* active_load */
		seq_printf(m, "%8lu \t", rq->avg.load_avg);

		/* nr_switches */
		seq_printf(m, "%8lu \t", rq->nr_switches);

		/* nr_running */
		seq_printf(m, "%8u \t", runqlen);

		/* nr_uninterruptible */
		seq_printf(m, "%8u\n", atomic_read(&rq->nr_uninterruptible));
	}

	// 输出调度器其他统计信息...
	return 0;
}

static int __init proc_schedstat_init(void)
{
	proc_create_single("schedstat", 0444, NULL, schedstat_show);
	return 0;
}
device_initcall(proc_schedstat_init);

技术亮点

  • 使用proc_create_single简化只读文件实现
  • for_each_online_cpu遍历在线CPU
  • cpu_rq获取每个CPU的运行队列
  • device_initcall控制初始化顺序

六、总结与进阶学习路径

6.1 核心知识点回顾

本文系统讲解了procfs接口开发的关键技术:

  1. proc_create函数族:根据功能需求选择合适的创建函数
  2. proc_dir_entry管理:正确的创建、初始化与清理流程
  3. proc_ops操作集:实现安全高效的读写处理函数
  4. 同步机制:根据场景选择合适的并发控制策略
  5. 性能优化:缓存、分页和延迟计算提升接口性能

6.2 常见问题排查清单

开发procfs接口时,建议按以下清单检查:

  •  权限设置是否遵循最小权限原则
  •  是否正确处理了proc_dir_entry的生命周期
  •  用户输入是否经过严格验证
  •  所有共享数据是否有适当的同步保护
  •  是否使用了安全的内核API(如seq_printf替代sprintf
  •  模块退出时是否释放了所有分配的资源

6.3 进阶学习资源

要深入掌握procfs和内核开发,推荐以下资源:

  1. 内核源码

    • fs/proc/generic.c:procfs核心实现
    • fs/proc/internal.h:内部数据结构定义
    • Documentation/filesystems/proc.rst:官方文档
  2. 书籍

    • 《Linux Kernel Development》(Robert Love)
    • 《Linux Device Drivers》(Jonathan Corbet)
  3. 工具

    • procdebug:procfs专用调试工具
    • ktap:内核动态追踪工具
    • perf:性能分析工具

6.4 下期预告:procfs高级应用

下一篇文章将介绍procfs的高级应用场景:

  • 实现seq_file分页大数据输出
  • procfs与netlink协同工作
  • 使用tracepoints跟踪procfs访问
  • 高性能procfs接口设计模式

如果你觉得本文有价值,请点赞、收藏并关注,不错过更多内核开发深度文章!

附录:proc_create相关函数参考

函数名功能描述参数说明返回值
proc_create创建procfs文件name:文件名
mode:权限
parent:父目录
proc_ops:操作集
成功返回proc_dir_entry*,失败返回NULL
proc_create_data创建带私有数据的procfs文件同上,增加data:私有数据指针同上
proc_create_single创建简单只读文件name:文件名
mode:权限
parent:父目录
show:显示函数
同上
proc_create_seq创建序列文件name:文件名
mode:权限
parent:父目录
seq_ops:序列操作集
同上
remove_proc_entry删除procfs文件name:文件名
parent:父目录
proc_mkdir创建procfs目录name:目录名
parent:父目录
成功返回proc_dir_entry*,失败返回NULL

【免费下载链接】linux Linux kernel source tree 【免费下载链接】linux 项目地址: https://gitcode.com/GitHub_Trending/li/linux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值