yd2763132的个人空间 https://www.eechina.com/space-uid-36266.html [收藏] [复制] [RSS]

博客

ramdisk

已有 969 次阅读2011-5-13 22:13 |个人分类:linux

一 什么是RamDisk

Ram:内存,Disk:磁盘,在Linux中可以将一部分内存当作分区来使用,称之为RamDisk。对于一些经常被访问、并且不会被更改的文件,可以将它们通过RamDisk放在内存中,能够明显地提高系统性能。RamDisk工作于虚拟文件系统(VFS)层,不能格式化,但可以创建多个RamDisk。虽然现在硬盘价钱越来越便宜,但对于一些我们想让其访问速度很高的情况下,RamDisk还是很好用的。

如果对计算速度要求很高,可以通过增加内存来实现,使用ramdisk技术。 一个A RamDisk就是把内存假设为一个硬盘驱动器,并且在它的上面存储文件。假设有几个文件要频繁的使用,如果将它们加到内存当中,程序运行速度会大幅度提高,因为内存的读写速度远高于硬盘。划出部分内存提高整体性能,不亚于更换新的CPU。像Web服务器这样的计算机,需要大量读取和交换特定的文件。因此,在Web服务器上建立RamDisk会大大提高网络读取速度。

二 如何使用RamDisk
格式化一个ramdisk并把他加到一个目录上。列出所有可用的ramdisk用“ls -al /dev/ram*”。这就会列出你现有可用的ramdisk。这些ramdisk并不抢夺内存,除非进行格式化的一类操作。这里有一个使用ramdisk的例子。
#dd if=/dev/zero of=initrd.img bs=1K count=8192 //创建一个内存分区
#mkfs.ext2 -F initrd.img //将一个内存空间格式化成ext2文件系统格式
#mkdir /mnt/initrd //建立一个挂载点
#mount -t ext2 -o loop initrd.img /mnt/initrd
//将一个指定大小的文件系统格式挂载到一个文件夹中
#cp /source/rootfs/* /mnt/initrd -a //将根文件系统的目录结构拷贝至挂载点
#unmount /mnt/initrd //取消挂载
#gzip --best -c initrd.img > initrd.img.gz //将根文件系统打包成镜像文件

如果对ramdisk的格式化失败,那就是你的内核不支持ramdisk。其内核配置选项是 CONFIG_BLK_DEV_RAM .
ramdisk的默认大小是 4Mb=4096 blocks. 在你进行mke2fs的时候你可以看到你的ramdisk的大小。mke2fs /dev/ram0 将产生类适于以下的信息:
mke2fs 1.14, 9-Jan-1999 for EXT2 FS 0.5b, 95/08/09
Linux ext2 filesystem format
Filesystem label=
1024 inodes, 4096 blocks
204 blocks (4.98%) reserved for the super user
First data block=1
Block size=1024 (log=0)
Fragment size=1024 (log=0)
1 block group
8192 blocks per group, 8192 fragments per group
1024 inodes per group
执行df -k /dev/ram0 你实际用了多少空间:
>df -k /dev/ram0
Filesystem 1k-blocks Used Available Use% Mounted on
/dev/ram0 3963 13 3746 0% /tmp/ramdisk0
那么麻烦是什么呢?在系统重新启动的时候,将刷新这个区域。不要将任何没有拷贝的数据放在这个区域。如果你对这个目录进行了修改,并且需要保留这些修改,采取一些办法进行备份。

三 改变RamDisks 的大小
要想使用RamDisk你必须或是得到内核的支持或是以模块的形式将他加载到系统中。其中内核的配置选项是 CONFIG_BLK_DEV_RAM . 把ramdisk编译成一个可加载的模块的好处是你可以在加载是重新确定ramdisk的大小。
第一个办法。在lilo.conf文件中加入:
ramdisk_size=10000 (or ramdisk=10000 for old kernels)
这样在你使用lilo命令和重新启动计算机之后,ramdisk的默认大小将会是10M。这是一个/etc/lilo.conf文件的例子:
boot=/dev/hda
map=/boot/map
install=/boot/boot.b
prompt
timeout=50
image=/boot/vmlinuz
label=linux
root=/dev/hda2
read-only
ramdisk_size=10000
确切的说,我只是使用了9M多的空间,文件系统也将占用一定空间。
当你以模块的形式编译ramdisk时,你可以在加载的时候决定ramdisk的大小。这也可以通过修改/etc/conf.modules 的选项设置来做到。
options rd rd_size=10000
或是在命令行中指定参数给ismod:
insmod rd rd_size=10000
以下是介绍如何使用这样的模块的例子:
卸载ramdisk,umount /tmp/ramdisk0 .
卸载模块(再上一节所提到的过程中自动加载), rmmod rd
加载ramdisk模块并且把它的大校设为20M,insmod rd rd_size=20000
创建一个文件系统, mke2fs /dev/ram0
加载ramdisk, mount /dev/ram0 /tmp/ramdisk0

