Linux rootfs_hdfs shell命令

Linux rootfs_hdfs shell命令本文阐述Linux中的文件系统部分,源代码来自基于IA32的2.4.20内核。总体上说Linux下的文件系统主要可分为三大块:一是上层的文件系统的系统调用,二是虚拟文件系统VFS(VirtualFilesystemSwitch),三是挂载到VFS中的各实际文件系统,例如ext2,jffs等。本文侧重于通过具体的代码分析来解释Linux内核中VFS的内在机制,在这过程

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE稳定放心使用

本文阐述 Linux 中的文件系统部分,源代码来自基于 IA32 的 2.4.20 内核。总体上说 Linux下的文件系统主要可分为三大块:一是上层的文件系统的系统调用,二是虚拟文件系统 VFS(Virtual FilesystemSwitch),三是挂载到 VFS 中的各实际文件系统,例如 ext2,jffs 等。本文侧重于通过具体的代码分析来解释 Linux内核中 VFS 的内在机制,在这过程中会涉及到上层文件系统调用和下层实际文件系统的如何挂载。文章试图从一个比较高的角度来解释Linux 下的 VFS 文件系统机制。

1.摘要

本 文阐述 Linux 中的文件系统部分,源代码来自基于 IA32 的 2.4.20 内核。总体上说 Linux下的文件系统主要可分为三大块:一是上层的文件系统的系统调用,二是虚拟文件系统 VFS(Virtual FilesystemSwitch),三是挂载到 VFS 中的各实际文件系统,例如 ext2,jffs 等。本文侧重于通过具体的代码分析来解释 Linux内核中 VFS 的内在机制,在这过程中会涉及到上层文件系统调用和下层实际文件系统的如何挂载。文章试图从一个比较高的角度来解释Linux 下的 VFS文件系统机制,所以在叙述中更侧重于整个模块的主脉络,而不拘泥于细节,同时配有若干张插图,以帮助读者理解。

相对来说,VFS 部分的代码比较繁琐复杂,希望读者在阅读完本文之后,能对 Linux 下的 VFS整体运作机制有个清楚的理解。建议读者在阅读本文前,先尝试着自己阅读一下文件系统的源代码,以便建立起 Linux下文件系统最基本的概念,比如至少应熟悉 super block, dentry, inode,vfsmount等数据结构所表示的意义,这样再来阅读本文以便加深理解。

2.VFS 概述

VFS 是一种软件机制,也许称它为 Linux的文件系统管理者更确切点,与它相关的数据结构只存在于物理内存当中。所以在每次系统初始化期间,Linux 都首先要在内存当中构造一棵VFS 的目录树(在 Linux 的源代码里称之为 namespace),实际上便是在内存中建立相应的数据结构。VFS 目录树在Linux 的文件系统模块中是个很重要的概念,希望读者不要将其与实际文件系统目录树混淆,在笔者看来,VFS中的各目录其主要用途是用来提供实际文件系统的挂载点,当然在 VFS中也会涉及到文件级的操作,本文不阐述这种情况。下文提到目录树或目录,如果不特别说明,均指 VFS 的目录树或目录。图 1是一种可能的目录树在内存中的影像:


图 1:VFS 目录树结构


这 里的文件系统是指可能会被挂载到目录树中的各个实际文件系统,所谓实际文件系统,即是指VFS中的实际操作最终要通过它们来完成而已,并不意味着它们一定要存在于某种特定的存储设备上。比如在笔者的 Linux 机器下就注册有”rootfs”、”proc”、”ext2″、”sockfs” 等十几种文件系统。

3.1 数据结构

在 Linux 源代码中,每种实际的文件系统用以下的数据结构表示:

struct file_system_type {


    const char *name;


    int fs_flags;
        

struct super_block *(*read_super) (struct super_block *, void *, int);
        

struct module *owner;


    struct file_system_type * next;


    struct list_head fs_supers;

};

