早期的Unix在设备文件目录/dev下设置了一个特殊文件,称为/dev/mem。通过这个文件可以读写系统的整个物理内存,而物理内存的地址就用读写文件内部的位移量。这个特殊文件同样适用于read、write、lseek等常规的文件操作,从而提供了一个在内核外部动态地读写包括内核映像和内核中各个数据结构以及堆栈内容的手段。这个手段既可以用于收集状态信息和统计信息,也可以用于程序调试,还可以动态地给内核打补丁或者改变一些数据结构或变量的内容。采用虚存以后,Unix又增加了一个特殊文件/dev/kmem,对应于系统的整个虚存空间。这两个特殊文件的作用和表现出来的重要性促使人们对其功能加以进一步的扩展,在系统中增设了一个/proc目录,每当创建一个进程时就以其pid为文件名在这个目录下建立起一个特殊文件,使得通过这个文件就可以读写相应进程的用户空间。而进程exit时则将此文件删除。显然,目录/proc的名称就是这样来的。
经过多年的发展,/proc成了一个特殊的文件系统,文件系统的类型就叫proc,其安装点则一般都固定为/proc,所以称其为proc文件系统,有时也(非正式地)称之为/proc文件系统。这个文件系统中所有的文件都是特殊文件,这些文件的内容都不存在与任何设备上,而是在读写的时候才根据系统所有的有关信息生成出来,或者映射到系统中的有关变量或数据结构。所以又称为“伪文件系统”。同时,这个子系统中的内容也已经扩展到了足以覆盖系统的几乎所有方面,而不再仅仅是关于各个进程的信息。限于篇幅,我们不在这里列出/proc目录下的内容,建议读者自己用命令"du -a /proc"看一下。
试了下这个命令,结果很长!大体上,这个目录下的内容主要包括如下几类:
- 系统中的每个进程都有一个以其PID为名的子目录,而每个子目录中则包括关于该进程执行的命令、所有环境变量、cpu占用时间、内存映射表、已打开文件的文件号以及进程状态等特殊文件。
- 系统中各种资源的管理信息,如/proc/slabinfo就是内存管理中关于各个slab缓冲块队列的信息,/proc/swaps就是关于系统的swap设备的信息,/proc/partitions就是关于各个磁盘分区的信息,等等。
- 系统中各种设备的有关信息,如/proc/pci就是关于系统的PCI总线上所有设备的一份清单,等等。
- 文件系统的信息,如/proc/mounts就是系统中已经安装的各种文件系统设备的清单,而/proc/filesystems则是系统中已经登记的每种文件系统(类型)的清单。
- 中断的使用,/proc/interrupts是一份关于中断源和它们的中断向量编号的清单。
- 与动态安装模块有关的信息,/proc/modules是一份系统中已经安装动态模块的清单,而/proc/ksyms则是内核中可安装模块动态链接的符号以及其地址的清单。
- 与前述/dev/mem类似的内存访问手段,如/proc/kcore。
- 系统的版本号以及其他各种统计和状态信息。
读者可以通过命令man proc, 看一下这些信息的说明。
不仅如此,动态安装模块还可以在/proc目录下动态地创建文件,并以此作为模块与用户进程间的界面。
由于proc文件系统并不物理地存在于任何设备上,它的安装过程是特殊的。对proc文件系统不能直接通过mount来安装,而要先由系统内核在内核初始化时自动地通过一个函数kern_mount安装一次,然后再由处理系统初始化的进程通过mount安装,实际上是重安装。我们来看有关的代码,首先是init_proc_fs,这是在内核初始化时调用的:
static DECLARE_FSTYPE(proc_fs_type, "proc", proc_read_super, FS_SINGLE);
static int __init init_proc_fs(void)
{
int err = register_filesystem(&proc_fs_type);
if (!err) {
proc_mnt = kern_mount(&proc_fs_type);
err = PTR_ERR(proc_mnt);
if (IS_ERR(proc_mnt))
unregister_filesystem(&proc_fs_type);
else
err = 0;
}
return err;
}
系统在初始化阶段对proc文件系统做两件事,一是通过register_filesystem向系统登记"proc"这么一种文件系统,二是通过kern_mount将一个具体的proc文件系统安装到系统中的/proc节点上。函数kern_mount的代码如下:
init_proc_fs=>kern_mount
struct vfsmount *kern_mount(struct file_system_type *type)
{
kdev_t dev = get_unnamed_dev();
struct super_block *sb;
struct vfsmount *mnt;
if (!dev)
return ERR_PTR(-EMFILE);
sb = read_super(dev, NULL, type, 0, NULL, 0);
if (!sb) {
put_unnamed_dev(dev);
return ERR_PTR(-EINVAL);
}
mnt = add_vfsmnt(NULL, sb->s_root, NULL);
if (!mnt) {
kill_super(sb, 0);
return ERR_PTR(-ENOMEM);
}
type->kern_mnt = mnt;
return mnt;
}
每个已安装的文件系统都要有个super_block数据结构,proc文件系统也不例外。由于super_block数据结构需要有个设备号来唯一的加以标识,尽管proc文件系统并不实际存在于任何设备上,却也得有个设备号,所以要通过get_unnamed_dev分配一个。这个函数的代码如下:
init_proc_fs=>kern_mount=>get_unnamed_dev
/*
* Unnamed block devices are dummy devices used by virtual
* filesystems which don't use real block-devices. -- jrs
*/
static unsigned int unnamed_dev_in_use[256/(8*sizeof(unsigned int))];
kdev_t get_unnamed_dev(void)
{
int i;
for (i = 1; i < 256; i++) {
if (!test_and_set_bit(i,unnamed_dev_in_use))
return MKDEV(UNNAMED_MAJOR, i);
}
return 0;
}
这个设备号的主设备号为UNNAMED_MAJOR,定义为0.
除此之外,kern_mount中调用的函数读者在linux文件系统-文件系统的安装与拆卸博客中都已经看过了。函数read_super分配一个空白的super_block数据结构,然后通过由具体文件系统的file_system_type数据结构中的函数指针read_super调用具体的函数来读入超级块。对于proc文件系统,这个函数为proc_read_super,这是在上面的宏定义DECLARE_FSTYPE中定义好的,其代码如下:
init_proc_fs=>kern_mount=>read_super=>proc_read_super
struct super_block *proc_read_super(struct super_block *s,void *data,
int silent)
{
struct inode * root_inode;
struct task_struct *p;
s->s_blocksize = 1024;
s->s_blocksize_bits = 10;
s->s_magic = PROC_SUPER_MAGIC;
s->s_op = &proc_sops;
root_inode = proc_get_inode(s, PROC_ROOT_INO, &proc_root);
if (!root_inode)
goto out_no_root;
/*
* Fixup the root inode's nlink value
*/
read_lock(&tasklist_lock);
for_each_task(p) if (p->pid) root_inode->i_nlink++;
read_unlock(&tasklist_lock);
s->s_root = d_alloc_root(root_inode);
if (!s->s_root)
goto out_no_root;
parse_options(data, &root_inode->i_uid, &root_inode->i_gid);
return s;
out_no_root:
printk("proc_read_super: get root inode failed\n");
iput(root_inode);
return NULL;
}
可见,说是读入超级块,实际上却是“生成超级块”。还有super_block结构中的super_operations的指针s_op被设置成指向proc_sops,定义如下:
static struct super_operations proc_sops = {
read_inode: proc_read_inode,
put_inode: force_delete,
delete_inode: proc_delete_inode,
statfs: proc_statfs,
};
读者将会看到,proc文件系统的inode结构也像其super_block结构一样,在设备上并没有对应物,而仅仅是在内存中生成的“空中楼阁”。这些函数正式为这些“空中楼阁”服务的。
不仅如此,proc文件系统的目录项结构,即dentry结构,在设备上也没有对应物,而以内存中的proc_dir_entry数据结构来代替,定义如下:
struct proc_dir_entry {
unsigned short low_ino;
unsigned short namelen;
const char *name;
mode_t mode;
nlink_t nlink;
uid_t uid;
gid_t gid;
unsigned long size;
struct inode_operations * proc_iops;
struct file_operations * proc_fops;
get_info_t *get_info;
struct module *owner;
struct proc_dir_entry *next, *parent, *subdir;
void *data;
read_proc_t *read_proc;
write_proc_t *write_proc;
atomic_t count; /* use count */
int deleted; /* delete flag */
kdev_t rdev;
};
显然,这个数据结构中的有些内容本身应该属于inode结构或索引节点的,所以实际上既是创建dentry结构的依据,又是inode结构中部分信息的来源。如果与ext2文件系统的ext2_dir_entry结构相比,则那是存储在设备上的目录项,而proc_dir_entry结构值存在于内存中,并且包含了更多的信息。这些proc_dir_entry结构多数都是在系统的运行中动态地分配空间而创立的,但是也有一些是静态定义的,其中最重要的就是proc文件系统的根节点,即/proc的目录项proc_root定义如下:
/*
* This is the root "inode" in the /proc tree..
*/
struct proc_dir_entry proc_root = {
low_ino: PROC_ROOT_INO,
namelen: 5,
name: "/proc",
mode: S_IFDIR | S_IRUGO | S_IXUGO,
nlink: 2,
proc_iops: &proc_root_inode_operations,
proc_fops: &proc_root_operations,
parent: &proc_root,
};
注意,这个数据结构中的指针proc_iops指向proc_root_inode_operations,而proc_fops指向proc_root_operations。还有,结构中的指针parent指向其自己,即proc_root。也就是说,这个节点是一个文件系统的根节点。
回到proc_read_super的代码中,数据结构proc_root就用来创建根节点/proc的inode结构,其中PROC_ROOT_INO的定义如下:
/*
* We always define these enumerators
*/
enum {
PROC_ROOT_INO = 1,
};
所以,用于/proc的inode节点号总是1,而设备上的1号索引节点是保留不用的。
函数proc_get_inode的代码如下:
init_proc_fs=>kern_mount=>read_super=>proc_read_super=>proc_get_inode
struct inode * proc_get_inode(struct super_block * sb, int ino,
struct proc_dir_entry * de)
{
struct inode * inode;
/*
* Increment the use count so the dir entry can't disappear.
*/
de_get(de);
#if 1
/* shouldn't ever happen */
if (de && de->deleted)
printk("proc_iget: using deleted entry %s, count=%d\n", de->name, atomic_read(&de->count));
#endif
inode = iget(sb, ino);
if (!inode)
goto out_fail;
inode->u.generic_ip = (void *) de;
if (de) {
if (de->mode) {
inode->i_mode = de->mode;
inode->i_uid = de->uid;
inode->i_gid = de->gid;
}
if (de->size)
inode->i_size = de->size;
if (de->nlink)
inode->i_nlink = de->nlink;
if (de->owner)
__MOD_INC_USE_COUNT(de->owner);
if (S_ISBLK(de->mode)||S_ISCHR(de->mode)||S_ISFIFO(de->mode))
init_special_inode(inode,de->mode,kdev_t_to_nr(de->rdev));
else {
if (de->proc_iops)
inode->i_op = de->proc_iops;
if (de->proc_fops)
inode->i_fop = de->proc_fops;
}
}
out:
return inode;
out_fail:
de_put(de);
goto out;
}
我们知道,inode结构包含着一个union,视具体的文件系统而用作不同的数据结构,例如对于ext2文件系统就用作ext2_inode_info结构,在inode数据结构的定义中列出了适用于不同文件系统的不同数据结构。如果具体的文件系统不在其列,则将这个union(的开头4个字节)解释为一个指针,这就是generic_ip。在这里,就将这个指针设置成指向相应的proc_dir_entry结构,使其在逻辑上成为inode结构的一部分。至于de_get,那只是递增数据结构中的使用计数而已,此外,iget是一个inline函数,我们已经在前几篇博客中看到过了。在这里,由于相应的inode结构还不存在,实际上会调用get_new_inode分配一个inode结构。
创建了proc文件系统根节点的inode结构以后,还要通过d_alloc_root创建其dentry结构。这个函数的代码我们在linux文件系统-文件系统的安装与拆卸 看到过了。
这里还有个有趣的事情,就是对系统中除0号进程以外的所有进程都递增该inode结构中的i_nlink字段。这样,只要这些进程中的任何一个还存在,就不能把这个inode结构删除。
回到kern_mount,函数add_vfsmnt的代码也是我们之前已经看到过的。但是,要注意这里调用这个函数时的参数。第一个参数nd是个nameidata结构指针,本来应该指向代表着安装点的nameidata结构,从这个结构里就可以得到指向安装点dentry结构的指针,可是,这里的调用参数却是NULL。第二个参数root是个dentry结构指针,指向待安装文件系统中根目录的dentry结构,在这里是proc文件系统根节点的dentry数据结构。可是,如果指向安装点的指针是NULL,那怎么安装呢?我们来看add_vfsmnt中的主体:
mnt->mnt_root = dget(root);
mnt->mnt_mountpoint = nd ? dget(nd->dentry) : dget(root);
mnt->mnt_parent = nd ? mntget(nd->mnt) : mnt;
if (nd) {
list_add(&mnt->mnt_child, &nd->mnt->mnt_mounts);
list_add(&mnt->mnt_clash, &nd->dentry->d_vfsmnt);
} else {
INIT_LIST_HEAD(&mnt->mnt_child);
INIT_LIST_HEAD(&mnt->mnt_clash);
}
INIT_LIST_HEAD(&mnt->mnt_mounts);
list_add(&mnt->mnt_instances, &sb->s_mounts);
list_add(&mnt->mnt_list, vfsmntlist.prev);
可见,在参数nd为NULL时,安装以后其vfsmount结构中的指针mnt_mountpoint指向待安装文件系统中根目录的dentry结构,即proc文件系统根节点的dentry结构本身;指针mnt_parent则指向这个vfsmount结构本身。并且,这个vfsmount结构的mnt_child和mnt_clash两个队列也空着不用。显然,这个vfsmount结构并没有把proc文件系统的根节点安装到什么地方。可是,回到kern_mount的代码中,下面还有一行重要的语句:
type->kern_mnt = mnt;
这个语句使proc文件系统的file_system_type数据结构与上面的vfsmount结构挂上了钩,使它的指针kern_mnt指向了这个vfsmount结构。可是,这并不意味着path_walk就能顺着路径名"/proc"找到proc文件系统的根节点,因为path_walk并不涉及file_system_type数据结构。
正因为如此,光是kern_mount还不够,还得由系统的初始化进程从内核外部通过系统调用mount再安装一次。通常,这个命令行为:
mount -nvt proc /dev/null /proc
就是说,把建立在空设备/dev/null上的proc文件系统安装在节点/proc上。从理论上也可以把它安装在其他节点上,但是实际上总是安装在/proc上。
前面我们提到过,proc文件系统的file_system_type数据结构中的FS_SINGLE标志为1,它起着重要的作用。为什么重要呢?因为它使sys_mount的主体do_mount通过get_sb_single,而不是get_sb_bdev,来取得所安装文件系统的super_block数据结构。我们回顾一下do_mount中与此有关的片段:
/* get superblock, locks mount_sem on success */
if (fstype->fs_flags & FS_NOMOUNT)
sb = ERR_PTR(-EINVAL);
else if (fstype->fs_flags & FS_REQUIRES_DEV)
sb = get_sb_bdev(fstype, dev_name, flags, data_page);
else if (fstype->fs_flags & FS_SINGLE)
sb = get_sb_single(fstype, flags, data_page);
我们在文件系统的安装与拆卸博客中阅读do_mount的代码时跳过了get_sb_single,下面要回过来看它的代码实现了:
sys_mount=>do_mount=>get_sb_single
static struct super_block *get_sb_single(struct file_system_type *fs_type,
int flags, void *data)
{
struct super_block * sb;
/*
* Get the superblock of kernel-wide instance, but
* keep the reference to fs_type.
*/
down(&mount_sem);
sb = fs_type->kern_mnt->mnt_sb;
if (!sb)
BUG();
get_filesystem(fs_type);
do_remount_sb(sb, flags, data);
return sb;
}
代码中通过file_system_type结构中的指针kern_mnt取得对于所安装文件系统的vfsmount结构,从而对其super_block结构的访问,而这正是在kern_mount中设置好了的。这里还调用了一个函数do_remount_sb,其代码如下:
sys_mount=>do_mount=>get_sb_single=>do_remount_sb
/*
* Alters the mount flags of a mounted file system. Only the mount point
* is used as a reference - file system type and the device are ignored.
*/
static int do_remount_sb(struct super_block *sb, int flags, char *data)
{
int retval;
if (!(flags & MS_RDONLY) && sb->s_dev && is_read_only(sb->s_dev))
return -EACCES;
/*flags |= MS_RDONLY;*/
/* If we are remounting RDONLY, make sure there are no rw files open */
if ((flags & MS_RDONLY) && !(sb->s_flags & MS_RDONLY))
if (!fs_may_remount_ro(sb))
return -EBUSY;
if (sb->s_op && sb->s_op->remount_fs) {
lock_super(sb);
retval = sb->s_op->remount_fs(sb, &flags, data);
unlock_super(sb);
if (retval)
return retval;
}
sb->s_flags = (sb->s_flags & ~MS_RMT_MASK) | (flags & MS_RMT_MASK);
/*
* We can't invalidate inodes as we can loose data when remounting
* (someone might manage to alter data while we are waiting in lock_super()
* or in foo_remount_fs()))
*/
return 0;
}
这个函数对于proc文件系统作用不大,因为proc并无特殊的remount_fs操作。标志位屏蔽码MS_RMT_MASK:
/*
* Flags that can be altered by MS_REMOUNT
*/
#define MS_RMT_MASK (MS_RDONLY|MS_NOSUID|MS_NODEV|MS_NOEXEC|\
MS_SYNCHRONOUS|MS_MANDLOCK|MS_NOATIME|MS_NODIRATIME)
经过do_remount_sb以后,原来super_block结构中的这些标志位就由用户所提供的相应标志位所取代。
取得了proc文件系统的super_block结构以后,回到do_mount的代码中,此后的操作就与普通文件系统的安装无异了。这样,就将proc文件系统安装到了节点/proc上。
前面讲过,整个proc文件系统都不存在于设备上,所以不光是它的根节点需要在内存中创造出来,自根节点以下的所有节点全都需要在运行时加以创造,这是由内核在初始化时调用proc_root_init完成的,其代码如下:
void __init proc_root_init(void)
{
proc_misc_init();
proc_net = proc_mkdir("net", 0);
#ifdef CONFIG_SYSVIPC
proc_mkdir("sysvipc", 0);
#endif
#ifdef CONFIG_SYSCTL
proc_sys_root = proc_mkdir("sys", 0);
#endif
proc_root_fs = proc_mkdir("fs", 0);
proc_root_driver = proc_mkdir("driver", 0);
#if defined(CONFIG_SUN_OPENPROMFS) || defined(CONFIG_SUN_OPENPROMFS_MODULE)
/* just give it a mountpoint */
proc_mkdir("openprom", 0);
#endif
proc_tty_init();
#ifdef CONFIG_PROC_DEVICETREE
proc_device_tree_init();
#endif
proc_bus = proc_mkdir("bus", 0);
}
首先是直接在/proc下面的叶节点,即文件节点,这是由proc_misc_init创建的,其代码如下:
proc_root_init=>proc_misc_init
void __init proc_misc_init(void)
{
struct proc_dir_entry *entry;
static struct {
char *name;
int (*read_proc)(char*,char**,off_t,int,int*,void*);
} *p, simple_ones[] = {
{"loadavg", loadavg_read_proc},
{"uptime", uptime_read_proc},
{"meminfo", meminfo_read_proc},
{"version", version_read_proc},
{"cpuinfo", cpuinfo_read_proc},
#ifdef CONFIG_PROC_HARDWARE
{"hardware", hardware_read_proc},
#endif
#ifdef CONFIG_STRAM_PROC
{"stram", stram_read_proc},
#endif
#ifdef CONFIG_DEBUG_MALLOC
{"malloc", malloc_read_proc},
#endif
#ifdef CONFIG_MODULES
{"modules", modules_read_proc},
{"ksyms", ksyms_read_proc},
#endif
{"stat", kstat_read_proc},
{"devices", devices_read_proc},
{"partitions", partitions_read_proc},
#if !defined(CONFIG_ARCH_S390)
{"interrupts", interrupts_read_proc},
#endif
{"filesystems", filesystems_read_proc},
{"dma", dma_read_proc},
{"ioports", ioports_read_proc},
{"cmdline", cmdline_read_proc},
#ifdef CONFIG_SGI_DS1286
{"rtc", ds1286_read_proc},
#endif
{"locks", locks_read_proc},
{"mounts", mounts_read_proc},
{"swaps", swaps_read_proc},
{"iomem", memory_read_proc},
{"execdomains", execdomains_read_proc},
{NULL,}
};
for (p = simple_ones; p->name; p++)
create_proc_read_entry(p->name, 0, NULL, p->read_proc, NULL);
/* And now for trickier ones */
entry = create_proc_entry("kmsg", S_IRUSR, &proc_root);
if (entry)
entry->proc_fops = &proc_kmsg_operations;
proc_root_kcore = create_proc_entry("kcore", S_IRUSR, NULL);
if (proc_root_kcore) {
proc_root_kcore->proc_fops = &proc_kcore_operations;
proc_root_kcore->size =
(size_t)high_memory - PAGE_OFFSET + PAGE_SIZE;
}
if (prof_shift) {
entry = create_proc_entry("profile", S_IWUSR | S_IRUGO, NULL);
if (entry) {
entry->proc_fops = &proc_profile_operations;
entry->size = (1+prof_len) * sizeof(unsigned int);
}
}
#ifdef __powerpc__
{
extern struct file_operations ppc_htab_operations;
entry = create_proc_entry("ppc_htab", S_IRUGO|S_IWUSR, NULL);
if (entry)
entry->proc_fops = &ppc_htab_operations;
}
#endif
entry = create_proc_read_entry("slabinfo", S_IWUSR | S_IRUGO, NULL,
slabinfo_read_proc, NULL);
if (entry)
entry->write_proc = slabinfo_write_proc;
}
这个函数的前半部是一个数据结构数组的定义。这个数组中的每一个元素都将/proc目录中的一个(文件)节点名与一个函数挂上钩。例如,节点/proc/cpuinfo就与cpuinfo_read_proc挂钩,当一个进程访问这个节点,要读出这个特殊文件的内容时,就由cpuinfo_read_proc从内核中收集有关的信息并临时生成该文件的内容。这个数组中所涉及的所有特殊文件都支持读操作,而不支持其他的文件操作(如写、lseek等)。看一下这个数组,即simple_ones,我们可以感受到在/proc下面的这些特殊文件所提供的信息是何等的充分和多样。所有这些函数都要通过create_proc_read_entry为之创建起proc_dir_entry结构和inode结构,并且与节点/proc的数据结构挂上钩,代码如下:
proc_root_init=>proc_misc_init=>create_proc_read_entry
extern inline struct proc_dir_entry *create_proc_read_entry(const char *name,
mode_t mode, struct proc_dir_entry *base,
read_proc_t *read_proc, void * data)
{
struct proc_dir_entry *res=create_proc_entry(name,mode,base);
if (res) {
res->read_proc=read_proc;
res->data=data;
}
return res;
}
对照一下调用时的参数,就可以看到这里除name和read_proc以外其他参数都是0或NULL,特别地,文件的mode为0,所以这里做的就是通过create_proc_entry建立起有关的数据结构并将所创建proc_dir_entry结构中的函数指针read_proc设置成指定的函数。
函数create_proc_entry的代码如下:
proc_root_init=>proc_misc_init=>create_proc_read_entry=>create_proc_entry
struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,
struct proc_dir_entry *parent)
{
struct proc_dir_entry *ent = NULL;
const char *fn = name;
int len;
if (!parent && xlate_proc_name(name, &parent, &fn) != 0)
goto out;
len = strlen(fn);
ent = kmalloc(sizeof(struct proc_dir_entry) + len + 1, GFP_KERNEL);
if (!ent)
goto out;
memset(ent, 0, sizeof(struct proc_dir_entry));
memcpy(((char *) ent) + sizeof(*ent), fn, len + 1);
ent->name = ((char *) ent) + sizeof(*ent);
ent->namelen = len;
if (S_ISDIR(mode)) {
if ((mode & S_IALLUGO) == 0)
mode |= S_IRUGO | S_IXUGO;
ent->proc_fops = &proc_dir_operations;
ent->proc_iops = &proc_dir_inode_operations;
ent->nlink = 2;
} else {
if ((mode & S_IFMT) == 0)
mode |= S_IFREG;
if ((mode & S_IALLUGO) == 0)
mode |= S_IRUGO;
ent->nlink = 1;
}
ent->mode = mode;
proc_register(parent, ent);
out:
return ent;
}
在这个情景中,进入这个函数的参数parent为NULL,所以在第505行调用xlate_proc_name,它将作为参数传下来的节点名(如"cpuinfo")转换成从/proc开始的路径名,并且通过副作用返回指向节点proc的proc_dir_entry结构的指针作为parent。显然,这个函数的作用在这里是很关键的,其代码如下:
proc_root_init=>proc_misc_init=>create_proc_read_entry=>create_proc_entry=>xlate_proc_name
/*
* This function parses a name such as "tty/driver/serial", and
* returns the struct proc_dir_entry for "/proc/tty/driver", and
* returns "serial" in residual.
*/
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;
int len;
de = &proc_root;
while (1) {
next = strchr(cp, '/');
if (!next)
break;
len = next - cp;
for (de = de->subdir; de ; de = de->next) {
if (proc_match(len, cp, de))
break;
}
if (!de)
return -ENOENT;
cp += len + 1;
}
*residual = cp;
*ret = de;
return 0;
}
这里proc_root就是根节点/proc的proc_dir_entry结构,结构中的subdir是一个队列。下面我们就会看到,/proc下所有节点的proc_dir_entry结构都在这个队列中。函数strchr在字符串cp中寻找第一个'/'字符并返回指向该字符的指针。函数中的while循环逐字节地检查作为相对路径名的字符串,在/proc下面的子目录队列中寻找匹配。对于字符串"cpuinfo"来说,由于字符串中并无'/'字符存在,因而strchr返回NULL而经由第177行的break语句结束while循环。所以,对于相对路径名"cpuinfo"而言,这个函数返回0,并且在返回create_proc_entry后使parent指向/proc的proc_dir_entry结构,而fn则保持不变。这么一来,在为节点"cpuinfo"分配proc_dir_entry结构并加以初始化后,当调用proc_register时,parent就一定指向/proc或其它给定父节点的proc_dir_entry。
函数proc_register将一个新节点的proc_dir_entry结构登记(即挂入)到父节点的proc_dir_entry结构内的subdir队列中,它的源代码如下:
proc_root_init=>proc_misc_init=>create_proc_read_entry=>create_proc_entry=>proc_register
static int proc_register(struct proc_dir_entry * dir, struct proc_dir_entry * dp)
{
int i;
i = make_inode_number();
if (i < 0)
return -EAGAIN;
dp->low_ino = i;
dp->next = dir->subdir;
dp->parent = dir;
dir->subdir = dp;
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;
}
return 0;
}
参数dir指向父节点,dp则指向要登记的节点,如前所述,/proc及其下属的所有节点在设备上都没有对应的索引节点,但是在内存中却都有inode数据结构。既然有inode结构,就要有索引节点号。proc文件系统的根节点即/proc节点索引节点号为1,可是其他节点呢?这就要通过make_inode_number予以分配了,此函数定义如下:
proc_root_init=>proc_misc_init=>create_proc_read_entry=>create_proc_entry=>proc_register=>make_inode_number
static int make_inode_number(void)
{
int i = find_first_zero_bit((void *) proc_alloc_map, PROC_NDYNAMIC);
if (i<0 || i>=PROC_NDYNAMIC)
return -1;
set_bit(i, (void *) proc_alloc_map);
return PROC_DYNAMIC_FIRST + i;
}
代码中用到了几个常量如下:
/* Finally, the dynamically allocatable proc entries are reserved: */
#define PROC_DYNAMIC_FIRST 4096
#define PROC_NDYNAMIC 4096
可见,这些节点的索引节点号都在4096-8192范围内。索引节点号并不需要在整个系统的范围内保持一致,而只要在同一个设备的范围中唯一即可。当然,/proc下面的节点都不属于任何设备,但是也有个设备,所以这些索引节点号也因此而不会与任何一个具体设备上的索引节点号冲突。
从代码中可以看到,proc文件系统由两个inode_operations以及两个file_operations数据结构,即proc_dir_operations和proc_link_inode_operations,视具体节点类型加以设置。例如,下面讲到的/proc/self就是一个链接节点,所以其proc_iops指向proc_link_inode_operations。
此外,proc_register中的代码就没有什么需要特别加以说明的了。不过,从“cpuinfo”这个例子可以看出,所谓subdir队列并非子目录的队列,而是下属节点的目录项的队列。
回到proc_misc_init的代码中,除数组simple_ones中的节点外,还有kmsg、kcore以及profile三个节点。由于这些特殊文件的访问权限有所不同,例如kmsg和kcore的mode都是S_IRUSR,也就是只有文件主即特权用户才有读访问权限,所以不能一律套用create_proc_read_entry,而要直接调用create_proc_entry,特殊文件/proc/kcore代表着映射到系统空间的物理内存,其起点为PAGE_OFFSET,即0x00000000,而high_memory则为系统的物理内存在系统空间映射的终点。
创建了这些直接在/proc目录中的特殊文件以后,proc_root_init还要在/proc目录中创建一些子目录,如net、fs、driver、bus等等。这些子目录都是通过proc_mkdir创建的。它与create_proc_entry在参数mode中的S_IFDIR为1而S_IALLUGO为0时相同,所以我们就不在这里列出它的代码了。
还有一个很特殊的子目录self,/proc/self代表着这个节点受到访问时的当前进程。也就是说,谁访问这个节点,它就代表谁,它总是代表着访问这个节点的进程自己。系统中的每一个进程在/proc目录中都有一个以其进程号为节点名的子目录,在子目录中则又有cmdline、cpu、cwd、environ等节点,反映着该进程各方面的状态和信息。其中多数节点是特殊文件,有的却是目录节点。如cwd就是个目录节点,连接到该进程的当前工作目录;而cmdline则为启动该进程的可执行程序时的命令行。这样,特权用户可以在运行时打开任何一个进程的有关文件节点或目录节点读取该进程各个方面的信息。一般用户也可以用自己的pid组装起路径名来获取有关其自身的信息,或者就通过/proc/self来获取有关其自身的信息。我们在机器上执行"more /proc/self/cmdline"命令行,看看是什么结构。这个子目录的特殊之处还在于:它并没有一个固定的proc_dir_entry数据结构,也没有固定的inode结构,而是在需要时临时予以生成。后面我们还会回到这个话题。
上述这些子目录基本上(除self以外)都是最底层的目录节点,在它们下面就只有文件而再没有其他目录节点了。但是/proc/tty却是一棵子树。在这个节点下面还有其他目录节点,所以专门有个函数proc_tty_init用来创建这棵子树,其代码如下:
proc_root_init=>proc_tty_init
/*
* Called by proc_root_init() to initialize the /proc/tty subtree
*/
void __init proc_tty_init(void)
{
if (!proc_mkdir("tty", 0))
return;
proc_tty_ldisc = proc_mkdir("tty/ldisc", 0);
proc_tty_driver = proc_mkdir("tty/driver", 0);
create_proc_read_entry("tty/ldiscs", 0, 0, tty_ldiscs_read_proc,NULL);
create_proc_read_entry("tty/drivers", 0, 0, tty_drivers_read_proc,NULL);
}
此外,如果系统不采用传统的基于主设备号、次设备号的/dev设备(文件)目录,而采用树状的设备目录/device_tree,则还要在proc_root_init中创建起/device_tree子树。这是由proc_device_tree_init完成的,其代码如下:
proc_root_init=>proc_device_tree_init
/*
* Called on initialization to set up the /proc/device-tree subtree
*/
void proc_device_tree_init(void)
{
struct device_node *root;
if ( !have_of )
return;
proc_device_tree = proc_mkdir("device-tree", 0);
if (proc_device_tree == 0)
return;
root = find_path_device("/");
if (root == 0) {
printk(KERN_ERR "/proc/device-tree: can't find root\n");
return;
}
add_node(root, proc_device_tree);
}
我们在这里并不关心设备驱动,所以不深入去看find_path_device和add_node的代码,不过我们从这两个函数名和调用参数可以猜出来它们的作用。
如前所述,系统中的每个进程在/proc目录中都有个以其进程号为节点名的子目录,但是这些子目录并不是在系统初始化的阶段创建的,而是要到/proc节点受到访问时临时地生成出来。只要想想进程的创建消亡是多么频繁,这就毫不足怪了。
除这些由内核本身创建并安装的节点以外,可安装模块也可以根据需要通过proc_register在/proc目录中创建其自己的节点,从而在模块与进程之间建立桥梁。可安装模块可以通过两种途径架设起与进程之间的桥梁,其一是通过在/dev目录中创建一个设备文件节点,其二就是在/proc目录中创建若干特殊文件。在老一些的版本中,可安装模块通过一个叫proc_register_dynamic的函数来创建proc文件,但是实际上可安装模块并不比进程更为动态,所以现在已经(通过宏定义)统一到了proc_register。当可以安装模块需要在/proc目录中创建一个特殊文件时,先准备好它自己的inode_operations结构和file_operations结构,再准备一个proc_dir_entry结构,然后调用proc_register将其登记到/proc目录中。这就是设计和实现设备驱动程序的两种途径之一。所以,proc_register对于要开发设备驱动程序的读者来说是一个非常重要的函数。我们在有关设备启动的博客中还会回到这个话题。
这里要着重指出,通过proc_register以及proc_mkdir登记的是一个proc_dir_entry结构。结构中包含了dentry结构和inode结构所需的大部分信息,但是它既不是dentry结构也不是inode结构。同时,代表着/proc的数据结构proc_root也是一个proc_dir_entry结构,所以由此而形成的是一棵以proc_root为根的树,树中的每个节点是一个proc_dir_entry结构。
可是,对于文件系统的操作,如path_walk等等,所涉及的却是dentry结构和inode结构,这两个方面是怎样同一起来的呢?我们在前面看到,proc文件系统的根节点/proc和inode结构,这是在proc_read_super中通过proc_get_inode分配并且根据proc_root的内容而设置的。同时,这个节点也有dentry结构,这是在proc_read_super中通过d_alloc_root创建的,并且proc文件系统的super_block结构中的指针s_root就指向这个dentry结构(这个d_entry结构中的指针d_inode则指向其inode结构)。所以,proc文件系统的根节点是一个正常的节点。在path_walk中首先会到达这个节点,从这以后,就由这个节点在其inode_operations、file_operations以及dentry_operations数据结构中提供的有关函数接管了进一步的操作。后面我们会看到,这些函数会临时为/proc子树中其他的节点生成出node结构来。当然,其依据就是该节点的proc_dir_entry结构。
下面,我们通过几个具体的情景来看proc文件系统中的文件操作。
第一个情景是对/proc/loadavg的访问,这个文件提供有关系统在过去1分钟、5分钟和15分钟内的平均负荷的统计信息。这个文件只支持读操作,其proc_dir_entry结构是在d_alloc_root中通过create_proc_read_entry创建的。首先,当然是通过系统调用open打开这个文件,为此我们要重温一下path_walk中的有关段落。在这个函数中,当沿着路径名搜索到了一个中间节点时,数据结构nameidata中的指针dentry指向这个中间节点的dentry结构,并企图继续向前搜索,而下一个节点名在一个qstr数据结构this中。就我们这个情景而言,下一个节点已经是路径名中的最后一个节点,所以转到了last_component标号处。在确认了要访问的正是这个节点本身(而不是其父节点),并且节点名并非"."或".."以后,就先通过cached_lookup在内存中寻找该节点的dentry结构,如果这个结构尚未创建则进而通过real_lookup在文件系统中从其父节点开始寻找并为之创建起dentry(以及inode)结构。代码如下:
dentry = cached_lookup(nd->dentry, &this, 0);
if (!dentry) {
dentry = real_lookup(nd->dentry, &this, 0);
err = PTR_ERR(dentry);
if (IS_ERR(dentry))
break;
}
while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry))
在我们这个情景中,path_walk首先发现/proc节点是个安装点,而从所安装的super_block结构中取得了proc文件系统根节点的dentry结构。如前所述,从这个意义上说这个节点是正常的文件系统根节点。所以,nd->dentry就指向该节点的dentry结构,而this中则含有下一个节点名"loadavg"。然后,先通过cached_lookup看看下一个节点的dentry结构是否已经建立在内存中,如果没有就要通过real_lookup从设备上读入该节点的目录项(以及索引节点)并在内存中为之创建起它的dentry结构。但是,那只是就常规的文件系统而言,而现在的节点/proc已经落在特殊的proc文件系统内,情况就不同了,先重温下real_lookup的有关代码:
result = d_lookup(parent, name);
if (!result) {
可见,在内存中不能发现目标节点的dentry结构时,到底怎么办取决于其父节点的inode结构中的指针i_op指向哪一个inode_operations数据结构以及这个结构中的函数指针lookup。对于节点/proc,它的i_op指针指向proc_root_inode_operations,这是在它的proc_dir_entry结构proc_root中定义好了的,具体定义如下:
/*
* The root /proc directory is special, as it has the
* <pid> directories. Thus we don't use the generic
* directory handling functions for that..
*/
static struct file_operations proc_root_operations = {
read: generic_read_dir,
readdir: proc_root_readdir,
};
/*
* proc root can do almost nothing..
*/
static struct inode_operations proc_root_inode_operations = {
lookup: proc_root_lookup,
};
我们在这里也一起列出了它的file_operations结构。从中可以看出,如果打开/proc并通过系统调用readdir或getdents读取目录的内容(如命令ls所做的那样),则调用的函数为proc_root_readdir,对于我们这个情景,则只是继续向前搜索,因而所调用的函数是proc_root_lookup,其代码如下:
static struct dentry *proc_root_lookup(struct inode * dir, struct dentry * dentry)
{
if (dir->i_ino == PROC_ROOT_INO) { /* check for safety... */
int nlink = proc_root.nlink;
nlink += nr_threads;
dir->i_nlink = nlink;
}
if (!proc_lookup(dir, dentry))
return NULL;
return proc_pid_lookup(dir, dentry);
}
参数dir指向父节点即/proc的inode结构,这个inode结构中的nlink字段也是特殊的,它的数值等于当前系统中进程(以及线程)的数量。由于这个数量随时都可能在变,所以每次调用proc_root_lookup时都要根据当时的情景予以更变。这里nr_theads是内核中的一个全局变量,反映着系统中的进程数量。另一方面,由于系统中的进程数量不会降到0,这个字段的数值也不可能为0.
/proc目录中的节点可以分为两类。一类是节点的proc_dir_entry结构已经向proc_root登记而挂入了其队列中;另一类则对应于当前系统中的各个进程而并不存在proc_dir_entry结构。前者需要通过proc_lookup找到其proc_dir_entry结构而设置其dentry结构并创建其inode结构。后者则需要根据节点名(进程号)在系统中找到进程的task_struct结构,再设置节点的dentry结构并创建inode结构。显然,/proc/loadavg属于前者,所以我们继续看proc_lookup的代码,如下:
proc_root_lookup=>proc_lookup
/*
* Don't create negative dentries here, return -ENOENT by hand
* instead.
*/
struct dentry *proc_lookup(struct inode * dir, struct dentry *dentry)
{
struct inode *inode;
struct proc_dir_entry * de;
int error;
error = -ENOENT;
inode = NULL;
de = (struct proc_dir_entry *) dir->u.generic_ip;
if (de) {
for (de = de->subdir; de ; de = de->next) {
if (!de || !de->low_ino)
continue;
if (de->namelen != dentry->d_name.len)
continue;
if (!memcmp(dentry->d_name.name, de->name, de->namelen)) {
int ino = de->low_ino;
error = -EINVAL;
inode = proc_get_inode(dir->i_sb, ino, de);
break;
}
}
}
if (inode) {
dentry->d_op = &proc_dentry_operations;
d_add(dentry, inode);
return NULL;
}
return ERR_PTR(error);
}
这里的参数dir指向父节点即/proc的inode结构,而dentry则指向已经分配用于目标节点的dentry结构。函数本身的逻辑很简单的,proc_get_inode的代码在前面已经看过了。至于d_add,则只是将dentry结构挂入杂凑队列,并使dentry结构与inode结构互相挂上钩,我们在之前的博客中已经看到过。
从proc_lookup一路正常返回到path_walk中时,沿着路径名的搜索就向前推进了一步。在我们这个情景中,路径名至此已经结束,所以path_walk已经完成了操作,找到了目标节点/proc/loadavg的dentry结构,此后就与常规的open操作没有什么两样了。
打开了文件以后,就通过系统调用read从文件中读。由于目标文件的dentry结构和inode结构均已建立,所以开始时的操作与常规文件的并无不同,直到根据file结构中的指针f_op找到相应的file_operations结构并进而找到其函数指针read。对于proc文件系统,file结构中的f_op指针来自目标文件的inode结构,而inode结构中的这个指针又来源于目标节点的proc_dir_entry结构(见proc_get_inode的代码)。在proc_register的代码中可以看出来,目录节点的proc_fops都指向proc_dir_operations;而普通文件节点(如/proc/loadavg)的proc_fops则都指向proc_file_operations。所以,/proc/loadavg的file_operations结构为proc_file_operations,定义如下:
static struct file_operations proc_file_operations = {
llseek: proc_file_lseek,
read: proc_file_read,
write: proc_file_write,
};
可见,为读文件操作提供的函数是proc_file_read。这是一个为proc特殊文件通用的函数,其代码如下:
static ssize_t
proc_file_read(struct file * file, char * buf, size_t nbytes, loff_t *ppos)
{
struct inode * inode = file->f_dentry->d_inode;
char *page;
ssize_t retval=0;
int eof=0;
ssize_t n, count;
char *start;
struct proc_dir_entry * dp;
dp = (struct proc_dir_entry *) inode->u.generic_ip;
if (!(page = (char*) __get_free_page(GFP_KERNEL)))
return -ENOMEM;
while ((nbytes > 0) && !eof)
{
count = MIN(PROC_BLOCK_SIZE, nbytes);
start = NULL;
if (dp->get_info) {
/*
* Handle backwards compatibility with the old net
* routines.
*/
n = dp->get_info(page, &start, *ppos, count);
if (n < count)
eof = 1;
} else if (dp->read_proc) {
n = dp->read_proc(page, &start, *ppos,
count, &eof, dp->data);
} else
break;
if (!start) {
/*
* For proc files that are less than 4k
*/
start = page + *ppos;
n -= *ppos;
if (n <= 0)
break;
if (n > count)
n = count;
}
if (n == 0)
break; /* End of file */
if (n < 0) {
if (retval == 0)
retval = n;
break;
}
/* This is a hack to allow mangling of file pos independent
* of actual bytes read. Simply place the data at page,
* return the bytes, and set `start' to the desired offset
* as an unsigned int. - [email protected]
*/
n -= copy_to_user(buf, start < page ? page : start, n);
if (n == 0) {
if (retval == 0)
retval = -EFAULT;
break;
}
*ppos += start < page ? (long)start : n; /* Move down the file */
nbytes -= n;
buf += n;
retval += n;
}
free_page((unsigned long) page);
return retval;
}
从总体上说,这个函数的代码并没有什么特殊,对我们不应该成问题。但是从中可以看出,具体的读操作是通过由节点的proc_dir_entry结构中的函数指针get_info或read_proc提供的。get_info是为了与老一些的版本兼容而保留的,现在已改用read_proc。与常规的文件系统如ext2相比,proc文件系统有个特殊之处:那就是它的每个具体的文件或节点都有自己的文件操作函数,而不像ext2那样由其file_operations结构中提供的函数可以用于同一个文件系统的所有文件。当然,这是由于在proc文件系统中每个节点都有其特殊性。正因为这样,proc文件系统的file_operations结构中只为读操作提供一个通用的函数proc_file_read,而由它进一步找到并调用具体节点所提供的read_proc函数。在前面proc_misc_init以及create_proc_read_entry的代码中,我们看到节点/proc/loadavg的这个指针指向loadavg_read_proc,其代码如下:
static int proc_calc_metrics(char *page, char **start, off_t off,
int count, int *eof, int len)
{
if (len <= off+count) *eof = 1;
*start = page + off;
len -= off;
if (len>count) len = count;
if (len<0) len = 0;
return len;
}
static int loadavg_read_proc(char *page, char **start, off_t off,
int count, int *eof, void *data)
{
int a, b, c;
int len;
a = avenrun[0] + (FIXED_1/200);
b = avenrun[1] + (FIXED_1/200);
c = avenrun[2] + (FIXED_1/200);
len = sprintf(page,"%d.%02d %d.%02d %d.%02d %d/%d %d\n",
LOAD_INT(a), LOAD_FRAC(a),
LOAD_INT(b), LOAD_FRAC(b),
LOAD_INT(c), LOAD_FRAC(c),
nr_running, nr_threads, last_pid);
return proc_calc_metrics(page, start, off, count, eof, len);
}
它的作用就是将数组avenrun中积累的在过去1分钟、5分钟以及15分钟内的系统平均CPU负荷等统计信息通过sprintf打印到缓冲区页面中。这些平均负荷的数值是每隔5秒钟在时钟中断服务程序中进行计算的。统计信息中还包括系统当前处于可运行状态(在运行队列中)的进程个数nr_running以及系统中进程的总数nr_threads,还有系统中已分配使用的最大进程号last_pid。
我们要看的第二个情景是对/proc/self/cwd的访问。前面讲过,/proc/self节点在受到访问时,会根据当前进程的进程号连接到/proc目录中以此进程号为节点名的目录节点,而这个目录节点下面的cwd则又连接到该进程的当前工作目录。所以,在这短短的路径名中就有两个连接节点,而且/proc/self/cwd是从proc文件系统中的节点到常规文件系统(ext2)中的节点的链接。我们对目标节点及当前工作目录中的内容本身并不感兴趣,而只是对path_walk这样两次跨文件系统进行搜索感兴趣。
第一次跨越文件系统时当path_walk发现/proc是个安装点而通过__follow_down找到所安装的super_block结构的过程。这方面并没有什么特殊,我们已经很熟悉了。找到了proc文件系统的根节点的dentry结构以后,nameidata结构中的指针dentry指向这个数据结构,并企图继续向前搜索路径名中的下一个节点self。由于这个节点并不是路径名中的最后一个节点,所执行的代码是从path_walk的494行开始的:
/* This does the actual lookups.. */
dentry = cached_lookup(nd->dentry, &this, LOOKUP_CONTINUE);
if (!dentry) {
dentry = real_lookup(nd->dentry, &this, LOOKUP_CONTINUE);
err = PTR_ERR(dentry);
if (IS_ERR(dentry))
break;
}
就所执行的代码本身而言,是与前一个情景一样的,所以最终也要通过proc_root_lookup调用proc_lookup,试图为节点建立起其dentry结构和inode结构。可是,如前所述,由于/proc/self并没有一个固定的proc_dir_entry结构,所以对proc_lookup的调用必然会失败(返回非0),因而会进一步调用proc_pid_lookup。这个函数的代码如下:
proc_root_lookup=>proc_pid_lookup
struct dentry *proc_pid_lookup(struct inode *dir, struct dentry * dentry)
{
unsigned int pid, c;
struct task_struct *task;
const char *name;
struct inode *inode;
int len;
pid = 0;
name = dentry->d_name.name;
len = dentry->d_name.len;
if (len == 4 && !memcmp(name, "self", 4)) {
inode = new_inode(dir->i_sb);
if (!inode)
return ERR_PTR(-ENOMEM);
inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
inode->i_ino = fake_ino(0, PROC_PID_INO);
inode->u.proc_i.file = NULL;
inode->u.proc_i.task = NULL;
inode->i_mode = S_IFLNK|S_IRWXUGO;
inode->i_uid = inode->i_gid = 0;
inode->i_size = 64;
inode->i_op = &proc_self_inode_operations;
d_add(dentry, inode);
return NULL;
}
可见,当要找寻的节点名为self时内核为之分配一个空白的inode数据结构,并使其inode_operations结构指针i_op指向专用于/proc/self的proc_self_inode_operations,这个结构定义如下:
static struct inode_operations proc_self_inode_operations = {
readlink: proc_self_readlink,
follow_link: proc_self_follow_link,
};
为此类节点建立的inode结构有着特殊的节点号,这是由进程的pid左移16位以后与常数PROC_PID_INO相或而形成的,常数PROC_PID_INO则定义为2.
从proc_root_lookup返回到path_walk中以后,接着要检查和处理两件事,第一件是新找到的节点是否为安装点;第二件是它是否是一个链接节点。这正是我们在这里关心的,因为/proc/self就是个链接节点。继续看path_walk中的下面两行:
if (inode->i_op->follow_link) {
err = do_follow_link(dentry, nd);
对于链接节点,通过其inode结构和inode_operations结构提供的函数指针follow_link存在。就/proc/self而言,由于proc_pid_lookup的执行,其函数指针follow_link已经指向了proc_self_follow_link,其代码如下:
static int proc_self_follow_link(struct dentry *dentry, struct nameidata *nd)
{
char tmp[30];
sprintf(tmp, "%d", current->pid);
return vfs_follow_link(nd,tmp);
}
它通过vfs_follow_link寻找以当前进程的pid的字符串为相对路径名的节点,找到以后就使nameidata结构中的指针dentry指向它的dentry结构。我们已经看到过vfs_follow_link的代码。这里就不重复了。只是要指出,在vfs_follow_link中将递归地调用path_walk来寻找链接的目标节点,所以又会调用其父节点/proc的lookup函数,即proc_root_lookup,不同的只是这次寻找的不是self,而是当前进程的pid字符串。这一次,在proc_root_lookup中对proc_lookup的调用同样会因为在proc_pid_lookup寻找(见上面proc_root_lookup的代码)。可是,这一次的节点名就不是self了,刚调用的proc_self_follow_link已经将当前进程的进程号转化为字符串形式,所以在proc_pid_lookup中所走的路线也不同了,我们看这个函数的后半段:
proc_root_lookup=>proc_pid_lookup
while (len-- > 0) {
c = *name - '0';
name++;
if (c > 9)
goto out;
if (pid >= MAX_MULBY10)
goto out;
pid *= 10;
pid += c;
if (!pid)
goto out;
}
read_lock(&tasklist_lock);
task = find_task_by_pid(pid);
if (task)
get_task_struct(task);
read_unlock(&tasklist_lock);
if (!task)
goto out;
inode = proc_pid_make_inode(dir->i_sb, task, PROC_PID_INO);
free_task_struct(task);
if (!inode)
goto out;
inode->i_mode = S_IFDIR|S_IRUGO|S_IXUGO;
inode->i_op = &proc_base_inode_operations;
inode->i_fop = &proc_base_operations;
inode->i_nlink = 3;
inode->i_flags|=S_IMMUTABLE;
dentry->d_op = &pid_base_dentry_operations;
d_add(dentry, inode);
return NULL;
out:
return ERR_PTR(-ENOENT);
}
这个函数将节点名转换成一个无符号整数,然后以此为pid从系统中寻找是否存在相应的进程。
如果找到了相应的进程,就通过proc_pid_make_inode为之创建一个inode结构,并初始化已经分配的dentry结构。这个函数的代码我们就不看了。同时还要使inode结构中的inode_operations结构指针i_op指向proc_base_inode_operations,而file_operations结构指针i_fop则指向proc_base_operations。此外,相应dentry结构中的指针d_op则指向pid_base_dentry_operations。从这里也可以看出,在proc文件系统中几乎每个节点都有其自己的file_operations结构和inode_operations结构。
于是,从proc_self_follow_link返回时,nd->dentry已指向代表着当前进程的目录节点的dentry结构。这样,当path_walk开始新一轮的循环时,就从这个节点(而不是/proc/self)继续向前搜索了。下一个节点是cwd,这一次所搜索的节点已经是路径名中的最后一个节点,所以如同第一个情景中那样转到了标号last_component的地方。但是同样也是real_lookup中通过其父节点的inode_operations结构中的lookup函数指针执行实际的操作,而现在这个数据结构就是proc_base_inode_operations,定义如下:
static struct inode_operations proc_base_inode_operations = {
lookup: proc_base_lookup,
};
函数proc_base_lookup的代码如下:
static struct dentry *proc_base_lookup(struct inode *dir, struct dentry *dentry)
{
struct inode *inode;
int error;
struct task_struct *task = dir->u.proc_i.task;
struct pid_entry *p;
error = -ENOENT;
inode = NULL;
for (p = base_stuff; p->name; p++) {
if (p->len != dentry->d_name.len)
continue;
if (!memcmp(dentry->d_name.name, p->name, p->len))
break;
}
if (!p->name)
goto out;
error = -EINVAL;
inode = proc_pid_make_inode(dir->i_sb, task, p->type);
if (!inode)
goto out;
inode->i_mode = p->mode;
/*
* Yes, it does not scale. And it should not. Don't add
* new entries into /proc/<pid>/ without very good reasons.
*/
switch(p->type) {
case PROC_PID_FD:
inode->i_nlink = 2;
inode->i_op = &proc_fd_inode_operations;
inode->i_fop = &proc_fd_operations;
break;
case PROC_PID_EXE:
inode->i_op = &proc_pid_link_inode_operations;
inode->u.proc_i.op.proc_get_link = proc_exe_link;
break;
case PROC_PID_CWD:
inode->i_op = &proc_pid_link_inode_operations;
inode->u.proc_i.op.proc_get_link = proc_cwd_link;
break;
case PROC_PID_ROOT:
inode->i_op = &proc_pid_link_inode_operations;
inode->u.proc_i.op.proc_get_link = proc_root_link;
break;
case PROC_PID_ENVIRON:
inode->i_fop = &proc_info_file_operations;
inode->u.proc_i.op.proc_read = proc_pid_environ;
break;
case PROC_PID_STATUS:
inode->i_fop = &proc_info_file_operations;
inode->u.proc_i.op.proc_read = proc_pid_status;
break;
case PROC_PID_STAT:
inode->i_fop = &proc_info_file_operations;
inode->u.proc_i.op.proc_read = proc_pid_stat;
break;
case PROC_PID_CMDLINE:
inode->i_fop = &proc_info_file_operations;
inode->u.proc_i.op.proc_read = proc_pid_cmdline;
break;
case PROC_PID_STATM:
inode->i_fop = &proc_info_file_operations;
inode->u.proc_i.op.proc_read = proc_pid_statm;
break;
case PROC_PID_MAPS:
inode->i_fop = &proc_maps_operations;
break;
#ifdef CONFIG_SMP
case PROC_PID_CPU:
inode->i_fop = &proc_info_file_operations;
inode->u.proc_i.op.proc_read = proc_pid_cpu;
break;
#endif
case PROC_PID_MEM:
inode->i_op = &proc_mem_inode_operations;
inode->i_fop = &proc_mem_operations;
break;
default:
printk("procfs: impossible type (%d)",p->type);
iput(inode);
return ERR_PTR(-EINVAL);
}
dentry->d_op = &pid_dentry_operations;
d_add(dentry, inode);
return NULL;
out:
return ERR_PTR(error);
}
这里用到一个全局性的数组base_stuff,有关定义如下:
struct pid_entry {
int type;
int len;
char *name;
mode_t mode;
};
enum pid_directory_inos {
PROC_PID_INO = 2,
PROC_PID_STATUS,
PROC_PID_MEM,
PROC_PID_CWD,
PROC_PID_ROOT,
PROC_PID_EXE,
PROC_PID_FD,
PROC_PID_ENVIRON,
PROC_PID_CMDLINE,
PROC_PID_STAT,
PROC_PID_STATM,
PROC_PID_MAPS,
PROC_PID_CPU,
PROC_PID_FD_DIR = 0x8000, /* 0x8000-0xffff */
};
#define E(type,name,mode) {(type),sizeof(name)-1,(name),(mode)}
static struct pid_entry base_stuff[] = {
E(PROC_PID_FD, "fd", S_IFDIR|S_IRUSR|S_IXUSR),
E(PROC_PID_ENVIRON, "environ", S_IFREG|S_IRUSR),
E(PROC_PID_STATUS, "status", S_IFREG|S_IRUGO),
E(PROC_PID_CMDLINE, "cmdline", S_IFREG|S_IRUGO),
E(PROC_PID_STAT, "stat", S_IFREG|S_IRUGO),
E(PROC_PID_STATM, "statm", S_IFREG|S_IRUGO),
#ifdef CONFIG_SMP
E(PROC_PID_CPU, "cpu", S_IFREG|S_IRUGO),
#endif
E(PROC_PID_MAPS, "maps", S_IFREG|S_IRUGO),
E(PROC_PID_MEM, "mem", S_IFREG|S_IRUSR|S_IWUSR),
E(PROC_PID_CWD, "cwd", S_IFLNK|S_IRWXUGO),
E(PROC_PID_ROOT, "root", S_IFLNK|S_IRWXUGO),
E(PROC_PID_EXE, "exe", S_IFLNK|S_IRWXUGO),
{0,0,NULL,0}
};
这样,在proc_base_lookup中只要在这个数组中逐项比对,就可以找到cwd所对应的类型,即相应inode号中的低16位,以及文件的模式。然后,在基于这个类型的switch语句中,对于所创建的inode结构进行具体的设置。对于代表着进程的某方面属性或状态的这些inode结构,其union部分被用作一个proc_inode_info结构proc_i,其定义如下:
struct proc_inode_info {
struct task_struct *task;
int type;
union {
int (*proc_get_link)(struct inode *, struct dentry **, struct vfsmount **);
int (*proc_read)(struct task_struct *task, char *page);
} op;
struct file *file;
};
结构中的指针task在proc_pid_make_inode中设置成指向inode结构所代表进程的task_struct结构。
对于节点cwd,要特别加以设置的内容有两项。第一项是inode结构中指针i_op设置成指向proc_pid_link_inode_operations数据结构;第二项是将上述proc_inode_info结构中的函数指针proc_get_link指向proc_cwd_link。此外,就没有什么特殊之处了。数据结构proc_pid_link_inode_operations定义如下:
static struct inode_operations proc_pid_link_inode_operations = {
readlink: proc_pid_readlink,
follow_link: proc_pid_follow_link
};
从proc_base_lookup经由real_lookup返回到path_walk时,nameidata结构中的指针dentry已经指向了这个特定cwd节点的dentry结构。但是接着同样要受到对其inode结构中的i_op指针以及相应inode_operations结构中的指针follow_link的检验,看path_walk中的相关代码:
inode = dentry->d_inode;
if ((lookup_flags & LOOKUP_FOLLOW)
&& inode && inode->i_op && inode->i_op->follow_link) {
err = do_follow_link(dentry, nd);
我们刚才已经看到,这个inode结构的指针follow_link非0,并且指向proc_cwd_link,其代码如下:
static int proc_cwd_link(struct inode *inode, struct dentry **dentry, struct vfsmount **mnt)
{
struct fs_struct *fs;
int result = -ENOENT;
task_lock(inode->u.proc_i.task);
fs = inode->u.proc_i.task->fs;
if(fs)
atomic_inc(&fs->count);
task_unlock(inode->u.proc_i.task);
if (fs) {
read_lock(&fs->lock);
*mnt = mntget(fs->pwdmnt);
*dentry = dget(fs->pwd);
read_unlock(&fs->lock);
result = 0;
put_fs_struct(fs);
}
return result;
}
如前所述,节点的inode中的union用作一个proc_inode_info结构,其中的指针task指向相应进程的task_struct结构,进而可以得到这个进程的fs_struct结构,而这个数据结构中的指针pwd即指向该进程的当前工作目录的dentry结构,同时指针pwdmnt指向该目录所在设备安装时的vfsmount结构。注意,这里的参数dentry和mnt都是双重指针,所以第96行和第97行实际上改变了nameidata结构中的dentry和mnt两个指针。这样,当从proc_cwd_link经由do_follow_link返回到path_walk中时,nameidata结构中的指针已经指向最终的目标,即当前进程的当前工作目录。从这个以后的操作就与常规的文件系统完全一样了。从这个情景可以看出,对于proc文件系统中的一些路径,其有关的数据结构以及这些数据结构之间的链接是非常动态的,每次都要在path_walk的过程中逐层地临时建立,而不像在常规文件系统如ext2中那样相对静态。
通过这两个情景,我们应该已经对proc文件系统的文件操作有了基本的了解和理解,自己不妨再读几段代码以加深理解,我们可以读一下/proc/meminfo和/proc/self/maps的访问,因为这个不仅可以加深对proc文件系统的理解,还可以帮助巩固对存储管理的理解。