[Linux]学习笔记系列 -- [fs][proc]


title: proc

categories:

  • linux
  • fs
    tags:
  • linux
  • fs
    abbrlink: 2bd08434
    date: 2025-10-03 09:01:49

https://github.com/wdfk-prog/linux-study

文章目录

fs/proc 进程和系统信息伪文件系统

历史与背景

这项技术是为了解决什么特定问题而诞生的?

proc 文件系统(procfs)的诞生是为了提供一个标准的、统一的、基于文件的接口 ,来取代早期Unix系统中那些不安全、不可移植的进程和内核信息获取方法。在proc出现之前,像ps这样的工具需要通过直接读取内核内存(例如,通过特殊的设备文件/dev/kmem)来获取进程信息。这种方法存在几个严重的问题:

  • 安全风险:允许用户空间程序任意读取内核内存是一个巨大的安全漏洞。
  • 稳定性差:这种方法严重依赖于特定内核版本的数据结构布局。内核的任何微小改动都可能导致用户空间工具失效甚至使系统崩溃。
  • 不可移植:每个硬件架构和操作系统变体都有不同的内存布局,使得编写可移植的监控工具几乎不可能。

procfs通过创建一个**伪文件系统(pseudo-filesystem)**解决了这些问题。它将内核内部的动态数据和状态信息,以普通文件的形式呈现给用户空间,使得任何程序都可以使用标准的open(), read(), write()系统调用来安全、稳定地访问这些信息。

它的发展经历了哪些重要的里程碑或版本迭代?
  • 起源与采纳procfs的概念并非Linux首创,其思想源于早期Unix系统(如System V Release 4)和Plan 9。Linux在早期就采纳并极大地扩展了这一概念。
  • 从进程到系统(The "Kitchen Sink" Era) :最初,procfs主要用于暴露进程信息(因此得名proc)。但它的便利性使其迅速成为暴露各种 内核信息的"万能接口"。内核开发者开始将网络统计(/proc/net)、内存信息(/proc/meminfo)、设备列表等所有东西都加入到proc中。
  • /proc/sys的引入 :一个重要的里程碑是引入了/proc/sys目录,它与sysctl系统调用相对应,提供了一个动态读写内核可调参数的接口。这使得系统管理员可以在不重启系统的情况下实时调整内核行为。
  • 混乱与反思 (The Rise of sysfs)proc的无限扩张导致其结构变得混乱,缺乏统一的规范。不同的文件格式各异,使得自动化解析变得困难。为了解决这个问题,内核在2.6版本引入了一个新的、结构更清晰的伪文件系统------sysfssysfs的设计目标是严格地将内核中的设备模型(kobjects)以"一个文件一个值"的原则暴露出来。
  • 职责划分 :随着sysfs的成熟,新的开发原则被确立:
    • sysfs 用于表示设备树和设备属性。
    • procfs 回归其本源,主要用于报告进程信息,并保留那些已经成为事实标准(de facto standard)的系统信息接口。
目前该技术的社区活跃度和主流应用情况如何?

procfs是任何现代Linux系统中一个不可或缺的核心组件。它非常稳定和成熟,并且被几乎所有的系统监控和管理工具所依赖:

  • 核心工具ps, top, htop, free, lsof, netstat, vmstat等基础命令都严重依赖procfs来获取数据。
  • 系统配置 :通过写入/proc/sys下的文件仍然是调整内核参数最直接、最常用的方法。
  • 事实上的API :尽管存在性能和格式上的一些问题,procfs提供的接口已经成为一个事实上的标准API,被无数的脚本和应用程序所使用。

核心原理与设计

它的核心工作原理是什么?

procfs是一个内存中的、动态生成的伪文件系统。它并不存储在任何物理硬盘上。

  1. VFS集成procfs作为一个文件系统类型注册到内核的虚拟文件系统(VFS)层。当用户挂载proc时,VFS会调用procfs的初始化函数。
  2. 动态节点创建procfs中的目录和文件不是预先存在的。当用户空间进程尝试访问(如lsopenproc中的一个路径时,VFS会将请求传递给procfs的实现。procfs会根据路径动态地查找或创建对应的目录项(dentry)和inode。例如,当访问/proc/123时,procfs会检查是否存在PID为123的进程,如果存在,就动态创建一个代表该目录的inode。
  3. 读写处理函数procfs中每个文件的inode都关联了一组特定的处理函数 (handler functions),而不是指向磁盘上的数据块。
    • 读操作 (read) : 当用户读取一个proc文件(如 cat /proc/meminfo)时,对应的读处理函数被调用。该函数会直接访问内核的内部数据结构(如全局的内存统计变量),将这些二进制数据动态格式化为人类可读的文本字符串,然后返回给用户空间。
    • 写操作 (write) : 当用户写入一个proc文件(如 echo 1 > /proc/sys/net/ipv4/ip_forward)时,对应的写处理函数被调用。该函数会解析用户传入的字符串,并调用相应的内核函数来修改内核内部的变量。
它的主要优势体现在哪些方面?
  • 统一和标准的接口:使用所有人都熟悉的文件I/O API,无需特殊的库或系统调用。
  • 实时性:提供的信息是内核状态的实时快照,总能反映系统的最新情况。
  • 人类可读性:大部分文件是文本格式,便于系统管理员直接查看和调试。
  • 易于脚本化 :可以非常方便地被shell, awk, grep等标准Unix工具处理。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
  • 性能开销 :内核将二进制数据格式化为文本,用户空间再解析文本,这个过程相比直接的二进制接口(如netlinksysctl系统调用)有显著的性能开销。
  • 格式不一致 :由于其历史发展,procfs中不同文件的输出格式缺乏统一标准,使得编写健壮的解析器变得困难。
  • 非原子性 :读取一个包含多行信息的大文件(如/proc/stat)不是一个原子操作。在读取过程中,内核的状态可能已经发生变化,导致读取到的不同行可能来自不同时间点的快照。
  • 结构混乱 :如前所述,它混合了进程信息、硬件信息、网络统计和配置等多种不同类型的数据,逻辑结构不如sysfs清晰。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

procfs是获取进程和核心系统状态信息唯一标准接口

  • 进程监控与管理
    • ps -ef 通过遍历/proc/[PID]目录并读取/proc/[PID]/stat/proc/[PID]/cmdline等文件来实现。
    • lsof (List Open Files) 通过扫描所有/proc/[PID]/fd/目录来找出进程打开的文件。
  • 系统资源监控
    • free 命令的数据直接来自/proc/meminfo
    • uptime 的数据来自/proc/uptime/proc/loadavg
    • vmstatiostat 的核心数据来自/proc/stat/proc/diskstats
  • 内核参数运行时调整
    • sysctl net.ipv4.ip_forward=1 命令实际上就是向/proc/sys/net/ipv4/ip_forward文件写入1
是否有不推荐使用该技术的场景?为什么?
  • 获取设备模型信息 :当需要了解系统中的设备拓扑结构、设备与驱动的绑定关系、或设备的特定属性(如电源状态)时,应该使用sysfs (/sys)sysfs提供了与内核设备模型严格对应的、结构化的视图。
  • 高性能网络监控 :对于需要高频率、低延迟地获取网络统计数据或通知的应用,使用netlink套接字是比轮询/proc/net/dev等文件更高效的方案。
  • 内核调试 :对于内核开发者进行深度调试,debugfs (/sys/kernel/debug) 提供了更灵活、更底层的接口,但其API不保证稳定性。

对比分析

请将其 与 其他相似技术 进行详细对比。
特性 procfs (/proc) sysfs (/sys) debugfs (/sys/kernel/debug) tmpfs
核心用途 进程和系统状态报告,以及内核参数调整。 表示内核的设备模型,管理设备和驱动。 内核调试,暴露内部状态给内核开发者。 内存中的文件系统,用于通用目的的临时文件存储。
数据来源 内核动态生成的文本数据。 内核的kobject数据结构,严格遵循"一文件一值"。 内核开发者任意导出的调试信息。 用户写入的数据
API稳定性 大部分是稳定的(事实标准),但不做绝对保证。 有严格的API稳定性保证,用户空间可以依赖它。 完全不保证稳定 ,随时可能更改。禁止在生产应用中依赖它。 作为文件系统,其API是稳定的。
结构 历史形成,逻辑结构有时不清晰。 高度结构化,与设备树紧密对应。 完全无结构,由开发者自行组织。 由用户自行组织。
典型场景 ps, top, free, sysctl udev, systemd-hwdb, 电源管理工具。 调试内存泄漏,跟踪锁竞争等。 /dev/shm, /run

include/uapi/linux/stat.h

文件类型和文件权限标志

c 复制代码
#define S_IFMT  00170000	/* 文件类型的掩码,用于提取文件类型信息 */
#define S_IFSOCK 0140000	/* 套接字文件类型 */
#define S_IFLNK	 0120000	/* 符号链接文件类型 */
#define S_IFREG  0100000	/* 常规文件类型 */
#define S_IFBLK  0060000	/* 块设备文件类型 */
#define S_IFDIR  0040000	/* 目录文件类型 */
#define S_IFCHR  0020000	/* 字符设备文件类型 */
#define S_IFIFO  0010000	/* 命名管道文件类型 */
#define S_ISUID  0004000	/* 设置用户 ID 位 */
#define S_ISGID  0002000	/* 设置组 ID 位 */
#define S_ISVTX  0001000	/* 目录粘滞位 */

#define S_ISLNK(m)	(((m) & S_IFMT) == S_IFLNK)
#define S_ISREG(m)	(((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m)	(((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m)	(((m) & S_IFMT) == S_IFCHR)
#define S_ISBLK(m)	(((m) & S_IFMT) == S_IFBLK)
#define S_ISFIFO(m)	(((m) & S_IFMT) == S_IFIFO)
#define S_ISSOCK(m)	(((m) & S_IFMT) == S_IFSOCK)

#define S_IRWXU 00700		/* 用户读、写和执行权限 */
#define S_IRUSR 00400
#define S_IWUSR 00200
#define S_IXUSR 00100

#define S_IRWXG 00070		/* 组读、写和执行权限 */
#define S_IRGRP 00040
#define S_IWGRP 00020
#define S_IXGRP 00010

#define S_IRWXO 00007		/* 其他用户读、写和执行权限 */
#define S_IROTH 00004
#define S_IWOTH 00002
#define S_IXOTH 00001

// include/linux/stat.h
#define S_IRWXUGO	(S_IRWXU|S_IRWXG|S_IRWXO)
#define S_IALLUGO	(S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO)	/* 文件的所有权限位 */
#define S_IRUGO		(S_IRUSR|S_IRGRP|S_IROTH)
#define S_IWUGO		(S_IWUSR|S_IWGRP|S_IWOTH)
#define S_IXUGO		(S_IXUSR|S_IXGRP|S_IXOTH)

include/linux/proc_fs.h

proc_create_seq 创建一个 proc 文件系统的序列文件

c 复制代码
#define proc_create_seq(name, mode, parent, ops) \
	proc_create_seq_private(name, mode, parent, ops, 0, NULL)

fs/proc/inode.c

proc_init_kmemcache 初始化 proc 文件系统的内核缓存

c 复制代码
void __init proc_init_kmemcache(void)
{
	proc_inode_cachep = kmem_cache_create("proc_inode_cache",
					     sizeof(struct proc_inode),
					     0, (SLAB_RECLAIM_ACCOUNT|
						SLAB_ACCOUNT|
						SLAB_PANIC),
					     init_once);
	pde_opener_cache =
		kmem_cache_create("pde_opener", sizeof(struct pde_opener), 0,
				  SLAB_ACCOUNT|SLAB_PANIC, NULL);
	proc_dir_entry_cache = kmem_cache_create_usercopy(
		"proc_dir_entry", SIZEOF_PDE, 0, SLAB_PANIC,
		offsetof(struct proc_dir_entry, inline_name),
		SIZEOF_PDE_INLINE_NAME, NULL);
	BUILD_BUG_ON(sizeof(struct proc_dir_entry) >= SIZEOF_PDE);
}

fs/proc/base.c

pid_entry 结构体定义

  • DIR表示目录
  • REG表示常规文件
  • ONE表示单个文件
  • NOD表示节点文件
  • LINK表示符号链接
c 复制代码
struct pid_entry {
	const char *name;
	unsigned int len;
	umode_t mode;
	const struct inode_operations *iop;
	const struct file_operations *fop;
	union proc_op op;
};

#define NOD(NAME, MODE, IOP, FOP, OP) {			\
	.name = (NAME),					\
	.len  = sizeof(NAME) - 1,			\
	.mode = MODE,					\
	.iop  = IOP,					\
	.fop  = FOP,					\
	.op   = OP,					\
}

