一: 概述
Proc文件系统是一个虚拟文件系统, 它是一个非常有用的文件系统, 用户通过它可以查看内核信息,修改内核设置的机制, 是一种用户空间和内核空间通讯的一种方法. Proc下的文件都是虚拟文件;是系统动态创建的. 本文将分析proc文件系统是如何实现 ,其实只要有了VFS知识以后再分析procfs还是比较简单的.
二: procfs初始化
既然procfs是一个文件系统, 那么我们首先要将它注册到内核中, 如果要使用它,还的需要安装此文件系统. 下面我们就从profs 的初始化入手吧.
如果CONFIG_PROC_FS编译选项打开以后,kernel在启动时就会调用proc_root_init()初始化procfs了.
先看看初始化的代码:
void __init proc_root_init(void)
{
int err = proc_init_inodecache();
if (err)
return;
err = register_filesystem(&proc_fs_type); //注册proc文件系统
if (err)
return;
proc_mnt = kern_mount_data(&proc_fs_type, &init_pid_ns); //挂载proc文件系统
err = PTR_ERR(proc_mnt);
if (IS_ERR(proc_mnt)) {
unregister_filesystem(&proc_fs_type);
return;
}
……………..
}
首先是注册proc文件系统register_filesystem(),先看看procfs定义:
static struct file_system_type proc_fs_type = {
.name = "proc",
.get_sb = proc_get_sb,
.kill_sb = proc_kill_sb,
};
接下来调用kern_mount_data()挂载procfs ,这个函数的流程已经在挂载rootfs文章中介绍了.
我们需要关注的就是在挂载过程中 proc_get_sb()这个回调函数的实现过程.
下面是proc_get_sb()的代码片段:
static int proc_get_sb(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data, struct vfsmount *mnt)
{
int err;
struct super_block *sb;
struct pid_namespace *ns;
struct proc_inode *ei;
if (proc_mnt) {
ei = PROC_I(proc_mnt->mnt_sb->s_root->d_inode);
if (!ei->pid)
ei->pid = find_get_pid(1);
}
if (flags & MS_KERNMOUNT)
ns = (struct pid_namespace *)data;
else
ns = current->nsproxy->pid_ns;
sb = sget(fs_type, proc_test_super, proc_set_super, ns); //获取procfs超级块
if (IS_ERR(sb))
return PTR_ERR(sb);
if (!sb->s_root) { //判断是否建立procfs根目录
sb->s_flags = flags;
err = proc_fill_super(sb);
if (err) {
up_write(&sb->s_umount);
deactivate_super(sb);
return err;
}
ei = PROC_I(sb->s_root->d_inode);
if (!ei->pid) {
rcu_read_lock();
ei->pid = get_pid(find_pid_ns(1, ns));
rcu_read_unlock();
}
sb->s_flags |= MS_ACTIVE;
ns->proc_mnt = mnt;
}
return simple_set_mnt(mnt, sb);
}
在这个函数中首先是为procfs分配一个超级块 ,然后填充这个超级块super , 这是一个比较重要的函数,因为在这个函数中会建立proc文件系统的根目录.
下面跟踪一下代码:
int proc_fill_super(struct super_block *s)
{
struct inode * root_inode;
s->s_flags |= MS_NODIRATIME | MS_NOSUID | MS_NOEXEC;
s->s_blocksize = 1024;
s->s_blocksize_bits = 10;
s->s_magic = PROC_SUPER_MAGIC;
s->s_op = &proc_sops; //超级块操作方法
s->s_time_gran = 1;
de_get(&proc_root);
root_inode = proc_get_inode(s, PROC_ROOT_INO, &proc_root); //分配procfs根目录索引节点
if (!root_inode)
goto out_no_root;
root_inode->i_uid = 0;
root_inode->i_gid = 0;
s->s_root = d_alloc_root(root_inode); //分配procfs根目录的目录项对象
if (!s->s_root)
goto out_no_root;
return 0;
out_no_root:
printk("proc_read_super: get root inode failed\n");
iput(root_inode);
de_put(&proc_root);
return -ENOMEM;
}
继续跟踪procfs根目录索引节点实现函数:首先需要看看proc_root的定义
struct proc_dir_entry proc_root = {
.low_ino = PROC_ROOT_INO, //inode编号
.namelen = 5,
.name = "/proc",
.mode = S_IFDIR | S_IRUGO | S_IXUGO, //目录文件
.nlink = 2,
.count = ATOMIC_INIT(1),
.proc_iops = &proc_root_inode_operations, //索引节点操作方法
.proc_fops = &proc_root_operations, //文件操作方法
.parent = &proc_root, //父对象
};
这个结构记录了procfs根目录的信息, 在后面建立procfs根索引节点时会利用它初始化根inode .
下面看看根目录索引节点是如何建立并初始化的:
struct inode *proc_get_inode(struct super_block *sb, unsigned int ino,
struct proc_dir_entry *de)
{
struct inode * inode;
if (!try_module_get(de->owner))
goto out_mod;
inode = iget_locked(sb, ino); //在索引节点高速缓存中分配一个inode
if (!inode)
goto out_ino;
if (inode->i_state & I_NEW) {
PROC_I(inode)->fd = 0;
PROC_I(inode)->pde = de;
if (de->proc_iops)
inode->i_op = de->proc_iops; //初始化索引节点操作方法
if (de->proc_fops) {
if (S_ISREG(inode->i_mode)) {
……….
inode->i_fop = &proc_reg_file_ops; //procfs普通文件的操作方法
} else {
inode->i_fop = de->proc_fops; //非普通文件的操作方法
}
}
unlock_new_inode(inode);
} else
module_put(de->owner);
return inode;
out_ino:
module_put(de->owner);
out_mod:
return NULL;
}
函数iget_locked()就是分配一个inode, procfs分配inode是比较特殊的. 先看看在alloc_inode()函数中的这段代码:
if (sb->s_op->alloc_inode)
inode = sb->s_op->alloc_inode(sb);
else
inode = (struct inode *) kmem_cache_alloc(inode_cachep, GFP_KERNEL);
procfs定义的超级块操作方法为:
static const struct super_operations proc_sops = {
.alloc_inode = proc_alloc_inode,
.destroy_inode = proc_destroy_inode,
.drop_inode = generic_delete_inode,
.delete_inode = proc_delete_inode,
.statfs = simple_statfs,
.remount_fs = proc_remount,
};
在这个结构体中可以看到定义了proc分配inode的方法 ,此函数为proc_alloc_inode()
跟踪一下代码:
static struct inode *proc_alloc_inode(struct super_block *sb)
{
struct proc_inode *ei;
struct inode *inode;
ei = (struct proc_inode *)kmem_cache_alloc(proc_inode_cachep, GFP_KERNEL);
if (!ei)
return NULL;
ei->pid = NULL;
ei->fd = 0;
ei->op.proc_get_link = NULL;
ei->pde = NULL;
inode = &ei->vfs_inode;
inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
return inode;
}
在这个函数中先是在cache中分配一个proc_inode结构体. 然后初始化它,最后返回proc_inode中嵌套的inode.
下面再回到proc_get_inode()函数中,在分配到procfs根目录的inode后就需要初始化这个inode了,
回到函数proc_get_inode(),注意函数PROC_I(inode)->pde = de;就是将proc_dir_entry对象与inode关联在一起(inode和proc_dir_entry是一一对应的) ,在建立proc文件后proc_dir_entry对象会在内存中建立起一颗目录树, 因为我们现在建立的是procfs根目录的inode所以这里的proc_dir_entry对象就是这个树的根proc_root .
后面的代码就是利用proc_dir_entry对象初始化我们根目录的inode了,其中就包括最重要的inode操作方法和proc文件操作方法.的初始化.
既然根目录索引节点inode已经建好, 下面就是调用函数d_alloc_root() 建立一个名字为”/”的根目录项对象dentry,,这个就是procfs的根了.
好了,这个时候procfs的根已经在内存中建立起来了. 其实在这个时候内存中只有根目录的inode和dentry存在,而其他文件的inode和dentry都是动态建立起来的. 那么下面就要分析如何在procfs中建立一个文件了.
三: 在procfs中创建文件
在procfs中创建一个文件用函数proc_create() 或者用create_proc_read_entry()创建一个只读文件.
这里我们用proc_create()函数来分析在procfs下是如何创建文件的:
static inline struct proc_dir_entry *proc_create(const char *name, mode_t mode,
struct proc_dir_entry *parent, const struct file_operations *proc_fops)
{
return proc_create_data(name, mode, parent, proc_fops, NULL);
}
接着跟踪proc_create_data():
struct proc_dir_entry *proc_create_data(const char *name, mode_t mode,
struct proc_dir_entry *parent,
const struct file_operations *proc_fops,
void *data)
{
struct proc_dir_entry *pde;
nlink_t nlink;
if (S_ISDIR(mode)) {
if ((mode & S_IALLUGO) == 0)
mode |= S_IRUGO | S_IXUGO; //可读写
nlink = 2;
} else {
if ((mode & S_IFMT) == 0)
mode |= S_IFREG; //普通文件
if ((mode & S_IALLUGO) == 0)
mode |= S_IRUGO; //可读
nlink = 1;
}
pde = __proc_create(&parent, name, mode, nlink); //创建proc_dir_entry对象
if (!pde)
goto out;
pde->proc_fops = proc_fops; //proc文件操作方法
pde->data = data;
if (proc_register(parent, pde) < 0) //注册proc_dir_entry对象
goto out_free;
return pde;
out_free:
kfree(pde);
out:
return NULL;
}
继续跟踪proc_dir_entry对象的创建过程.
static struct proc_dir_entry *__proc_create(struct proc_dir_entry **parent,
const char *name, mode_t mode, nlink_t nlink)
{
struct proc_dir_entry *ent = NULL;
const char *fn = name;
int len;
if (!name || !strlen(name)) goto out;
if (xlate_proc_name(name, parent, &fn) != 0) //查找要建立的proc_dir_entry对象是否存在
goto out;
if (strchr(fn, '/'))
goto out;
len = strlen(fn);
ent = kmalloc(sizeof(struct proc_dir_entry) + len + 1, GFP_KERNEL); //分配proc_dir_entry对象
if (!ent) goto out;
memset(ent, 0, sizeof(struct proc_dir_entry));
memcpy(((char *) ent) + sizeof(struct proc_dir_entry), fn, len + 1);
ent->name = ((char *) ent) + sizeof(*ent);
ent->namelen = len; //初始化名字长度
ent->mode = mode; //初始化模式
……………
out:
return ent;
}
xlate_proc_name()函数首先判断parent对象是否为空, 如果为空将proc_root指定为默认的parent对象 ,在xlate_proc_name()函数中还需要判断要建立的roc_dir_entry对象是否已经存在.如果存在就会返回错误 . 接下来就是在内存中建立并根据传入的参数初始化proc_dir_entry对象 .
在建立好proc_dir_entry对象后当然需要把这个它注册到内核中, 以便在后面动态建立该文件时根据名字找到这个proc_dir_entry对象.其实注册proc_dir_entry对象就是把它加到以proc_root为根的目录树中.
下面来看一下注册proc_dir_entry对象的代码:
static int proc_register(struct proc_dir_entry * dir, struct proc_dir_entry * dp)
{
unsigned int i;
struct proc_dir_entry *tmp;
i = get_inode_number(); //动态分配inode num
if (i == 0)
return -EAGAIN;
dp->low_ino = i;
if (S_ISDIR(dp->mode)) { //目录文件
if (dp->proc_iops == NULL) {
dp->proc_fops = &proc_dir_operations; //默认的目录文件操作方法
dp->proc_iops = &proc_dir_inode_operations; //默认目录文件的索引节点操作方法
}
dir->nlink++;
} else if (S_ISLNK(dp->mode)) {
if (dp->proc_iops == NULL)
dp->proc_iops = &proc_link_inode_operations; //默认符号链接文件操作方法
} else if (S_ISREG(dp->mode)) {
if (dp->proc_fops == NULL)
dp->proc_fops = &proc_file_operations; //默认普通文件的操作方法
if (dp->proc_iops == NULL)
dp->proc_iops = &proc_file_inode_operations; //默认普通文件索引节点操作方法
}
spin_lock(&proc_subdir_lock);
for (tmp = dir->subdir; tmp; tmp = tmp->next)
if (strcmp(tmp->name, dp->name) == 0) { //判断是否已经注册
printk(KERN_WARNING "proc_dir_entry '%s' already "
"registered\n", dp->name);
dump_stack();
break;
}
dp->next = dir->subdir;
dp->parent = dir;
dir->subdir = dp;
spin_unlock(&proc_subdir_lock);
return 0;
}
到这里proc_dir_entry对象就已经创建和注册到内核中了. 在内核中会建立一个以proc_root为根的树结构, 一个proc_dir_entry对象就对应一个文件. 在动态建立文件时,会根据文件名查找这棵树,找到对应的proc_dir_entry对象, 将其中的信息初始化给文件索引节点inode.
那么procfs是如何动态建立文件的呢 ,在以前的文章中介绍过vfs路径查找的流程 do_path_lookup()->…..-> real_lookup()->dir->i_op->lookup() ; lookup这个回调函数就是在内存中建立文件inode .
在procfs中默认定义的目录文件索引节点操作方法是:
static const struct inode_operations proc_dir_inode_operations = {
.lookup = proc_lookup,
.getattr = proc_getattr,
.setattr = proc_notify_change,
};
看看proc_lookup()中是如何建立inode的:proc_lookup()->proc_lookup_de()->proc_get_inode();
在找到父目录proc_dir_entry对象后, 调用函数proc_lookup_de() 根据目标文件的dentry->d_name查找到对应的proc_dir_entry对象 ,然后调用proc_get_inode()分配inode,并由proc_dir_entry对象初始化inode .此时文件操作方法也是在proc_dir_entry对象中取得.
四 : procfs文件续写
还是找个具体例子来看看吧. 在/proc下有个devices文件,就拿它来分析: 我们read这个虚拟的device文件就会打印中注册到内核中的字符和块设备.
首先在procfs初始化的时候,调用proc_create("devices", 0, NULL, &proc_devinfo_operations);在proc顶层目录创建一个device文件.
在以前已经分析过了VFS读写文件的过程 ,现在就来看看读device的流程, 如果cat /proc/device 函数执行流程为devinfo_open()->seq_read()->devinfo_show()
static int devinfo_show(struct seq_file *f, void *v)
{
int i = *(loff_t *) v;
if (i < CHRDEV_MAJOR_HASH_SIZE) {
if (i == 0)
seq_printf(f, "Character devices:\n");
chrdev_show(f, i);
}
#ifdef CONFIG_BLOCK
else {
i -= CHRDEV_MAJOR_HASH_SIZE;
if (i == 0)
seq_printf(f, "\nBlock devices:\n");
blkdev_show(f, i);
}
#endif
return 0;
}
chrdev_show()和blkdev_show();就是分别遍历注册在内核中的字符和块设备,返回设备信息.
五:小结
可见procfs是一个伪文件系统,它只存在于内存中,而porcfs下面的文件全部都是虚拟的文件,他们的实际大小都是0 . 其实在分析完procfs文件系统以后再去分析Linux其他伪文件系统就比较简单了,如: tmpfs,ramfs,sysfs…原理都是一样的了哦.