DPDK初始化

rte_eal_init
│
├──rte_cpu_is_supported:检查cpu是否支持
│
├──rte_atomic32_test_and_set:操作静态局部变量run_once确保函数只执行一次
│
├──pthread_self() 获取主线程的线程ID,只是用于打印
│
├──eal_reset_internal_config:初始化内部全局配置变量struct internal_config
│
├──eal_log_level_parse 解析命令行参数,只处理“--log-level”
│   │
│   └──eal_parse_log_level 填充struct rte_logs rte_logs全局log控制变量
│
├──rte_eal_cpu_init:赋值全局结构struct lcore_config
│   │
│   ├──rte_eal_get_configuration:获取全局配置结构struct rte_config,初始指向全局变量early_mem_config
│   │
│   ├──eal_cpu_detected
│   │   │
│   │   └──如果文件“/sys/devices/system/cpu/cpu%u/topology/core_id”存在,则存在此编号的cpu
│   │
│   ├──eal_cpu_core_id
│   │   │
│   │   └──eal_parse_sysfs_value:读取文件“/sys/devices/system/cpu/cpu%u/topology/core_id”,
│   │         获取core number onsocket for this lcore
│   │
│   ├──eal_cpu_socket_id
│   │   │
│   │   └──如果目录“/sys/devices/system/node/node%u/cpu%u”存在,得到physical socket id for this lcore
│   │
│   └──计数得到number of available logical cores,保存在struct rte_config.lcore_count中
│
├──eal_parse_args:解析处理EAL的命令行参数,赋值struct internal_config 结构的相关字段
│
├──eal_plugins_init (EAL的“-d”选项可以指定需要载入的动态链接库)
│   │
│   ├──如果全局变量 default_solib_dir 所指的Default path of external loadable drivers有效
│   │   │
│   │   └──eal_plugin_add
│   │         │
│   │         ├──malloc一个struct shared_driver结构,拷贝路径名称
│   │         │
│   │         └──将此struct shared_driver结构挂载到List of external loadable drivers中 solib_list
│   │
│   └──遍历solib_list上挂载的所有struct shared_driver结构
│         │
│         ├──如果当前struct shared_driver结构所保存的路径是目录
│         │   │
│         │   └──eal_plugindir_init
│         │         │
│         │         └──对目录中的每个普通文件,执行eal_plugin_add
│         │                (将文件挂载到Listof external loadable drivers的尾部,待接下来的遍历循环进行处理)
│         │
│         └──否则,是共享库的情况
│               │
│               └──调用dlopen打开指定的动态链接库
│
├──eal_option_device_parse (EAL的“-b、-w、--vdev”选项可以指定需要解析支持的设备 eal_option_device_add)
│   │
│   ├──如果全局变量链表有挂载指定支持的设备的话devopt_list
│   │   │
│   │   └──rte_devargs_parse 解析后,存放到全局链表 devargs_list 里面
│   │         │
│   │         ├──calloc一个struct rte_devargs 结构
│   │         │
│   │         ├──rte_devargs_parse读取系统所有设备,如果能找到表示属于合法设备
│   │         │
│   │         └──将此struct rte_devargs结构挂载到devargs_list
│   │
│   └──解析后的struct device_option从devopt_list删除
│
├──rte_config_init
│   │
│   ├──主进程的情况(RTE_PROC_PRIMARY) rte_config.mem_config这块内存,在初始化巨页之前,使用本地全局变量&early_mem_config临时使用
│   │   │      在rte_eal_config_create里面,rte_config.mem_config的内存改为配置文件/var/run/config,用来主从进程共享使用.
│   │   │
│   │   ├──rte_eal_config_create
│   │   │      │
│   │   │      ├──eal_runtime_config_path:获取runtime配置文件路径,如“/var/run/config”
│   │   │      │
│   │   │      ├──如果EAL配置了巨页映射的虚拟地址的话,在这里把它保存到 rte_mem_cfg_addr 变量里面
│   │   │      │
│   │   │      ├──打开文件,上锁,mmap映射文件到内存
│   │   │      │
│   │   │      ├──将early configuration structure(全局变量early_mem_config)拷贝到此内存中,
│   │   │      │   rte_config.mem_config指向这块内存,early_mem_config在巨页没初始化之前,用来当做早期简单配置使用的.
│   │   │      │
│   │   │      └──映射地址保存在rte_config.mem_config->mem_cfg_addr中,用于从应用将来映射到相同的地址
│   │   │
│   │   └──eal_update_mem_config 更新rte_config里面的rte_mem_config legacy_mem和single_file_segments变量,让从进程可以读取这个信息eal_update_internal_config
│   │   
│   └──从进程的情况(RTE_PROC_SECONDARY) 从进程先mmap /var/run/config,然后读取主进程的rte_mem_cfg_addr地址,最后在重新mmap /var/run/config,
│         │   这样做的目的是保证主从进程的rte_mem_cfg_addr虚拟地址和物理地址都是完全一样的.
│         │
│         ├──rte_eal_config_attach
│         │   │
│         │   ├──eal_runtime_config_path
│         │   │
│         │   ├──打开文件,mmap映射文件到内存
│         │   │
│         │   └──rte_config.mem_config指向映射的内存
│         │
│         ├──rte_eal_mcfg_wait_complete
│         │   │
│         │   └──如果struct rte_mem_config结构的magic成员没有被写成RTE_MAGIC,就继续等待
│         │          (主应用ready后会将struct rte_mem_config结构的magic成员写成RTE_MAGIC)
│         │
│         ├──rte_eal_config_reattach
│         │      │
│         │      ├──从前面mmap映射文件中获取主应用mmap的映射地址(即rte_config.mem_config->mem_cfg_addr)
│         │      │
│         │      ├──munmap解除先前的映射
│         │      │
│         │      ├──指定主应用映射地址重新执行mmap映射,如果最终映射地址和指定映射地址不一致,则出错退出
│         │      │
│         │      └──将rte_config.mem_config指向重新映射的内存
│         │
│         └──eal_update_internal_config读取主进程初始化的内存配置,保存到本地的internal_config.legacy_mem和internal_config.single_file_segments 
│
├──rte_eal_intr_init 初始化中断系统,dpdk把中断注册到epoll里面,通过epoll来处理发送的中断事件
│   │
│   ├──初始化global interrupt source head (struct rte_intr_source_list intr_sources)
│   │
│   ├──创建pipe,主要是用来唤醒eal_intr_thread_main重建中断列表,一般在用户注册了中断事件后,会write pipe,epoll read后重建中断列表
│   │
│   └──创建线程来等待处理中断,线程执行函数为eal_intr_thread_main
│      │
│      └──线程运行循环
│            │
│            ├──epoll_create:创建epoll文件描述符
│            │
│            ├──epoll_ctl:把前面创建的the read end of the pipe,添加到epoll wait list中
│            │
│            ├──遍历以global interrupt source head为头部的struct rte_intr_source结构链表
│            │   │
│            │   ├──如果当前struct rte_intr_source结构没有挂载的callback函数,跳过
│            │   │
│            │   └──把所有的uio device file descriptor,添加到epoll wait list中
│            │
│            ├──eal_intr_handle_interrupts
│            │   │
│            │   └──循环
│            │         │
│            │         ├──epoll_wait:wait for an I/O event on an epoll file descriptor
│            │         │
│            │         ├──eal_intr_process_interrupts
│            │         │   │
│            │         │   └──遍历所有发生的I/O event
│            │         │         │
│            │         │         ├──如果the read end of the pipe可用,执行read操作,函数返回
│            │         │         │    (此时会rebuild the wait list)
│            │         │         │
│            │         │         ├──遍历struct rte_intr_source结构链表,查找当前I/O event对应的structrte_intr_source结构
│            │         │         │
│            │         │         ├──根据interrupt handle type(uio/alarm/…),确定需要读取的字节长度
│            │         │         │
│            │         │         ├──执行文件read操作
│            │         │         │
│            │         │         └──如果read数据成功,执行当前struct rte_intr_source结构挂载的所有callback函数
│            │         │
│            │         └──调用eal_intr_process_interrupts返回负数,本次中断处理结束返回
│            │
│            └──关闭epoll文件描述符
│
├──rte_mp_channel_init 创建主从进程通信socket接口,使用AF_UNIX socket 类型,处理任务 mp_handle
│   │
│   ├──初始化socket路径 /var/run/mp_socket
│   │
│   ├──打开目录/var/run 加锁
│   │
│   ├──创建socket,主进程使用/var/run/mp_socket,从进程使用/var/run/mp_socket_getpid_rte_rdtsc, fd使用全局变量保存mp_fd
│   │
│   └──创建mp处理进程 mp_handle
│
├──rte_mp_dev_hotplug_init 注册主从进程通信的消息处理回调钩子,该动作是承接rte_mp_channel_init,
│   │                      rte_mp_channel_init初始化了处理任务mp_handle-->process_msg-->查找hotplug action
│   │
│   ├──主进程流程
│   │   │
│   │   ├── malloc 一个struct action_entry
│   │   │
│   │   ├── 初始化action_entry的action_name(EAL_DEV_MP_ACTION_REQUEST)和action(handle_secondary_request)
│   │   │
│   │   └── 增加action_entry到全局链表中action_entry_list
│   │
│   └──从进程流程
│       │
│       ├── malloc 一个struct action_entry
│       │
│       ├── 初始化action_entry的action_name(EAL_DEV_MP_ACTION_REQUEST)和action(handle_primary_request)
│       │
│       └── 增加action_entry到全局链表中action_entry_list
│
├──rte_bus_scan 扫描所有已注册设备的总线,主要有pci bus,还有其他厂家提供的总线,如DPAA bus,Intel FPGA bus
│       │
│       ├──循环获取设备总线链表 rte_bus_list
│       │
│       └──对每一个注册到的设备总线最新scan
│           │
│           ├──设备总线注册使用RTE_REGISTER_BUS
│           │
│           └──设备bus注册需要提供scan,probe,find_device,plug,unplug,parse,dev_iterate等函数实现,pci总线是 rte_pci_bus
│
├──初始化系统巨页 
│   │
│   ├──主进程 eal_hugepage_info_init:赋值struct hugepage_info数组(internal_config.hugepage_info)
│   │   │
│   │   ├──hugepage_info_init 读取系统巨页的信息,填充到internal_config.hugepage_info数组里面如果没有巨页数量可用,这里返回失败,
│   │   │    否则,hugepage_info数组里面按照巨页大小排序存储hugepage_info[0]>hugepage_info[1]>hugepage_info[2],这里是巨页大小不是巨页数量
│   │   │     │
│   │   │     ├── 打开系统的/sys/kernel/mm/hugepages目录,里面存在系统当前使用的巨页大小对应的信息
│   │   │     │
│   │   │     ├── 遍历/sys/kernel/mm/hugepages子目录信息,如果子目录以hugepages-开头,则继续处理,否则不处理,
│   │   │     │   一般有hugepages-2048kB或者hugepages-1048576kB两种存在
│   │   │     │
│   │   │     ├── /sys/kernel/mm/hugepages子目录数量不能超过dpdk运行的最大数,目前为3个
│   │   │     │
│   │   │     ├── 以数组方式,把系统不同巨页信息按顺序存放在internal_config.hugepage_info[]数组里面
│   │   │     │    │
│   │   │     │    └── get_hugepage_dir 根据系统巨页的大小,获取系统巨页mount路径
│   │   │     │         │
│   │   │     │         ├── 读取系统的/proc/mounts目录,里面存放了用户所有mount信息
│   │   │     │         │
│   │   │     │         ├── 获取默认的巨页大小,说白了就是获取系统默认的巨页大小,一般是2MB, 获取方式,cat /proc/meminfo |grep Hugepagesize
│   │   │     │         │
│   │   │     │         └── 循环判断/proc/mounts所有信息,如果有hugetlbfs的标识,表示是系统的巨页挂载点
│   │   │     │              │ 
│   │   │     │              ├── 一条mount信息包含四个选项,按顺序分别是device、mountpt、fstype、options分别按顺序存放到splitstr指针数组里面
│   │   │     │              │ 
│   │   │     │              ├── 如果用户设置了启动参数--huge-dir选项,就只检查这个目录是否存在
│   │   │     │              │ 
│   │   │     │              └── 查找mount信息里面的options,是否存在"pagesize="选项,如果是,则直接使用该页大小,否则使用默认的页大小
│   │   │     │         
│   │   │     ├── 系统总的可用巨页数量保存到internal_config.num_hugepage_sizes  
│   │   │     │    
│   │   │     └── 到此为止,internal_config里面关于巨页相关的变量基本都初始化了 
│   │   │
│   │   └──如果设置了不共享巨页信息,直接返回,否则下面会创建一个文件,把internal_config.hugepage_info信息存放到eal_hugepage_info_path里面,
│   │      让从进程可以attach,共享同一块内存
│   │       │
│   │       └──create_shared_memory 创建共享文件/var/run/hugepage_info,把internal_config.hugepage_info保存进去,让从进程可以读取
│   │
│   └──从进程 eal_hugepage_info_read
│              │
│              └──读取主进程存放在/var/run/hugepage_info文件里面的信息,读取出来保存到本地的internal_config.hugepage_info
│
├──rte_srand(rte_rdtsc()) 将当前时间作为种子,产生伪随机数序列
│

