+
+
+ 10100-00+00
+
+
+tcp丢包分析系列文章代码来自谢宝友老师,由西邮陈莉君教授研一学生进行解析,本文由戴君毅整理,梁金荣编辑,贺东升校对。
+
+
最初开发 /proc
文件系统是为了提供有关系统中进程的信息。但是这个文件系统非常有用, /proc
文件系统包含了一些目录(用作组织信息的方式)和虚拟文件。虚拟文件可以向用户呈现内核中的一些信息,也可以用作一种从用户空间向内核发送信息的手段。
+
/proc
文件系统可以为提供很多信息, 在左边是一系列数字编号,每个实际上都是一个目录,表示系统中的一个进程。由于在Linux中创建的第一个进程是 init
进程,因此它的 process-id
为 1。
+

+
右边的目录包含特定信息,比如cpuinfo
包含了CPU的信息,modules
包含了内核模块的信息。
+

+
为了解决一些实际问题,我们需要在/proc
下创建条目捕获信息,使用文件系统通用方法肯定是不行的,需要使用相关API编写内核模块来实现。
+
在做谢宝友老师写的“TCP丢包分析”实验里,首先就会在/proc
下创建条目,较为简单,先来看init
和exit
:
+
static int drop_packet_init(void)
+{
+ int ret;
+ struct proc_dir_entry *pe;
+
+ proc_mkdir("mooc", NULL);
+ proc_mkdir("mooc/net", NULL);
+
+ ret = -ENOMEM;
+ pe = proc_create("mooc/net/drop-packet",
+ S_IFREG | 0644,
+ NULL,
+ &drop_packet_fops);
+ if (!pe)
+ goto err_proc;
+
+ printk("drop-packet loaded.\n");
+
+ return 0;
+
+err_proc:
+ return ret;
+}
+
+static void drop_packet_exit(void)
+{
+ remove_proc_entry("mooc/net/drop-packet", NULL);
+ remove_proc_entry("mooc/net", NULL);
+ remove_proc_entry("mooc", NULL);
+
+ printk("drop-packet unloaded.\n");
+}
+
框架还是比较清晰的,需要深入源码来感受一下,第一部分代码:
+
*struct proc_dir_entry pe;
+
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 pde_opener instances */
+ spinlock_t pde_unload_lock;
+ struct completion *pde_unload_completion;
+ const struct inode_operations *proc_iops;
+ const struct file_operations *proc_fops;
+ const struct dentry_operations *proc_dops;
+ union {
+ const struct seq_operations *seq_ops;
+ int (*single_show)(struct seq_file *, void *);
+ };
+ proc_write_t write;
+ void *data;
+ unsigned int state_size;
+ unsigned int low_ino;
+ nlink_t nlink;
+ kuid_t uid;
+ kgid_t gid;
+ loff_t size;
+ struct proc_dir_entry *parent;
+ struct rb_root subdir;
+ struct rb_node subdir_node;
+ char *name;
+ umode_t mode;
+ u8 namelen;
+ char inline_name[];
+} __randomize_layout;
+
结构proc_dir_entry
定义在<fs/proc/internal.h>
下,可以称为一个pde
,在创建一个文件或目录时就会创建一个pde
来管理它们。而在打开它们的时候,则会创建一个proc_inode
结构:
+
struct proc_inode {
+ struct pid *pid;
+ unsigned int fd;
+ union proc_op op;
+ struct proc_dir_entry *pde;
+ struct ctl_table_header *sysctl;
+ struct ctl_table *sysctl_entry;
+ struct hlist_node sysctl_inodes;
+ const struct proc_ns_operations *ns_ops;
+ struct inode vfs_inode;
+} __randomize_layout;
+
可以使用PROC_I
宏,也就是我们熟悉的container_of
,从虚拟文件系统的inode
得到proc_inode
,进而得到pde
。
+
static inline struct proc_inode *PROC_I(const struct inode *inode)
+{
+ return container_of(inode, struct proc_inode, vfs_inode);
+}
+
+static inline struct proc_dir_entry *PDE(const struct inode *inode)
+{
+ return PROC_I(inode)->pde;
+}
+
回到proc_dir_entry
结构,很多信息从字段名字就可以看出,pde
需要指向创建自己的父pde
结构,subdir
的组织方式是红黑树,还需要我们实现操作集以及一些引用计数和命名规则等等。
+
有意思的是,除了操作集之外还有一个proc_write_t
,对于一些功能比较简单的proc
文件,我们只要实现这个函数即可,而不用设置inode_operations
结构,在注册proc
文件的时候,会自动为proc_fops
设置一个缺省的 file_operations
结构。
+
此时,我们可以想象以下模型:
+

