返回顶部
首页 > 资讯 > 后端开发 > Python >Ext3 mount过程分析
  • 143
分享到

Ext3 mount过程分析

过程mount 2023-01-31 07:01:19 143人浏览 安东尼

Python 官方文档:入门教程 => 点击学习

摘要

Ext3 mount原理   本质上,Ext3 mount的过程实际上是inode被替代的过程。例如,/dev/sdb块设备被mount到/mnt/alan目录。那么mount这个过程所需要解决的问题就是将/mnt/alan的dentry目

Ext3 mount原理

 
本质上,Ext3 mount的过程实际上是inode被替代的过程。例如,/dev/sdb块设备被mount到/mnt/alan目录。那么mount这个过程所需要解决的问题就是将/mnt/alan的dentry目录项所指向的inode屏蔽掉,然后重新定位到/dev/sdb所表示的inode索引节点。在没有分析阅读linux vfs mount代码的时候,我的想法是修改dentry所指向的inode索引节点,以此实现mount文件系统的访问。经过分析,在实际的vfs mount实现过程中,还是和我原始的想法略有差别,但是,基本目标还是相同的。
 
Linux VFS的mount过程基本原理如下图所示:
 

 当用户输入”mount /dev/sdb /mnt/alan”命令后,Linux会解析/mnt/alan字符串,并且从Dentry Hash表中获取相关的dentry目录项,然后将该目录项标识成DCACHE_MOUNTED。一旦该dentry被标识成DCACHE_MOUNTED,也就意味着在访问路径上对其进行了屏蔽。

 
在mount /dev/sdb设备上的ext3文件系统时,内核会创建一个该文件系统的superblock对象,并且从/dev/sdb设备上读取所有的superblock信息,初始化该内存对象。Linux内核维护了一个全局superblock对象链表。s_root是superblock对象所维护的dentry目录项,该目录项是该文件系统的根目录。即新mount的文件系统内容都需要通过该根目录进行访问。在mount的过程中,VFS会创建一个非常重要的vfsmount对象,该对象维护了文件系统mount的所有信息。Vfsmount对象通过HASH表进行维护,通过path地址计算HASH值,在这里vfsmount的HASH值通过“/mnt/alan”路径字符串进行计算得到。Vfsmount中的mnt_root指向superblock对象的s_root根目录项。因此,通过/mnt/alan地址可以检索VFSMOUNT Hash Table得到被mount的vfsmount对象,进而得到mnt_root根目录项。
 
例如,/dev/sdb被mount之后,用户想要访问该设备上的一个文件ab.c,假设该文件的地址为:/mnt/alan/ab.c。在打开该文件的时候,首先需要进行path解析。在解析到/mnt/alan的时候,得到/mnt/alan的dentry目录项,并且发现该目录项已经被标识为DCACHE_MOUNTED。之后,会采用/mnt/alan计算HASH值去检索VFSMOUNT Hash Table,得到对应的vfsmount对象,然后采用vfsmount指向的mnt_root目录项替代/mnt/alan原来的dentry,从而实现了dentry和inode的重定向。在新的dentry的基础上,解析程序继续执行,最终得到表示ab.c文件的inode对象。
 
关键数据结构说明
Linux VFS mount所涉及的关键数据结构分析如下。
Vfsmount数据结构
 