注册过程实际上将表示各实际文件系统的 struct file_system_type数据结构的实例化,然后形成一个链表,内核中用一个名为 file_systems 的全局变量来指向该链表的表头。

3.2 注册 rootfs 文件系统

在 众多的实际文件系统中,之所以单独介绍 rootfs 文件系统的注册过程,实在是因为该文件系统 VFS 的关系太过密切,如果说ext2/ext3 是 Linux 的本土文件系统,那么 rootfs 文件系统则是 VFS 存在的基础。一般文件系统的注册都是通过module_init 宏以及 do_initcalls() 函数来完成(读者可通过阅读module_init 宏的声明及arch\i386\vmlinux.lds 文件来理解这一过程),但是 rootfs 的注册却是通过 init_rootfs()这一初始化函数来完成,这意味着 rootfs 的注册过程是 Linux 内核初始化阶段不可分割的一部分。

在2.6中,

kernel_start()

   |____start_kernel()

             |____vfs_caches_init()(fs/dcache.c)

                       |____dcache_init()

                       |____inode_init()

                       |____files_init()

                       |____mnt_init()

                            |____init_rootfs()

                                   |___register_filesystem(&rootfs_fs_type)

                            |____init_mount_tree()

                       |____bdev_cache_init()

                       |____chrdev_init()

init_rootfs() 通过调用register_filesystem(&rootfs_fs_type) 函数来完成 rootfs文件系统注册的,其中rootfs_fs_type 定义如下:

 struct file_system_type rootfs_fs_type = { 

name:          "rootfs", 

read_super:       ramfs_read_super, 

fs_flags: FS_NOMOUNT|FS_LITTER,

owner:         THIS_MODULE, 

}
 

注册之后的 file_systems 链表结构如下图2所示:


图 2: file_systems链表结构


既 然是树,所以根是其赖以存在的基础,本节阐述 Linux 在初始化阶段是如何建立根结点的,即 “/”目录。这其中会包括挂载rootfs 文件系统到根目录 “/” 的具体过程。构造根目录的代码是在 init_mount_tree() 函数(fs\namespace.c) 中。

首 先,init_mount_tree() 函数会调用 do_kern_mount(“rootfs”, 0, “rootfs”,NULL) 来挂载前面已经注册了的 rootfs文件系统。这看起来似乎有点奇怪,因为根据前面的说法,似乎是应该先有挂载目录,然后再在其上挂载相应的文件系统,然而此时 VFS似乎并没有建立其根目录。没关系,这是因为这里我们调用的是do_kern_mount(),这个函数内部自然会创建我们最关心也是最关键的根目录(在 Linux 中,目录对应的数据结构是struct dentry)。

在这个场景里,do_kern_mount() 做的工作主要是:

1)调用 alloc_vfsmnt() 函数在内存里申请了一块该类型的内存空间(struct vfsmount*mnt),并初始化其部分成员变量。(mnt : avfs-internal representation of a mount point)

2) 调用 get_sb_nodev() 函数在内存中分配一个超级块结构 (struct super_block)sb,并初始化其部分成员变量,将成员 s_instances 插入到 rootfs 文件系统类型结构中的 fs_supers指向的双向链表中。

(sb:描述文件系统信息,例如块大小、文件系统版本号、上次mount 的时间等等。)

3) 通过 rootfs 文件系统中的 read_super 函数指针调用 ramfs_read_super()函数。还记得当初注册rootfs 文件系统时,其成员 read_super 指针指向了 ramfs_read_super()函数,参见图2.

4) ramfs_read_super() 函数调用 ramfs_get_inode() 在内存中分配了一个 inode 结构(struct inode) inode,并初始化其部分成员变量,其中比较重要的有 i_op、i_fop 和 i_sb:

inode->i_op = &ramfs_dir_inode_operations;


inode->i_fop = &dcache_dir_ops;


inode->i_sb = sb;

