一場由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,去查詢字元驅動的表(其實是一個數組),然後就能定位到相應的驅動裝置的操作。
本人水平有限,有些不足或者錯誤之處還望多多指出