Vfsmount数据结构是vfs mount最为重要的数据结构,其维护了一个mount点的所有信息。该数据结构描述如下:
 
  1. struct vfsmount {  
  2.     struct list_head mnt_hash;    
  3.     struct vfsmount *mnt_parent;      
  4.     struct dentry *mnt_mountpoint;    
  5.     struct dentry *mnt_root;      
  6.     struct super_block *mnt_sb;   
  7. #ifdef CONFIG_SMP  
  8.     struct mnt_pcp __percpu *mnt_pcp;  
  9.     atomic_t mnt_longterm;        
  10. #else  
  11.     int mnt_count;  
  12.     int mnt_writers;  
  13. #endif  
  14.     struct list_head mnt_mounts;      
  15.     struct list_head mnt_child;   
  16.     int mnt_flags;  
  17.       
  18. #ifdef CONFIG_FSNOTIFY  
  19.     __u32 mnt_fsnotify_mask;  
  20.     struct hlist_head mnt_fsnotify_marks;  
  21. #endif  
  22.     const char *mnt_devname;      
  23.     struct list_head mnt_list;  
  24.     struct list_head mnt_expire;      
  25.     struct list_head mnt_share;   
  26.     struct list_head mnt_slave_list;  
  27.     struct list_head mnt_slave;   
  28.     struct vfsmount *mnt_master;      
  29.     struct mnt_namespace *mnt_ns;     
  30.     int mnt_id;           
  31.     int mnt_group_id;         
  32.     int mnt_expiry_mark;          
  33.     int mnt_pinned;  
  34.     int mnt_ghosts;  
  35. };  
在Linux内核中不仅存在VFSMOUNT的Hash Table,而且还维护了一棵Mount对象树,通过该mount树,我们可以了解到各个文件系统之间的关系。该mount树描述如下:
 

上图所示为三层mount文件系统树。第一层为系统根目录“/”;第二层有两个mount点,一个为/mnt/a,另一个是/mnt/b;第三层在/mnt/a的基础上又创建了两个mount点,分别为/mnt/a/c和/mnt/a/d。通过mount树,可以对整个系统的mount结构一目了然。
 
Superblock数据结构
 
每个文件系统都会拥有一个superblock对象对其基本信息进行描述。对于像ext3之类的文件系统而言,在磁盘上会持久化存储一份superblock元数据信息,内存的superblock对象由磁盘上的信息初始化。对于像block device 之类的“伪文件系统”而言,在mount的时候也会创建superblock对象,只不过很多信息都是临时生成的,没有持久化信息。Vfs superblock数据结构定义如下:
 
  1. struct super_block {  
  2.     struct list_head    s_list;       
  3.     dev_t           s_dev;        
  4.     unsigned char       s_dirt;  
  5.     unsigned char       s_blocksize_bits;  
  6.     unsigned long       s_blocksize;  
  7.     loff_t          s_maxbytes;   
  8.     struct file_system_type *s_type;  
  9.     const struct super_operations   *s_op;        
  10.     const struct dquot_operations   *dq_op;  
  11.     const struct quotactl_ops   *s_qcop;  
  12.     const struct export_operations *s_export_op;  
  13.     unsigned long       s_flags;  
  14.     unsigned long       s_magic;  
  15.     struct dentry       *s_root;      
  16.     struct rw_semaphore s_umount;  
  17.     struct mutex        s_lock;  
  18.     int         s_count;  
  19.     atomic_t        s_active;  
  20. #ifdef CONFIG_SECURITY  
  21.     void                    *s_security;  
  22. #endif  
  23.     const struct xattr_handler **s_xattr;  
  24.  
  25.     struct list_head    s_inodes;     
  26.     struct hlist_bl_head    s_anon;       
  27. #ifdef CONFIG_SMP  
  28.     struct list_head __percpu *s_files;  
  29. #else  
  30.     struct list_head    s_files;  
  31. #endif  
  32.       
  33.     struct list_head    s_dentry_lru;     
  34.     int         s_nr_dentry_unused;   
  35.  
  36.       
  37.     spinlock_t      s_inode_lru_lock ____cacheline_aligned_in_smp;  
  38.     struct list_head    s_inode_lru;          
  39.     int         s_nr_inodes_unused;   
  40.  
  41.     struct block_device *s_bdev;  
  42.     struct backing_dev_info *s_bdi;  
  43.     struct mtd_info     *s_mtd;  
  44.     struct list_head    s_instances;  
  45.     struct quota_info   s_dquot;      
  46.  
  47.     int         s_frozen;  
  48.     wait_queue_head_t   s_wait_unfrozen;  
  49.  
  50.     char s_id[32];                
  51.     u8 s_uuid[16];                
  52.  
  53.     void            *s_fs_info;   
  54.     fmode_t         s_mode;  
  55.  
  56.       
  57.     u32        s_time_gran;  
  58.  
  59.       
  60.     struct mutex s_vfs_rename_mutex;      
  61.  
  62.       
  63.     char *s_subtype;  
  64.  
  65.       
  66.     char __rcu *s_options;  
  67.     const struct dentry_operations *s_d_op;   
  68.  
  69.       
  70.     int cleancache_poolid;  
  71.  
  72.     struct shrinker s_shrink;     
  73. };  

