1. procfs 是什么 众所周知,文件系统是 Linux 的骨架,proc 文件系统就是其中一个展示程序运行状态 的虚拟 文件系统。
如果你查看 /proc
目录下的内容,会发现,除了一堆数字命名的文件夹,还有很多杂七杂八的文件,比如 kallsyms cpuinfo meminfo
,这些文件也都是系统的一些信息,比如 kallsyms 可以展示当前 vmlinux 的所有符号,cpuinfo 展示的是 cpu 的一些参数,meminfo 展示的是内存的一些参数。这些实现都比较简单;稍微复杂一些的是以数字命名的文件夹,也就是所有进程的信息,这也正是 proc 文件系统名字的由来。
之前的一篇 文章 中已经介绍了 procfs 下 /proc/kallsyms
以及 seq_file 的实现机制,而本文着重 procfs 的整体框架,并着重分析和 process 有关的 /proc/${pid}
一系列内容
2. procfs 的 inode 以及 file_operations 了解一个文件系统,可以直接从 inode_operations && file_operations 入手
在 fs/proc/root.c
可以查到,procfs 的根目录为 /proc
,根目录项为 proc_root
,对应 inode_operations 为 proc_root_inode_operations
,最关键的就是 lookup 回调函数
其实这种虚拟文件系统的 lookup 已经比 ext 这种磁盘文件系统简化很多了,大部分 dentry 都是非常有规律地保存在内存中的,而不像磁盘那样还要和硬件存取打交道。
static const struct inode_operations proc_root_inode_operations = { .lookup = proc_root_lookup, };struct proc_dir_entry proc_root = { .proc_iops = &proc_root_inode_operations, .proc_dir_ops = &proc_root_operations, .name = "/proc" , };
proc_root_lookup
逻辑非常简单:首先尝试 proc_pid_lookup
,失败之后尝试 proc_lookup
。从名字可以猜想一波,是不是前者和进程 pid 有关,后者比较普通呢?
1 2 3 4 5 6 7 static struct dentry *proc_root_lookup (struct inode * dir, struct dentry * dentry, unsigned int flags) { if (!proc_pid_lookup(dentry, flags)) return NULL ; return proc_lookup(dir, dentry, flags); }
The anwser is: Yes!
上面已经提到,procfs 的内容正好可以分为两大类:1.进程信息;2.额外的系统信息,而 /proc/${pid}
和 /proc/cpuinfo
这两种文件,对应的查找方式就分别为 proc_pid_lookup
和 proc_lookup
后者是比较常见一些的,通过 rb_tree 进行目录项的查找,前者是 procfs 的基础,所以接下来探究一下 proc_pid_lookup 做了什么
如下所示,proc_pid_lookup
主要做了下面的几件事:
通过 dentry 获取文件的 fs_info,从而得到 namespace
通过 pid 查找进程 task_struct 结构体
判断该进程是否是隐藏起来的,如果是,就无法在 procfs 中显示
初始化 pid 对应的 dentry
最后一点非常有意思,什么叫初始化 dentry?这是因为,procfs 是一个虚拟文件系统,所有的文件并没有持久化保存,而是随用随取,自然,这些目录项也没有必要在系统启动的时候就创建好,而是等到 lookup 操作的时候再按需创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 struct dentry *proc_pid_lookup (struct dentry *dentry, unsigned int flags) { struct task_struct *task ; unsigned tgid; struct proc_fs_info *fs_info ; struct pid_namespace *ns ; struct dentry *result = ERR_PTR(-ENOENT); tgid = name_to_int(&dentry->d_name); if (tgid == ~0U ) goto out; fs_info = proc_sb_info(dentry->d_sb); ns = fs_info->pid_ns; rcu_read_lock(); task = find_task_by_pid_ns(tgid, ns); if (task) get_task_struct(task); rcu_read_unlock(); if (!task) goto out; if (fs_info->hide_pid == HIDEPID_NOT_PTRACEABLE) { if (!has_pid_permissions(fs_info, task, HIDEPID_NO_ACCESS)) goto out_put_task; } result = proc_pid_instantiate(dentry, task, NULL ); out_put_task: put_task_struct(task); out: return result; }
proc_pid_instantiate
主要是分配了 inode,并且设置了对应的 inode_operations && file_operations
,这样的话,下一次更深入一层,在 /proc/${pid}
下查找目录项的时候,就会触发 proc_tgid_base_inode_operations.proc_tgid_base_lookup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static const struct inode_operations proc_tgid_base_inode_operations = { .lookup = proc_tgid_base_lookup, };static struct dentry *proc_pid_instantiate (struct dentry * dentry, struct task_struct *task, const void *ptr) { struct inode *inode ; inode = proc_pid_make_inode(dentry->d_sb, task, S_IFDIR | S_IRUGO | S_IXUGO); if (!inode) return ERR_PTR(-ENOENT); inode->i_op = &proc_tgid_base_inode_operations; inode->i_fop = &proc_tgid_base_operations; inode->i_flags|=S_IMMUTABLE; set_nlink(inode, nlink_tgid); pid_update_inode(task, inode); d_set_d_op(dentry, &pid_dentry_operations); return d_splice_alias(inode, dentry); }
重新整理一下,从 /proc
到 /proc/${pid}
,其 inode 对应的操作是不同的,前者需要兼顾进程信息和普通系统信息两种类型的文件,后者则负责查找每一个进程所对应的虚拟文件(进程状态)
2. procfs 如何显示进程状态 前面说到,通过一级级的 dentry 查找,触发 inode 的 lookup 函数之后,来到了 proc_tgid_base_lookup
。
比如,如果执行命令 cat /proc/1/maps
,就会进入 proc_pident_lookup
,参数中 dentry->d_name.name == 'maps'
,所以这个函数中就需要比较 /proc/1
目录下,是否有和 ‘maps’ 名字相同的文件
暂时不了解 pid_entry 结构没有关系,就算盲猜,也知道这个结构当中保存了目录项的 name,name's length
两个属性,这个函数的操作也就非常简单了,就是通过一个 proc_pident_lookup
,循环比较 pid_entry
数组中的所有元素和当前查找的 dentry,比较名字是否相同。有意思的一点是,时间复杂度并不是 O(N),因为 /proc/${pid}
下的所有文件都是固定的,几乎没有改动,所以复杂度还是 O(1) 的,源码中也注明了: ‘Yes, it does not scale. And it should not’
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 static struct dentry *proc_pident_lookup (struct inode *dir, struct dentry *dentry, const struct pid_entry *p, const struct pid_entry *end) { struct task_struct *task = get_proc_task(dir); struct dentry *res = ERR_PTR(-ENOENT); if (!task) goto out_no_task; for (; p < end; p++) { if (p->len != dentry->d_name.len) continue ; if (!memcmp (dentry->d_name.name, p->name, p->len)) { res = proc_pident_instantiate(dentry, task, p); break ; } } put_task_struct(task); out_no_task: return res; }static struct dentry *proc_tgid_base_lookup (struct inode *dir, struct dentry *dentry, unsigned int flags) { return proc_pident_lookup(dir, dentry, tgid_base_stuff, tgid_base_stuff + ARRAY_SIZE(tgid_base_stuff)); }
struct pid_entry
用来描述一个进程下的虚拟文件,其结构如下,核心也是 iop 和 fop 这两个 operations,这样就可以控制 /proc/${pid}
下面每一个文件的打开读取方式,以及每一个子目录的查找方式
1 2 3 4 5 6 7 8 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 ; };
并且,通过 NOD,DIR 等一系列宏,又可以减少很多 boilerplate 样板代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #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(LSM, NAME, MODE) \ NOD(NAME, (S_IFREG|(MODE)), \ NULL, &proc_pid_attr_operations, \ { .lsm = LSM })
因此,一个文件的所有属性,或者说状态信息,都被抽象为了一个个子文件,或者子文件夹,也就可以使用 pid_entry 数组来表示这个结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 static const struct pid_entry tid_base_stuff [] = { DIR("fd" , S_IRUSR|S_IXUSR, proc_fd_inode_operations, proc_fd_operations), DIR("fdinfo" , S_IRUSR|S_IXUSR, 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),#ifdef CONFIG_SCHED_DEBUG REG("sched" , S_IRUGO|S_IWUSR, proc_pid_sched_operations),#endif 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
宏表示的 regular file,设定好 file_operations 之后,就可以通过 seq_file 接口和 open/read
等系统调用交互了,如果是 DIR
宏表示的 directory file,还需要提供新的 inode_operations,如果下一级还有目录,再提供一个 inode_operations 即可 ……