四 RamDisk的优缺点

RamDisk就是将内存模拟为硬盘空间。无论什么时候你使用RamDisk,实际上你是在使用内存而不是硬盘。在这一点上既有优点又有缺点。最基本的,最大的优点是你是在使用内存,你所做的一切都会快一些,因为硬盘的速度较内存慢。最大的缺点是如果你改变了数据库服务器的内容并且重新启动机器时,所做的一切改动都将丢失。

五。ARM中的Ramdisk的使用
Linux启动时,initrd可以在内存中,也可以在 Flash或其它可用的设备上;initrd文件格式为: romfs/Minix/ext2/gzip;相关的函数:drivers/block/rd.c: identify_ramdisk_image ;
加载位置: init/main.c: prepare_namespace;
需要的参数为:initrd_start 和 initrd_end;

ARM如何传递initrd参数:ARM传递initrd参数可以分为两种情况:
1.从外部获取的情况:
arch/arm/kernel/setup.c: 从 TAG参数中获取(tags简单来说内存中一段具有一定格式的标签数据和参数,核心和Loader所共知的一种格式,由loader来构造,由核心来读 取);通过Loader或核心配置的命令行参数中应包含 root=/dev/ram的参数;

2.不从外部获取的情况:
可以在 fix_up 中来设置,就像上面代码中提到的一样:
aster2_fixup(struct machine_desc *desc, struct param_struct *unused,
char **cmdline, struct meminfo *mi)
{
ROOT_DEV = MKDEV(RAMDISK_MAJOR, 0);
setup_ramdisk(1, 0, 0, CONFIG_BLK_DEV_RAM_SIZE);
setup_initrd(0x04200000, 4 * 1024 * 1024);
}

这段代码的效果有几个:
(1)第一行:设置根文件系统为 ramdisk(相当于root=/dev/ram0,这种情况下核心命令行参数可以不需要这一行)
(2) 第二行:加载ramdisk;
(3) 第三行:通过setup_initrd设置initrd的参数,即(虚拟的)起始和结束地址;

Loader向内核传参数通过这样的代码: BOOT_PARAMS(0x04000100); 0x04000100是个非常重要的地址,核心将从这个地址开始分析 tag参数,可以传递的参数包括:物理内存信息(ATAG_MEM)、ramdisk信息(ATAG_RAMDISK已经不再使用),initrd信息 (ATAG_INITRD2),序列号(ATAG_SERIAL),版本号(ATAG_REVISION),命令行参数(ATAG_CMDLINE)等。 Loader可以部份或全部设定这些标记。

传递 initrd 参数的条件是, image文件中包含 initrd文件,即通过 mkimage生成一个核心与initrd混合的image文件,然后用bootm,这个参数就可以传给核心了。核心代码中定义的参数地址 BOOT_PARAMS(0x04000100)与u-boot中的参数地址必须一致。
如果要能加载 initrd 到 ramdisk中,那么 Loader应该将 initrd加载到 (虚)0x04200000位置,让Loader自已传参数给核心