(一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型(常规、目录、符号链接等),权限,文件大小,创建/修改/访问时间等,也就是
ls-l
命令看到的那些信息,这些信息存在inode中而不是数据块中。每个文件都有一个inode,一个块组中的所有 inode组成了inode表。)

这使得将来通过文件系统调用对 VFS 发起的文件操作等指令将被 rootfs 文件系统中相应的函数接口所接管。


图3


6) 将 mnt 中的 mnt_sb 指针指向 sb,mnt_root 和 mnt_mountpoint 指针指向dentry,而 mnt_parent指针则指向自身。

 

init_task是系统中的0号进程,也就是第一个进程,这个进程永远不会被撤消,它的描述符被静态的分配到内核数据段中,也就是说init_task的进程描述符是预先由编译器分配的,在运行的过程中保持不变,而其它描述符是在运行的过程中,由系统根据当前的内存状况随机分配的,撤消的时再在归还给系统。

init_task进程0,(start_kernel()->sched_init()->init_idle(current)????)总结一下有如下几个要点:

1. 进程0是所有其他进程的祖先,也称作idle进程或swapper进程。
2. 进程0是在系统初始化时由kernel自身从无到有创建。(过程集中在start_kernel函数内)
3. 进程0的数据成员大部分是静态定义的,即由预先定义好的(如上)INIT_TASK, INIT_MM等宏初始化。

进程0的描述符init_task定义在arch/mips/kernel/init_task.c,由INIT_TASK宏初始化。init_mm等结构体定义在include/linux/init_task.h内,为init_task成员的初始值,分别由对应的初始化宏如INIT_MM等初始化,

进程1:

进程0最终会通过调用kernel_thread创建一个内核线程去执行init函数,这个新创建的内核线程即Process1(这时还是共享着内核线程0的资源属性如地址空间等)。init函数继续完成剩余的内核初始化,并在函数的最后调用execve系统调用装入用户空间的可执行程序/sbin/init,这时进程1就拥有了自己的属性资源,成为一个普通进程(init进程)。至此,内核初始化和启动过程结束。下面就进入了用户空间的初始化,最后运行shell登陆界面。

(注:Init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。)

 

 

进程1的创建过程如下:

start_kernel-> rest_init -> kernel_thread-> init

 

static void noinline rest_init(void)

   __releases(kernel_lock)

{

   system_state = SYSTEM_BOOTING_SCHEDULER_OK;

 

   kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);

   numa_default_policy();

   unlock_kernel();

 

   

   __preempt_enable_no_resched();

   schedule();

   preempt_disable();

 

   

   cpu_idle();

}

执行schedule()启动进程1后,进程0便调用cpu_idle()函数进入睡眠状态。

 

 

下面看一下kernel_thread函数

pid_t kernel_thread(int (*fn)(void *), void *arg, unsignedlong flags)

{

   struct pt_regs regs;

    longpid;

 

   memset(&regs, 0, sizeof(regs));

 

   regs.ARM_r1 = (unsigned long)arg;

   regs.ARM_r2 = (unsigned long)fn;

   regs.ARM_r3 = (unsigned long)do_exit;

   regs.ARM_pc = (unsigned long)kernel_thread_helper;

   regs.ARM_cpsr = SVC_MODE;

 

    pid= do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, &regs,0, NULL, NULL);

 

   MARK(kernel_thread_create, “%ld %p”, pid, fn);

   return pid;

}

 

可以发现kernel_thread 最终也是调用的do_fork完成内核线程的创建。

do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, pregs, 0, NULL,NULL);

flag参数标志决定了新创建的进程的行为方式以及父子进程之间共享的资源种类,这里用到的几个flag标志意思如下:

CLONE_FS      父子进程共享文件系统信息

CLONE_SIGHAND  父子进程共享信号处理函数

CLONE_VM      父子进程共享地址空间

CLONE_UNTRACED 防止跟踪进程在子进程上强制执行CLONE_PTRACE

 

 

看看新创建的内核线程1(Process 1)做了些什么事: ( 即init函数)

static int init(void * unused)

