linux块设备的块大小,Linux块设备子系统的初始化

Linux块设备子系统的初始化

每个块设备用一个块设备结构体进行描述,即block_device结构,它描述一个逻辑上的块设备,可以是一整个磁盘,也可以仅仅是磁盘上的一个分区。其定义为:

---------------------------------------------------------------------

include/linux/fs.h

647

struct block_device {

648

dev_tbd_dev;/* not a kdev_t - it's a search key */

649struct inode *bd_inode;/* will die */

650struct super_block *bd_super;

651intbd_openers;

652

struct mutexbd_mutex;/* open/close mutex */

653struct list_headbd_inodes;

654void *bd_holder;

655intbd_holders;

656

#ifdef CONFIG_SYSFS

657struct list_headbd_holder_list;

658

#endif

659struct block_device *bd_contains;

660unsignedbd_block_size;

661struct hd_struct *bd_part;

662/* number of times partitions within

this device have been opened. */

663unsignedbd_part_count;

664intbd_invalidated;

665struct gendisk *bd_disk;

666struct list_headbd_list;

667/*

668* Private data.You must have bd_claim'ed the block_device

669* to use this. NOTE:bd_claim allows an owner to claim

670* the same device multiple times, the

owner must take special

671* care to not mess up bd_private for

that case.

672*/

673unsigned longbd_private;

674

675/* The counter of freeze processes */

676intbd_fsfreeze_count;

677/* Mutex for freeze */

678struct mutexbd_fsfreeze_mutex;

679

};

---------------------------------------------------------------------

块设备的管理是以一个块设备伪文件系统组织的,每个块设备是这个文件系统上的一个块设备文件,其对应的文件索引节点结构为bdev_inode,其定义为:

---------------------------------------------------------------------

fs/block_dev.c

32 struct bdev_inode {

33struct block_device bdev;

34struct inode vfs_inode;

35 };

---------------------------------------------------------------------

它只是把一个block_device结构和一个虚拟文件系统索引节点结构结合在一起了而已。

块设备子系统的初始化由两个部分组成,第一个是vfs_caches_init()中调用的bdev_cache_init()函数,它初始化bdev伪文件系统。另外一个genhd_device_init()函数。

然后来看的是vfs_caches_init()中调用的bdev_cache_init()函数,它初始化bdev虚拟文件系统。它的定义为:

---------------------------------------------------------------------

fs/block_dev.c

435

static struct kmem_cache * bdev_cachep __read_mostly;

510

void __init bdev_cache_init(void)

511