其驱动程序如下:
#include <linux/config.h>
#include <linux/string.h>
#include <linux/slab.h>
#include <asm/atomic.h>
#include <linux/bio.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/devfs_fs_kernel.h>
#include <linux/pagemap.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/buffer_head.h>  /* for invalidate_bdev() */
#include <linux/backing-dev.h>
#include <linux/blkpg.h>
#include <linux/writeback.h>
#include <asm/uaccess.h>  
  static struct gendisk *rd_disks[CONFIG_BLK_DEV_RAM_COUNT];//内存硬盘通用银盘结构数组
  static struct block_device *rd_bdev[CONFIG_BLK_DEV_RAM_COUNT];//内存硬盘块设备结构数组
  static struct request_queue *rd_queue[CONFIG_BLK_DEV_RAM_COUNT];//内存硬盘请求队列
  
  int rd_size = CONFIG_BLK_DEV_RAM_SIZE;//内存硬盘设备大小以1K为单位
  static int rd_blocksize = BLOCK_SIZE; //块大小为1K,不是扇区
  
  //为了使页的缓冲最新需要将非最新的缓冲置零。进行操作的前提是将该页锁定
  static void make_page_uptodate(struct page *page)
  {
        //page->private被设定 #define PagePrivate(page) test_bit(PG_private, &(page)->flags)
        if (page_has_buffers(page)) {
        struct buffer_head *bh = page_buffers(page);//页作为缓冲页,匹配一个缓冲头到page->private
        struct buffer_head *head = bh;
  
        do {
             if (!buffer_uptodate(bh)) {//如果bh没有uptodata
             memset(bh->b_data, 0, bh->b_size);
             set_buffer_uptodate(bh);//uptodata的页对应uptodata的缓冲头}
        } while ((bh = bh->b_this_page) != head);
      } else {//page->private未设定
        memset(page_address(page), 0, PAGE_CACHE_SIZE);//将该缓冲设0
    }
        flush_dcache_page(page);//清除页缓冲
        SetPageUptodate(page);//设置页为可用即BH_Uptodate
  }
  
  static int ramdisk_readpage(struct file *file, struct page *page)//读操作
  {
        if (!PageUptodate(page))//没有Uptodate
        make_page_uptodate(page);//设置Uptodate
        unlock_page(page);
        return 0;
  }
  
  static int ramdisk_prepare_write(struct file *file, struct page *page,
   unsigned offset, unsigned to)
  {
        if (!PageUptodate(page))
        make_page_uptodate(page);//是页面可用
        return 0;
  }
  
  static int ramdisk_commit_write(struct file *file, struct page *page,
   unsigned offset, unsigned to)
  {
        set_page_dirty(page);//设置页尾脏等待线程更新
        return 0;
  }
  
   //写块设备的映射页必须再次脏页以致vm不运行。我们返回AOP_WRITEPAGE_ACTIVATE防止VM
   //一会儿再写该页。实际上页面不需要加入到高速缓存中
  static int ramdisk_writepage(struct page *page, struct writeback_control *wbc)//写
  {
        if (!PageUptodate(page))
        make_page_uptodate(page);
        SetPageDirty(page);
        if (wbc->for_reclaim)
        return AOP_WRITEPAGE_ACTIVATE;
        unlock_page(page);
        return 0;
  }
  
  //回写内存盘页面缓冲
  static int ramdisk_writepages(struct address_space *mapping,
   struct writeback_control *wbc)
  {
        return 0;
  }
  
  //内存盘页面设置dirty
  static int ramdisk_set_page_dirty(struct page *page)
  {
        SetPageDirty(page);
        return 0;
  }
  
  //高速缓存的操作集,供块层操作
  static struct address_space_operations ramdisk_aops = {
        .readpage = ramdisk_readpage,//读
        .prepare_write = ramdisk_prepare_write,//准备写操作
        .commit_write = ramdisk_commit_write,
        .writepage = ramdisk_writepage,//写
        .set_page_dirty = ramdisk_set_page_dirty,//设置页面为脏,等待线程将其刷新到磁盘
        .writepages = ramdisk_writepages,//回写
  };
  
  static int rd_blkdev_pagecache_IO(int rw, struct bio_vec *vec, sector_t sector,
   struct address_space *mapping)
  {
         pgoff_t index = sector >> (PAGE_CACHE_SHIFT - 9);//ramdisk扇区所在页号
         //(PAGE_CACHE_SHIFT - 9)为页面中包含的扇区数对arm为8个扇区
         unsigned int vec_offset = vec->bv_offset;//ramdisk片段页首偏移值
         int offset = (sector << 9) & ~PAGE_CACHE_MASK;//缓存页内偏移量,字节单位
         int size = vec->bv_len;//片段长度
         int err = 0;
  
         do {
                   int count;
                   struct page *page;
                   char *src;//源数据
                   char *dst;//目标数据
  
                   count = PAGE_CACHE_SIZE - offset;
                   if (count > size)//io操作未跨页时
                   count = size;
                   size -= count;
                   //分配一个页面,并进行锁定。即建立一个锁定缓冲页面
                  page = grab_cache_page(mapping, index);
                  if (!page) {
                        err = -ENOMEM;
                        goto out;
                  }
  
           if (!PageUptodate(page))//页面是否为uptodate
           make_page_uptodate(page);//设置缓冲页面为BH_uptodate
  
           index++;//下一页
  
           if (rw == READ) {//将缓存中的数据读取到设备
           src = kmap_atomic(page, KM_USER0) + offset;//将缓存首地址临时映射(非阻塞)
           dst = kmap_atomic(vec->bv_page, KM_USER1) + vec_offset;//将ramdisk设备数据首地址临时映射
         } else {//将设备中的数据写到缓存
           src = kmap_atomic(vec->bv_page, KM_USER0) + vec_offset;
           dst = kmap_atomic(page, KM_USER1) + offset;
       }
           offset = 0;//从下一页起始处开始
           vec_offset += count;
  
           memcpy(dst, src, count);//读写操作
  
           kunmap_atomic(src, KM_USER0);
           kunmap_atomic(dst, KM_USER1);//释放临时映射
  
           if (rw == READ)
               flush_dcache_page(vec->bv_page);//将ramdisk页面对应得缓冲清除
           else
               set_page_dirty(page);//为写时需将缓存页面设置为dirty,以将缓存中数据更新到ramdisk
           unlock_page(page);//解锁
           put_page(page);//释放页指针
       } while (size);
  
          out:
              return err;
     }
  
  static int rd_make_request(request_queue_t *q, struct bio *bio)//处理bio对应的操作,bio对应ramdisk设备
  {
         struct block_device *bdev = bio->bi_bdev;//块io操作的块设备
         struct address_space * mapping = bdev->bd_inode->i_mapping;//设备映射地址值
         sector_t sector = bio->bi_sector;//待传递的一个扇区
         unsigned long len = bio->bi_size >> 9;//总字节大小
         int rw = bio_data_dir(bio);//读写方向
         struct bio_vec *bvec;
         int ret = 0, i;
  
         if (sector + len > get_capacity(bdev->bd_disk))//超出块设备存储范围
              goto fail;
  
         if (rw==READA)
              rw=READ;
  
         bio_for_each_segment(bvec, bio, i) {//查找该bio上所有的bio_vec
              ret |= rd_blkdev_pagecache_IO(rw, bvec, sector, mapping);//处理每一个bio_vec的io操作
              sector += bvec->bv_len >> 9;//下一扇区
         }
         if (ret)
              goto fail;
  
         bio_endio(bio, bio->bi_size, 0);//bio操作结束后处理
         return 0;
    fail:
         bio_io_error(bio, bio->bi_size);//bio操作错误处理
         return 0;
    }
  
  static int rd_ioctl(struct inode *inode, struct file *file,
   unsigned int cmd, unsigned long arg)//块设备io命令
  {
        int error;
        struct block_device *bdev = inode->i_bdev;
  
        if (cmd != BLKFLSBUF)//不是刷新缓冲区命令
             return -ENOTTY;
  
        error = -EBUSY;
        down(&bdev->bd_sem);
        if (bdev->bd_openers <= 2) {//第一次打开该设备时刷新缓冲区
              truncate_inode_pages(bdev->bd_inode->i_mapping, 0);//刷新缓冲区将页面初始为0
        error = 0;
        }
        up(&bdev->bd_sem);
        return error;
      }
  
      //这是用于块设备索引节点的预读信息。索引节点不需要回写
  static struct backing_dev_info rd_backing_dev_info = {
       .ra_pages = 0, /* No readahead */
       .capabilities = BDI_CAP_NO_ACCT_DIRTY | BDI_CAP_NO_WRITEBACK | BDI_CAP_MAP_COPY,
       .unplug_io_fn = default_unplug_io_fn,
  };
  
  //用于顶层的ramdisk设备文件;文件需要页面回写并且他们有助于脏内存
  static struct backing_dev_info rd_file_backing_dev_info = {
       .ra_pages = 0, /* No readahead */
       .capabilities = BDI_CAP_MAP_COPY, /* Does contribute to dirty memory */
       .unplug_io_fn = default_unplug_io_fn,
  };
  
  static int rd_open(struct inode *inode, struct file *filp)
  {
        unsigned unit = iminor(inode);//次设备号
  
        if (rd_bdev[unit] == NULL) {//块文件系统中没有对应的块设备
        struct block_device *bdev = inode->i_bdev;//获取节点对应的块设备
        struct address_space *mapping;
        unsigned bsize;
        gfp_t gfp_mask;
  
        inode = (bdev->bd_inode);
        //在初始化时将新建的块文件系统中节点保存到bdev->bd_inode,此操作即将
        //VFS指向块文件系统索引节点
        rd_bdev[unit] = bdev;
        bdev->bd_openers++;
        bsize = bdev_hardsect_size(bdev);
        bdev->bd_block_size = bsize; //从申请队列读取块大小给新建的块设备
        inode->i_blkbits = blksize_bits(bsize);//节点对应块设备大小位数
        inode->i_size = get_capacity(bdev->bd_disk)<<9;//节点对应块设备总大小
  
        mapping = inode->i_mapping;//i_mapping=&inode->i_data即设备地址映射值
        mapping->a_ops = &ramdisk_aops;//页高速缓存操作集
        mapping->backing_dev_info = &rd_backing_dev_info;//预读缓存信息
        bdev->bd_inode_backing_dev_info = &rd_file_backing_dev_info;//预读块设备信息
  
        gfp_mask = mapping_gfp_mask(mapping);//读取映射地址页面掩码值
        gfp_mask &= ~(__GFP_FS|__GFP_IO);//防止块文件系统死锁
        gfp_mask |= __GFP_HIGH;
        mapping_set_gfp_mask(mapping, gfp_mask);//打开__GFP_HIGH仍可以分配缓冲池
      }
       return 0;
  }
  
  static struct block_device_operations rd_bd_op = {//块设备操作函数集
      .owner = THIS_MODULE,
      .open = rd_open,
      .ioctl = rd_ioctl,
  };
  
  static void __exit rd_cleanup(void)
  {
       int i;
  
       for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++) {
             struct block_device *bdev = rd_bdev[i];
             rd_bdev[i] = NULL;
             if (bdev) {
                invalidate_bdev(bdev, 1);//在释放设备之前,失效所有被保护的块缓冲区
                blkdev_put(bdev);//块设备引用减1
             }
             del_gendisk(rd_disks[i]);//释放设备号 释放通用硬盘
             put_disk(rd_disks[i]);
             blk_cleanup_queue(rd_queue[i]);//释放请求队列结构
        }
        devfs_remove("rd");
        unregister_blkdev(RAMDISK_MAJOR, "ramdisk");
  }
  
  /*
   * This is the registration and initialization section of the RAM disk driver
   */
  static int __init rd_init(void)
  {
        int i;
        int err = -ENOMEM;
  
        //检查块大小是否合适,它必须小于一个内存页面的大小,且要大于512字节,还要是2的N次幂。
        if (rd_blocksize > PAGE_SIZE || rd_blocksize < 512 ||
        (rd_blocksize & (rd_blocksize-1))) {
            printk("RAMDISK: wrong blocksize %d, reverting to defaults\n",
                     rd_blocksize);
            rd_blocksize = BLOCK_SIZE;
        }
  
        for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++) {
            rd_disks[i] = alloc_disk(1);//分配通用硬盘结构初始化rd_disks数组
            if (!rd_disks[i])
                goto out;
        }
  
        if (register_blkdev(RAMDISK_MAJOR, "ramdisk")) {//将主设备号和名称存放到major_names
            err = -EIO;
            goto out;
        }
  
        devfs_mk_dir("rd");//创建文件目录/dev/rd
  
        for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++) {
             struct gendisk *disk = rd_disks[i];
  
             rd_queue[i] = blk_alloc_queue(GFP_KERNEL);//从request_cachep池中分配一个申请队列对象
             if (!rd_queue[i])
                  goto out_queue;
  
             blk_queue_make_request(rd_queue[i], &rd_make_request);
             //将rd_make_request生成请求函数与给队列以初始化
             blk_queue_hardsect_size(rd_queue[i], rd_blocksize);//设置块为1K包含两个扇区
  
             disk->major = RAMDISK_MAJOR;
             disk->first_minor = i;//获取次设备号
             disk->fops = &rd_bd_op;//块设备操作集
             disk->queue = rd_queue[i];//对应的申请队列
            disk->flags |= GENHD_FL_SUPPRESS_PARTITION_INFO;//不需要分区信息出现在 /proc/partitions
  
             sprintf(disk->disk_name, "ram%d", i);
             sprintf(disk->devfs_name, "rd/%d", i);
             set_capacity(disk, rd_size * 2);//设置每个通用磁盘拥有2个块
             add_disk(rd_disks[i]);//注册设备号 通用硬盘注册 注册请求队列结构
        }
  
        /* rd_size is given in kB */
        printk("RAMDISK driver initialized: "
        "%d RAM disks of %dK size %d blocksize\n",
        CONFIG_BLK_DEV_RAM_COUNT, rd_size, rd_blocksize);
  
        return 0;
  out_queue:
        unregister_blkdev(RAMDISK_MAJOR, "ramdisk");//注销块设备
  out:
        while (i--) {
           put_disk(rd_disks[i]);
           blk_cleanup_queue(rd_queue[i]);//释放request_cachep池中申请队列对象
        }
        return err;
     }
  
  module_init(rd_init);
  module_exit(rd_cleanup);
  
  /* options - nonmodular */
  #ifndef MODULE //未定义模式
  static int __init ramdisk_size(char *str)
  {
        rd_size = simple_strtol(str,NULL,0);
        return 1;
  }
  static int __init ramdisk_size2(char *str) /* kludge */
  {
        return ramdisk_size(str);
  }
  static int __init ramdisk_blocksize(char *str)
  {
        rd_blocksize = simple_strtol(str,NULL,0);
        return 1;
  }
  __setup("ramdisk=", ramdisk_size);
  __setup("ramdisk_size=", ramdisk_size2);
  __setup("ramdisk_blocksize=", ramdisk_blocksize);
  #endif
  
  /* options - modular */
  module_param(rd_size, int, 0);
  MODULE_PARM_DESC(rd_size, "Size of each RAM disk in kbytes.");
  module_param(rd_blocksize, int, 0);
  MODULE_PARM_DESC(rd_blocksize, "Blocksize of each RAM disk in bytes.");
  MODULE_ALIAS_BLOCKDEV_MAJOR(RAMDISK_MAJOR);
  
  MODULE_LICENSE("GPL");