#define DIR(NAME, MODE, iops, fops)	\
	NOD(NAME, (S_IFDIR|(MODE)), &iops, &fops, {} )
#define LNK(NAME, get_link)					\
	NOD(NAME, (S_IFLNK|S_IRWXUGO),				\
		&proc_pid_link_inode_operations, NULL,		\
		{ .proc_get_link = get_link } )
#define REG(NAME, MODE, fops)				\
	NOD(NAME, (S_IFREG|(MODE)), NULL, &fops, {})
#define ONE(NAME, MODE, show)				\
	NOD(NAME, (S_IFREG|(MODE)),			\
		NULL, &proc_single_file_operations,	\
		{ .proc_show = show } )
#define ATTR(LSMID, NAME, MODE)				\
	NOD(NAME, (S_IFREG|(MODE)),			\
		NULL, &proc_pid_attr_operations,	\
		{ .lsmid = LSMID })

tid_base_stuff 线程级文件项

  • tid_base_stuff 则用来描述与单个线程(task/thread id)相关的 proc 文件条目,通常这些条目位于 /proc// 下。这个数组中定义的条目往往和线程特定的信息有关,比如与线程的文件描述符、调度信息或统计数据等。
c 复制代码
static const struct pid_entry tid_base_stuff[] = {
	DIR("fd",        S_IRUSR|S_IXUSR, proc_fd_inode_operations, proc_fd_operations),
	DIR("fdinfo",    S_IRUGO|S_IXUGO, proc_fdinfo_inode_operations, proc_fdinfo_operations),
	DIR("ns",	 S_IRUSR|S_IXUGO, proc_ns_dir_inode_operations, proc_ns_dir_operations),
#ifdef CONFIG_NET
	DIR("net",        S_IRUGO|S_IXUGO, proc_net_inode_operations, proc_net_operations),
#endif
	REG("environ",   S_IRUSR, proc_environ_operations),
	REG("auxv",      S_IRUSR, proc_auxv_operations),
	ONE("status",    S_IRUGO, proc_pid_status),
	ONE("personality", S_IRUSR, proc_pid_personality),
	ONE("limits",	 S_IRUGO, proc_pid_limits),
	REG("sched",     S_IRUGO|S_IWUSR, proc_pid_sched_operations),
	NOD("comm",      S_IFREG|S_IRUGO|S_IWUSR,
			 &proc_tid_comm_inode_operations,
			 &proc_pid_set_comm_operations, {}),
#ifdef CONFIG_HAVE_ARCH_TRACEHOOK
	ONE("syscall",   S_IRUSR, proc_pid_syscall),
#endif
	REG("cmdline",   S_IRUGO, proc_pid_cmdline_ops),
	ONE("stat",      S_IRUGO, proc_tid_stat),
	ONE("statm",     S_IRUGO, proc_pid_statm),
	REG("maps",      S_IRUGO, proc_pid_maps_operations),
#ifdef CONFIG_PROC_CHILDREN
	REG("children",  S_IRUGO, proc_tid_children_operations),
#endif
#ifdef CONFIG_NUMA
	REG("numa_maps", S_IRUGO, proc_pid_numa_maps_operations),
#endif
	REG("mem",       S_IRUSR|S_IWUSR, proc_mem_operations),
	LNK("cwd",       proc_cwd_link),
	LNK("root",      proc_root_link),
	LNK("exe",       proc_exe_link),
	REG("mounts",    S_IRUGO, proc_mounts_operations),
	REG("mountinfo",  S_IRUGO, proc_mountinfo_operations),
#ifdef CONFIG_PROC_PAGE_MONITOR
	REG("clear_refs", S_IWUSR, proc_clear_refs_operations),
	REG("smaps",     S_IRUGO, proc_pid_smaps_operations),
	REG("smaps_rollup", S_IRUGO, proc_pid_smaps_rollup_operations),
	REG("pagemap",    S_IRUSR, proc_pagemap_operations),
#endif
#ifdef CONFIG_SECURITY
	DIR("attr",      S_IRUGO|S_IXUGO, proc_attr_dir_inode_operations, proc_attr_dir_operations),
#endif
#ifdef CONFIG_KALLSYMS
	ONE("wchan",     S_IRUGO, proc_pid_wchan),
#endif
#ifdef CONFIG_STACKTRACE
	ONE("stack",      S_IRUSR, proc_pid_stack),
#endif
#ifdef CONFIG_SCHED_INFO
	ONE("schedstat", S_IRUGO, proc_pid_schedstat),
#endif
#ifdef CONFIG_LATENCYTOP
	REG("latency",  S_IRUGO, proc_lstats_operations),
#endif
#ifdef CONFIG_PROC_PID_CPUSET
	ONE("cpuset",    S_IRUGO, proc_cpuset_show),
#endif
#ifdef CONFIG_CGROUPS
	ONE("cgroup",  S_IRUGO, proc_cgroup_show),
#endif
#ifdef CONFIG_PROC_CPU_RESCTRL
	ONE("cpu_resctrl_groups", S_IRUGO, proc_resctrl_show),
#endif
	ONE("oom_score", S_IRUGO, proc_oom_score),
	REG("oom_adj",   S_IRUGO|S_IWUSR, proc_oom_adj_operations),
	REG("oom_score_adj", S_IRUGO|S_IWUSR, proc_oom_score_adj_operations),
#ifdef CONFIG_AUDIT
	REG("loginuid",  S_IWUSR|S_IRUGO, proc_loginuid_operations),
	REG("sessionid",  S_IRUGO, proc_sessionid_operations),
#endif
#ifdef CONFIG_FAULT_INJECTION
	REG("make-it-fail", S_IRUGO|S_IWUSR, proc_fault_inject_operations),
	REG("fail-nth", 0644, proc_fail_nth_operations),
#endif
#ifdef CONFIG_TASK_IO_ACCOUNTING
	ONE("io",	S_IRUSR, proc_tid_io_accounting),
#endif
#ifdef CONFIG_USER_NS
	REG("uid_map",    S_IRUGO|S_IWUSR, proc_uid_map_operations),
	REG("gid_map",    S_IRUGO|S_IWUSR, proc_gid_map_operations),
	REG("projid_map", S_IRUGO|S_IWUSR, proc_projid_map_operations),
	REG("setgroups",  S_IRUGO|S_IWUSR, proc_setgroups_operations),
#endif
#ifdef CONFIG_LIVEPATCH
	ONE("patch_state",  S_IRUSR, proc_pid_patch_state),
#endif
#ifdef CONFIG_PROC_PID_ARCH_STATUS
	ONE("arch_status", S_IRUGO, proc_pid_arch_status),
#endif
#ifdef CONFIG_SECCOMP_CACHE_DEBUG
	ONE("seccomp_cache", S_IRUSR, proc_pid_seccomp_cache),
#endif
#ifdef CONFIG_KSM
	ONE("ksm_merging_pages",  S_IRUSR, proc_pid_ksm_merging_pages),
	ONE("ksm_stat",  S_IRUSR, proc_pid_ksm_stat),
#endif
};