{

512int err;

513struct vfsmount *bd_mnt;

514

515bdev_cachep =

kmem_cache_create("bdev_cache",

516sizeof(struct bdev_inode),0, (SLAB_HWCACHE_ALIGN|

517SLAB_RECLAIM_ACCOUNT| SLAB_MEM_SPREAD|SLAB_PANIC),

518init_once);

519err =

register_filesystem(&bd_type);

520if (err)

521panic("Cannot register

bdev pseudo-fs");

522bd_mnt = kern_mount(&bd_type);

523if (IS_ERR(bd_mnt))

524panic("Cannot create bdev

pseudo-fs");

525/*

526* This vfsmount structure is only

used to obtain the

527* blockdev_superblock, so tell

kmemleak not to report it.

528*/

529kmemleak_not_leak(bd_mnt);

530blockdev_superblock =

bd_mnt->mnt_sb;/* For writeback */

531

}

---------------------------------------------------------------------

这个函数操作如下:

1、调用kmem_cache_create(),来创建struct bdev_inode结构类型的slab缓冲区,并由静态变量bdev_cachep来引用这个缓冲区。而这里值得注意的就是kmem_cache_create()形参ctor的实参为函数init_once()函数,这个函数用于在申请的内存返回给申请者之前来对分配的结构进行一定的初始化。这个函数的定义为:

---------------------------------------------------------------------

fs/block_dev.c

452

static void init_once(void *foo)

453

{

454struct bdev_inode *ei = (struct

bdev_inode *) foo;

455struct block_device *bdev =

&ei->bdev;

456

457memset(bdev, 0, sizeof(*bdev));

458mutex_init(&bdev->bd_mutex);

459INIT_LIST_HEAD(&bdev->bd_inodes);

460INIT_LIST_HEAD(&bdev->bd_list);

461

#ifdef CONFIG_SYSFS

462INIT_LIST_HEAD(&bdev->bd_holder_list);

463

#endif

464inode_init_once(&ei->vfs_inode);

465/* Initialize mutex for freeze. */

466mutex_init(&bdev->bd_fsfreeze_mutex);

467

}

---------------------------------------------------------------------

主要就是初始化struct bdev_inode结构vfs_inode成员的各个字段。

2、调用register_filesystem(&bd_type)来向系统注册bdev伪文件系统。

3、调用kern_mount(&bd_type)挂载bdev伪文件系统。bdev伪文件系统类型定义如下:

---------------------------------------------------------------------

fs/block_dev.c

502

static struct file_system_type bd_type = {

503.name= "bdev",

504.get_sb= bd_get_sb,

505.kill_sb= kill_anon_super,

506

};

---------------------------------------------------------------------

一个文件系统的许许多多的特性正是通过super_block对象实例的一个个字段的特定值来向我们展示的,所以,在这里我们最为关心的还是bd_get_sb()方法,正是它为文件系统创建super_block对象的。

调用环境嘛,同样是在vfs_kern_mount()函数中,调用alloc_vfsmnt(name)分配了vfsmount对象,初始化了vfsmount的mnt_devname字段,对于bdev来说,也就是"bdev"了,初始化了mnt_flags为MNT_INTERNAL,还有其他各种字段。然后就是调用type->get_sb(type,

flags, name, data, mnt)来创建super_block对象,其实也就是bd_get_sb()函数,对于bdev来说参数type为bd_type,flags为MS_KERNMOUNT,name为"bdev",data为NULL,mnt则为刚刚创建的vfsmount对象。

接着来看bd_get_sb()函数的定义:

---------------------------------------------------------------------

fs/block_dev.c

488

static const struct super_operations bdev_sops = {

489.statfs = simple_statfs,

490.alloc_inode = bdev_alloc_inode,

491.destroy_inode = bdev_destroy_inode,

492.drop_inode = generic_delete_inode,

493.clear_inode = bdev_clear_inode,

494

};

495

496

static int bd_get_sb(struct file_system_type *fs_type,

497int flags, const char *dev_name, void

*data, struct vfsmount *mnt)

498

{

499return get_sb_pseudo(fs_type,

"bdev:", &bdev_sops, 0x62646576, mnt);

500

}

---------------------------------------------------------------------

很简单,只是对于get_sb_pseudo()函数的封装而已。get_sb_pseudo()函数定义为:

---------------------------------------------------------------------

fs/libfs.c

204

/*

205* Common helper for pseudo-filesystems

(sockfs, pipefs, bdev - stuff that

206* will never be mountable)

207

*/

208

int get_sb_pseudo(struct file_system_type *fs_type, char *name,

209const struct super_operations *ops,

unsigned long magic,

210struct vfsmount *mnt)

211

{

212struct super_block *s = sget(fs_type,

NULL, set_anon_super, NULL);

213struct dentry *dentry;

214struct inode *root;

215struct qstr d_name = {.name = name,

.len = strlen(name)};

216

217if (IS_ERR(s))

218return PTR_ERR(s);

219

220s->s_flags = MS_NOUSER;

221s->s_maxbytes = MAX_LFS_FILESIZE;

222s->s_blocksize = PAGE_SIZE;

223s->s_blocksize_bits = PAGE_SHIFT;

224s->s_magic = magic;

225s->s_op = ops ? ops :

&simple_super_operations;

226s->s_time_gran = 1;

227root = new_inode(s);

228if (!root)

229goto Enomem;

230/*

231* since this is the first inode, make

it number 1. New inodes created

232* after this must take care not to

collide with it (by passing

233* max_reserved of 1 to iunique).

234*/

235root->i_ino = 1;

236root->i_mode = S_IFDIR | S_IRUSR |

S_IWUSR;

237root->i_atime = root->i_mtime =

root->i_ctime = CURRENT_TIME;

238dentry = d_alloc(NULL, &d_name);

239if (!dentry) {

240iput(root);

241goto Enomem;

242}

243dentry->d_sb = s;

244dentry->d_parent = dentry;

245d_instantiate(dentry, root);

246s->s_root = dentry;

247s->s_flags |= MS_ACTIVE;

248simple_set_mnt(mnt, s);

249return 0;

250

251

Enomem:

252deactivate_locked_super(s);

253return -ENOMEM;

254

}

---------------------------------------------------------------------

通过这个函数,我们来看bdev这类伪文件系统的一些特性。这个函数完成如下操作:

a.调用sget(fs_type, NULL,

set_anon_super, NULL)来获得一个super_block对象。sget()函数本质上会分配一个super_block对象,然后初始化其s_type指向文件系统类型,将文件系统类型的name字段拷贝到super_block的s_id字段,将超级块对象添加进系统超级块对象链表super_blocks和文件系统超级快链表type->fs_supers中。用合适的方式设置超级块的s_dev字段:主设备号为0次设备号不同于其他已挂载的的特殊文件系统的次设备号。

b.设置超级块s_flags为MS_NOUSER,说明文件系统不会被安装;

设置s_maxbytes字段为MAX_LFS_FILESIZE,最大文件的大小;

设置s_blocksize,也就是块大小,为PAGE_SIZE,一个页的大小;

设置s_blocksize_bits为PAGE_SHIFT;

设置s_magic,文件系统对应的幻数,为0x62646576;

设置s_op,也就是super_block的操作表,为bdev_sops;

设置s_time_gran,访问时间等的粒度,以ns为单位的值,为1。

c.调用new_inode(s)来为文件系统的根创建inode节点。new_inode(s)定义为:

---------------------------------------------------------------------

fs/block_dev.c

647

struct inode *new_inode(struct super_block *sb)

648

{

649/*

650* On a 32bit, non LFS stat() call,

glibc will generate an

651* EOVERFLOW error if st_ino won't fit

in target struct field.

652* Use 32bit counter here to attempt

to avoid that.

653*/

654static unsigned int last_ino;

655struct inode *inode;

656

657spin_lock_prefetch(&inode_lock);

658

659inode = alloc_inode(sb);

660if (inode) {

661spin_lock(&inode_lock);

662__inode_add_to_lists(sb, NULL,

inode);

663inode->i_ino = ++last_ino;

664inode->i_state = 0;

665spin_unlock(&inode_lock);

666}

667return inode;

668

}

669

EXPORT_SYMBOL(new_inode);

---------------------------------------------------------------------

new_inode()函数调用完成如下操作:

(1).调用alloc_inode(sb)函数来分配inode,而它本质上会调用sb->s_op->alloc_inode(sb)方法来分配inode,也就是bdev_alloc_inode()函数。而bdev_alloc_inode()只是在bdev_cachep缓存中分配bdev_inode,并将其vfs_inode成员的地址返回而已。然后将inode初始化。

(2).调用__inode_add_to_lists(sb,

NULL, inode)将inode添加进inode_in_use和超级块的所有inode链表sb->s_inodes中。

(3).设置inode->i_ino为++last_ino,last_ino是一个静态变量,所有调用new_inode()的函数共用同一个inode号空间,所以这个分配肯定是无效的。设置inode->i_state为0。

(3).返回inode地址。

d.设置root->i_ino为1,即跟inode的索引节点号为1。设置root->i_mode为S_IFDIR | S_IRUSR | S_IWUSR,更新root->i_atime、root->i_mtime、root->i_ctime为当前时间CURRENT_TIME。

e.调用d_alloc(NULL,

&d_name),从目录项缓存中分配dentry并初始化其d_name字段。

f.初始化dentry->d_sb指向super_block对象,初始化dentry->d_parent为其自身。

g.调用d_instantiate(dentry,

root)将目录项和inode联系起来,主要就是将dentry(利用d_alias字段)添加进inode的dentry链表inode->i_dentry,并使dentry的d_inode字段指向inode。

h.设置超级块的s_root指向dentry。设置s_flags的MS_ACTIVE表示,超级块为活跃的。

i.调用simple_set_mnt(mnt,

s)将vfsmount和super_block联系起来,也就是使mnt->mnt_sb指向sb,使mnt->mnt_root指向sb->s_root。

4、vfsmount结构仅仅被用来获得blockdev_superblock,所以调用函数kmemleak_not_leak(bd_mnt)来告诉kmemleak不要报告它。

5、使全局变量blockdev_superblock指向创建的super_block对象。

整个内核里,只有两个地方使用了这个blockdev_superblock,一个是sb_is_blkdev_sb函数:

---------------------------------------------------------------------

fs/internal.h

23 static inline int

sb_is_blkdev_sb(struct super_block *sb)

24 {

25return sb == blockdev_superblock;

26 }

---------------------------------------------------------------------

另外一个地方,便是fs/block_dev.c, line 556的bdget(dev_t dev)函数,实在是耐人回味啊。

接着来看block子系统初始化的这第二部分。genhd_device_init()函数定义为:

---------------------------------------------------------------------

block/genhd.c

792

static int __init genhd_device_init(void)

793

{

794int error;

795

796block_class.dev_kobj =

sysfs_dev_block_kobj;

797error = class_register(&block_class);

798if (unlikely(error))

799return error;

800bdev_map = kobj_map_init(base_probe,

&block_class_lock);

801blk_dev_init();

802

803register_blkdev(BLOCK_EXT_MAJOR,

"blkext");

804

805

#ifndef CONFIG_SYSFS_DEPRECATED

806/* create top-level block dir */

807block_depr =

kobject_create_and_add("block", NULL);

808

#endif

809return 0;

810

}

811

812

subsys_initcall(genhd_device_init);

1009

struct class block_class = {

1010.name= "block",

1011

};

---------------------------------------------------------------------

这个函数完成如下操作:

1、设置类block_class的dev_kobj为sysfs_dev_block_kobj,在前面初始化Driver Model的driver_init()函数里,我们看到它调用了devices_init(),而正是在devices_init()创建了一个以dev_kobj为父kobject的kobject。

2、调用class_register(&block_class)来向系统注册“block”类。来仔细的看下这个注册类的宏,以对类的本质有更多的了解。class_register()定义为:

---------------------------------------------------------------------

include/linux/device.h

224

#define class_register(class)\

225

({\

226static struct lock_class_key

__key;\

227__class_register(class,

&__key);\

228

})

---------------------------------------------------------------------

这个宏主要还是调用__class_register(class, &__key)来完成任务。而__class_register()定义为:

---------------------------------------------------------------------

driver/base/class.c

int

__class_register(struct class *cls, struct lock_class_key *key)

{

struct class_private *cp;

int error;

pr_debug("device class '%s':

registering\n", cls->name);

cp = kzalloc(sizeof(*cp), GFP_KERNEL);

if (!cp)

return -ENOMEM;

klist_init(&cp->class_devices,

klist_class_dev_get, klist_class_dev_put);

INIT_LIST_HEAD(&cp->class_interfaces);

kset_init(&cp->class_dirs);

__mutex_init(&cp->class_mutex,

"struct class mutex", key);

error =

kobject_set_name(&cp->class_subsys.kobj, "%s", cls->name);

if (error) {

kfree(cp);

return error;

}

/* set the default /sys/dev directory for devices

of this class */

if (!cls->dev_kobj)

cls->dev_kobj = sysfs_dev_char_kobj;

#if

defined(CONFIG_SYSFS_DEPRECATED) && defined(CONFIG_BLOCK)

/* let the block class directory show up in

the root of sysfs */

if (cls != &block_class)

cp->class_subsys.kobj.kset =

class_kset;

#else

cp->class_subsys.kobj.kset = class_kset;

#endif

cp->class_subsys.kobj.ktype =

&class_ktype;

cp->class = cls;

cls->p = cp;

error =

kset_register(&cp->class_subsys);

if (error) {

kfree(cp);

return error;

}

error = add_class_attrs(class_get(cls));

class_put(cls);

return error;

}

EXPORT_SYMBOL_GPL(__class_register);

---------------------------------------------------------------------

我们最为关心的还是,注册的类是如何同我们之前了解到的Driver Model初始化过程中初始化的那些数据结构相联系的。这个函数完成的操作如下:

a.调用kzalloc(sizeof(*cp),

GFP_KERNEL)来为一个class_private结构分配对象。这个结构定义如下:

---------------------------------------------------------------------

drivers/base/base.h

55 struct class_private {

56struct kset class_subsys;

57struct klist class_devices;

58struct list_head class_interfaces;

59struct kset class_dirs;

60struct mutex class_mutex;

61struct class *class;

62 };

---------------------------------------------------------------------

class结构体driver core部分用于保存私有数据的结构。各个字段的意义:

class_subsys

–定义这个class的kset结构体,是主要的kobject。

class_devices

–与这个class相联系的设备的表。

class_interfaces

–与这个class相联系的class_interfaces链表。

class_dirs

–与这个class相关的用于虚拟设备的"glue"目录

class_mutex

–用于保护chidren,devices,和接口链表的互斥体

class

–指向这个结构相关的class的指针

b.调用klist_init(&cp->class_devices,

klist_class_dev_get, klist_class_dev_put)来初始化一个klist结构,其定义为:

---------------------------------------------------------------------

lib/klist.c

85 void klist_init(struct

klist *k, void (*get)(struct klist_node *),

86 void (*put)(struct klist_node

*))

87 {

88INIT_LIST_HEAD(&k->k_list);

89spin_lock_init(&k->k_lock);

90k->get = get;

91k->put = put;

92 }

---------------------------------------------------------------------

c.初始化cp->class_interfaces,调用kset_init(&cp->class_dirs)来初始化cp->class_dirs,初始化类互斥体cp->class_mutex,为cp->class_subsys.kobj设置名字。

d.cls->dev_kobj为NULL的情况下,会被设置为sysfs_dev_char_kobj。

如果cls不为block_class,设置cp->class_subsys.kobj.kset为class_kset,在函数driver_init曾调用classes_init()函数来创建kset,并由class_kset来引用这个kset。

若为block_class,则使cp->class_subsys.kobj.kset为空,也就意味着class的kobject的父kobject及kset均为空,在这种情况下,我们知道,相关的函数是会以sysfs的根sysfs_dirent结构sysfs_root为父sysfs_dirent来为其创建sysfs_dirent的,也就是在sysfs文件系统的根目录下创建目录。

设置cp->class_subsys.kobj.ktype为class_ktype。来看一下class_ktype,以了解class释放的时候内核都执行了哪些操作:

---------------------------------------------------------------------

drivers/base/class.c

50 static void

class_release(struct kobject *kobj)

51 {

52struct

class_private *cp = to_class(kobj);

53struct

class *class = cp->class;

54

55pr_debug("class

'%s': release.\n", class->name);

56

57if

(class->class_release)

58class->class_release(class);

59else

60pr_debug("class '%s' does not have a release() function, "

61"be

careful\n", class->name);

62

63kfree(cp);

64 }

71 static struct kobj_type class_ktype = {

72.sysfs_ops= &class_sysfs_ops,

73.release= class_release,

74 };

---------------------------------------------------------------------

使cp->class指向class对象cls,cls->p指向cp。

e.调用kset_register(&cp->class_subsys)来注册kset。

f.调用add_class_attrs(class_get(cls))来为class添加属性。

这里主要处理的就是class->p->class_subsys。

3、调用kobj_map_init(base_probe,

&block_class_lock)来为块设备子系统创建kobject映射域,由静态变量bdev_map。在前面“Linux字符设备管理”博文中我们曾提到过kobject映射域,在那里,它主要用于通过设备号来来获得对应的cdev结构体。这里的用户也比较相似。

4、调用blk_dev_init()函数,其定义为:

---------------------------------------------------------------------

block/blk-core.c

2503

int __init blk_dev_init(void)

2504

{

2505BUILD_BUG_ON(__REQ_NR_BITS > 8 *

2506sizeof(((struct request

*)0)->cmd_flags));

2507

2508kblockd_workqueue =

create_workqueue("kblockd");

2509if (!kblockd_workqueue)

2510panic("Failed to create

kblockd\n");

2511

2512request_cachep =

kmem_cache_create("blkdev_requests",

2513sizeof(struct request), 0,

SLAB_PANIC, NULL);

2514

2515blk_requestq_cachep =

kmem_cache_create("blkdev_queue",

2516sizeof(struct request_queue), 0,

SLAB_PANIC, NULL);

2517

2518return 0;

2519

}

---------------------------------------------------------------------

这个函数完成三件事:

a.创建一个名为kblockd工作队列,由静态变量

kblockd_workqueue来引用这个工作队列。块设备子系统有许多工作,是不应该占用进程的执行时间的,比如刷新缓存中数据到磁盘,这个操作甚至会使进程进入睡眠状态,还有其他各种复杂的工作,所以要创建专门的工作队列来完成块设备子系统的某些工作。

b.调用kmem_cache_create("blkdev_requests",sizeof(struct

request), 0, SLAB_PANIC, NULL)函数来为请求结构体request创建slab缓存。后面会有对request的更详细说明。

c.调用blk_requestq_cachep

= kmem_cache_create("blkdev_queue", sizeof(struct request_queue), 0,

SLAB_PANIC, NULL)来为请求队列创建请求队列。每个磁盘,物理磁盘,不是分区,会有一个请求队列结构。后面有详细说明。

5、调用register_blkdev(BLOCK_EXT_MAJOR,

"blkext")来注册"blkext"块设备。BLOCK_EXT_MAJOR定义为259。我们来看下register_blkdev()函数,其定义为:

---------------------------------------------------------------------

block/genhd.c

279

int register_blkdev(unsigned int major, const char *name)

280

{

281struct blk_major_name **n, *p;

282int index, ret = 0;

283

284mutex_lock(&block_class_lock);

285

286/* temporary */

287if (major == 0) {

288for (index = ARRAY_SIZE(major_names)-1;

index > 0; index--) {

289if (major_names[index] == NULL)

290

break;

291}

292

293if (index == 0) {

294printk("register_blkdev:

failed to get major for %s\n",

295name);

296ret = -EBUSY;

297goto out;

298}

299major = index;

300ret = major;

301}

302

303p = kmalloc(sizeof(struct blk_major_name),

GFP_KERNEL);

304if (p == NULL) {

305ret = -ENOMEM;

306goto out;

307}

308

309p->major = major;

310strlcpy(p->name, name,

sizeof(p->name));

311p->next = NULL;

312index = major_to_index(major);

313

314for (n = &major_names[index]; *n; n =

&(*n)->next) {

315if ((*n)->major == major)

316break;

317}

318if (!*n)

319*n = p;

320else

321ret = -EBUSY;

322

323if (ret < 0) {

324printk("register_blkdev:

cannot get major %d for %s\n",

325major, name);

326kfree(p);

327}

328

out:

329mutex_unlock(&block_class_lock);

330return ret;

331

}

---------------------------------------------------------------------

这里主要来看major_names的定义:

---------------------------------------------------------------------

block/genhd.c

237

static struct blk_major_name {

238struct blk_major_name *next;

239int major;

240char name[16];

241

} *major_names[BLKDEV_MAJOR_HASH_SIZE];

---------------------------------------------------------------------

看到这儿,应该就全明白了,register_blkdev()和我们在“Linux字符设备管理”中介绍的几个“register”的函数其实差不多,大同小异而已,也同样是申请设备号。

6、如果没有配置CONFIG_SYSFS_DEPRECATED选项的话,会在sysfs顶层创建“block”目录。配置的话,前面我们看到,则是在调用函数class_register(&block_class),即向系统注册“block”类的时候来创建。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux网络设备子系统Linux内核中的一个重要组成部分,它负责处理网络数据的接收和发送。该子系统包含了网络设备驱动程序、网络协议栈以及相关的数据处理功能。 在Linux网络设备子系统中,数据的处理流程可以分为初始化、接收和发送三个主要阶段。初始化阶段主要是对网络设备进行初始化设置,包括网卡的配置以及相关的硬件中断设置。接收阶段是指当网络设备接收到数据包时,通过网络设备驱动程序将数据包传递给协议栈的过程。在这个过程中,数据包会经过一系列的处理,包括校验和计算、数据包解析和路由选择等步骤。发送阶段是指当协议栈需要发送数据包时,通过网络设备驱动程序将数据包传递给网络设备的过程。在这个过程中,数据包会经过一系列的处理,包括数据包封装、路由选择和发送队列管理等步骤。 Linux网络设备子系统的设计目标是提供高性能和可扩展性的网络数据处理能力。它支持多队列网卡,可以实现并行处理多个数据包,提高系统的网络吞吐量。同时,它还提供了丰富的网络资源管理和调优功能,可以根据系统需求进行灵活的配置和优化。 总之,Linux网络设备子系统Linux内核中负责处理网络数据的重要组成部分,通过网络设备驱动程序和协议栈的协同工作,实现了高性能和可扩展的网络数据处理能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值