根文件系统的概念

根文件系统是控制权从linux内核转移到用户空间的一个桥梁。linux内核就类似于一个黑匣子,只向用户提供各种功能的接口,但是功能的具体实现不可见,用户程序通过对这些功能接口的不同整合实现不同的功能需求。以用户的角度来说,应用程序调用内核的接口实现不同的功能,此时系统的控制权在用户手中,但是实际上却是先有内核的初始化提供这些接口,用户才可以使用这些接口的,也就是系统的控制权最初应该属于内核。那么控制权是如何从内核转交到用户的呢?通过调用init程序实现,而一般把存在init程序的文件系统称之为根文件系统。

文件系统是基于物理存储设备至上的一种机制,用于存储空间的管理,并维护文件内容与磁盘单元之间的对应关系,便于对文件内容的访问。由前面所述,init程序存储在文件系统之中,如果需要访问init程序必须能够识别对应文件系统的格式(通过挂载实现),而文件系统又建立在物理存储设备之上,所以需要物理存储设备的驱动程序已准备就绪。

文件系统的挂载需要提供挂载点(挂载目录),linux内核在初始化时会初始化一个虚拟的“/”目录用于根文件系统的挂载,其初始化过程如下:

start_kernelvfs_caches_init()mnt_init()init_rootfs()register_filesystem(&rootfs_fs_type)                     //注册虚拟的rootfs文件系统init_mount_tree()                                              //创建“/”目录bdev_cache_init()chrdev_init()rest_init()kernel_thread(kernel_init, NULL, CLONE_FS)kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

内核资料分享:Linux内核源码技术学习路线+视频教程代码资料https://link.zhihu.com/?target=https%3A//docs.qq.com/doc/DUGZVQk1qWVBHTEl3

内核学习:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂是不是学完操作系统原理后觉得纸上谈兵不过瘾?是不是面对浩若烟海的Linux内核源代码迷失在代码的海洋里不知所措?这门课可以带您用理论结合实践的方法一步一步抓住Linux内核最核心的部分代码,理解Linux操作系统运行的基本过程及涉及的核心机制。https://ke.qq.com/course/4032547?flowToken=1044374

initramfs和initrd

若init程序存在的物理磁盘设备在内核访问时已准备就绪,内核便可以直接挂载根文件系统并运行init程序,实现kernel空间到用户空间的跳转。但是用户程序可能根据不同的需要存储在诸如IDE、SCSI、USB等多种介质当中,如果将所有的驱动程序都编译进内核,将导致内核异常臃肿,因此,为了使内核适应不同的存储介质,同时不将非必要的驱动程序编译进内核,可使用一种过渡根文件系统。过渡根文件系统与linux内核存放在同一个存储设备中,因此可以直接挂载,而此时该文件系统中的init程序只是加载实际根文件系统的驱动和其他的一些初始化工作,待初始化完成,该init程序会挂载实际的根文件系统,并再次跳转到实际根文件系统中执行实际的init程序。

过渡的根文件系统根据是否直接编译进内核分为initramfs和initrd,而initrd根据文件系统的打包格式又分为cpio-initrd和image-initrd,通过cpio打包的文件系统可以直接释放到“/”,而无需挂载过程,initramfs也是cpio的打包格式。

根文件系统的整体挂载流程如下图所示:

首先,内核根据配置利用过渡根文件系统或默认设置初始化“/”目录;然后检测初始化的“/”目录中是否存在指定应用程序(默认为init程序),如果存在,则直接执行该init程序;否则,根据配置挂载实际根文件系统。与之对应的函数流程为:

kernel_initkernel_init_freeable()do_basic_setup()do_initcalls()do_initcall_level(level)
[1]                 do_one_initcallif (!ramdisk_execute_command)                                        //如果没有定义rdinit参数,则默认执行rootfs中的/init程序ramdisk_execute_command = "/init";[2]        sys_access((const char __user *) ramdisk_execute_command, 0)        //查看rootfs是否有/init程序,若包含则直接运行,否则运行prepare_namespaceramdisk_execute_command = NULL;                                   //并将ramdisk_execute_command置为NULL
[3]         prepare_namespace()//此时已经处于实际根文件系统的目录下if (ramdisk_execute_command) {                                            //首先尝试运行rdinit=指定的程序run_init_process(ramdisk_execute_command);[4]  if (execute_command)                                                    //再尝试运行init=指定的程序run_init_process(execute_command)if (!try_to_run_init_process("/sbin/init") ||                          //如果没有rdinit= 和 init= 指定的程序,则查找如下的程序运行!try_to_run_init_process("/etc/init")  ||!try_to_run_init_process("/bin/init")  ||!try_to_run_init_process("/bin/sh"))

【1】初始化“/”目录

内核初始化时会依次调用处于.init段的初始化函数,其中与根文件系统相关的初始化函数为default_rootfs和populate_rootfs,两个函数根据内核配置项的选择决定是否会被运行:

Makefile:obj-y                           += noinitramfs.oobj-$(CONFIG_BLK_DEV_INITRD)  += initramfs.omounts-$(CONFIG_BLK_DEV_INITRD) += do_mounts_initrd.oinitramfs.crootfs_initcall(populate_rootfs);noinitramfs.c:#if !IS_BUILTIN(CONFIG_BLK_DEV_INITRD)rootfs_initcall(default_rootfs);#endif

由上述代码段可知,在初始化阶段,default_rootfs和papulate_rootfs是互斥的,并由CONFIF_BLK_DEV_INITRD决定哪一个函数会在初始化阶段被执行。CONFIF_BLK_DEV_INITRD配置内核对过渡根文件系统的支持,当配置CONFIF_BLK_DEV_INITRD=y时,包含papulate_rootfs的initramfs.c被编译进内核并在初始化时被执行,并对过渡根文件系统进行解析,而相反的包含default_rootfs的noinitramfs虽然被包含进内核,但是却由于#if语句不会被执行。相反,当CONFIF_BLK_DEV_INITRD不被配置时,default_rootfs会在初始化时被执行,而papulate_rootfs则不会被运行。default_rootfs只是在“/”目录下初始相关节点即完成初始化,如下:

default_rootfssys_mkdir((const char __user __force *) "/dev", 0755)sys_mknod((const char __user __force *) "/dev/console",S_IFCHR | S_IRUSR | S_IWUSR,new_encode_dev(MKDEV(5, 1)));sys_mkdir((const char __user __force *) "/root", 0700);

papulate_rootfs则对过渡根文件系统进行解析,如果过渡根文件系统是cpio格式的initramfs和initrd,则直接解压并释放到“/”目录中,以initramfs或initrd中的内容对“/”进行初始化,而如果是image格式的initrd则需要通过创建虚拟的ramdisk对过渡根文件系统进行挂载才能访问,具体过程如下:

static int __init populate_rootfs(void)
{
[1.1]   if (do_skip_initramfs) {                                                //跳过initrd过程if (initrd_start)free_initrd();return default_rootfs();                                         //只创建包含基本目录结构的default_rootfs}
[1.2]   err = unpack_to_rootfs(__initramfs_start, __initramfs_size);           //首先尝试解压initramfs到rootfs[1.3]   if (initrd_start && !IS_ENABLED(CONFIG_INITRAMFS_FORCE)) {          //如果存在initrd
#ifdef CONFIG_BLK_DEV_RAM                                                   //如果定义了CONFIG_BLK_DEV_RAM,则需要以cpio.initrd和image.initrd两种格式尝试对initrd进行解析
[1.4]       err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start);if (!err) {                                                            //若以cipo.initrd的格式将initrd解压到rootfs成功,则释放initrd占用的空间      free_initrd();goto done;} else {clean_rootfs();unpack_to_rootfs(__initramfs_start, __initramfs_size);           //若以cpio.initrd解压到rootfs失败,则以initramfs unpack将rootfs还原到初始状态}[1.5]        fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 0700);          //以image.initrd的格式对initrd进行解析if (fd >= 0) {                                                     //将initrd内容复制到rootfs的/initrd.image目录下,即ramdisk中ssize_t written = xwrite(fd, (char *)initrd_start, initrd_end - initrd_start);sys_close(fd);                                                     free_initrd();                                                  //释放initrd占用的内存空间}done:
#else                                                                       //如果没有定义CONFIG_BLK_DEV_RAM,则只以cpio.initrd的格式解压initrd到rootfs
[1.4]       err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start);free_initrd();                                                     //释放initrd原来占用的空间
#endif}
}