https://www.cnblogs.com/jiayy/p/dpdk-memory.html

1 dpdk 内存初始化源码解析
入口:
  rte_eal_init(int argc,char ** argv) dpdk 运行环境初始化入口函数
      —— eal_hugepage_info_init 这4个是内存相关的初始化函数
      ——rte_config_init
      ——rte_eal_memory_init
      ——rte_eal_memzone_init
1.1 eal_hugepage_info_init
这个函数比较简单,主要是从 /sys/kernel/mm/hugepages 目录下面读取目录名和文件名,从而获取系统的hugetlbfs文件系统数,
以及每个 hugetlbfs 的大页面数目和每个页面大小,并保存在一个文件里,这个函数,只有主进程会调用。存放在internal_config结构里

2.2 rte_config_init
构造 rte_config 结构
  rte_config_init
    ——rte_eal_config_create 主进程执行
    ——rte_eal_config_attach 从进程执行
rte_eal_config_create 和 rte_eal_config_attach 做的事情比较简单,就是将 /var/run/.config 文件shared 型

mmap 到自己的进程空间的 rte_config.mem_config结构上,这样主进程和从进程都可以访问这块内存

rte_eal_config_attach

1.3 rte_eal_memory_init

 rte_eal_memory_init

    ——rte_eal_hugepage_init   主进程执行,dpdk 内存初始化核心函数

    ——rte_eal_hugepage_attach  从进程执行