tgid_base_stuff 线程组级文件项

  • tgid_base_stuff 用于描述与线程组(即整个进程)相关的 proc 文件条目,也就是说,这个数组定义了当你访问 /proc/(通常 与进程号相同)时所显示的各种文件。例如,像 maps、cmdline、status 等条目的创建和对应的操作会在这里注册。
c 复制代码
static const struct pid_entry tgid_base_stuff[] = {
	DIR("task",       S_IRUGO|S_IXUGO, proc_task_inode_operations, proc_task_operations),
	DIR("fd",         S_IRUSR|S_IXUSR, proc_fd_inode_operations, proc_fd_operations),
	DIR("map_files",  S_IRUSR|S_IXUSR, proc_map_files_inode_operations, proc_map_files_operations),
	DIR("fdinfo",     S_IRUGO|S_IXUGO, proc_fdinfo_inode_operations, proc_fdinfo_operations),
	DIR("ns",	  S_IRUSR|S_IXUGO, proc_ns_dir_inode_operations, proc_ns_dir_operations),
#ifdef CONFIG_NET
	DIR("net",        S_IRUGO|S_IXUGO, proc_net_inode_operations, proc_net_operations),
#endif
	REG("environ",    S_IRUSR, proc_environ_operations),
	REG("auxv",       S_IRUSR, proc_auxv_operations),
	ONE("status",     S_IRUGO, proc_pid_status),
	ONE("personality", S_IRUSR, proc_pid_personality),
	ONE("limits",	  S_IRUGO, proc_pid_limits),
	REG("sched",      S_IRUGO|S_IWUSR, proc_pid_sched_operations),
#ifdef CONFIG_SCHED_AUTOGROUP
	REG("autogroup",  S_IRUGO|S_IWUSR, proc_pid_sched_autogroup_operations),
#endif
#ifdef CONFIG_TIME_NS
	REG("timens_offsets",  S_IRUGO|S_IWUSR, proc_timens_offsets_operations),
#endif
	REG("comm",      S_IRUGO|S_IWUSR, proc_pid_set_comm_operations),
#ifdef CONFIG_HAVE_ARCH_TRACEHOOK
	ONE("syscall",    S_IRUSR, proc_pid_syscall),
#endif
	REG("cmdline",    S_IRUGO, proc_pid_cmdline_ops),
	ONE("stat",       S_IRUGO, proc_tgid_stat),
	ONE("statm",      S_IRUGO, proc_pid_statm),
	REG("maps",       S_IRUGO, proc_pid_maps_operations),
#ifdef CONFIG_NUMA
	REG("numa_maps",  S_IRUGO, proc_pid_numa_maps_operations),
#endif
	REG("mem",        S_IRUSR|S_IWUSR, proc_mem_operations),
	LNK("cwd",        proc_cwd_link),
	LNK("root",       proc_root_link),
	LNK("exe",        proc_exe_link),
	REG("mounts",     S_IRUGO, proc_mounts_operations),
	REG("mountinfo",  S_IRUGO, proc_mountinfo_operations),
	REG("mountstats", S_IRUSR, proc_mountstats_operations),
#ifdef CONFIG_PROC_PAGE_MONITOR
	REG("clear_refs", S_IWUSR, proc_clear_refs_operations),
	REG("smaps",      S_IRUGO, proc_pid_smaps_operations),
	REG("smaps_rollup", S_IRUGO, proc_pid_smaps_rollup_operations),
	REG("pagemap",    S_IRUSR, proc_pagemap_operations),
#endif
#ifdef CONFIG_SECURITY
	DIR("attr",       S_IRUGO|S_IXUGO, proc_attr_dir_inode_operations, proc_attr_dir_operations),
#endif
#ifdef CONFIG_KALLSYMS
	ONE("wchan",      S_IRUGO, proc_pid_wchan),
#endif
#ifdef CONFIG_STACKTRACE
	ONE("stack",      S_IRUSR, proc_pid_stack),
#endif
#ifdef CONFIG_SCHED_INFO
	ONE("schedstat",  S_IRUGO, proc_pid_schedstat),
#endif
#ifdef CONFIG_LATENCYTOP
	REG("latency",  S_IRUGO, proc_lstats_operations),
#endif
#ifdef CONFIG_PROC_PID_CPUSET
	ONE("cpuset",     S_IRUGO, proc_cpuset_show),
#endif
#ifdef CONFIG_CGROUPS
	ONE("cgroup",  S_IRUGO, proc_cgroup_show),
#endif
#ifdef CONFIG_PROC_CPU_RESCTRL
	ONE("cpu_resctrl_groups", S_IRUGO, proc_resctrl_show),
#endif
	ONE("oom_score",  S_IRUGO, proc_oom_score),
	REG("oom_adj",    S_IRUGO|S_IWUSR, proc_oom_adj_operations),
	REG("oom_score_adj", S_IRUGO|S_IWUSR, proc_oom_score_adj_operations),
#ifdef CONFIG_AUDIT
	REG("loginuid",   S_IWUSR|S_IRUGO, proc_loginuid_operations),
	REG("sessionid",  S_IRUGO, proc_sessionid_operations),
#endif
#ifdef CONFIG_FAULT_INJECTION
	REG("make-it-fail", S_IRUGO|S_IWUSR, proc_fault_inject_operations),
	REG("fail-nth", 0644, proc_fail_nth_operations),
#endif
#ifdef CONFIG_ELF_CORE
	REG("coredump_filter", S_IRUGO|S_IWUSR, proc_coredump_filter_operations),
#endif
#ifdef CONFIG_TASK_IO_ACCOUNTING
	ONE("io",	S_IRUSR, proc_tgid_io_accounting),
#endif
#ifdef CONFIG_USER_NS
	REG("uid_map",    S_IRUGO|S_IWUSR, proc_uid_map_operations),
	REG("gid_map",    S_IRUGO|S_IWUSR, proc_gid_map_operations),
	REG("projid_map", S_IRUGO|S_IWUSR, proc_projid_map_operations),
	REG("setgroups",  S_IRUGO|S_IWUSR, proc_setgroups_operations),
#endif
#if defined(CONFIG_CHECKPOINT_RESTORE) && defined(CONFIG_POSIX_TIMERS)
	REG("timers",	  S_IRUGO, proc_timers_operations),
#endif
	REG("timerslack_ns", S_IRUGO|S_IWUGO, proc_pid_set_timerslack_ns_operations),
#ifdef CONFIG_LIVEPATCH
	ONE("patch_state",  S_IRUSR, proc_pid_patch_state),
#endif
#ifdef CONFIG_STACKLEAK_METRICS
	ONE("stack_depth", S_IRUGO, proc_stack_depth),
#endif
#ifdef CONFIG_PROC_PID_ARCH_STATUS
	ONE("arch_status", S_IRUGO, proc_pid_arch_status),
#endif
#ifdef CONFIG_SECCOMP_CACHE_DEBUG
	ONE("seccomp_cache", S_IRUSR, proc_pid_seccomp_cache),
#endif
#ifdef CONFIG_KSM
	ONE("ksm_merging_pages",  S_IRUSR, proc_pid_ksm_merging_pages),
	ONE("ksm_stat",  S_IRUSR, proc_pid_ksm_stat),
#endif
};

pid_entry_nlink 计算 PID 相关的链接计数

c 复制代码
/*
 * 计算 pid_entry 表的硬链接数量,排除 . 和 .. 链接。
 */
static unsigned int __init pid_entry_nlink(const struct pid_entry *entries,
	unsigned int n)
{
	unsigned int i;
	unsigned int count;

	/* 硬链接计数从 2 开始是因为 Linux 文件系统中目录的硬链接默认包含两个特殊条目:"." 和 ".." */
	count = 2;
	for (i = 0; i < n; ++i) {
		if (S_ISDIR(entries[i].mode))
			++count;
	}

	return count;
}
c 复制代码
/* 注意:
 *	在 /proc 中实现 inode 权限操作几乎肯定是错误的。
 *	权限检查需要在每次系统调用期间进行,而不是在打开时进行。
 *	原因是我们希望检查的 /proc 中的权限大多在运行时变化。
 *
 *	一个经典的问题例子是,在任务执行 suid 可执行文件之前,
 *	在 /proc 中打开文件描述符。
 */

static u8 nlink_tid __ro_after_init;
static u8 nlink_tgid __ro_after_init;

void __init set_proc_pid_nlink(void)
{
	nlink_tid = pid_entry_nlink(tid_base_stuff, ARRAY_SIZE(tid_base_stuff));
	nlink_tgid = pid_entry_nlink(tgid_base_stuff, ARRAY_SIZE(tgid_base_stuff));
}

fs/proc/generic.c

proc_alloc_inum 分配一个 inode 编号

c 复制代码
static DEFINE_IDA(proc_inum_ida);

#define PROC_DYNAMIC_FIRST 0xF0000000U