当内核支持过渡根文件系统时,由于内核并不知道过渡根文件系统以何种形式存在,所以总是试图以initramfs、cpio-initrd到image-initrd的顺序对过渡根文件系统进行解析/挂载。若采用initramfs的格式对过渡根文件系统进行封装,则需要配置CONFIG_INITRAMFS_SOURCE项,指定过渡根文件系统的路径并将该路径下的内容编译进内核。该路径下的内容可以是一个包含所有过渡根文件系统所需文件的目录、或是一个已将内容打包完成的cpio包,亦或是一个指导内核打包相关文件的配置文件。当路径下的内容为一个目录或一个配置文件时,编译时会自动生成cpio格式的initramfs并包含进内核,而cpio包则需要与cpio-initrd一样预先打包完成。编译出来时,initramfs的位置由内核指定,因此内核可直接通过地址__initramfs_start对其进行访问。即使当该项为空时,内核同样会在该位置编译一个空的包。

【1.1】do_skip_initramfs

do_skip_initramfs为一个布尔变量,默认为0,可以通过kernel cmdline中的skip_initramfs参数将其指定为1,如下:

static int __initdata do_skip_initramfs;
static int __init skip_initramfs_param(char *str)
{if (*str)return 0;do_skip_initramfs = 1;return 1;
}
__setup("skip_initramfs", skip_initramfs_param);

当指定该参数为1时,表示内核忽略过渡根文件系统,直接以default_rootfs初始化“/”目录,并直接挂载实际根文件系统。如果此时存在过渡根文件系统,并且过渡根文件系统同样被配置为一个具有实际功能的文件系统(不仅仅起过渡作用),那么可以通过cmdline的skip_initramfs指引内核转移到具有不同功能的用户空间中,如android的recovery模式和boot模式。

【1.2】unpack_to_rootfs(initramfs)

无论内核中是否存在实际的initramfs,内核总是会首先以initramfs的格式试图对其进行解析。

【1.3】initrd解析

initrd与内核一样由uboot加载到内存的指定位置以待处理,此时内核并不知道initrd在内存中的位置,需要通过uboot传递的参数进行指定,在内核中该参数表现为initrd_start变量。当该变量不为0时,表示存在initrd需要解析,而initrd又分为cpio-initrd和image-initrd,cpio-initrd可以直接被解压释放到“/”目录中,而image-initrd则需要借用虚拟设备ramdisk,首先将image-initrd的内容装载到ramdisk中,然后对ramdisk进行挂载,才能对image-initrd中的内容进行访问。

内核是否支持建立虚拟设备ramdisk由CONFIG_BLK_DEV_RAM配置项决定。当配置CONFIG_BLK_DEV_RAM=y时,内核需要分别以cpio-initrd和image-initrd的格式对initrd_start处的内容进行解析(【1.4】和【1.5】),否则只以cpio-initrd的格式进行解析(【1.4】)。

【1.4】unpack_to_rootfs(initrd)

cpio格式的initrd也直接解压并释放到“/”目录中,与initramfs中不同的是,initramfs存在于内核的所占内存,释放到“/”目录之后,原initramfs所占内存不会被释放。而initrd由uboot加载进内存,当initrd被释放到“/”目录之后,initrd所占的内存可以被释放以节约内存空间。

【1.5】image-initrd

若initrd并不是cpio格式,则以image-initrd的格式对其进行解析,此时只是将image-initrd的内容转移到“/initrd.image”,并释放initrd的占用空间,具体的处理过程在prepare_namespace中的load_initrd中。

【2】检查初始化程序是否存在

在无论哪种形式完成“/”目录的初始化之后,检查“/”目录中是否包含初始化init程序,如果有则直接运行该程序并跳转到用户空间。该init程序的具体执行流程由用户决定。如果过渡根文件系统不是最终的根文件系统,用户可通过init程序安装实际根文件系统的驱动并挂载真正的根文件系统。