关于页高速缓存的操作过程如下:
1.readpage方法:首先一个address_space对象和一个偏移量会被传递过来,用于在页高速缓存中收索想要的数据如:page=find_get_page(mapping,index)如果搜索的页没在高速缓存中,那么内核将分配一个新的页面后将其加入到页高速缓存中。
如:cached_page=page_cache_alloc_cold(mapping);//从缓存池中分配一个页面
error=add_to_page_cache_lru(cached_page,mapping,index,GFP_KERNEL);//将到映射地址与新页匹配起来
最后,需要的数据从磁盘被读入,再被加入到页高速缓存,然后返回给用户:
error=mapping->a_ops->readpage(file,page),即应用程序是调用readpage来读取缓存页面间接读取ramdisk块.

2.写操作和读操作有些不同。当页被修改了VM会调用setpagedirty将缓存页设为脏,当线程唤醒后将调用writepage把缓存页写入块设备中。对特定文件的写操作比较复杂:
page=__grab_cache_page(mapping,index,&cached_page,&lru_pvec)在页高速缓存中收索需要的页,若无则分配;下一步,调用prepare_write创建一个写请求;接着调用filemap_copy_from_user(page,offset,buf,bytes)将数据被从用户空间复制到内核缓冲;最后通过commit_write函数将数据写入到磁盘。

路过

鸡蛋

鲜花

握手

雷人

评论 (0 个评论)

facelist

您需要登录后才可以评论 登录 | 立即注册

关于我们  -  服务条款  -  使用指南  -  站点地图  -  友情链接  -  联系我们
电子工程网 © 版权所有   京ICP备16069177号 | 京公网安备11010502021702
返回顶部