/*
 * 返回一个介于 PROC_DYNAMIC_FIRST 和
 * 0xffffffff 之间的 inode 编号,失败时返回零。
 */
int proc_alloc_inum(unsigned int *inum)
{
	int i;

	i = ida_alloc_max(&proc_inum_ida, UINT_MAX - PROC_DYNAMIC_FIRST,
			  GFP_KERNEL);
	if (i < 0)
		return i;

	*inum = PROC_DYNAMIC_FIRST + (unsigned int)i;
	return 0;
}

proc_match 用于比较 proc 目录条目的名称

c 复制代码
static int proc_match(const char *name, struct proc_dir_entry *de, unsigned int len)
{
	if (len < de->namelen)
		return -1;
	if (len > de->namelen)
		return 1;

	return memcmp(name, de->name, len);
}

pde_subdir_find 查找 proc 子目录

c 复制代码
static struct proc_dir_entry *pde_subdir_find(struct proc_dir_entry *dir,
					      const char *name,
					      unsigned int len)
{
	struct rb_node *node = dir->subdir.rb_node;
	/* 开始遍历红黑树节点 */
	while (node) {
		/* 从红黑树节点提取 */
		struct proc_dir_entry *de = rb_entry(node,
						     struct proc_dir_entry,
						     subdir_node);
		/* 比较当前节点的名称与目标名称 */
		int result = proc_match(name, de, len);

		if (result < 0)
			node = node->rb_left;
		else if (result > 0)
			node = node->rb_right;
		else
			return de;
	}
	return NULL;
}

xlate_proc_name 翻译 proc 名称

c 复制代码
/*
 * 此函数解析名称,例如 "tty/driver/serial",并返回 "/proc/tty/driver" 的 struct proc_dir_entry,
 * 同时在 residual 中返回 "serial"。
 */
static int __xlate_proc_name(const char *name, struct proc_dir_entry **ret,
			     const char **residual)
{
	const char     		*cp = name, *next;
	struct proc_dir_entry	*de;

	de = *ret ?: &proc_root;
	/* 	调用 pde_subdir_find 在当前目录项的子目录中查找匹配的名称。
		如果找不到对应的子目录项,发出警告并返回错误码 -ENOENT */
	while ((next = strchr(cp, '/')) != NULL) {
		de = pde_subdir_find(de, cp, next - cp);
		if (!de) {
			WARN(1, "name '%s'\n", name);
			return -ENOENT;
		}
		cp = next + 1;
	}
	*residual = cp;
	*ret = de;
	return 0;
}


static int xlate_proc_name(const char *name, struct proc_dir_entry **ret,
			   const char **residual)
{
	int rv;

	read_lock(&proc_subdir_lock);
	rv = __xlate_proc_name(name, ret, residual);
	read_unlock(&proc_subdir_lock);
	return rv;
}

__proc_create 创建一个新的 proc 目录条目

c 复制代码
static struct proc_dir_entry *__proc_create(struct proc_dir_entry **parent,
					  const char *name,
					  umode_t mode,
					  nlink_t nlink)
{
	struct proc_dir_entry *ent = NULL;
	const char *fn;
	struct qstr qstr;

	/* 在parent的路径下搜索name的路径,返回最后一级的名称
	 * 如果路径的目录下找不到返回异常*/
	if (xlate_proc_name(name, parent, &fn) != 0)
		goto out;
	qstr.name = fn;
	qstr.len = strlen(fn);
	if (qstr.len == 0 || qstr.len >= 256) {
		WARN(1, "name len %u\n", qstr.len);
		return NULL;
	}
	if (qstr.len == 1 && fn[0] == '.') {
		WARN(1, "name '.'\n");
		return NULL;
	}
	if (qstr.len == 2 && fn[0] == '.' && fn[1] == '.') {
		WARN(1, "name '..'\n");
		return NULL;
	}
	/* 如果父目录是 /proc 根目录,并且名称可以转换为一个有效的数字(见 name_to_int(&qstr)),
	 * 则会拒绝手工创建类似 /proc/123 的条目,因为这些目录一般由内核自动管理 */
	if (*parent == &proc_root && name_to_int(&qstr) != ~0U) {
		WARN(1, "create '/proc/%s' by hand\n", qstr.name);
		return NULL;
	}
	/* 如果父目录是永久空目录 
		return S_ISDIR(pde->mode) && !pde->proc_iops;*/
	if (is_empty_pde(*parent)) {
		WARN(1, "attempt to add to permanently empty directory");
		return NULL;
	}

	ent = kmem_cache_zalloc(proc_dir_entry_cache, GFP_KERNEL);
	if (!ent)
		goto out;

	/* 根据名称长度决定是否使用内联名称存储(inline_name)或动态分配内存 */
	if (qstr.len + 1 <= SIZEOF_PDE_INLINE_NAME) {
		ent->name = ent->inline_name;
	} else {
		ent->name = kmalloc(qstr.len + 1, GFP_KERNEL);
		if (!ent->name) {
			pde_free(ent);
			return NULL;
		}
	}

	memcpy(ent->name, fn, qstr.len + 1);
	ent->namelen = qstr.len;
	ent->mode = mode;
	ent->nlink = nlink;
	ent->subdir = RB_ROOT;
	refcount_set(&ent->refcnt, 1);
	spin_lock_init(&ent->pde_unload_lock);
	INIT_LIST_HEAD(&ent->pde_openers);
	/* 设置用户和组 ID
		de->uid = uid;
		de->gid = gid; */
	proc_set_user(ent, (*parent)->uid, (*parent)->gid);

	ent->proc_dops = &proc_misc_dentry_ops;
	/* 重新验证 /proc/${pid}/net 下的所有内容 
		pde->proc_dops = &proc_net_dentry_ops;*/
	if ((*parent)->proc_dops == &proc_net_dentry_ops)
		pde_force_lookup(ent);

out:
	return ent;
}

pde_subdir_insert 将一个新的 proc 目录项插入到父目录的子目录红黑树中

c 复制代码
static bool pde_subdir_insert(struct proc_dir_entry *dir,
			      struct proc_dir_entry *de)
{
	struct rb_root *root = &dir->subdir;
	struct rb_node **new = &root->rb_node, *parent = NULL;

	/* 找出新节点的放置位置 */
	while (*new) {
		struct proc_dir_entry *this = rb_entry(*new,
						       struct proc_dir_entry,
						       subdir_node);
		int result = proc_match(de->name, this, de->namelen);

		parent = *new;
		if (result < 0)
			new = &(*new)->rb_left;
		else if (result > 0)
			new = &(*new)->rb_right;
		else
			return false;
	}

	/* 添加新节点和再平衡树. */
	rb_link_node(&de->subdir_node, parent, new);
	rb_insert_color(&de->subdir_node, root);
	return true;
}

proc_register 将一个新的目录项 dp 注册到 proc 文件系统的父目录 dir 中

c 复制代码
/* 返回注册的条目,或在失败时释放 dp 并返回 NULL */
struct proc_dir_entry *proc_register(struct proc_dir_entry *dir,
		struct proc_dir_entry *dp)
{
	/* 为新目录项分配一个 inode 编号 */
	if (proc_alloc_inum(&dp->low_ino))
		goto out_free_entry;

	write_lock(&proc_subdir_lock);
	dp->parent = dir;
	/* 将新目录项插入到父目录的子目录红黑树中 */
	if (pde_subdir_insert(dir, dp) == false) {
		WARN(1, "proc_dir_entry '%s/%s' already registered\n",
		     dir->name, dp->name);
		write_unlock(&proc_subdir_lock);
		goto out_free_inum;
	}
	dir->nlink++;
	write_unlock(&proc_subdir_lock);

	return dp;
out_free_inum:
	proc_free_inum(dp->low_ino);
out_free_entry:
	pde_free(dp);
	return NULL;
}

proc_mkdir 创建一个新的 proc 目录

c 复制代码
/* 
 * parent:父目录的 proc_dir_entry 指针。如果传入的父目录为 NULL,则目录会创建在 /proc 的根目录下;如果指定了父目录,则新目录会作为子目录存在
 * force_lookup:是一个布尔值,表示是否希望对该目录强制启用查找功能(内部通过调用 pde_force_lookup 实现,保证目录条目在查找过程中不会被忽略或丢弃)
 * proc 文件系统为了提高性能和管理内部对象,有时会对目录或文件条目采取一些延迟或缓存的策略,可能导致某些条目在查找时被忽略或延迟加载。而"强制启用目录的查找功能"正是为了确保这些目录条目在查找过程中始终被考虑,而不会因优化或缓存机制而被跳过或丢弃
*/
struct proc_dir_entry *_proc_mkdir(const char *name, umode_t mode,
		struct proc_dir_entry *parent, void *data, bool force_lookup)
{
	struct proc_dir_entry *ent;

	/* mode == 0),默认设置为用户可读和可执行 */
	if (mode == 0)
		mode = S_IRUGO | S_IXUGO;

	/* 创建目录条目,设置其类型为目录(S_IFDIR)并应用权限 
		2 表示硬链接数,通常为目录的标准值(包含 . 和 ..)
		一个是 "." --- 目录自身的链接。
		另一个是 ".." --- 指向父目录的链接。*/
	ent = __proc_create(&parent, name, S_IFDIR | mode, 2);
	if (ent) {
		ent->data = data;
		ent->proc_dir_ops = &proc_dir_operations;
		ent->proc_iops = &proc_dir_inode_operations;
		if (force_lookup) {
			pde_force_lookup(ent);
		}
		ent = proc_register(parent, ent);
	}
	return ent;
}
EXPORT_SYMBOL_GPL(_proc_mkdir);