rte_eal_hugepage_init  函数分几个步骤:

/*
* Prepare physical memory mapping: fill configuration structure with
* these infos, return 0 on success.
* 1. map N huge pages in separate files in hugetlbfs
* 2. find associated physical addr
* 3. find associated NUMA socket ID
* 4. sort all huge pages by physical address
* 5. remap these N huge pages in the correct order
* 6. unmap the first mapping
* 7. fill memsegs in configuration with contiguous zones
*/

  

函数一开始,将rte_config_init函数获取的配置结构放到本地变量 mcfg 上,然后检查系统是否开启hugetlbfs,如果

不开启,则直接通过系统的malloc函数申请配置需要的内存,然后跳出这个函数。

接下来主要是构建 hugepage 结构的数组 tmp_hp(上图)

下面就是重点了。。

构建hugepage 结构数组分下面几步

首先,循环遍历系统所有的hugetlbfs 文件系统,一般来说,一个系统只会使用一种hugetlbfs ,所以这一层的循环可以认为

没有作用,一种 hugetlbfs 文件系统对应的基础数据包括:页面大小,比如2M,页面数目,比如2K个页面

其次,将特定的hugetlbfs的全部页面映射到本进程,放到本进程的 hugepage 数组管理,这个过程主要由 map_all_hugepages函数完成,