{

 

 

   lock_kernel();

   set_cpus_allowed(current, CPU_MASK_ALL);

   child_reaper = current;

   smp_prepare_cpus(max_cpus);

   init_hardirqs();

   do_pre_smp_initcalls();

   smp_init();

   sched_init_smp();

   cpuset_init_smp();

   ……

 

   run_init_process(“/sbin/init”);

   run_init_process(“/etc/init”);

   run_init_process(“/bin/init”);

   run_init_process(“/bin/sh”);

 

   panic(“No init found.  Try passing init= option tokernel.”);

}

这里的 run_init_process 实际上就是通过execve()来运行 init程序。这里首先运行/sbin/init,如果失败再运行/etc/init,然后是/bin/init,然后是/bin/sh(也就是说,init可执行文件可以放在上面代码中的4 个目录中都可以),如果都失败,则可以通过在系统

启动时在添加的启动参数来指定init,比如init=/ linuxrc。

 

run_init_process源码如下,最终通过execve系统调用执行/sbin/init程序。

static void run_init_process(char*init_filename)

{

   argv_init[0] = init_filename;

   execve(init_filename, argv_init, envp_init);

}

Exec函数族(execve为其中一种)提供了一个在进程中启动另外一个程序执行的方法。它可以根据指定的文件名或目录找到可执行文件,并用它来取代原先调用进程的属性包括数据段,代码段和堆栈段等。在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。这里在执行execve之前,进程1还是共享着内核线程0资源属性的内核线程。执行execve后(即执行了用户空间的init程序),此时,进程1就拥有了自己的地址空间等属性,成为一个普通进程。

这 样,当 do_kern_mount() 函数返回时,以上分配出来的各数据结构和 rootfs 文件系统的关系将如上图 3所示。图中 mnt、sb、inode、dentry结构块下方的数字表示它们在内存里被分配的先后顺序。限于篇幅的原因,各结构中只给出了部分成员变量,读者可以对照源代码根据图中所示按图索骥,以加深理解。

最后,init_mount_tree() 函数会为系统最开始的进程(即 init_task进程)准备它的进程数据块中的namespace 域,主要目的是将 do_kern_mount() 函数中建立的 mnt 和dentry 信息记录在了 init_task 进程的进程数据块中,这样所有以后从 init_task 进程 fork出来的进程也都先天地继承了这一信息,在后面用sys_mkdir 在 VFS中创建一个目录的过程中,我们可以看到这里为什么要这样做。为进程建立 namespace 的主要代码如下:

       namespace = kmalloc(sizeof(*namespace), GFP_KERNEL);
   

list_add(&mnt->mnt_list, &namespace->list);  

//mnt is returned by do_kern_mount()
        

namespace->root = mnt;


        init_task.namespace = namespace;
        for_each_task(p) {


                      get_namespace(namespace);
                p->namespace = namespace;


        }
        