struct proc_dir_entry *proc_mkdir_data(const char *name, umode_t mode,
		struct proc_dir_entry *parent, void *data)
{
	return _proc_mkdir(name, mode, parent, data, false);
}
EXPORT_SYMBOL_GPL(proc_mkdir_data);
struct proc_dir_entry *proc_mkdir(const char *name,
		struct proc_dir_entry *parent)
{
	return proc_mkdir_data(name, 0, parent, NULL);
}
EXPORT_SYMBOL(proc_mkdir);

proc_create_mount_point 创建一个新的 proc 挂载点

  • 挂载点
    • 定义:挂载点是一个目录,用于将某个文件系统或设备挂载到该目录上。挂载点本身并不存储数据,而是作为访问挂载的文件系统或设备的入口。
    • 功能:挂载点的主要作用是将外部文件系统(如硬盘分区、网络文件系统、虚拟文件系统等)与系统的目录结构连接起来。
    • 动态性:挂载点的内容取决于挂载的文件系统或设备。例如,挂载 nfsd 后,该目录的内容由 NFS 服务动态生成。
    • 访问:挂载点的内容通常由挂载的文件系统提供,用户通过挂载点访问该文件系统的数据。
c 复制代码
struct proc_dir_entry *proc_create_mount_point(const char *name)
{
	umode_t mode = S_IFDIR | S_IRUGO | S_IXUGO;
	struct proc_dir_entry *ent, *parent = NULL;

	ent = __proc_create(&parent, name, mode, 2);
	if (ent) {
		ent->data = NULL;
		ent->proc_dir_ops = NULL;
		ent->proc_iops = NULL;
		ent = proc_register(parent, ent);
	}
	return ent;
}
EXPORT_SYMBOL(proc_create_mount_point);

proc_create_reg 创建一个 proc 文件系统的常规文件

c 复制代码
struct proc_dir_entry *proc_create_reg(const char *name, umode_t mode,
		struct proc_dir_entry **parent, void *data)
{
	struct proc_dir_entry *p;

	if ((mode & S_IFMT) == 0)
		/* 默认设置为常规文件类 */
		mode |= S_IFREG;
	if ((mode & S_IALLUGO) == 0)	/* 文件的所有权限位 */
		mode |= S_IRUGO;			/* 默认设置为用户、组和其他用户可读权限 */
	if (WARN_ON_ONCE(!S_ISREG(mode)))	/* 验证 mode 是否表示常规文件类型 */
		return NULL;

	p = __proc_create(parent, name, mode, 1);
	if (p) {
		p->proc_iops = &proc_file_inode_operations;
		p->data = data;
	}
	return p;
}

pde_set_flags 设置 proc 目录项的标志

c 复制代码
static void pde_set_flags(struct proc_dir_entry *pde)
{
	/* 确保 /proc 条目不会被动态删除 */
	if (pde->proc_ops->proc_flags & PROC_ENTRY_PERMANENT)
		pde->flags |= PROC_ENTRY_PERMANENT;
	/* 支持 iter 方式 读取数据的 /proc 文件,通常用于 高效的数据传输 */
	if (pde->proc_ops->proc_read_iter)
		pde->flags |= PROC_ENTRY_proc_read_iter;
#ifdef CONFIG_COMPAT
	/* 支持 32-bit 和 64-bit 兼容的 ioctl 操作 */
	if (pde->proc_ops->proc_compat_ioctl)
		pde->flags |= PROC_ENTRY_proc_compat_ioctl;
#endif
}

proc_create_seq_private 创建一个 proc 文件系统的序列文件

  • 动态生成内容:序列文件的内容不是静态存储的,而是通过内核中的回调函数动态生成。适合展示需要实时计算或动态更新的数据。
  • 分块输出:序列文件支持分块输出,避免一次性生成大量数据导致内存占用过高。用户读取文件时,内核会逐块生成数据并返回。
c 复制代码
struct proc_dir_entry *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)
{
	struct proc_dir_entry *p;

	p = proc_create_reg(name, mode, &parent, data);
	if (!p)
		return NULL;
	p->proc_ops = &proc_seq_ops;
	p->seq_ops = ops;
	p->state_size = state_size;
	pde_set_flags(p);
	return proc_register(parent, p);
}
EXPORT_SYMBOL(proc_create_seq_private);

fs/proc/self.c

  • 在 Linux 中,/proc/self 是一个 动态符号链接,始终指向 当前进程的 /proc/PID 目录

proc_self_init 为 /proc/self 文件系统条目分配唯一的 inode 编号

c 复制代码
static unsigned self_inum __ro_after_init;

void __init proc_self_init(void)
{
	/* 分配一个 inode 编号 */
	proc_alloc_inum(&self_inum);
}

fs/proc/thread_self.c

  • /proc/thread-self 是 Linux 内核中 proc 文件系统的一个"魔法"符号链接(magic symlink),它类似于 /proc/self,不过专门指向当前线程的 proc 目录,而 /proc/self 指向当前进程的 proc 目录。
  • /proc/self:当进程访问这个符号链接时,它解析为当前进程的目录(通常是 /proc/)。
  • /proc/thread-self:当线程访问这个链接时,它解析为对应于当前线程的目录,即实际路径类似于 /proc//task/。这种设计允许在多线程进程中区分并访问属于每个线程的独立信息。

使用场景 在多线程环境下,有时需要访问每个线程特定的信息,例如调度、资源统计、文件描述符等。使用 /proc/thread-self 可以确保线程所查看或操作的信息仅限于该线程本身,而不会出现混淆(而 /proc/self 总是指向整个进程)

proc_thread_self_init 为 thread-self 文件系统条目分配唯一的 inode 编号

c 复制代码
static unsigned thread_self_inum __ro_after_init;
void __init proc_thread_self_init(void)
{
	proc_alloc_inum(&thread_self_inum);
}

fs/proc/proc_tty.c

proc_tty_init 初始化 /proc/tty 目录

c 复制代码
/*
 * 由 proc_root_init() 调用以初始化 /proc/tty 子树
 */
void __init proc_tty_init(void)
{
	if (!proc_mkdir("tty", NULL))
		return;
	proc_mkdir("tty/ldisc", NULL);	/*保留:它是用户空间可见的 */
	/*
	 * /proc/tty/driver/serial 显示了串行链接的确切字符计数,
	 * 这很容易被滥用来推断密码长度和密码输入期间的按键间隔时间。
	 */
	proc_tty_driver = proc_mkdir_mode("tty/driver", S_IRUSR|S_IXUSR, NULL);
	/* 创建序列文件 */
	proc_create_seq("tty/ldiscs", 0, NULL, &tty_ldiscs_seq_ops);	/* 该文件用于提供终端线路规程(line discipline)的信息 */
	proc_create_seq("tty/drivers", 0, NULL, &tty_drivers_op);	/* 该文件用于列出系统中注册的终端驱动 */
}

fs/proc/root.c

proc_init_fs_context

c 复制代码
static const struct fs_context_operations proc_fs_context_ops = {
	.free		= proc_fs_context_free,
	.parse_param	= proc_parse_param,
	.get_tree	= proc_get_tree,
	.reconfigure	= proc_reconfigure,
};

static int proc_init_fs_context(struct fs_context *fc)
{
	struct proc_fs_context *ctx;

	ctx = kzalloc(sizeof(struct proc_fs_context), GFP_KERNEL);
	if (!ctx)
		return -ENOMEM;

	ctx->pid_ns = get_pid_ns(task_active_pid_ns(current));
	put_user_ns(fc->user_ns);
	fc->user_ns = get_user_ns(ctx->pid_ns->user_ns);
	fc->fs_private = ctx;
	fc->ops = &proc_fs_context_ops;
	return 0;
}

proc_root_init 定义和初始化 Linux 内核中的 proc 文件系统

c 复制代码
void __init proc_root_init(void)
{
    proc_init_kmemcache(); // 初始化与 /proc 文件系统相关的内核缓存
    set_proc_pid_nlink(); // 设置与进程 PID 相关的链接计数
    proc_self_init(); // 为 /proc/self 文件系统条目分配唯一的 inode 编号
    proc_thread_self_init(); //为 thread-self 文件系统条目分配唯一的 inode 编号
    proc_symlink("mounts", NULL, "self/mounts"); // 创建符号链接 /proc/mounts -> /proc/self/mounts

	/* return 0; */
    proc_net_init(); // 初始化 /proc/net
    proc_mkdir("fs", NULL); // 创建 /proc/fs 目录
    proc_mkdir("driver", NULL); // 创建 /proc/driver 目录
    proc_create_mount_point("fs/nfsd"); // 创建 /proc/fs/nfsd 挂载点
#if defined(CONFIG_SUN_OPENPROMFS) || defined(CONFIG_SUN_OPENPROMFS_MODULE)
    proc_create_mount_point("openprom"); // 如果启用 OpenPROMFS,创建 /proc/openprom 挂载点
#endif
    proc_tty_init(); // 初始化 /proc/tty
    proc_mkdir("bus", NULL); // 创建 /proc/bus 目录
    proc_sys_init(); // 初始化 /proc/sys

    /*
     * 最后注册文件系统类型,确保 /proc 文件系统被内核识别。
     */
    register_filesystem(&proc_fs_type);
}

fs/proc/nommu.c (无MMU版本):展示内核空间内存区域布局