第一次映射的虚拟地址存放在 hugepage结构的 orig_va变量

第三,遍历hugepage数组,找到每个虚拟地址对应的物理地址和所属的物理cpu,将这些信息也记入 hugepage数组,物理地址

记录在hugepage结构的phyaddr变量,物理cpu号记录在 hugepage结构的socket_id变量

第四,跟据物理地址大小对hugepage数组做排序

第五,根据排序结果重新映射,这个也是由函数 map_all_hugepages完成,重新映射后的虚拟地址存放在hugepage结构的final_va变量

第六,将第一次映射关系解除,即将orig_va 变量对应的虚拟地址空间返回给内核

下面看 map_all_hugepages的实现过程

这个函数是复用的,共有两次调用。

对于第一次调用,就是根据hugetlbfs 文件系统的页面数m,构造

m个文件名称并创建文件,每个文件对应一个大页面,然后通过mmap系统调用映射到进程的一块虚拟地址

空间,并将虚拟地址存放在hugepage结构的orig_va地址上。如果该hugetlbfs有1K个页面,最终会在

hugetlbfs 挂载的目录上生成 1K 个文件,这1K 个文件mmap到进程的虚拟地址由进程内部的hugepage数组维护

对于第二次调用,由于hugepage数组已经基于物理地址排序,这些有序的物理地址可能有2种情况,一种是连续的,