+
第二部分代码是:
+
proc_mkdir(“mooc”, NULL);
+
proc_mkdir(“mooc/net”, NULL);
+
remove_proc_entry(“mooc/net”, NULL);
+
remove_proc_entry(“mooc”, NULL);
+
易知其功能是在/proc
下创建和删除条目mooc/net
,以创建操作为例,看下内核代码如何实现的:
+
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);
+
struct proc_dir_entry *proc_mkdir_data(const char *name, umode_t mode,
+ struct proc_dir_entry *parent, void *data)
+{
+ struct proc_dir_entry *ent;
+
+ if (mode == 0)
+ mode = S_IRUGO | S_IXUGO;
+
+ ent = __proc_create(&parent, name, S_IFDIR | mode, 2);
+ if (ent) {
+ ent->data = data;
+ ent->proc_fops = &proc_dir_operations;
+ ent->proc_iops = &proc_dir_inode_operations;
+ parent->nlink++;
+ ent = proc_register(parent, ent);
+ if (!ent)
+ parent->nlink--;
+ }
+ return ent;
+}
+EXPORT_SYMBOL_GPL(proc_mkdir_data);
+
这里逻辑很简单,proc_mkdir
实际上是proc_mkdir_data
默认了权限为 S_IRUGO | S_IXUGO
,再调用 __proc_create
初始化一个局部pde
,如果成功则初始化操作集,并调用proc_register
注册这个pde
到父pde
下并返回。
+
__proc_create
调用kmem_cache_zalloc
从cache
中获取空间给pde
,并且对条目名称进行检查。如果成功,则对名称、模式等属性赋值,设置引用计数并初始化锁。
+
__proc_register
接收两个参数,一个父亲pde
,一个当前pde
,目的是把当前pde
挂到父亲名下,前面提到subdir
的组织形式是红黑树,那么肯定涉及相关代码,来看:
+
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)))
+ return NULL;
+
+ p = __proc_create(parent, name, mode, 1);
+ if (p) {
+ p->proc_iops = &proc_file_inode_operations;
+ p->data = data;
+ }
+ return p;
+}
+
首先判断当前pde
的id
是否越界,如果没有打开子目录锁,把当前pde
的parent
字段指向父亲pde
,并尝试在红黑树中插入子目录,成功后重新上锁并返回当前pde
。
+
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;
+
+ /* Figure out where to put new node */
+ 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;
+ }
+
+ /* Add new node and rebalance tree. */
+ rb_link_node(&de->subdir_node, parent, new);
+ rb_insert_color(&de->subdir_node, root);
+ return true;
+}
+
红黑树的插入操作篇幅所限不再叙述。
+
下面看第三部分代码:
+
pe = proc_create("mooc/net/drop-packet",
+ S_IFREG | 0644,
+ NULL,
+ &drop_packet_fops);
+
struct proc_dir_entry *proc_create(const char *name, umode_t mode,
+ struct proc_dir_entry *parent,
+ const struct file_operations *proc_fops)
+{
+ return proc_create_data(name, mode, parent, proc_fops, NULL);
+}
+EXPORT_SYMBOL(proc_create);
+
proc_create
内部也是调用proc_create_data
,但还需自行指定权限以及操作集回调,用于创建一个proc
文件,在3.10内核中取代create_proc_entry
这个旧的接口。
+
回到实验代码,我们为加入的条目编写操作集接口。
+
const struct file_operations drop_packet_fops = {
+ .open = drop_packet_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .write = drop_packet_write,
+ .release = single_release,
+};
+
一般地,内核通过在procfs
文件系统下建立文件来向用户空间提供输出信息,用户空间可以通过任何文本阅读应用查看该文件信息,但是procfs
有一个缺陷,如果输出内容大于1个内存页,需要多次读,因此处理起来很难,另外,如果输出太大,速度比较慢,有时会出现一些意想不到的情况,Alexander Viro
实现了一套新的功能,使得内核输出大文件信息更容易,它们叫做seq_file
,所以在使用它们的操作集时需要包含seq_file.h
头文件。
+
Drop_packet_open
实际上是调用了single_open
:
+
static int drop_packet_open(struct inode *inode, struct file *filp)
+{
+ return single_open(filp, drop_packet_show, NULL);
+}
+
为什么这么做?内核文档给出了相关描述:
+

+
https://www.kernel.org/doc/Documentation/filesystems/seq_file.txt
+
你可能发现,内核文档里显示的是seq_open
,而实验里是single_open
,它们有什么区别呢?实际上内核文档的最后给出了答案:
+

+
谢宝友老师的实验中运用seq_file
的极简版本(extra-simple version),只需定义一个show()
函数。完整的情况我们还需要实现start()
,next()
等迭代器来对seq_file
进行操作。极简版本中,open
方法需要调用single_open
,对应的,release
方法调用single_release
。
+
推荐阅读https://www.ibm.com/developerworks/cn/linux/l-proc.html
+
+