天天看點

linux檔案系統-特殊檔案系統/proc

早期的Unix在裝置檔案目錄/dev下設定了一個特殊檔案,稱為/dev/mem。通過這個檔案可以讀寫系統的整個實體記憶體,而實體記憶體的位址就用讀寫檔案内部的位移量。這個特殊檔案同樣适用于read、write、lseek等正常的檔案操作,進而提供了一個在核心外部動态地讀寫包括核心映像和核心中各個資料結構以及堆棧内容的手段。這個手段既可以用于收集狀态資訊和統計資訊,也可以用于程式調試,還可以動态地給核心打更新檔或者改變一些資料結構或變量的内容。采用虛存以後,Unix又增加了一個特殊檔案/dev/kmem,對應于系統的整個虛存空間。這兩個特殊檔案的作用和表現出來的重要性促使人們對其功能加以進一步的擴充,在系統中增設了一個/proc目錄,每當建立一個程序時就以其pid為檔案名在這個目錄下建立起一個特殊檔案,使得通過這個檔案就可以讀寫相應程序的使用者空間。而程序exit時則将此檔案删除。顯然,目錄/proc的名稱就是這樣來的。

經過多年的發展,/proc成了一個特殊的檔案系統,檔案系統的類型就叫proc,其安裝點則一般都固定為/proc,是以稱其為proc檔案系統,有時也(非正式地)稱之為/proc檔案系統。這個檔案系統中所有的檔案都是特殊檔案,這些檔案的内容都不存在與任何裝置上,而是在讀寫的時候才根據系統所有的有關資訊生成出來,或者映射到系統中的有關變量或資料結構。是以又稱為“僞檔案系統”。同時,這個子系統中的内容也已經擴充到了足以覆寫系統的幾乎所有方面,而不再僅僅是關于各個程序的資訊。限于篇幅,我們不在這裡列出/proc目錄下的内容,建議讀者自己用指令"du -a /proc"看一下。

試了下這個指令,結果很長!大體上,這個目錄下的内容主要包括如下幾類:

  1. 系統中的每個程序都有一個以其PID為名的子目錄,而每個子目錄中則包括關于該程序執行的指令、所有環境變量、cpu占用時間、記憶體映射表、已打開檔案的檔案号以及程序狀态等特殊檔案。
  2. 系統中各種資源的管理資訊,如/proc/slabinfo就是記憶體管理中關于各個slab緩沖塊隊列的資訊,/proc/swaps就是關于系統的swap裝置的資訊,/proc/partitions就是關于各個磁盤分區的資訊,等等。
  3. 系統中各種裝置的有關資訊,如/proc/pci就是關于系統的PCI總線上所有裝置的一份清單,等等。
  4. 檔案系統的資訊,如/proc/mounts就是系統中已經安裝的各種檔案系統裝置的清單,而/proc/filesystems則是系統中已經登記的每種檔案系統(類型)的清單。
  5. 中斷的使用,/proc/interrupts是一份關于中斷源和它們的中斷向量編号的清單。
  6. 與動态安裝子產品有關的資訊,/proc/modules是一份系統中已經安裝動态子產品的清單,而/proc/ksyms則是核心中可安裝子產品動态連結的符号以及其位址的清單。
  7. 與前述/dev/mem類似的記憶體通路手段,如/proc/kcore。
  8. 系統的版本号以及其他各種統計和狀态資訊。

讀者可以通過指令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檔案系統的了解,還可以幫助鞏固對存儲管理的了解。

繼續閱讀