另一种是不连续的,这时候的调用会遍历这个hugepage数组,然后统计连续物理地址的最大内存,这个统计有什么好处?

因为第二次的映射需要保证物理内存连续的其虚拟内存也是连续的,在获取了最大连续物理内存大小后,比如是100个页面大小,

会调用 get_virtual_area 函数向内涵申请100个页面大小的虚拟空间,如果成功,说明虚拟地址可以满足,然后循环100次,

每次映射mmap的首个参数就是get_virtual_area函数返回的虚拟地址+i*页面大小,这样,这100个页面的虚拟地址和物理地址

都是连续的,虚拟地址存放到final_va 变量上。

下面看 find_physaddr的实现过程

这个函数的作用就是找到hugepage数组里每个虚拟地址对应的物理地址,并存放到 phyaddr变量上,最终实现由函数

rte_mem_virt2phy(const void * virt)函数实现,其原理相当于页表查找,主要是通过linux的页表文件 /proc/self/pagemap 实现

/proc/self/pagemap 页表文件记录了本进程的页表,即本进程虚拟地址到物理地址的映射关系,主要是通过虚拟地址的前面若干位

定位到物理页框,然后物理页框+虚拟地址偏移构成物理地址,其实现如下

下面看 find_numasocket的实现过程

这个函数的作用是找到hugepage数组里每个虚拟地址对应的物理cpu号,基本原理是通过linux提供的 /proc/self/numa_maps 文件,

/proc/self/numa_maps 文件记录了本 进程的虚拟地址与物理cpu号(多核系统)的对应关系,在遍历的时候将非huge page的虚拟地址

过滤掉,剩下的虚拟地址与hugepage数组里的orig_va 比较,实现如下

sort_by_physaddr 根据hugepage结构的phyaddr 排序,比较简单

unmap_all_hugepages_orig 调用 mumap 系统调用将 hugepage结构的orig_va 虚拟地址返回给内核

上面几步就完成了hugepage数组的构造,现在这个数组对应了某个hugetlbfs系统的大页面,数组的每一个节点是一个

hugepage结构,该结构的phyaddr存放着该页面的物理内存地址,final_va存放着phyaddr映射到进程空间的虚拟地址,

socket_id存放着物理cpu号,如果多个hugepage结构的final_va虚拟地址是连续的,则其 phyaddr物理地址也是连续的。

下面是rte_eal_hugepage_init函数的余下部分,主要分两个方面,一是将hugepage数组里 属于同一个物理cpu,物理内存连续

的多个hugepage 用一层 memseg 结构管理起来。 一个memseg 结构维护的内存必然是同一个物理cpu上的,虚拟地址和物理

地址都连续的内存,最终的memzone 接口是通过操作memseg实现的;2是将 hugepage数组和memseg数组的信息记录到共享文件里,

方便从进程获取;

 

遍历hugepage数组,将物理地址连续的hugepage放到一个memseg结构上,同时将该memseg id 放到 hugepage结构

的 memseg_id 变量上

下面是创建文件 hugepage_info 到共享内存上,然后hugepage数组的信息拷贝到这块共享内存上,并释放hugepage数组,

其他进程通过映射 hugepage_info 文件就可以获取 hugepage数组,从而管理hugepage共享内存

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/191405.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

掌握未来技术趋势,Python编程引领人工智能时代