本代码片段实现了在没有内存管理单元(MMU)的Linux系统(通常称为uClinux)上,/proc/maps 这个虚拟文件的后端逻辑。在标准的、有MMU的Linux系统上,/proc/[pid]/maps 用于显示一个进程的虚拟内存 地址空间布局。然而,在无MMU系统中,没有进程独立的虚拟地址空间,所有代码和数据都运行在一个共享的、平坦的物理地址空间中。因此,这个文件被重新定义,用于展示内核自身 所知道的所有**物理内存区域(vm_region)**的布局。它是一个关键的调试工具,用于理解无MMU系统上的内存是如何被内核划分和使用的。

实现原理分析

该文件的实现与我们之前分析的 /proc/locks 非常相似,同样是基于 seq_file 的迭代器模型。

  1. 核心数据结构 (nommu_region_tree):

    • 内核在无MMU模式下,会维护一个全局的红黑树(red-black tree) nommu_region_tree
    • 这个树中的每一个节点都是一个 vm_region 结构体,它描述了一段连续的物理内存区域,包括其起始地址(vm_start)、结束地址(vm_end)、访问权限(vm_flags)以及是否与某个文件关联(vm_file)。
    • 红黑树被用来高效地存储和查找这些内存区域,保证它们按地址有序且不重叠。
  2. Seq_file 迭代器实现:

    • nommu_region_list_start : 当用户开始读取/proc/maps时调用。
      a. down_read(&nommu_region_sem): 获取保护 nommu_region_tree 的读写信号量的共享读锁 。这确保了在遍历期间,树的结构不会被其他任务修改。
      b. for (p = rb_first(...); ...): 它从红黑树的第一个节点(地址最低的区域)开始遍历,通过不断递减pos来定位到用户请求的起始位置。
    • nommu_region_list_next : 当需要下一个条目时调用。它简单地调用 rb_next(v) 来获取红黑树中的下一个节点,逻辑非常简单。
    • nommu_region_list_show : 这是核心的显示函数。对于迭代器返回的每一个红黑树节点_p,它首先通过rb_entry宏获取其宿主结构体 vm_region 的指针,然后调用 nommu_region_show 来格式化输出。
    • nommu_region_list_stop : 当读取结束时调用,它只做一件事:up_read(&nommu_region_sem),释放读锁。
  3. 信息格式化 (nommu_region_show):

    • 这个函数负责将一个vm_region结构体中的信息,转换成与标准/proc/maps格式类似的一行文本。
    • 它会输出:
      • 地址范围 : vm_start-vm_end。在无MMU系统上,这些是物理地址
      • 权限 : rwxprwxs 等。r=读,w=写,x=执行。第四位表示共享属性:p=私有,s=共享(可写),S=共享(不可写)。
      • 文件偏移: 如果区域与文件映射相关,显示其在文件中的偏移量。
      • 设备与Inode: 如果与文件相关,显示文件所在设备的主/次设备号和inode号。
      • 文件路径: 如果与文件相关,显示文件的路径。
  4. 初始化 (proc_nommu_init):

    • 在文件系统初始化阶段,通过proc_create_seq/proc的根目录下创建一个名为maps的文件(注意,它不在[pid]子目录下),并将其与proc_nommu_region_list_seqop操作集关联起来。

代码分析

c 复制代码
// ... (头文件包含) ...

// nommu_region_show: 将单个vm_region的信息格式化并显示到seq_file。
static int nommu_region_show(struct seq_file *m, struct vm_region *region)
{
	// ... (变量定义) ...

	// 如果区域与文件关联,获取文件的inode和设备信息。
	if (file) {
		struct inode *inode = file_inode(region->vm_file);
		dev = inode->i_sb->s_dev;
		ino = inode->i_ino;
	}

	// ... (使用seq_printf和seq_puts格式化输出) ...
	// 输出格式:地址范围 权限 文件偏移 设备号:Inode号 文件路径
	seq_printf(m,
		   "%08lx-%08lx %c%c%c%c %08llx %02x:%02x %lu ",
		   region->vm_start,
		   region->vm_end,
		   flags & VM_READ ? 'r' : '-',
		   flags & VM_WRITE ? 'w' : '-',
		   flags & VM_EXEC ? 'x' : '-',
		   flags & VM_MAYSHARE ? flags & VM_SHARED ? 'S' : 's' : 'p',
		   ((loff_t)region->vm_pgoff) << PAGE_SHIFT,
		   MAJOR(dev), MINOR(dev), ino);
	// ... (输出文件路径) ...
	return 0;
}

// nommu_region_list_show: seq_file的show回调。
static int nommu_region_list_show(struct seq_file *m, void *_p)
{
	struct rb_node *p = _p;
	// 将红黑树节点转换为vm_region结构体,并调用显示函数。
	return nommu_region_show(m, rb_entry(p, struct vm_region, vm_rb));
}

// nommu_region_list_start: seq_file的start回调。
static void *nommu_region_list_start(struct seq_file *m, loff_t *_pos)
{
	struct rb_node *p;
	loff_t pos = *_pos;

	// 获取读锁以保护红黑树。
	down_read(&nommu_region_sem);

	// 从头遍历红黑树,直到找到pos指定的位置。
	for (p = rb_first(&nommu_region_tree); p; p = rb_next(p))
		if (pos-- == 0)
			return p; // 返回找到的红黑树节点作为迭代器。
	return NULL;
}

// nommu_region_list_stop: seq_file的stop回调。
static void nommu_region_list_stop(struct seq_file *m, void *v)
{
	// 释放读锁。
	up_read(&nommu_region_sem);
}

// nommu_region_list_next: seq_file的next回调。
static void *nommu_region_list_next(struct seq_file *m, void *v, loff_t *pos)
{
	(*pos)++;
	// 返回红黑树中的下一个节点。
	return rb_next((struct rb_node *) v);
}

// 定义seq_operations结构体,将回调函数组织起来。
static const struct seq_operations proc_nommu_region_list_seqop = {
	.start	= nommu_region_list_start,
	.next	= nommu_region_list_next,
	.stop	= nommu_region_list_stop,
	.show	= nommu_region_list_show
};

// proc_nommu_init: 初始化函数。
static int __init proc_nommu_init(void)
{
	// 在/proc下创建名为"maps"的seq_file。
	proc_create_seq("maps", S_IRUGO, NULL, &proc_nommu_region_list_seqop);
	return 0;
}

// 在文件系统初始化阶段调用。
fs_initcall(proc_nommu_init);

fs/proc/cmdline.c 文件实现:向用户空间展示内核启动参数

本代码片段实现了Linux内核中一个基础但非常重要的信息接口:/proc/cmdline 虚拟文件。其核心功能极其简单直接:当用户空间程序读取这个文件时,它会原封不动地返回内核在启动时接收到的命令行参数。这些参数通常由引导加载程序(bootloader,如U-Boot, GRUB等)传递给内核,用于控制内核的各种行为,例如指定根文件系统的位置、设置内核调试选项、配置硬件参数等。

实现原理分析

该文件的实现是 /proc 文件系统中最简单直接的一种,它使用了 proc_create_single 这个便捷的辅助函数,专门用于创建那些内容固定不变、只需一个show函数就能处理的 /proc 文件。

  1. 内核启动参数的存储:

    • 在内核启动的极早期,引导加载程序传递的命令行字符串会被解析并存储在一个全局的、静态的字符数组 saved_command_line 中。同时,其长度被保存在 saved_command_line_len 中。
    • 这部分存储逻辑发生在内核自举(bootstrapping)代码中,早于 procfs 的初始化。
  2. show 函数的实现 (cmdline_proc_show):

    • 这是一个典型的 seq_file show 函数,但由于内容极其简单,它不需要任何复杂的迭代逻辑。
    • seq_puts(m, saved_command_line): 将全局变量 saved_command_line 的全部内容一次性地写入 seq_file 的缓冲区。
    • seq_putc(m, '\n'): 在末尾添加一个换行符,以符合UNIX文本文件的惯例。
    • 函数总是返回0,表示显示成功。
  3. 文件创建与初始化 (proc_cmdline_init):

    • 时机 : fs_initcall 确保此函数在内核文件系统子系统初始化期间被调用。
    • proc_create_single : 这是一个高级的procfs辅助函数,它极大地简化了创建简单文件的过程。
      • "cmdline": 在/proc根目录下创建的文件名。
      • 0: 文件权限。这里设置为0,但 procfs 最终会应用默认权限,通常是0444(所有用户只读)。
      • NULL: 父目录,NULL表示在/proc根目录下创建。
      • cmdline_proc_show: 将我们之前定义的show函数与这个文件关联起来。
    • pde_make_permanent(pde) : 这是一个重要的调用。它将这个proc条目标记为"永久的"。这意味着即使创建它的模块(在本例中是内建内核)被卸载(虽然内建内核不会),这个/proc条目也不会 被移除。这保证了像/proc/cmdline这样基础的接口在系统的整个生命周期内都是可用的。
    • pde->size = ... : 手动设置proc条目的大小。这主要是一个给ls -l等命令看的"提示",让它能显示一个正确的、非零的文件大小。

代码分析

c 复制代码
// ... (头文件包含) ...

// cmdline_proc_show: /proc/cmdline文件的 "show" 回调函数。
// 当用户空间读取该文件时,此函数被调用。
static int cmdline_proc_show(struct seq_file *m, void *v)
{
	// 使用seq_puts将全局变量saved_command_line的内容写入seq_file缓冲区。
	// saved_command_line在内核启动早期由引导加载程序填充。
	seq_puts(m, saved_command_line);
	// 在末尾添加一个换行符,以符合文本文件格式。
	seq_putc(m, '\n');
	// 返回0表示成功。
	return 0;
}