【3】挂载实际根文件系统

如果“/”目录下不存在初始化程序,则尝试在内核中直接挂载根文件系统,分为两种情况:根文件系统存在于MTD/UBI设备,驱动程序在内核初始化阶段已安装,可直接挂载;根文件系统存在其他存储设备中,以过渡根文件系统安装存储设备的驱动,最后由内核挂载根文件系统。具体过程如下:

void __init prepare_namespace(void)
{if (root_delay) {                                          //root_delay由rootdelay= 参数指定,__setup("rootdelay=", root_delay_setup);ssleep(root_delay);                                     //再挂载根文件系统之前等待一段时间,待驱动程序准备就绪}wait_for_device_probe();                                    //等待device设备初始化完成md_run_setup();dm_run_setup();if (saved_root_name[0]) {                                    //saved_root_name由root=参数指定,__setup("root=", root_dev_setup)root_device_name = saved_root_name;if (!strncmp(root_device_name, "mtd", 3) ||!strncmp(root_device_name, "ubi", 3)) {               //若root=指定的根设备为mtd或ubi分区,则直接挂载根文件系统,不经过initrd
[3.1]       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=指定的根设备为/dev下的节点,则需要通过initrd挂载真正的根文件系统root_device_name += 5;}[2.2] if (initrd_load())                                      //若CONFIG_BLK_DEV_INITRD没有被定义,该函数返回为0goto out;if ((ROOT_DEV == 0) && root_wait) {printk(KERN_INFO "Waiting for root device %s...\n",saved_root_name);while (driver_probe_done() != 0 ||(ROOT_DEV = name_to_dev_t(saved_root_name)) == 0)msleep(5);async_synchronize_full();}is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;if (is_floppy && rd_doload && rd_load_disk(0))ROOT_DEV = Root_RAM0;[3.3] mount_root();                                           //如果不支持initrd,或initrd挂载根文件系统失败,则会直接尝试mount_root挂载实际的根文件系统
out:devtmpfs_mount("dev");                                    sys_mount(".", "/", NULL, MS_MOVE, NULL);                   //将当前目录移动到“/”目录下sys_chroot(".");                                          //将当前目录设置为系统根目录
}

【3.1】如果根文件系统在MTD/UBI设备上,则直接进行挂载。因为其驱动程序为内核必须,所以在内核初始化时便会加载。此时将根文件系统将被挂载到“/root”下:

mount_block_root:mount_block_root(root_device_name, root_mountflags);do_mount_root(name, p, flags, root_mount_data);sys_mount(name, "/root", fs, flags, data);

【3.2】load_initrd

当根文件系统存储在其他存储设备上时,此时需要利用过渡根文件系统对存储设备进行识别并挂载实际的根文件系统,当然前提是使能了initrd和ramdisk。image-initrd格式的数据并不能直接被释放到“/”目录下,而需要通过挂载来访问,而挂载的前提是存储设备驱动已安装,内核将内存中特定的一段区域抽象为一个虚拟的存储设备以供挂载使用,即ramdisk。将image-initrd中的内容加载到ramdisk中,并通过挂载ramdisk,便能够对image-initrd中的内容进行访问了。如果ramdisk被指定为最终的文件系统存储设备,则此时只是将image-initrd的内容加载到ramdisk中,但并不在此对其进行挂载,而在最终的mount_root对其进行挂载。过程如下:

bool __init initrd_load(void)
{if (mount_initrd) {                                                    //mount_initrd默认为1,可通过noinitrd将其指定为0,__setup("noinitrd", no_initrd);create_dev("/dev/ram", Root_RAM0);                                //创建/dev/ram节点if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) { //如果/initrd.image中存在initrd,则将其装载到/dev/ram中,并且判断/dev/ram是否被指定为最终的文件系统sys_unlink("/initrd.image");                                //如果存在image.initrd,且/dev/ram不是指定的根设备,则需要通过initrd挂载实际的根文件系统handle_initrd();                                            //挂载实际根文件系统return true;}}sys_unlink("/initrd.image");                                     //如果initrd不存在,或者/dev/ram即为最终根文件系统,则直接返回,通过mount_root进行挂载return false;
}
————————————————
版权声明:本文为CSDN博主「LeoSoldOut」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_35018427/article/details/105441605

【3.3】挂载实际根文件系统

对于存储在除MTD和UBI的其他存储设备的文件系统,若不存在过渡根文件系统,或ramdisk为实际的根文件系统,则尝试直接对实际根文件系统进行挂载。实际上,如果根文件系统所在的驱动程序已经编译进内核,则不需要initramfs和initrd的过渡,内核会跳到此处直接对其进行挂载。

void __init mount_root(void)
{
#ifdef CONFIG_ROOT_NFSif (ROOT_DEV == Root_NFS) {if (mount_nfs_root())return;ROOT_DEV = Root_FD0;}
#endif
#ifdef CONFIG_BLK_DEV_FDif (MAJOR(ROOT_DEV) == FLOPPY_MAJOR) {if (rd_doload==2) {if (rd_load_disk(1)) {ROOT_DEV = Root_RAM1;root_device_name = NULL;}} elsechange_floppy("root floppy");}
#endif
#ifdef CONFIG_BLOCK{int err = create_dev("/dev/root", ROOT_DEV);     mount_block_root("/dev/root", root_mountflags);}
#endif
}

实际根文件系统挂载到“root”目录下,通过sys_mount的MS_MOVE参数将根文件系统的内容移动到“/”目录下,并且将当前目录切换为系统根目录,即此后从init程序派生的子进程的根目录为当前目录。MS_MOVE参数的意义:

mount --move olddir newdir
mount -M olddir newdir
这个mount动作是将原来在olddir中的所有文件内容,都显示到newdir中。原文件内容的保存位置不变。此时olddir必须是一个挂载点。

【文章福利】小编推荐自己的Linux内核技术交流群:【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)

【4】执行用户初始化程序

此时根文件系统已经挂载完成,则查找根文件系统中的初始化程序并执行,init程序一般由kernel cmdline或dts指定:

     __setup("init=", init_setup);

当“/”目录下不存在init程序时,内核也会从sbin/etc/bin等其他目录下查找默认的可执行程序。

写在后面

实际上是否采用initramfs/initrd由实际需求而定,当实际存储根文件系统的存储设备驱动已安装时,没必要采用initramfs/initrd的过渡。如果需要initramfs/initrd的过渡,则需要根据以上的根文件系统挂载流程判断使用哪种格式的过渡根文件系统,然后对其进行配置。cpio格式的文件系统根据init程序决定该文件系统是过渡文件系统还是本身已是最终的根文件系统,该init程序的内容由用户自行定义。

init程序为用户空间的第一个程序,是其他所有程序的祖先,可以将init程序成为用户空间的内核,为用户空间其他程序的运行提供基础环境的初始化,init程序在多个init管理系统中呈现不同的运行、配置规则,如sysvinit、busybox init、systemd等,如果我们需要在init程序中添加自己对环境的配置,则需要了解这些init系统的运行规则。