掌握未来技术趋势,Python编程引领人工智能时代 摘要:Python作为一种高级编程语言,在人工智能领域中扮演着越来越重要的角色。本文将通过介绍Python编程的特点、应用场景及发展前景,展望Python未来的发展趋势,并结合代…

数据结构 栈与队列详解!!

一.栈 关于内存中的栈和数据结构中的栈是不同的,本章着重讲的是数据结构的栈。 这是一张关于栈的表达图。从图中可以看出栈很像是一副卡牌,发牌时只能从上取出,即出栈。 而入栈则是像你出牌后,要把你出的牌压在上一张出的牌上面。…

Linux系统编程学习 NO.9——git、gdb

前言 本篇文章简单介绍了Linux操作系统中两个实用的开发工具git版本控制器和gdb调试器。 git 什么是git? git是一款开源的分布式版本控制软件。它不仅具有网络功能,还是服务端与客户端一体的软件。它可以高效的处理程序项目中的版本管理。它是Linux内…

Windows10下Maven3.9.5安装教程

文章目录 1.下载maven2.安装3.配置系统变量3.1.新建系统变量 MAVEN_HOME3.2.编辑系统变量Path 4.CMD命令测试是否安装成功5.配置maven本地仓库6.配置国内镜像仓库 1.下载maven 官网 https://maven.apache.org/download.cgi 点击下载。 2.安装 解压到指定目录 D:\installSoft…

Git企业开发级讲解(四)

📘北尘_:个人主页 🌎个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上,不忘来时的初心 文章目录 一、理解分⽀二、创建分支三、切换分⽀四、合并分⽀五、删除分⽀六、合并冲突七、分⽀管理策略…

搭建mysql主从错误集合

1 mysqld --verbose --help --log-bin-index/tmp/tmp.Frnt2oibYI mysqld: Cant read dir of /etc/mysql/conf.d/ my.cnf是在/etc/mysql/conf.d/文件夹下,所以挂载的时候不要写/etc/mysql 2 COLLATION utf8_unicode_ci is not valid for CHARACTER SET latin1 配…

大数据研发工程师课前环境搭建

大数据研发工程师课前环境搭建 第一章 VMware Workstation 安装 在Windows的合适的目录来进行安装,如下图 1.1 双击打开 1.2 下一步,接受协议 1.3 选择安装位置 1.4 用户体验设置 1.5 快捷方式 已经准备好安装,点击安装 1.6 安装中 1.7 安装…

【算法每日一练]-图论(保姆级教程 篇5(LCA,最短路,分层图)) #LCA #最短路计数 #社交网络 #飞行路线 # 第二短路

今天讲最短路统计和分层图 目录 题目:LCA 思路: 题目:最短路计数 思路: 题目:社交网络 思路: 题目:飞行路线 思路: 题目:第二短路 思路: 题目&a…

轻量服务器和云服务器的区别,轻量应用服务器和云服务器区别对比

在云计算时代,服务器作为互联网应用的基础设施,扮演着重要的角色。对于个人用户、个人开发者、学生用户和个人站长来说,选择一款适合自己的服务器是一个关键的决策。本文将介绍轻量服务器和标准云服务器的优点和应用场景,帮助读者…

Confluence 快速安装教程

安装jdk yum install -y java-1.8.0-openjdk.x86_64 java -version 安装MySQL mkdir -p /data/mysql/data chmod 777 /data/mysql/datadocker rm -f mysql docker run -d --name mysql \-p 3306:3306 \-e MYSQL_ROOT_PASSWORDfingard1 \-v /data/mysql/data:/var/lib/mysql …

​软考-高级-系统架构设计师教程(清华第2版)【第15章 面向服务架构设计理论与实践(P527~554)-思维导图】​

软考-高级-系统架构设计师教程(清华第2版)【第15章 面向服务架构设计理论与实践(P527~554)-思维导图】 课本里章节里所有蓝色字体的思维导图

视频剪辑技巧:简单步骤,批量剪辑并随机分割视频

随着社交媒体平台的广泛普及和视频制作需求的急剧增加,视频剪辑已经成为了当今社会一项不可或缺的技能。然而,对于许多初学者来说,视频剪辑可能是一项令人望而生畏的复杂任务。可能会面临各种困难,如如何选择合适的软件和硬件、如…