// proc_cmdline_init: 初始化函数,用于创建/proc/cmdline文件。
// __init 标记表示此函数仅在内核初始化时执行。
static int __init proc_cmdline_init(void)
{
	struct proc_dir_entry *pde;

	// 使用proc_create_single创建一个简单的、只读的、内容固定的proc文件。
	// "cmdline" 是文件名。
	// 0 是初始权限(procfs会应用默认权限)。
	// NULL 表示父目录是/proc。
	// cmdline_proc_show 是处理读操作的回调函数。
	pde = proc_create_single("cmdline", 0, NULL, cmdline_proc_show);

	// 将这个proc条目标记为"永久的",防止意外移除。
	pde_make_permanent(pde);

	// 设置文件大小,以便'ls -l'等命令可以显示正确的大小。
	// 大小是字符串长度 + 1 (为了末尾的换行符或NULL终止符)。
	pde->size = saved_command_line_len + 1;
	
	return 0;
}
// fs_initcall: 将初始化函数注册为在文件系统初始化阶段执行。
fs_initcall(proc_cmdline_init);

/proc/consoles 文件实现:展示已注册的内核控制台

本代码片段实现了 /proc/consoles 这个虚拟文件。其核心功能是遍历内核中所有已注册的**控制台(console)**驱动,并将每个控制台的详细信息格式化成人类可读的文本,展示给用户空间。这个文件对于系统管理员和嵌入式开发者来说,是检查和确认哪些设备(如UART、虚拟终端、netconsole等)正在作为内核消息输出目的地的关键工具。

实现原理分析

与我们之前分析过的许多 /proc 文件一样,/proc/consoles 的实现也严格遵循了 seq_file 的迭代器模型。

  1. 核心数据结构 (console_list):

    • 内核维护一个全局的哈希链表 console_list,其中包含了所有通过 register_console() 注册的 struct console 实例。
    • 这个链表由一个专门的互斥锁 console_mutex(通过 console_list_lock/unlock 宏访问)保护,以防止在遍历或修改时发生竞态条件。
  2. Seq_file 迭代器实现:

    • c_start : 当用户开始读取文件时调用。
      a. console_list_lock(): 获取互斥锁,以安全地遍历 console_list
      b. for_each_console(con) ...: 这是一个宏,用于遍历 console_list。它通过不断递减 pos 来定位到用户请求的起始控制台实例。
    • c_next : 当需要下一个条目时调用。它使用 hlist_entry_safe 来安全地获取链表中的下一个 console 实例。_safe 变体可以在遍历时安全地处理节点的移除。
    • show_console_dev : 核心的显示函数。对于迭代器返回的每一个 console 对象,它都被调用来格式化输出。
    • c_stop : 当读取结束时调用,它只做一件事:console_list_unlock(),释放互斥锁。
  3. 信息格式化 (show_console_dev):

    • 这个函数负责将一个 struct console 结构体中的信息,转换成一行详细的文本。
    • 设备号获取 :
      • con->device(con, &index): 它会调用控制台驱动提供的 device 回调函数。这个回调函数返回与该控制台关联的TTY驱动(tty_driver)以及它的索引号(index)。
      • console_lock(): 在调用 device 回调前后,它会获取并释放 console_lock。这是为了与切换虚拟终端(VT)等操作同步,因为这些操作可能会动态地改变哪个TTY是"前景控制台"(fg_console)。
      • MKDEV(...): 根据获取到的主设备号(major)和次设备号(minor_start + index),计算出该控制台对应的设备号(dev_t)。
    • 标志位解析 : 它遍历一个预定义的 con_flags 数组,将 con->flags 中的二进制标志位(如CON_ENABLED, CON_CONSDEV)转换成易于理解的单字符标志(如E, C)。
    • 格式化输出 : 最后,它使用 seq_printf 将所有信息格式化输出,包括:
      • 名称和索引 : 如 ttyS0
      • 读/写/清屏能力 : R, W, U 标志。
      • 标志字符串 : 如 (ECB a)
      • 设备号 : 如 4:64
  4. 初始化 (proc_consoles_init):

    • 在文件系统初始化阶段,通过 proc_create_seq/proc 根目录下创建名为 consoles 的文件,并将其与 consoles_op 操作集关联。

代码分析

c 复制代码
// ... (头文件包含) ...

// show_console_dev: /proc/consoles文件的 "show" 回调函数。
static int show_console_dev(struct seq_file *m, void *v)
{
	// 定义一个结构体数组,用于将console的flags标志位映射为可读的字符。
	static const struct {
		short flag;
		char name;
	} con_flags[] = { /* ... */ };
	char flags[ARRAY_SIZE(con_flags) + 1];
	struct console *con = v;
	// ... (其他变量定义) ...

	// 如果console与一个TTY设备关联...
	if (con->device) {
		// ... 调用其device()回调函数来获取TTY驱动和索引号。
		// console_lock()用于同步,例如与虚拟终端切换操作。
		console_lock();
		driver = con->device(con, &index);
		console_unlock();

		// 如果成功获取,则计算出设备号。
		if (driver) {
			dev = MKDEV(driver->major, driver->minor_start);
			dev += index;
		}
	}

	// 遍历con_flags数组,将con->flags转换为字符串。
	for (a = 0; a < ARRAY_SIZE(con_flags); a++)
		flags[a] = (con->flags & con_flags[a].flag) ?
			con_flags[a].name : ' ';
	flags[a] = 0;

	// 使用seq_printf和相关函数格式化输出一行信息。
	// 格式:名称+索引 (读/写/清屏能力) (标志字符串) [主设备号:次设备号]
	seq_setwidth(m, 21 - 1);
	seq_printf(m, "%s%d", con->name, con->index);
	seq_pad(m, ' ');
	seq_printf(m, "%c%c%c (%s)", con->read ? 'R' : '-', /* ... */);
	if (dev)
		seq_printf(m, " %4d:%d", MAJOR(dev), MINOR(dev));
	seq_putc(m, '\n');
	return 0;
}

// c_start: seq_file的start回调。
static void *c_start(struct seq_file *m, loff_t *pos)
	__acquires(&console_mutex) // 静态分析注解:获取锁
{
	struct console *con;
	loff_t off = 0;

	// 获取锁以安全地遍历全局console_list。
	console_list_lock();
	// 遍历链表,直到找到pos指定的位置。
	for_each_console(con)
		if (off++ == *pos)
			break;
	// 返回找到的console指针作为迭代器。
	return con;
}

// c_next: seq_file的next回调。
static void *c_next(struct seq_file *m, void *v, loff_t *pos)
{
	struct console *con = v;

	++*pos;
	// 返回哈希链表中的下一个console实例。
	return hlist_entry_safe(con->node.next, struct console, node);
}

// c_stop: seq_file的stop回调。
static void c_stop(struct seq_file *m, void *v)
	__releases(&console_mutex) // 静态分析注解:释放锁
{
	console_list_unlock();
}

// consoles_op: 将回调函数组织成seq_operations结构体。
static const struct seq_operations consoles_op = {
	.start	= c_start,
	.next	= c_next,
	.stop	= c_stop,
	.show	= show_console_dev
};

// proc_consoles_init: 初始化函数。
static int __init proc_consoles_init(void)
{
	// 在/proc下创建名为"consoles"的seq_file。
	proc_create_seq("consoles", 0, NULL, &consoles_op);
	return 0;
}
// 在文件系统初始化阶段调用。
fs_initcall(proc_consoles_init);

/proc/cpuinfo 文件创建:为CPU信息展示提供VFS接口

本代码片段负责创建 /proc/cpuinfo 这个虚拟文件,并将其与一组文件操作(proc_ops)关联起来。它本身并不实现 具体的CPU信息展示逻辑,而是充当一个桥梁 ,将 /proc/cpuinfo 这个文件系统节点链接到真正负责生成内容的、由体系结构特定代码 (例如 arch/arm/kernel/setup.c)实现的 seq_file 操作集 cpuinfo_op

实现原理分析

这个文件的实现展示了 procfs 中一种更通用的文件创建方式,它直接使用 proc_ops 结构,而不是像之前看到的 proc_create_singleproc_create_seq 那样的高度封装的辅助函数。

  1. 分离的 seq_operations:

    • extern const struct seq_operations cpuinfo_op;
    • 这一行是理解本文件功能的关键。它声明了一个外部seq_operations 结构体 cpuinfo_op。这意味着 start, next, show, stop 这些真正负责遍历CPU、格式化CPU信息的函数,是在别的文件中定义的。
    • 这种设计是高度模块化 的体现。/proc/cpuinfo 的内容是强体系结构相关 的(ARM, x86, MIPS等CPU的特性完全不同),因此,将其内容生成的逻辑放在各自的体系结构代码 (arch/...) 中是唯一合理的设计。而文件创建的逻辑是通用的,所以放在 fs/proc 目录下。
  2. open 方法的实现 (cpuinfo_open):

    • 当一个用户进程打开 /proc/cpuinfo 文件时,VFS会调用 cpuinfo_proc_ops 中指定的 .proc_open 函数,即 cpuinfo_open
    • return seq_open(file, &cpuinfo_op);
    • cpuinfo_open 的工作非常简单:它调用 seq_openseq_openseq_file 框架的核心函数,它会:
      a. 分配一个 seq_file 实例。
      b. 将这个实例与外部的 cpuinfo_op 操作集关联起来。
      c. 将这个 seq_file 实例存储在 struct fileprivate_data 字段中。
    • 从这一刻起,对这个已打开文件的所有后续操作(如 read)都将由 seq_file 框架接管。
  3. proc 文件操作 (cpuinfo_proc_ops):

    • 这是一个 proc_ops 结构体,它定义了 /proc/cpuinfo 的VFS接口。
    • .proc_flags = PROC_ENTRY_PERMANENT;: 将此文件标记为永久的。
    • .proc_open: 指向我们上面分析的 cpuinfo_open
    • .proc_read_iter, .proc_lseek, .proc_release: 它们分别指向 seq_file 框架提供的通用 读、定位和释放函数(seq_read_iter, seq_lseek, seq_release)。这些通用函数会从 file->private_data 中取出 seq_file 实例,并调用其内部关联的 cpuinfo_op 中的 start/next/show/stop 来完成工作。
  4. 初始化 (proc_cpuinfo_init):

    • 使用 proc_create 函数,在 /proc 根目录下创建一个名为 cpuinfo 的文件,并将其所有文件操作指向我们定义的 cpuinfo_proc_ops

