上一篇博文我們詳細講解了與檔案系統安裝相關的資料結構,現在我們就來談談一個特定的檔案系統是怎麼安裝的。
1 安裝普通檔案系統
在sys_mount中調用do_mount()函數執行檔案系統安裝實務:
retval = do_mount((char *)dev_page, dir_page, (char *)type_page,
flags, (void *)data_page);
do_mount()函數通過執行下列操作處理真正的安裝操作:
long do_mount(char *dev_name, char *dir_name, char *type_page,
unsigned long flags, void *data_page)
{
struct nameidata nd;
int retval = 0;
int mnt_flags = 0;
/* Discard magic */
if ((flags & MS_MGC_MSK) == MS_MGC_VAL)
flags &= ~MS_MGC_MSK;
/* Basic sanity checks */
if (!dir_name || !*dir_name || !memchr(dir_name, 0, PAGE_SIZE))
return -EINVAL;
if (dev_name && !memchr(dev_name, 0, PAGE_SIZE))
return -EINVAL;
if (data_page)
((char *)data_page)[PAGE_SIZE - 1] = 0;
/* 如果已安裝檔案系統對象中的安裝標誌MS_NOSUID、MS_NODEV、MS_NOATIME、MS_NODIRATIME、MS_NODEV或MS_NOEXEC中任一個被設定,
* 則清除它們,並在已安裝檔案系統對象中設定相應的標誌(MNT_NOSUID、MNT_NODEV、MNT_NOEXEC、MNT_NOATIME、MNT_NODIRATIME)。*/
if (flags & MS_NOSUID)
mnt_flags |= MNT_NOSUID;
if (flags & MS_NODEV)
mnt_flags |= MNT_NODEV;
if (flags & MS_NOEXEC)
mnt_flags |= MNT_NOEXEC;
if (flags & MS_NOATIME)
mnt_flags |= MNT_NOATIME;
if (flags & MS_NODIRATIME)
mnt_flags |= MNT_NODIRATIME;
flags &= ~(MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_ACTIVE |
MS_NOATIME | MS_NODIRATIME);
/* ... and get the mountpoint 調用path_lookup()尋找安裝點的路徑名;
* 該函數把路徑名尋找的結果存放在nameidata類型的局部變數nd中(參見下一博的“路徑名尋找”)。*/
retval = path_lookup(dir_name, LOOKUP_FOLLOW, &nd);
if (retval)
return retval;
retval = security_sb_mount(dev_name, &nd, type_page, flags, data_page);
if (retval)
goto dput_out;
/* 如果MS_REMOUNT標誌被指定,其目的通常是改變超級塊對象s_flags欄位的安裝標誌,
* 以及已安裝檔案系統對象mnt_flags欄位的安裝檔案系統標誌。do_remount()函數執行這些改變。*/
if (flags & MS_REMOUNT)
retval = do_remount(&nd, flags & ~MS_REMOUNT, mnt_flags,
data_page);
/* 否則,檢查MS_BIND標誌。如果它被指定,則使用者要求在系統分類樹的另一個安裝點上的檔案或目錄能夠可見。*/
else if (flags & MS_BIND)
retval = do_loopback(&nd, dev_name, flags & MS_REC);
else if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE))
retval = do_change_type(&nd, flags);
/* 否則,檢查MS_MOVE標誌。如果它被指定,則使用者要求改變已安裝檔案系統的安裝點。do_move_mount()函數原子地完成這一任務。*/
else if (flags & MS_MOVE)
retval = do_move_mount(&nd, dev_name);
/* 否則,調用do_new_mount()。這是最普通的情況。
* 當使用者要求安裝一個特殊檔案系統或存放在磁碟分割中的普通檔案系統時,觸發該函數。*/
else
retval = do_new_mount(&nd, type_page, flags, mnt_flags,
dev_name, data_page);
dput_out:
path_release(&nd);
return retval;
}
我們還是來看最普通的情況:
static int do_new_mount(struct nameidata *nd, char *type, int flags,
int mnt_flags, char *name, void *data)
{
struct vfsmount *mnt;
if (!type || !memchr(type, 0, PAGE_SIZE))
return -EINVAL;
/* we need capabilities... */
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
mnt = do_kern_mount(type, flags, name, data);
if (IS_ERR(mnt))
return PTR_ERR(mnt);
return do_add_mount(mnt, nd, mnt_flags, NULL);
}
do_new_mount調用do_kern_mount()函數,給它傳遞的參數為檔案系統類型、安裝標誌以及塊裝置名稱。
do_kern_mount()處理實際的安裝操作並返回一個新安裝檔案系統描述符的地址:
struct vfsmount *
do_kern_mount(const char *fstype, int flags, const char *name, void *data)
{
struct file_system_type *type = get_fs_type(fstype); /* 我們熟悉的get_fs_type函數,請看上一篇博文 */
struct vfsmount *mnt;
if (!type)
return ERR_PTR(-ENODEV);
mnt = vfs_kern_mount(type, flags, name, data);
put_filesystem(type);
return mnt;
}
struct vfsmount *
vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
{
struct vfsmount *mnt;
char *secdata = NULL;
int error;
if (!type)
return ERR_PTR(-ENODEV);
error = -ENOMEM;
mnt = alloc_vfsmnt(name); /* 呵呵,我們熟悉的alloc_vfsmnt函數,請看上一篇博文 */
if (!mnt)
goto out;
if (data) {
secdata = alloc_secdata();
if (!secdata)
goto out_mnt;
error = security_sb_copy_data(type, data, secdata);
if (error)
goto out_free_secdata;
}
/* 調用依賴於檔案系統的type->get_sb()函數分配,並初始化一個新的超級到mnt->mnt_sb */
error = type->get_sb(type, flags, name, data, mnt);
if (error < 0)
goto out_free_secdata;
error = security_sb_kern_mount(mnt->mnt_sb, secdata);
if (error)
goto out_sb;
/* 將mnt->mnt_root欄位初始化為與檔案系統根目錄對應的目錄項對象的地址,
並增加該目錄項對象的引用計數器值。*/
mnt->mnt_mountpoint = mnt->mnt_root;
/* 用mnt中的值初始化mnt->mnt_parent欄位(對於普通檔案系統,
* 當後面講到的graft_tree()把已安裝檔案系統的描述符插入到合適的鏈表中時,
* 要把mnt_parent欄位置為合適的值)。 */
mnt->mnt_parent = mnt;
up_write(&mnt->mnt_sb->s_umount);
free_secdata(secdata);
return mnt;
out_sb:
dput(mnt->mnt_root);
up_write(&mnt->mnt_sb->s_umount);
deactivate_super(mnt->mnt_sb);
out_free_secdata:
free_secdata(secdata);
out_mnt:
free_vfsmnt(mnt);
out:
return ERR_PTR(error);
}
然後,do_new_mount()函數調用do_add_mount():
int do_add_mount(struct vfsmount *newmnt, struct nameidata *nd,
int mnt_flags, struct list_head *fslist)
{
int err;
down_write(&namespace_sem);
/* Something was mounted here while we slept */
while (d_mountpoint(nd->dentry) && follow_down(&nd->mnt, &nd->dentry))
;
err = -EINVAL;
if (!check_mnt(nd->mnt))
goto unlock;
/* Refuse the same filesystem on the same mount point */
err = -EBUSY;
if (nd->mnt->mnt_sb == newmnt->mnt_sb &&
nd->mnt->mnt_root == nd->dentry)
goto unlock;
err = -EINVAL;
if (S_ISLNK(newmnt->mnt_root->d_inode->i_mode))
goto unlock;
newmnt->mnt_flags = mnt_flags;
if ((err = graft_tree(newmnt, nd)))
goto unlock;
if (fslist) {
/* add to the specified expiration list */
spin_lock(&vfsmount_lock);
list_add_tail(&newmnt->mnt_expire, fslist);
spin_unlock(&vfsmount_lock);
}
up_write(&namespace_sem);
return 0;
unlock:
up_write(&namespace_sem);
mntput(newmnt);
return err;
}
其本質上執行下列操作:
1、獲得當前進程的寫訊號量namespace_sem,因為函數要更改namespace結構。
2、do_kern_mount()函數可能讓當前進程睡眠;同時,另一個進程可能在完全相同的安裝點上安裝檔案系統或者甚至更改根檔案系統(current->namespace->root)。驗證在該安裝點上最近安裝的檔案系統是否仍指向當前的namespace;如果不是,則釋放讀/寫訊號量並返回一個錯誤碼。
3、如果要安裝的檔案系統已經被安裝在由系統調用的參數所指定的安裝點上,或該安裝點是一個符號連結,則釋放讀/寫訊號量並返回一個錯誤碼。
4、初始化由do_kern_mount()分配的新安裝檔案系統對象的mnt_flags欄位的標誌。
5、調用graft_tree()把新安裝的檔案系統對象插入到namespace鏈表、散列表中(在graft_tree()函數中調用attach_recursive_mnt函數實現)
6、父檔案系統的子鏈表中。
7、釋放namespace_sem讀/寫訊號量並返回。
static int graft_tree(struct vfsmount *mnt, struct nameidata *nd)
{
int err;
if (mnt->mnt_sb->s_flags & MS_NOUSER)
return -EINVAL;
if (S_ISDIR(nd->dentry->d_inode->i_mode) !=
S_ISDIR(mnt->mnt_root->d_inode->i_mode))
return -ENOTDIR;
err = -ENOENT;
mutex_lock(&nd->dentry->d_inode->i_mutex);
if (IS_DEADDIR(nd->dentry->d_inode))
goto out_unlock;
err = security_sb_check_sb(mnt, nd);
if (err)
goto out_unlock;
err = -ENOENT;
if (IS_ROOT(nd->dentry) || !d_unhashed(nd->dentry))
err = attach_recursive_mnt(mnt, nd, NULL);
out_unlock:
mutex_unlock(&nd->dentry->d_inode->i_mutex);
if (!err)
security_sb_post_addmount(mnt, nd);
return err;
回到do_mount()函數,最後調用path_release()終止安裝點的路徑名尋找(參見下一篇博文“路徑名尋找”)並返回。
2 分配超級塊對象
檔案系統對象的get_sb方法通常是由單行函數實現的。例如,在Ext2檔案系統中該方法的實現如下:
//fs/ext2/Super.c
static int ext2_get_sb(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data, struct vfsmount *mnt)
{
return get_sb_bdev(fs_type, flags, dev_name, data, ext2_fill_super, mnt);
}
get_sb_bdev() VFS函數分配並初始化一個新的適合於磁碟檔案系統的超級塊;它接收ext2_fill_super()函數的地址,該函數從Ext2磁碟分割讀取磁碟超級塊。
為了分配適合於特殊檔案系統的超級塊,VFS也提供get_sb_pseudo()函數,對於沒有安裝點的特殊檔案系統,例如pipefs()、get_sb_single()函數等(對於具有唯一安裝點的特殊檔案系統,例如.sysfs)以及get_sb_nodev()函數(對於可以安裝多次的特殊檔案系統,例如tmpfs;參見下面)。
get_sb_bdev()函數位於/fs/Super.c,代碼如下:
int get_sb_bdev(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data,
int (*fill_super)(struct super_block *, void *, int),
struct vfsmount *mnt)
{
struct block_device *bdev;
struct super_block *s;
int error = 0;
/* 調用open_bdev_excl()開啟裝置檔案名稱為dev_name的塊裝置。*/
bdev = open_bdev_excl(dev_name, flags, fs_type);
if (IS_ERR(bdev))
return PTR_ERR(bdev);
/*
* once the super is inserted into the list by sget, s_umount
* will protect the lockfs code from trying to start a snapshot
* while we are mounting
*/
down(&bdev->bd_mount_sem);
/* 調用sget()搜尋檔案系統的超級塊對象鏈表(type->fs_supers,參見前面的博文中“檔案系統安裝資料結構”部分)。
* 如果找到一個與塊裝置相關的超級塊,則返回它的地址。否則,分配並初始化一個新的超級塊對象,
* 把它插入到檔案系統鏈表和超級塊全域鏈表中,並返回其地址。*/
s = sget(fs_type, test_bdev_super, set_bdev_super, bdev);
up(&bdev->bd_mount_sem);
if (IS_ERR(s))
goto error_s;
if (s->s_root) { /* 如果不是新的超級塊(注意,是通過s->s_root是否為空白來判斷的) */
if ((flags ^ s->s_flags) & MS_RDONLY) {
up_write(&s->s_umount);
deactivate_super(s);
error = -EBUSY;
goto error_bdev;
}
close_bdev_excl(bdev);
} else {
char b[BDEVNAME_SIZE];
/* 把參數flags中的值拷貝到超級塊的s_flags欄位,
* 並將s_id、s_old_blocksize以及s_blocksize欄位設定為塊裝置的合適值。*/
s->s_flags = flags;
strlcpy(s->s_id, bdevname(bdev, b), sizeof(s->s_id));
sb_set_blocksize(s, block_size(bdev));
/* 調用依賴檔案系統的函數(例子中是ext2_fill_super函數,我們在討論ext2的時候會講它)
* 訪問磁碟上的超級塊資訊,並填充新超級塊對象的其他欄位。*/
error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);
if (error) {
up_write(&s->s_umount);
deactivate_super(s);
goto error;
}
s->s_flags |= MS_ACTIVE;
bdev_uevent(bdev, KOBJ_MOUNT);
}
return simple_set_mnt(mnt, s);
error_s:
error = PTR_ERR(s);
error_bdev:
close_bdev_excl(bdev);
error:
return error;
}
3 安裝根檔案系統
安裝根檔案系統是系統初始化的關鍵區段。這是一個相當複雜的過程,因為Linux核心允許根檔案系統存放在很多不同的地方,比如硬碟分區、磁碟片、通過NFS共用的遠程檔案系統,甚至儲存在ramdisk中(RAM中的虛擬塊裝置)。
為了使敘述變得簡單,讓我們假定根檔案系統存放在硬碟分區(畢竟這是最常見的情行)。當系統啟動時,核心就要在變數ROOT_DEV中尋找包含根檔案系統的磁碟主裝置號:
//init/Do_mounts.c
dev_t ROOT_DEV;
當編譯核心時,或者向最初的啟動裝入程式傳遞一個合適的“root”選項時,根檔案系統可以被指定為/dev目錄下的一個裝置檔案。類似地,根檔案系統的安裝標誌存放在root_mountflags變數中:
//init/Do_mounts.c
int root_mountflags = MS_RDONLY | MS_SILENT;
使用者可以指定這些標誌,或者通過對已編譯的核心映像使用rdev外部程式,或者向最初的啟動裝入程式傳遞一個合適的rootflags選項來達到。
安裝根檔案系統分兩個階段:
(1)核心安裝特殊rootfs檔案系統,該檔案系統僅提供一個作為初始安裝點的空目錄。
(2)核心在空目錄上安裝實際根檔案系統。
為什麼核心不怕麻煩,要在安裝實際根檔案系統之前安裝rootfs檔案系統呢?這是因為,rootfs檔案系統允許核心容易地改變實際根檔案系統。實際上,在大多數情況下,系統初始化是核心會逐個地安裝和卸載幾個根檔案系統。例如,一個發布版的初始啟動光碟片可能把具有一組最小驅動程式的核心裝人RAM中,核心把存放在ramdisk中的一個最小的檔案系統作為根安裝。接下來,在這個初始根檔案系統中的程式探測系統的硬體(例如,它們判斷硬碟是否是EIDE、SCSI等等),裝入所有必需的核心模組,並從物理塊裝置重新安裝根檔案系統。
階段1:安裝rootfs檔案系統
第一階段是由init_rootfs()和init_mount_tree()函數完成的,它們在系統初始化過程中執行。
init_rootfs()函數註冊特殊檔案系統類型rootfs:
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.get_sb = rootfs_get_sb,
.kill_sb = kill_litter_super,
};
init_mount_tree()函數主要完成根檔案系統的初始化:
static void __init init_mount_tree(void)
{
struct vfsmount *mnt;
struct namespace *namespace;
struct task_struct *g, *p;
mnt = do_kern_mount("rootfs", 0, "rootfs", NULL);
if (IS_ERR(mnt))
panic("Can't create rootfs");
namespace = kmalloc(sizeof(*namespace), GFP_KERNEL);
if (!namespace)
panic("Can't allocate initial namespace");
atomic_set(&namespace->count, 1);
INIT_LIST_HEAD(&namespace->list);
init_waitqueue_head(&namespace->poll);
namespace->event = 0;
list_add(&mnt->mnt_list, &namespace->list);
namespace->root = mnt;
mnt->mnt_namespace = namespace;
init_task.namespace = namespace;
read_lock(&tasklist_lock);
do_each_thread(g, p) {
get_namespace(namespace);
p->namespace = namespace;
} while_each_thread(g, p);
read_unlock(&tasklist_lock);
set_fs_pwd(current->fs, namespace->root, namespace->root->mnt_root);
set_fs_root(current->fs, namespace->root, namespace->root->mnt_root);
}
看到了吧,init_mount_tree首先調用do_kern_mount()函數,把字串“rootfs”作為檔案系統型別參數傳遞給它,檔案系統標誌是0,沒有data,並把該函數返回的新安裝檔案系統描述符的地址儲存在mnt局部變數中。正如前面介紹的,do_kern_mount()最終調用rootfs檔案系統的get_sb方法,也即rootfs_get_sb()函數:
static int rootfs_get_sb(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data, struct vfsmount *mnt)
{
return get_sb_nodev(fs_type, flags|MS_NOUSER, data, ramfs_fill_super,
mnt);
}
get_sb_nodev()函數前面一件提到了,針對rootfs檔案系統:
1、調用sget()函數分配新的超級塊,傳遞set_anon_super()函數的地址作為參數。接下來,用合適的方式設定超級快的s_dev欄位:主裝置號為O,次裝置號不同於其他已安裝的特殊檔案系統的次裝置號。
2、將flags參數的值拷貝到超級塊的s_flags欄位中。
3、調用ramfs_fill_super()函數分配索引節點對象和對應的目錄項對象並填充超級塊欄位值。由於rootfs是一種特殊檔案系統,沒有磁碟超級塊,因此只需執行兩個超級塊操作:
static int ramfs_fill_super(struct super_block * sb, void * data, int silent)
{
struct inode * inode;
struct dentry * root;
sb->s_maxbytes = MAX_LFS_FILESIZE;
sb->s_blocksize = PAGE_CACHE_SIZE;
sb->s_blocksize_bits = PAGE_CACHE_SHIFT;
sb->s_magic = RAMFS_MAGIC;
sb->s_op = &ramfs_ops;
sb->s_time_gran = 1;
inode = ramfs_get_inode(sb, S_IFDIR | 0755, 0);
if (!inode)
return -ENOMEM;
root = d_alloc_root(inode);
if (!root) {
iput(inode);
return -ENOMEM;
}
sb->s_root = root;
return 0;
}
4、返回新超級塊的地址。
回到init_mount_tree()函數,繼續:
為進程0的命名空間分配一個namespace對象,並將它插入到由do_kern_mount()函數返回的已安裝檔案系統描述符中:
namespace = kmalloc(sizeof(*namespace), GFP_KERNEL);
list_add(&mnt->mnt_list, &namespace->list);
namespace->root = mnt;
mnt->mnt_namespace = init_task.namespace = namespace;
將系統中其他每個進程的namespace欄位設定為namespace對象的地址;同時初始化引用計數器namespace->count(預設情況下,所有的進程共用同一個初始namespace)。
將進程0的根目錄和當前工作目錄設定為根檔案系統。
set_fs_pwd(current->fs, namespace->root, namespace->root->mnt_root);
set_fs_root(current->fs, namespace->root, namespace->root->mnt_root);
階段2:安裝實際根檔案系統
根檔案系統安裝操作的第二階段是由核心在系統初始化即將結束時進行的。根據核心被編譯時間所選擇的選項,和核心裝入程式所傳遞的啟動選項,可以有幾種方法安裝實際根檔案系統。為了簡單起見,我們只考慮磁碟檔案系統的情況,它的裝置檔案名稱已通過“root”啟動參數傳遞給核心。同時我們假定除了rootfs檔案系統外,沒有使用其他初始特殊檔案系統。
核心主要是調用prepare_namespace()函數執行安裝實際根檔案系統的操作:
void __init prepare_namespace(void)
{
int is_floppy;
if (root_delay) {
printk(KERN_INFO "Waiting %dsec before mounting root device.../n",
root_delay);
ssleep(root_delay);
}
md_run_setup();
if (saved_root_name[0]) {
/* 把root_device_name變數置為從啟動參數“root”中擷取的裝置檔案名稱。
* 同樣,把ROOT_DEV變數置為同一裝置檔案的主裝置號和次裝置號。*/
root_device_name = saved_root_name;
if (!strncmp(root_device_name, "mtd", 3)) {
/* 調用mount_block_root()函數,將最常用的塊裝置作為rootfs檔案系統的子檔案系統 */
mount_block_root(root_device_name, root_mountflags);
goto out;
}
ROOT_DEV = name_to_dev_t(root_device_name);
if (strncmp(root_device_name, "/dev/", 5) == 0)
root_device_name += 5;
}
is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;
if (initrd_load())
goto out;
if (is_floppy && rd_doload && rd_load_disk(0))
ROOT_DEV = Root_RAM0;
mount_root();
out:
/* 移動rootfs檔案系統根目錄上的已安裝檔案系統的安裝點。 */
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
security_sb_post_mountroot();
}
注意,rootfs特殊檔案系統沒有被卸載:它只是隱藏在基於磁碟的根檔案系統下了。
4 卸載檔案系統
umount()系統調用用來卸載一個檔案系統。我們不去詳細討論它的代碼了,比較簡單。相應的sys_umount()服務常式作用於兩個參數:檔案名稱(多是安裝點目錄或是塊裝置檔案名稱)和一組標誌。該函數執行下列操作:
1.調用path_lookup()尋找安裝點路徑名;該函數把返回的尋找操作結果存放在nameidata類型的局部變數nd中。
2.如果尋找的最終目錄不是檔案系統的安裝點,則設定retval返回碼為-EINVAL並跳到第6步。這種檢查是通過驗證nd->mnt->mnt_root(它包含由nd.dentry指向的目錄項對象地址)進行的。
3.如果要卸載的檔案系統還沒有安裝在命名空間中,則設定retval返回碼為-EINVAL並跳到第6步(回想一下,某些特殊檔案系統沒有安裝點)。這種檢查是通過在nd->mnt上調用check_mnt()函數進行的。
4.如果使用者不具有卸載檔案系統的特權,則設定retval返回碼為-EPERM並跳到第6步。
5.調用do_umount(),傳遞給它的參數為nd.mnt(已安裝檔案系統對象)和flags(一組標誌)。該函數執行下列操作:
a)從已安裝檔案系統對象的mnt_sb欄位檢索超級塊對象sb的地址。
b)如果使用者要求強制卸載操作,則調用umount_begin超級塊操作中斷任何進行中的安裝操作。
c)如果要卸載的檔案系統是根檔案系統,且使用者並不要求真正地把它卸載下來則調用do_remount_sb()重新安裝根檔案系統為唯讀並終止。
d)為進行寫操作而擷取當前進程的namespace->sem讀/寫訊號量和vfsmount_lock自旋鎖。
e)如果已安裝檔案系統不包含任何子安裝檔案系統的安裝點,或者使用者要求強制卸載檔案系統,則調用umount_tree()卸載檔案系統(及其所有子檔案系統)
f)釋放vfsmount_lock自旋鎖和當前進程的namespace->sem讀/寫訊號量。
6.減少相應檔案系統根目錄的目錄項對象和已安裝檔案系統描述符的引用計數器值;這些計數器值由path_lookup()增加。
7.返回retval的值。