set_fs_pwd(current->fs, namespace->root, namespace->root->mnt_root);


        set_fs_root(current->fs, namespace->root, namespace->root->m 

该段代码的最后两行便是将 do_kern_mount() 函数中建立的 mnt 和 dentry 信息记录在了当前进程的 fs结构中。

以 上讲了一大堆数据结构的来历,其实最终目的不过是要在内存中建立一颗 VFS 目录树而已,更确切地说,init_mount_tree() 这个函数为 VFS 建立了根目录”/”,而一旦有了根,那么这棵数就可以发展壮大,比如可以通过系统调用 sys_mkdir在这棵树上建立新的叶子节点等,所以系统设计者又将 rootfs 文件系统挂载到了这棵树的根目录上。关于 rootfs这个文件系统,读者如果看一下前面图 2 中它的file_system_type 结构,会发现它的一个成员函数指针 read_super指向的是 ramfs_read_super,单从这个函数名称中的ramfs,读者大概能猜测出这个文件所涉及的文件操作都是针对内存中的数据对象,事实上也的确如此。从另一个角度而言,因为 VFS本身就是内存中的一个数据对象,所以在其上的操作仅限于内存,那也是非常合乎逻辑的事。在接下来的章节中,我们会用一个具体的例子来讨论如何利用rootfs所提供的函树为 VFS 增加一个新的目录节点。

VFS中各目录的主要用途是为以后挂载文件系统提供挂载点。所以真正的文件操作还是要通过挂载后的文件系统提供的功能接口来进行。

在2.6中,

init_mount_tree()(fs/namespace.c)

   |____do_kern_mount(“rootfs”,0,”rootfs”,0)(fs/super.c)

         |____vfs_kern_mount(“rootfs”)(fs/super.c)

                 |___mnt=alloc_vfsmnt(“rootfs”)(分配mnt结构)

                 |___get_sb(fs_type,…,ramfs_fill_super,mnt)

                     (就是调用rootfs_get_sb(),get_sb_nodev(),ramfs_fill_super()分配sb块)

                         |___inode=ramfs_get_inode(sb,…)(分配inode)

                          |___root=d_alloc_root(inode)(分配root结构,填充’/’目录)

注意:start_kernel()–>…–>kernel_thread(“init”,***)–>init()(在运行内核init前,根文件系统已经建立起来了)–>do_basic_setup();(调用rootfs_initcall()由cpio档填充文件系统内容,并作为根文件系统)–>如果在文件系统中发现了/init文件,交给init处理,否则调用prepare_namespace()继续装载其它文件系统,并mount到之前的rootfs上,并作为根文件系统和运行其上的/init)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/188053.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • mfc窗口置顶_mfc treecontrol

    mfc窗口置顶_mfc treecontrol版权声明:www.gudianxiaoshuo.com原创文章版权–古典小说网         WM_CONTEXTMENU消息用来响应鼠标右键消息,它响应的不是鼠标右键按下消息,也不是鼠标右键弹起消息而是鼠标右键依次按下弹起后触发的消息。          因此,若程序还响应了鼠标右键按下的消息OnRButtonDown,且在此响应函数中有模特对话

    2022年10月17日
  • spss分析方法聚类分析_变量聚类分析

    spss分析方法聚类分析_变量聚类分析聚类分析是根据研究对象的特征,按照一定标准对研究对象进行分类的一种分析方法。下面我们主要从下面四个方面来解说:一、实际应用聚类分析的目标就是在相似的基础上收集数据来分类。聚类源于很多领域,包括数学,计算机科学,统计学,生物学和经济学。在不同的应用领域,很多聚类技术都得到了发展,这些技术方法被用作描述数据,衡量不同数据源间的相似性,以及把数据源分类到不同的簇中。商业上:聚类分析被用来发现不同的客户群,并且通过购买模式刻画不同的客户群的特征。聚类分析是细分市场的有效工具,同时也可用于研究消费者行为

    2022年10月18日
  • SpringBoot2+Netty+WebSocket(netty实现websocket,支持URL参数)

    SpringBoot2+Netty+WebSocket(netty实现websocket,支持URL参数)关于NettyNetty是一个利用Java的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的API的客户端/服务器框架。MAVEN依赖 <dependencies> <!–https://mvnrepository.com/artifact/io.netty/netty-all–> <dependency> <g…

  • 大数的阶乘算法

    大数的阶乘算法用data数组来存放阶乘的每一位数字,首先令第一位的数值为1,位数为1,然后将每次相乘的乘积存回数组,并循环处理每个数组中超过10的数,若数值超过10,则需要进位,将位数加1,原来的数除以10,商数加前一位数的数值后存回前一位数的数组中,再将余数存回原来位数的数组中。例如求5!的值步骤一:1!=1位数1数组内容0     0     0     1步骤二:2!=2

  • MySQL数据查询之多表查询[通俗易懂]

    多表查询多表联合查询#创建部门CREATETABLEIFNOTEXISTSdept(didintnotnullauto_incrementPRIMARYKEY,dna

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号