1. 程式人生 > >一場由mknod引發的扯淡

一場由mknod引發的扯淡

我們註冊完字元、塊驅動裝置後,一般會用mknod去建立,應用層與驅動的管道。例如mtd的字元驅動,我們會用mknod  /dev/mtdchar1  c 30 0,建立/dev/mtdchar1來對映mtd第一個字元裝置。

由於程式碼沒法紅字,程式碼中有color:#ff6666  開頭的行數,為重點分析的

接下來我們用程式碼來分析,這個過程。

第一個函式asmlinkage long sys_mknod(const char __user *filename, int mode, unsigned dev)    //在fs/namei.c裡,filename就是所建立的路徑:/dev/mtdchar1,mode: S_IFCHR  | S_IRUSR | S_IWUSR,這個意思是可讀可寫的字元特殊檔案;dev為裝置號,主裝置號跟次裝置號的結合體。

上面那個函式沒什麼可分析的,再下來進入 long sys_mknodat(int dfd, const char __user *filename, int mode,unsigned dev)

程式碼一:

asmlinkage long sys_mknodat(int dfd, const char __user *filename, int mode,
				unsigned dev)
{
	int error = 0;
	char * tmp;
	struct dentry * dentry;
	struct nameidata nd;

	if (S_ISDIR(mode))
		return -EPERM;
	tmp = getname(filename);//比較安全的一種方式把filename放到tmp所指的空間裡
	if (IS_ERR(tmp))
		return PTR_ERR(tmp);

	error = do_path_lookup(dfd, tmp, LOOKUP_PARENT, &nd);
//nd->dentry為當前檔案系統(rootfs)的主dentry //當前fs下,也就是rootfs('/') if (error) goto out; dentry = lookup_create(&nd, 0); //生成一個新的dentry //dentry->d_parnet=nd.dentry error = PTR_ERR(dentry); if (!IS_POSIXACL(nd.dentry->d_inode)) mode &= ~current->fs->umask; if (!IS_ERR(dentry)) { switch (mode & S_IFMT) { case 0: case S_IFREG: error = vfs_create(nd.dentry->d_inode,dentry,mode,&nd); break; case S_IFCHR: case S_IFBLK: error = vfs_mknod(nd.dentry->d_inode,dentry,mode, new_decode_dev(dev));
//生成一個inode掛載到dentry //inode->i_sb=nd.dentry->d_inode->i_sb inode->i_rdev=dev break; case S_IFIFO: case S_IFSOCK: error = vfs_mknod(nd.dentry->d_inode,dentry,mode,0); break; case S_IFDIR: error = -EPERM; break; default: error = -EINVAL; } dput(dentry); } mutex_unlock(&nd.dentry->d_inode->i_mutex); path_release(&nd); out: putname(tmp); return error; }

上面那個函式裡字型加紅的部分為重點部分。再進行分析之前,我們先要搞清楚,dentry,inode之間的關係,其實嘛很簡單,就是dentry->d_inode=inode,就是通過dentry可以找到相對應的inode,至於inode裡面的其他操作就各有風情。我個人是這麼理解的,單細胞生物考慮問題就這麼簡單。

一:

下面進行那兩個函式的分析。第一個error = do_path_lookup(dfd, tmp, LOOKUP_PARENT, &nd);第一眼看到這個函式,就應該知道這個函式主要作用是給nd做配置。

程式碼二:

static int fastcall do_path_lookup(int dfd, const char *name,
				unsigned int flags, struct nameidata *nd)
{
	int retval = 0;
	int fput_needed;
	struct file *file;
	struct fs_struct *fs = current->fs;

	nd->last_type = LAST_ROOT; /* if there are only slashes... */
	nd->flags = flags;
	nd->depth = 0;

	if (*name=='/') {		//表明在根目錄下,那麼對應的mnt和dentry很簡單就是根目錄的mnt和dentry
		read_lock(&fs->lock);
		if (fs->altroot && !(nd->flags & LOOKUP_NOALT)) {
			nd->mnt = mntget(fs->altrootmnt);
			nd->dentry = dget(fs->altroot);
			read_unlock(&fs->lock);
			if (__emul_lookup_dentry(name,nd))
				goto out; /* found in altroot */
			read_lock(&fs->lock);
		}
		nd->mnt = mntget(fs->rootmnt);
		nd->dentry = dget(fs->root);
		read_unlock(&fs->lock);
	} else if (dfd == AT_FDCWD) {
		read_lock(&fs->lock);
		nd->mnt = mntget(fs->pwdmnt);
		nd->dentry = dget(fs->pwd);
		read_unlock(&fs->lock);
	} else {
…………//此處省略1024,為了排版好看點
	}
	current->total_link_count = 0;
	retval = link_path_walk(name, nd);	//nd的dentry,inode,last
out:
	if (likely(retval == 0)) {
		if (unlikely(!audit_dummy_context() && nd && nd->dentry &&
				nd->dentry->d_inode))
		audit_inode(name, nd->dentry->d_inode);
	}
out_fail:
	return retval;

fput_fail:
	fput_light(file, fput_needed);
	goto out_fail;
}

題外話:客官或許對current->fs有疑問,這個我一開始也蛋疼了很久,其實嘛很簡單,在start_kernel函式,也就是linux起來初始化的時候對其進行了初始化。start_kernel有個vfs_caches_init,vfs_caches_init裡有個mnt_init,mnt裡有個init_mount_tree,在init_mount_tree裡有兩行程式碼,對current->fs進行了賦值。

	mnt = do_kern_mount("rootfs", 0, "rootfs", NULL);//ns->mnt為rootfs
set_fs_pwd(current->fs, ns->root, ns->root->mnt_root);	//設定當前pwd為ns->root
		//fs->pwdmnt=mnt   fs->pwd=dentry
	set_fs_root(current->fs, ns->root, ns->root->mnt_root);//設定當前root為ns->root
		//fs->rootmnt=mnt   fs->root=dentry

題外話完畢,上面那個函式的重點,是紅字部分,也就是link_path_walk(name, nd);這個函式幹嘛呢,看名字好像跟路徑有關係,管它呢,接著一條道走到黑,進去再說。

程式碼三:

int fastcall link_path_walk(const char *name, struct nameidata *nd)
{
	struct nameidata save = *nd;
	int result;

	/* make sure the stuff we saved doesn't go away */
	dget(save.dentry);	//儲存資訊,出錯的時候可以糾正
	mntget(save.mnt);

	result = __link_path_walk(name, nd);	//nd的dentry和mnt,last進行查詢
									//比如name為dev/console
									//dentry,mnt均為dev的,last裡儲存console資訊
	if (result == -ESTALE) {
		*nd = save;
		dget(nd->dentry);
		mntget(nd->mnt);
		nd->flags |= LOOKUP_REVAL;
		result = __link_path_walk(name, nd);
	}

	dput(save.dentry);
	mntput(save.mnt);

	return result;
}
這個函式看上去簡單很多了,老規矩,接著紅字部分

程式碼四:

static fastcall int __link_path_walk(const char * name, struct nameidata *nd)
{
.......//為了看上去短點,這裡省略一些程式碼,具體可以對照namei.c
	while (*name=='/')//如果是根目錄的話,去掉根目錄
name++;
	inode = nd->dentry->d_inode;
	/* At this point we know we have a real path component. */
	for(;;) {
		unsigned long hash;
		struct qstr this;
		unsigned int c;

		nd->flags |= LOOKUP_CONTINUE;
		err = exec_permission_lite(inode, nd);//可執行
		this.name = name;
		c = *(const unsigned char *)name;

		hash = init_name_hash();
		do {				//一個路徑一個路徑的hash。比如dev/mtdchar1,先dev三個字元hash一下,去查詢
			name++;
			hash = partial_name_hash(c, hash);
			c = *(const unsigned char *)name;
		} while (c && (c != '/'));
		this.len = name - (const char *) this.name;
		this.hash = end_name_hash(hash);	//這個hash值不是this.name的,而是其中一個路徑的,例如dev的

		/* remove trailing slashes? */
		if (!c)
			goto last_component;	//如果是最後一項了,比如已經到mtdchar1了
		while (*++name == '/');
		if (!*name)
			goto last_with_slashes;

	..........//省略一些程式碼
		err = do_lookup(nd, &this, &next);	//根據name.hash查詢dentry和mnt,next指向這些
							//比如/dev/mtdchar1 ,第一次這裡先找到了dev的dentry和mnt
	……//省略
		
		
		if (inode->i_op->follow_link) {
			err = do_follow_link(&next, nd);
			inode = nd->dentry->d_inode;

		} else
			path_to_nameidata(&next, nd);//nd的mnt和dentry指向next的mnt和dentry
					//然後nd的mnt和dentry也有了變化

		continue;	//翻轉上去
		/* here ends the main loop */

last_with_slashes:
		lookup_flags |= LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
last_component:
		nd->flags &= lookup_flags | ~LOOKUP_CONTINUE;
		if (lookup_flags & LOOKUP_PARENT)
			goto lookup_parent;
		……//省略一部分程式碼
		err = do_lookup(nd, &this, &next);
		if (err)
			break;
		inode = next.dentry->d_inode;
		if ((lookup_flags & LOOKUP_FOLLOW)
		    && inode && inode->i_op && inode->i_op->follow_link) {
			err = do_follow_link(&next, nd);
			if (err)
				goto return_err;
			inode = nd->dentry->d_inode;
		} else
			path_to_nameidata(&next, nd);
		err = -ENOENT;
		if (!inode)
			break;
		if (lookup_flags & LOOKUP_DIRECTORY) {
			err = -ENOTDIR; 
			if (!inode->i_op || !inode->i_op->lookup)
				break;
		}
		goto return_base;
lookup_parent:
		nd->last = this;	//nd的last指向最後那個this?
				//比如/dev/console的話,這個nd->last就指向了this,也就是name是console,相應的hash也是console
		nd->last_type = LAST_NORM;
		if (this.name[0] != '.')
			goto return_base;
		if (this.len == 1)
			nd->last_type = LAST_DOT;
		else if (this.len == 2 && this.name[1] == '.')
			nd->last_type = LAST_DOTDOT;
		else
			goto return_base;
return_reval:
……//省略
return_base:
		return 0;
out_dput:
		dput_path(&next, nd);
		break;
	}
	path_release(nd);
return_err:
	return err;
}
上面這個函式太長了,裁掉一些懶得分析的,或者說太煩的,或者說不懂的,主要還是不懂的^_^。接下來函式功能分析,一開始假設我們進來的/dev/mtdchar1,是根目錄,進來的第一件事是去掉根目錄,剩下dev/mtdchar1,然後進入一個大迴圈,這個為什麼要大迴圈呢,作用就是把路徑一層一層給剝掉。dev/mtdchar1剝成dev和mtdchar1,這個過程就是while (c && (c != '/'))這個小迴圈裡乾的,在這個小迴圈裡將路徑轉換成hash,通過err = do_lookup(nd, &this, &next);去查詢dentry和mnt,也就是根據this裡的hash值去nd裡面查相對應的dentry,沒有的話,建立一個,建立的dentry->d_parent=nd->dentry。關於dentry,後面有空再分析吧,比較煩的。

接下來path_to_nameidata(&next, nd);這個函式的功能一目瞭然,就是將next中的值賦予nd,這時候nd指向下一個路徑。比如/dev/mtdchar1 ,第一進入大迴圈的時候,nd是根目錄"/",那麼經過這一步後,nd就是“dev”了,即nd->dentry->d_name.name=“dev”。continue,進入第二次大迴圈。

如果if (!c)    goto last_component;//最後一項了。this.hash的值為mtdchar1。如果nd->flag==LOOKUP_PARENT的話,就進入lookup_parent,對了在很久很久之前,前到我邊寫邊忘記,還得查查。sys_mknodat裡的do_path_lookup,也就是第一段程式碼裡,標誌了LOOKUP_PARENT。直接就返回了,不幹什麼事。

這個函式這麼長的一堆,最後return 0,它閒的蛋疼啊。我們再看看入口引數,對了它也在對nd這個引數進行修改那好我們總結一下nd哪些東西變了,最最重要的變化,nd->dentry這個不再是根目錄了,它變成了路徑最後一個目錄了,這裡具體一點,nd->dentry->d_name.name=“dev”,這說明dentry再也不是剛進來的那個清純少年了。還有nd->last,這個引數裡儲存了路徑最後一個節點的資訊。nd->last.name=“mtdchar1”,hash=hash(mtdchar1)。nd的flag跟last_type也有了變化。總之nd已有點面目全非,快要最終目的了。

走完程式碼四,我們又得反過頭回到程式碼一種去。經過程式碼二、三、四,已經將程式碼一中的nd進行了賦值。

題外話:例如這裡的nd->dentry->d_name.name=“dev”,nd->last.name=“mtdchar1”,hash=hash(mtdchar1)。

二:

接下來進入dentry = lookup_create(&nd, 0);這個的是生成一個新的dentry父節點是nd->dentry,dentry->d_name=nd->last。

程式碼五:

struct dentry *lookup_create(struct nameidata *nd, int is_dir)
{
	struct dentry *dentry = ERR_PTR(-EEXIST);

	mutex_lock_nested(&nd->dentry->d_inode->i_mutex, I_MUTEX_PARENT);
	
	if (nd->last_type != LAST_NORM)
		goto fail;
	nd->flags &= ~LOOKUP_PARENT;
	nd->flags |= LOOKUP_CREATE;
	nd->intent.open.flags = O_EXCL;

	/*
	 * Do the final lookup.
	 */
	dentry = lookup_hash(nd);//這裡根據nd->last生成一個dentry掛到nd->dentry上
	if (IS_ERR(dentry))
		goto fail;

	/*
	 * Special case - lookup gave negative, but... we had foo/bar/
	 * From the vfs_mknod() POV we just have a negative dentry -
	 * all is fine. Let's be bastards - you had / on the end, you've
	 * been asking for (non-existent) directory. -ENOENT for you.
	 */
	if (!is_dir && nd->last.name[nd->last.len] && !dentry->d_inode)
		goto enoent;
	return dentry;
enoent:
	dput(dentry);
	dentry = ERR_PTR(-ENOENT);
fail:
	return dentry;
}
老規矩,紅字搞起。

程式碼六:

static struct dentry *lookup_hash(struct nameidata *nd)//根據last的hash去查詢nd->dentry上的節點,沒有的話生成一個新的
{
	return __lookup_hash(&nd->last, nd->dentry, nd);
}
再進去

程式碼七:

static struct dentry * __lookup_hash(struct qstr *name, struct dentry * base, struct nameidata *nd)
	//根據name查詢dentry,如果沒有則生成一個,掛載base下面
{
	struct dentry * dentry;
	struct inode *inode;
	int err;

	inode = base->d_inode;
	err = permission(inode, MAY_EXEC, nd);
	dentry = ERR_PTR(err);
	if (err)
		goto out;

	/*
	 * See if the low-level filesystem might want
	 * to use its own hash..
	 */
	if (base->d_op && base->d_op->d_hash) {
		err = base->d_op->d_hash(base, name);
		dentry = ERR_PTR(err);
		if (err < 0)
			goto out;
	}

	dentry = cached_lookup(base, name, nd);	//根據name.hash在base上找dentry
	if (!dentry) {	//如果在base上沒有的話
		struct dentry *new = d_alloc(base, name);	//生成一個dentry
					//新dentry的d_parent為base,base->d_subdirs中存放dentry
		dentry = ERR_PTR(-ENOMEM);
		if (!new)
			goto out;
		dentry = inode->i_op->lookup(inode, new, nd);
		if (!dentry)
			dentry = new;
		else
			dput(new);
	}
out:
	return dentry;
}
上面程式碼裡dentry是怎麼產生呢,其實很一目瞭然了,先去根據name.hash去查詢base下有沒有對應的子dentry,如果沒有,就生成一個新的dentry,並且它的父dentry為base。父子dentry關係,子可以通過dentry->d_parent找到父;父可以通過dentry->d_subdirs連結串列找到子。父找到子的程式碼,可以參考下面的程式碼八。遍歷父節點下所有的子節點。

程式碼八:

struct list_head *child;
struct dentry *de;
list_for_each(child,&this_dentry->d_subdirs){
	de = list_entry(child,struct dentry,d_u.d_child);
	//printk("de_name:%s\n",de->d_name.name);
	
}
好了,dentry = lookup_create(&nd, 0)也完成了,得到一個dentry,它的父節點是nd->dentry,例項化的話,就是生成一個dentry->d_name.name="mtdchar1"的dentry,dentry->d_parent->d_name.name=“dev”。

三:

接下來進入最後一步,因為我們的mode是S_IFCHR,所以呼叫error = vfs_mknod(nd.dentry->d_inode,dentry,mode,new_decode_dev(dev));

程式碼九:

int vfs_mknod(struct inode *dir, struct dentry *dentry, int mode, dev_t dev)
{
	int error = may_create(dir, dentry, NULL);

	if (error)
		return error;

	if ((S_ISCHR(mode) || S_ISBLK(mode)) && !capable(CAP_MKNOD))
		return -EPERM;

	if (!dir->i_op || !dir->i_op->mknod)
		return -EPERM;

	error = security_inode_mknod(dir, dentry, mode, dev);
	if (error)
		return error;

	DQUOT_INIT(dir);
	error = dir->i_op->mknod(dir, dentry, mode, dev);	//呼叫相應檔案系統的api
				//例如呼叫ramfs/inode.c 中的mknod
				//生成inode掛載到dentry上。
				//inode->i_sb=dir->i_sb,inode->i_rdev=dev
	if (!error)
		fsnotify_create(dir, dentry);
	return error;
}

紅色部分分析,因為當前使用的檔案系統是rootfs,所以在ramfs/inode.c中找到相應的mknod:ramfs_mknod

題外話:關於如何定位dir->i_op->mknod,發現在這個函式與dir有關係,也就是路徑,不妨假設沒有dev這個路徑,沒有這個路徑mknod的時候會報錯,所以要先mkdir這個路徑,因為是在根目錄下mkdir的,這個i_op->mknod肯定在根目錄下。我們的根目錄是roorfs,所以在ramfs/inode.c中很容易得到根目錄的mknod為ramfs_mknod,再通過ramfs_get_inode得到新路徑的inode->i_op。其中i_op->mknod為ramfs_mknod

程式碼十:

static int
ramfs_mknod(struct inode *dir, struct dentry *dentry, int mode, dev_t dev)
{
	struct inode * inode = ramfs_get_inode(dir->i_sb, mode, dev);	//得到一個新的inode
				//sb跟inode的關係,inode->i_sb=sb    sb->s_inddes連結串列中有inode->i_sb_list
				//inode->i_rdev=dev  裝置號
	int error = -ENOSPC;

	if (inode) {
		if (dir->i_mode & S_ISGID) {
			inode->i_gid = dir->i_gid;
			if (S_ISDIR(mode))
				inode->i_mode |= S_ISGID;
		}
		d_instantiate(dentry, inode);	//將生成的inode掛至dentry上   dentry->d_inode=inode
		dget(dentry);	/* Extra count - pin the dentry in core */
		error = 0;
		dir->i_mtime = dir->i_ctime = CURRENT_TIME;
	}
	return error;
}

紅色部分進去ramfs_get_inode為得到一個新的inode

程式碼十一:

struct inode *ramfs_get_inode(struct super_block *sb, int mode, dev_t dev)
{
	struct inode * inode = new_inode(sb);

	if (inode) {
		inode->i_mode = mode;
		inode->i_uid = current->fsuid;
		inode->i_gid = current->fsgid;
		inode->i_blocks = 0;
		inode->i_mapping->a_ops = &ramfs_aops;
		inode->i_mapping->backing_dev_info = &ramfs_backing_dev_info;
		inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;
		switch (mode & S_IFMT) {
		default:
			init_special_inode(inode, mode, dev);	//掛裝置號
			break;
		case S_IFREG:
			inode->i_op = &ramfs_file_inode_operations;
			inode->i_fop = &ramfs_file_operations;
			break;
		case S_IFDIR:
			inode->i_op = &ramfs_dir_inode_operations;
			inode->i_fop = &simple_dir_operations;

			/* directory inodes start off with i_nlink == 2 (for "." entry) */
			inc_nlink(inode);
			break;
		case S_IFLNK:
			inode->i_op = &page_symlink_inode_operations;
			break;
		}
	}
	return inode;
}
因為我們的mode不是路徑也不是檔案,所以進入default

程式碼十二:

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
	inode->i_mode = mode;
	if (S_ISCHR(mode)) {
		inode->i_fop = &def_chr_fops;
		inode->i_rdev = rdev;
	} else if (S_ISBLK(mode)) {
		inode->i_fop = &def_blk_fops;
		inode->i_rdev = rdev;
	} else if (S_ISFIFO(mode))
		inode->i_fop = &def_fifo_fops;
	else if (S_ISSOCK(mode))
		inode->i_fop = &bad_sock_fops;
	else
		printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o)\n",
		       mode);
}
根據上面加紅部分,可以看到驅動裝置號已經跟inode關聯起來的。mknod的使命也完成了。

最後:

一些廢話:在sys_open開啟這個檔案時,open = f->f_op->open,會根據inode->i_rdev,去查詢字元驅動的表(其實是一個數組),然後就能定位到相應的驅動裝置的操作。

本人水平有限,有些不足或者錯誤之處還望多多指出