代码流程分析

 
Linux中实现mount操作需要一定的代码量,下面对Linux VFS Mount代码进行分析说明,整个分析过程按照mount操作函数调用流程进行。代码分析基于Linux-3.2版本。
 
当用户在用户层执行mount命令时,会执行系统调用从用户态陷入linux内核,执行如下函数(namespace.c):
  1. SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name,  
  2.         char __user *, type, unsigned long, flags, void __user *, data)  
  3. {  
  4.     int ret;  
  5.     char *kernel_type;  
  6.     char *kernel_dir;  
  7.     char *kernel_dev;  
  8.     unsigned long data_page;  
  9.       
  10.     ret = copy_mount_string(type, &kernel_type);  
  11.     if (ret < 0)  
  12.         Goto out_type;  
  13.       
  14.     kernel_dir = getname(dir_name);  
  15.     if (IS_ERR(kernel_dir)) {  
  16.         ret = PTR_ERR(kernel_dir);  
  17.         goto out_dir;  
  18.     }  
  19.       
  20.     ret = copy_mount_string(dev_name, &kernel_dev);  
  21.     if (ret < 0)  
  22.         goto out_dev;  
  23.       
  24.     ret = copy_mount_options(data, &data_page);  
  25.     if (ret < 0)  
  26.         goto out_data;  
  27.       
  28.     ret = do_mount(kernel_dev, kernel_dir, kernel_type, flags,  
  29.         (void *) data_page);  
  30.  
  31.     free_page(data_page);  
  32. out_data:  
  33.     kfree(kernel_dev);  
  34. out_dev:  
  35.     putname(kernel_dir);  
  36. out_dir:  
  37.     kfree(kernel_type);  
  38. out_type:  
  39.     return ret;  

 

do_mount()函数是mount操作过程中的核心函数,在该函数中,通过mount的目录字符串找到对应的dentry目录项,然后通过do_new_mount()函数完成具体的mount操作。do_mount()函数分析如下:

  1. long do_mount(char *dev_name, char *dir_name, char *type_page,  
  2.           unsigned long flags, void *data_page)  
  3. {  
  4.     struct path path;  
  5.     int retval = 0;  
  6.     int mnt_flags = 0;  
  7.  
  8. 。。。  
  9.  
  10.       
  11.     retval = kern_path(dir_name, LOOKUP_FOLLOW, &path);  
  12.     if (retval)  
  13.         return retval;  
  14.  
  15.     。。。  
  16.  
  17.       
  18.     if (flags & MS_NOSUID)  
  19.         mnt_flags |= MNT_NOSUID;  
  20.     if (flags & MS_NODEV)  
  21.         mnt_flags |= MNT_NODEV;  
  22.     if (flags & MS_NOEXEC)  
  23.         mnt_flags |= MNT_NOEXEC;  
  24.     if (flags & MS_NOATIME)  
  25.         mnt_flags |= MNT_NOATIME;  
  26.     if (flags & MS_NODIRATIME)  
  27.         mnt_flags |= MNT_NODIRATIME;  
  28.     if (flags & MS_STRICTATIME)  
  29.         mnt_flags &= ~(MNT_RELATIME | MNT_NOATIME);  
  30.     if (flags & MS_RDONLY)  
  31.         mnt_flags |= MNT_READONLY;  
  32.  
  33.     flags &= ~(MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_ACTIVE | MS_BORN |  
  34.            MS_NOATIME | MS_NODIRATIME | MS_RELATIME| MS_KERNMOUNT |  
  35.            MS_STRICTATIME);  
  36.  
  37.       
  38.     if (flags & MS_REMOUNT)  
  39.         retval = do_remount(&path, flags & ~MS_REMOUNT, mnt_flags,  
  40.                     data_page);  
  41.     else if (flags & MS_BIND)  
  42.         retval = do_loopback(&path, dev_name, flags & MS_REC);  
  43.     else if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE))  
  44.         retval = do_change_type(&path, flags);  
  45.     else if (flags & MS_MOVE)  
  46.         retval = do_move_mount(&path, dev_name);  
  47.     else  
  48.           
  49.         retval = do_new_mount(&path, type_page, flags, mnt_flags,  
  50.                       dev_name, data_page);  
  51. dput_out:  
  52.     path_put(&path);  
  53.     return retval;  