一文讲解Linux内核中根文件系统挂载流程相关推荐

  1. 嵌入式烧写Linux内核,嵌入式linux 内核和根文件系统烧写方式简介

    总体来说,嵌入式Linux内核和根文件的引导与PC机差不多. 嵌入式linux内核和根文件系统可以存放在各种可能的存储设备中,一般情况下我们将内核和根文件系统直接烧入到Flash中(包括NOR和NAN ...

  2. Linux内核与根文件系统的关系详解

    Linux内核与根文件系统的关系 开篇题外话:对于Linux初学者来说,这是一个很纠结的问题,但这也是一个很关键的问题! 一语破天机: "尽管内核是 Linux 的核心,但文件却是用户与操作 ...

  3. 嵌入式Linux内核和文件系统,在IXP435上移植嵌入式Linux内核和根文件系统

    简要介绍如何在IXP435上移植嵌入式Linux内核和根文件系统 1.安装交叉编译工具 为什么要先安装交叉编译工具?由于我们的Linux操作系统是安装在嵌入式处理器平台上的,需要在主机上编译出开发板需 ...

  4. 一文讲解Linux 内核网络协议栈-数据从接收到ip层

    [推荐阅读] 一文了解Linux上TCP的几个内核参数调优 一文剖析Linux内核中内存管理 分析linux启动内核源码 此处主要讲的是从数据来到,中断到最终数据包被处理的过程. 0:首先来介绍一下I ...

  5. linux 0.11根文件系统,linux内核与根文件系统之间的关联的理解

    学者 于 2011-10-19 12:46:08发表: 哦,原来还有一个initrd镜像,后缀名为".img",我一直以为只有一个内核镜像呢: 还有引导程序的路径表示与系统不同啊! ...

  6. 嵌入式Linux内核以及根文件系统制作

    内核制作 注意: 我测试的使用nandflsh中bootloader启动,sd卡bootloader启动有问题 制作嵌入式平台使用的Linux内核,方法和制作PC平台的Linux内核基本一致. 清除原 ...

  7. linux程序获取透传参数,Linux内核中TCP SACK处理流程分析

    frankzfz2014-07-27 17:32 demo121:frankzfz您好: 我想请教一个问题,就是将写好的GenericApp项目(没有配置工具),我加入zigbee协议栈的配置工具后还 ...

  8. linux内核 删除文件_Linux内核与根文件系统的关系详解

    Linux内核与根文件系统的关系 开篇题外话:对于Linux初学者来说,这是一个很纠结的问题,但这也是一个很关键的问题! 一语破天机: "尽管内核是 Linux 的核心,但文件却是用户与操作 ...

  9. 嵌入式linux加载引导内核和根文件系统的方法

    总体来说,嵌入式Linux内核和根文件的引导与PC机差不多. 嵌入式linux内核和根文件系统可以存放在各种可能的存储设备中,一般情况下我 们将内核和根文件系统直接烧入到Flash中(包括NOR和NA ...

最新文章

  1. 让Redis在你的系统中发挥更大作用的几点建议
  2. 如何使用DrawerLayout在操作栏/工具栏上方和状态栏下方显示?
  3. 案例 | 撇开虚荣指标,如何策划一场成功的拉新活动?
  4. 【DM8168学习笔记5】EZSDK目录结构
  5. Java线程面试的前50个问题,面向初学者和经验丰富的程序员
  6. 核心技术java基础_JAVA核心技术I---JAVA基础知识(集合set)
  7. Linux环境下创建运行.java文件
  8. 2017互联网技术人薪资报告,你搬的砖够绕地球几圈?
  9. js学习(利用websocket监控服务器)
  10. js 的arguments的一些理解资料
  11. python实现小型搜索引擎设计_Python实现:设计克隆模式
  12. 爱客影院自动采集程序源码v3.5.5
  13. jQuery API .ajaxComplete()
  14. Cent os 7 使用vnc远程访问
  15. 制定小目标的软件APP哪款好
  16. Nginx可视化配置工具—NginxWebUI
  17. CY8C5888AXQ-LP096 CY8C5888AXI-LP096,IC MCU 32BIT
  18. 架构设计---技术栈01
  19. ROS学习【2】-----ubuntu16.04中进行ROS通信编程(话题编程)
  20. Tomcat 多实例安装 发布3个java项目: 8080 8081 8082

热门文章

  1. 中央处理器——硬连线控制器
  2. git push报错 emote: error: GH007
  3. 查询GPU使用情况以及杀死GPU上的多个无用进程
  4. 李宏毅老师《机器学习》课程笔记-4.2 Batch Normalization
  5. 西门子安装未找到ssf文件_三菱、西门子软件安装常见出错解决方法「技成周报40期」...
  6. shell 删除simatic_西门子技术--TIA Portal 软件安装时注册表的删除
  7. 重启计算机怎么一键还原系统还原,小编教你电脑怎么一键还原系统
  8. 计算机网络统考在线试题打不开,电脑上通用考试客户端打不开怎么办
  9. 空间解析几何:圆柱面一般式方程的推导——已知中轴线和半径
  10. 224除以10为什么等于22c语言,C语言 编程练习22