代码分析

c 复制代码
// ... (头文件包含) ...

// 声明一个外部的seq_operations结构体。
// 这个结构的实际定义在特定于体系结构的代码中(例如, arch/arm/kernel/setup.c)。
extern const struct seq_operations cpuinfo_op;

// cpuinfo_open: /proc/cpuinfo文件的open回调函数。
static int cpuinfo_open(struct inode *inode, struct file *file)
{
	// 调用seq_open,将文件对象与cpuinfo_op操作集关联起来。
	// seq_file框架将接管后续的读/定位/释放操作。
	return seq_open(file, &cpuinfo_op);
}

// cpuinfo_proc_ops: 定义/proc/cpuinfo文件的proc_ops(文件操作)集合。
static const struct proc_ops cpuinfo_proc_ops = {
	// 标记为永久条目。
	.proc_flags	= PROC_ENTRY_PERMANENT,
	// 指定open操作的处理函数。
	.proc_open	= cpuinfo_open,
	// 将read操作指向seq_file提供的通用读函数。
	.proc_read_iter	= seq_read_iter,
	// 将lseek操作指向seq_file提供的通用定位函数。
	.proc_lseek	= seq_lseek,
	// 将release/close操作指向seq_file提供的通用释放函数。
	.proc_release	= seq_release,
};

// proc_cpuinfo_init: 初始化函数。
static int __init proc_cpuinfo_init(void)
{
	// 使用proc_create,在/proc根目录下创建一个名为"cpuinfo"的文件,
	// 并将其VFS操作指向cpuinfo_proc_ops。
	proc_create("cpuinfo", 0, NULL, &cpuinfo_proc_ops);
	return 0;
}
// 将初始化函数注册为在文件系统初始化阶段执行。
fs_initcall(proc_cpuinfo_init);

/proc/devices 文件实现:展示已注册的设备驱动

本代码片段实现了 /proc/devices 这个虚拟文件。其核心功能是向用户空间提供一个当前内核中所有已注册的字符设备块设备驱动程序的列表。输出内容分为两部分,分别列出字符设备和块设备,并显示每个驱动程序所注册的**主设备号(Major Number)**以及其名称。这个文件是Linux系统中一个基础的、用于查看和管理设备驱动的接口。

实现原理分析

该文件的实现同样基于 seq_file 的迭代器模型,但其迭代方式非常独特:它不是遍历一个链表 ,而是遍历一个数字范围,即从0到最大设备主设备号。

  1. 数字迭代器 (Numeric Iterator):

    • devinfo_start: 当开始读取时,它检查请求的位置 *pos 是否在有效的主设备号范围内 (< BLKDEV_MAJOR_MAX + CHRDEV_MAJOR_MAX)。如果有效,它直接返回 pos 指针本身作为迭代器 v。这意味着迭代器 v 在这个实现中,就是指向当前要处理的主设备号的指针。
    • devinfo_next: 当需要下一个条目时,它简单地将 *pos 加一,然后检查是否越界。如果未越界,返回新的 pos 指针。
    • devinfo_stop: 无事可做,因为没有需要清理的资源。
    • 这种设计巧妙地将一次文件读取过程转换成了一个对所有可能的主设备号的遍历。
  2. 分发与显示 (devinfo_show):

    • 这是核心的显示函数。它接收迭代器 v(即 pos 指针),并解引用得到当前要处理的主设备号 i
    • 分发逻辑 :
      a. if (i < CHRDEV_MAJOR_MAX): 函数首先检查 i 是否在字符设备的主设备号范围内。如果是,它就调用字符设备子系统 提供的 chrdev_show(f, i) 函数,让该子系统去查找并显示注册在主设备号 i 上的驱动信息。
      b. #ifdef CONFIG_BLOCK ...: 如果内核配置了块设备支持,它会继续处理块设备的主设备号。i -= CHRDEV_MAJOR_MAX 将索引重新映射到块设备的主设备号范围(从0开始),然后调用块设备子系统 提供的 blkdev_show(f, i)
    • 打印表头 : 当 i 为0时(在各自的范围内),函数会打印出 "Character devices:" 或 "Block devices:" 的表头,以组织输出格式。
  3. 初始化 (proc_devices_init):

    • 通过 proc_create_seq/proc 根目录下创建名为 devices 的文件,并将其与 devinfo_ops 操作集关联起来。
    • pde_make_permanent(pde) 确保该文件在系统生命周期内始终存在。

代码分析

c 复制代码
// ... (头文件包含) ...

// devinfo_show: /proc/devices文件的 "show" 回调函数。
static int devinfo_show(struct seq_file *f, void *v)
{
	// 迭代器v就是指向位置pos的指针,解引用得到当前要处理的主设备号。
	int i = *(loff_t *) v;

	// --- 处理字符设备 ---
	if (i < CHRDEV_MAJOR_MAX) {
		// 当i为0时,打印字符设备的表头。
		if (i == 0)
			seq_puts(f, "Character devices:\n");
		// 调用字符设备子系统的函数,显示主设备号i对应的驱动信息。
		chrdev_show(f, i);
	}
// 如果内核配置了块设备支持...
#ifdef CONFIG_BLOCK
	// --- 处理块设备 ---
	else {
		// 将索引i映射到块设备的主设备号范围(从0开始)。
		i -= CHRDEV_MAJOR_MAX;
		// 当块设备索引为0时,打印块设备的表头。
		if (i == 0)
			seq_puts(f, "\nBlock devices:\n");
		// 调用块设备子系统的函数,显示主设备号i对应的驱动信息。
		blkdev_show(f, i);
	}
#endif
	return 0;
}

// devinfo_start: seq_file的start回调。
static void *devinfo_start(struct seq_file *f, loff_t *pos)
{
	// 检查请求的位置是否在有效的主设备号范围内。
	if (*pos < (BLKDEV_MAJOR_MAX + CHRDEV_MAJOR_MAX))
		// 如果有效,返回位置指针本身作为迭代器。
		return pos;
	// 否则,返回NULL表示迭代结束。
	return NULL;
}

// devinfo_next: seq_file的next回调。
static void *devinfo_next(struct seq_file *f, void *v, loff_t *pos)
{
	// 将位置(主设备号)加一。
	(*pos)++;
	// 检查是否超出范围。
	if (*pos >= (BLKDEV_MAJOR_MAX + CHRDEV_MAJOR_MAX))
		return NULL;
	// 返回新的位置指针作为下一个迭代器。
	return pos;
}

// devinfo_stop: seq_file的stop回调。
static void devinfo_stop(struct seq_file *f, void *v)
{
	/* 无事可做 */
}

// devinfo_ops: 将回调函数组织成seq_operations结构体。
static const struct seq_operations devinfo_ops = {
	.start = devinfo_start,
	.next  = devinfo_next,
	.stop  = devinfo_stop,
	.show  = devinfo_show
};

// proc_devices_init: 初始化函数。
static int __init proc_devices_init(void)
{
	struct proc_dir_entry *pde;

	// 在/proc下创建名为"devices"的seq_file。
	pde = proc_create_seq("devices", 0, NULL, &devinfo_ops);
	// 将其标记为永久条目。
	pde_make_permanent(pde);
	return 0;
}
// 在文件系统初始化阶段调用。
fs_initcall(proc_devices_init);
相关推荐
瞬间&永恒~2 分钟前
【Kubernetes】(十五)维护与升级、ETCD 备份和恢复
linux·运维·docker·云原生·kubernetes
王的宝库4 分钟前
【无标题】
笔记·restful
xxwl5856 分钟前
Git常用命令的学习
git·学习
具身AGI11 分钟前
从演示到作业,物理AI 人类学习路线 的深意
人工智能·学习
susplus11 分钟前
【linux应用软件编程】进程间的通信方式2【网络通信从入门到UDP编程】
linux·udp·ip
MSTcheng.12 分钟前
【Linux】Linux学习第三弹——Linux基本指令2
linux·windows·学习·ubuntu·操作系统
Capricorn198812 分钟前
科研智能体出现召回率低与数据覆盖冲突怎么排查?知芽 Notebook Skill 机制拆解
大数据·论文阅读·人工智能·笔记
希望奇迹很安静18 分钟前
Linux基本使用命令
linux·运维·服务器
tju新生代魔迷21 分钟前
Verilog HDL学习笔记(三)| 第3章 基本概念(上)——词法约定与数据类型
笔记·学习
邪修king23 分钟前
Re:Linux 系统篇(十七):进程篇(六):环境变量深度解析|命令行参数、环境变量表、4 种获取方式与继承机制万字详解
linux·服务器·c++