do_new_mount()函数主要分成两大部分:第一部分建立vfsmount对象和superblock对象,必要时从设备上获取文件系统元数据;第二部分将vfsmount对象加入到mount树和Hash Table中,并且将原来的dentry对象无效掉。do_new_mount函数说明如下:

  1. static int do_new_mount(struct path *path, char *type, int flags,  
  2.             int mnt_flags, char *name, void *data)  
  3. {  
  4.     struct vfsmount *mnt;  
  5.     int err;  
  6.  
  7.     。。。  
  8.  
  9.       
  10.     mnt = do_kern_mount(type, flags, name, data);  
  11.     if (IS_ERR(mnt))  
  12.         return PTR_ERR(mnt);  
  13.       
  14.     err = do_add_mount(mnt, path, mnt_flags);  
  15.     if (err)  
  16.         mntput(mnt);  
  17.     return err;  

do_new_mount()中的第一步调用do_kern_mount()函数,该函数的主干调用路径如下:

do_kern_mount--> vfs_kern_mount--> mount_fs
在mount_fs()函数中会调用特定文件系统的mount方法,如果mount是ext3文件系统,那么在mount_fs函数中最终会调用ext3的mount方法。Ext3的mount方法定义在super.c文件中:
  1. static struct file_system_type ext3_fs_type = {  
  2.     .owner      = THIS_MODULE,  
  3.     .name       = "ext3",  
  4.     .mount      = ext3_mount,         
  5.     .kill_sb    = kill_block_super,  
  6.     .fs_flags   = FS_REQUIRES_DEV,  
  7. }; 

Ext3 mount函数主干调用路径为:ext3_mount--> mount_bdev。Mount_bdev()函数主要完成superblock对象的内存初始化,并且加入到全局superblock链表中。该函数说明如下:

  1. struct dentry *mount_bdev(struct file_system_type *fs_type,  
  2.     int flags, const char *dev_name, void *data,  
  3.     int (*fill_super)(struct super_block *, void *, int))  
  4. {  
  5.     struct block_device *bdev;  
  6.     struct super_block *s;  
  7.     fmode_t mode = FMODE_READ | FMODE_EXCL;  
  8.     int error = 0;  
  9.  
  10.     if (!(flags & MS_RDONLY))  
  11.         mode |= FMODE_WRITE;  
  12.       
  13.     bdev = blkdev_get_by_path(dev_name, mode, fs_type);  
  14.     if (IS_ERR(bdev))  
  15.         return ERR_CAST(bdev);  
  16.  
  17.       
  18.     mutex_lock(&bdev->bd_fsfreeze_mutex);  
  19.     if (bdev->bd_fsfreeze_count > 0) {  
  20.         mutex_unlock(&bdev->bd_fsfreeze_mutex);  
  21.         error = -EBUSY;  
  22.         goto error_bdev;  
  23.     }  
  24.       
  25.     s = sget(fs_type, test_bdev_super, set_bdev_super, bdev);  
  26.     mutex_unlock(&bdev->bd_fsfreeze_mutex);  
  27.     if (IS_ERR(s))  
  28.         goto error_s;  
  29.  
  30.     if (s->s_root) {  
  31.           
  32.         if ((flags ^ s->s_flags) & MS_RDONLY) {  
  33.             deactivate_locked_super(s);  
  34.             error = -EBUSY;  
  35.             goto error_bdev;  
  36.         }  
  37.  
  38.           
  39.         up_write(&s->s_umount);  
  40.         blkdev_put(bdev, mode);  
  41.         down_write(&s->s_umount);  
  42.     } else {  
  43.           
  44.         char b[BDEVNAME_SIZE];  
  45.  
  46.         s->s_flags = flags | MS_NOSEC;  
  47.         s->s_mode = mode;  
  48.         strlcpy(s->s_id, bdevname(bdev, b), sizeof(s->s_id));  
  49.         sb_set_blocksize(s, block_size(bdev));  
  50.           
  51.         error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);  
  52.         if (error) {  
  53.             deactivate_locked_super(s);  
  54.             goto error;  
  55.         }  
  56.  
  57.         s->s_flags |= MS_ACTIVE;  
  58.         bdev->bd_super = s;  
  59.     }  
  60.       
  61.     return dget(s->s_root);  
  62.  
  63. error_s:  
  64.     error = PTR_ERR(s);  
  65. error_bdev:  
  66.     blkdev_put(bdev, mode);  
  67. error:  
  68.     return ERR_PTR(error);  

 

do_new_mount()函数的第二步是将创建的vfsmount对象加入到mount树和VFSMOUNT Hash Table中,并且将老的dentry目录项无效掉。该过程主干函数调用过程如下所示:

do_new_mount--> do_add_mount--> graft_tree--> attach_recursive_mnt
attach_recursive_mnt()函数完成第二步过程的主要操作。至此,文件系统的mount操作已经完成。Mount完成之后,如果用户想要访问新mount文件系统中的文件,那么需要在path解析过程中重定位dentry,该过程主要在follow_managed()函数中完成。在该函数中会判断一个dentry是否已经被标识成DCACHE_MOUNTED,如果该标志位已经被设置,那么通过VFSMOUNT Hash Table可以重定位dentry。
 
如有不对之处,敬请指出更正(tl_wzj@yahoo.com.cn)。

--结束END--

本文标题: Ext3 mount过程分析

本文链接: https://lsjlt.com/news/190901.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Ext3 mount过程分析
    Ext3 mount原理   本质上,Ext3 mount的过程实际上是inode被替代的过程。例如,/dev/sdb块设备被mount到/mnt/alan目录。那么mount这个过程所需要解决的问题就是将/mnt/alan的dentry目...
    99+
    2023-01-31
    过程 mount
  • PostgreSQL中vacuum过程分析
    本篇内容主要讲解“PostgreSQL中vacuum过程分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“PostgreSQL中vacuum过程分析”吧!一、数...
    99+
    2024-04-02
  • MySQL的查询过程分析
    关系型数据库管理系统查询处理一般分为4个阶段: 见下图 怎么验证这几个阶段对应在MySQL的关系呢? 这里实验的数据库版本:5.6.16-64.2-56 OS:CentOS release 6.5 Ker...
    99+
    2024-04-02
  • Oracle存储过程Procedure分析
    本篇内容介绍了“Oracle存储过程Procedure分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!我...
    99+
    2024-04-02
  • 分析PHP转Golang的过程
    随着互联网技术的不断发展,编程语言也在不断地涌现出来,不同的编程语言有不同的特点,以适应不同领域的需求。PHP 和 Golang 是两种比较常见的编程语言,它们各自有着自己的特点和优缺点,下面我们将对它们进行比较,并介绍 PHP 转 Gol...
    99+
    2023-05-14
  • Android Service的启动过程分析
    Android Service的启动过程分析 刚开始学习Service的时候以为它是一个线程的封装,也可以执行耗时操作。其实不然,Service是运行在主线程的。直接执行耗时操...
    99+
    2022-06-06
    android service service 启动 Android
  • MYSQL 连接登录过程分析
      MYSQL 连接登录过程分析 在较新MYSQL版本中,默认没有开启线程池的功能,每个客户连接在服务器进程中都拥有自己的线程。 当客户端(应用)连接到M...
    99+
    2024-04-02
  • function.procedure函数下的过程分析
    function.procedure函数下的过程分析,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。 问题:PKG_...
    99+
    2024-04-02
  • mysql存储过程举例分析
    这篇文章主要讲解了“mysql存储过程举例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“mysql存储过程举例分析”吧!(1).格式MySQL存储过程创...
    99+
    2024-04-02
  • MySQL中Bug发现过程分析
    本篇内容介绍了“MySQL中Bug发现过程分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!使用的是PXC...
    99+
    2024-04-02
  • MySQL中AFTER_SYNC/AFTER_COMMIT的过程分析
    本篇内容主要讲解“MySQL中AFTER_SYNC/AFTER_COMMIT的过程分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“MySQL中AFTER_SY...
    99+
    2024-04-02
  • MySQL 崩溃恢复过程分析
    天有不测风云,数据库有旦夕祸福。 前面写 Redo 日志的文章介绍过,数据库正常运行时,Redo 日志就是个累赘。 现在,终于到了 Redo 日志扬眉吐气,大显身手的时候了。 本文我们一起来看看,My...
    99+
    2023-09-16
    mysql 数据库 php java 程序员
  • SpringBoot执行过程实例分析
    今天小编给大家分享一下SpringBoot执行过程实例分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。每个Spring B...
    99+
    2023-07-02
  • crash定位过程实例分析
    这篇“crash定位过程实例分析”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“crash定位过程实例分析”文章吧。一、问题从...
    99+
    2023-06-04
  • PostgreSQL存储过程源码分析
    这篇文章主要介绍了PostgreSQL存储过程源码分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇PostgreSQL存储过程源码分析文章都会有所收获,下面我们一起来看看吧。游标PL/pgSQL 游标允许我们...
    99+
    2023-07-05
  • Android Service绑定过程完整分析
    通常我们使用Service都要和它通信,当想要与Service通信的时候,那么Service要处于绑定状态的。然后客户端可以拿到一个Binder与服务端进行通信,这个过程是很自...
    99+
    2022-06-06
    android service service Android
  • Android Service启动过程完整分析
    刚开始学习Service的时候以为它是一个线程的封装,也可以执行耗时操作。其实不然,Service是运行在主线程的。直接执行耗时操作是会阻塞主线程的。长时间就直接ANR了。 我...
    99+
    2022-06-06
    android service service Android
  • mysql存储过程的案例分析
    这篇文章主要介绍mysql存储过程的案例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!1、什么是存储过程为以后的使用而保存的一条或多条MySQL语句的集合。存储过程思想上就是数据...
    99+
    2024-04-02
  • PostgreSQL中vacuum过程HeapTupleSatisfiesVacuum函数分析
    本篇内容主要讲解“PostgreSQL中vacuum过程HeapTupleSatisfiesVacuum函数分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“P...
    99+
    2024-04-02
  • 分析PostgreSQL创建函数的过程
    本篇内容主要讲解“分析PostgreSQL创建函数的过程”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“分析PostgreSQL创建函数的过程”吧!一、数据结构F...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作