驱动 | Linux | NVMe - 1. 内核驱动
本文总结 NVMe
的 Linux
驱动是如何实现的。
Update: 2022 / 11 / 2
系列文章
驱动 | Linux | NVMe - 1. 内核驱动
- 系列文章
- 总览
- NVMe 命令
- PCI 总线
- 注册和初始化驱动
- 创建 NVMe 块设备
- 硬件层面
- 软件层面
- NVMe 设备的 IO 流程
- DMA
- 参考链接
总览
NVMe (Non-VolatileMemory express)
,是一种建立在 M.2
接口上的类似 AHCI
的一种协议,是专门为闪存类存储设计的协议。
NVMe
具体优势包括:
- 性能有数倍的提升;
- 可降低延迟超过50%;
NVMe PCIe SSD
可提供的IOPs
十倍于高端企业级SATA SSD
;- 自动功耗状态切换和动态能耗管理功能大大降低功耗;
- 支持未来十年技术发展的可扩展能力。
码农该怎么理解?——
- 问:它是一个存储协议,既然是存储协议是不是需要快速的读写?
答:对。 PCIe
才是最快的协议啊,为啥不用PCIe
呢?
答:PCIe
很复杂的。- 问:那我们给
PCIe
穿个马甲,就可以?
答:NVMe
就是给PCIe
穿个马甲。 - 问:
NVMe
是怎么做到的?
答:PCIe
是作文题,NVMe
是选词填空,最后的结果却一样。 - 问:怎么填?填什么?
答:按照这个表格填写,发什么就填什么,总共64
字节,不需要的填0
就行了。
IO Command
|
|||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
appmask | apptag | reftag | dsmgmt | slba | addr | metadata | rsvd | nblocks | control | Flags | Opcode |
Admin Command
|
|||||||||||
rsvd11 | numd | offset | lid | prp2 | prp1 | rsvd1 | command_id | flags | Opcode |
NVMe
是一种 Host
与 SSD
之间通讯的协议,制定了 Host
与 SSD
之间通讯的命令,以及命令如何执行的,它在协议栈中隶属高层,
NVMe
离不开 PCIe
,NVMe SSD
是 PCIe
的 endpoint
。PCIe
是 x86
平台上一种流行的 bus
总线,由于其 Plug and Play
的特性,目前很多外设都通过 PCI Bus
与 Host
通信,甚至不少 CPU
的集成外设都通过 PCI Bus
连接,如 APIC
等。
NVMe SSD
在 PCIe
接口上使用新的标准协议 NVMe
,由大厂 Intel
推出并交由 nvmexpress
组织推广,现在被全球大部分存储企业采纳 1。
NVMe SSD
本身是一个块设备,因此 NVMe
的驱动也是遵循块设备的驱动架构。
本文基于 Linux 4.1.12
版本的内核( 其它版本的内核代码可能略有不同,但不影响理解)通过两部分介绍 NVMe
的驱动程序 2:
- 操作系统如何创建
NVMe
块设备 NVMe
的主要流程,包括读写流程和管理流程等
NVMe 命令
参考这里 1’ 3
NVMe Host
和 NVMe Controller
通过 NVMe Command
进行信息交互。
NVMe Command
是 Host
与 SSD Controller
交流的基本单元,应用的 I/O
请求也要转化成 NVMe Command
。
NVMe Spec
中定义了 NVMe Command
的格式,占用 64
字节。
NVMe Command
分为 Admin Command
和 IO Command
两大类,前者主要是 Host
用于管理和控制 SSD
,后者用于 Host
和 SSD
之间的数据传输。
发送的太快我来不及执行咋办?——
搞两个缓冲区吧:
- 发送缓冲区
SubmissionQueue
(SQ
) - 完成缓冲区
CompletionQueue
(CQ
)
处理完了,我该怎么告诉你呢?——
- 写
Doorbell Register
(DB
)
这个系统结构可以下图表示,
这个 namespace
是什么?——
每个 flash
块就是一个 namaspce
,它有个 id
,叫 namaspce ID
。
NVMe
到 SDD
是怎么玩的?——
举例 Host
需要从 flash
地址 0x02000000
上读取 nblock = 2
的数据,PRP1
给出内存地址是0x10000000
,该怎么操作?
首先我们得组包 nvme_cmd
,这个包为读命令,它包含我们读地址( 0x02000000
)、长度( nblock = 2
)、和读到什么地方( PRP
),然后把这个包扔给 SQ
,写 doorbell
通知控制器来取命令,控制器取出命令来转换为 TLP
包通过 PCIe Memory
方式把 0x02000000
的数据写入到0x10000000
中,然后在 CQ
的尾部写入完成标志,再写 doorbell
告诉控制器我的事干完了。
- 这个命令放在
SQ
里;
- 这个命令放在
Host
通过写SQ
的Tail DB
,通知SSD
来取命令;
SSD
收到通知,去Host
端的SQ
中取指。PCIe
是通过发一个Memory Read TLP
到Host
的SQ
中取命令的;
SSD
执行读命令,把数据从闪存中读到缓存中,然后把数据传给Host
;
SSD
往Host
的CQ
中返回状态;
SSD
采用中断的方式告诉Host
去处理CQ
;
Host
处理相应的CQ
PCI 总线
参考这里 1
在系统启动时,BIOS
会枚举整个 PCI
的总线,之后将扫描到的设备通过 ACPI tables
传给操作系统。当操作系统加载时,PCI Bus
驱动则会根据此信息读取各个 PCI
设备的 Header Config
空间,从 class code
寄存器获得一个特征值。
class code
是 PCI bus
用来选择哪个驱动加载设备的唯一根据。NVMe Spec
定义的 class code
是 010802h
。NVMe SSD
内部的 Controller
PCIe Header
中 class code
都会设置成 010802h
。
所以,需要在驱动中指定 class code
为 010802h
,将 010802h
放入 pci_driver nvme_driver
的 id_table
。之后当nvme_driver
注册到 PCI Bus
后,PCI Bus
就知道这个驱动是给 class code=010802h
的设备使用的。nvme_driver
中有一个 probe
函数,nvme_probe()
,这个函数才是真正加载设备的处理函数。
#define PCI_CLASS_STORAGE_EXPRESS 0x010802static const struct pci_device_id nvme_id_table[] = {……{ PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff) },……};
注册和初始化驱动
参考这里 1
我们知道首先是驱动需要注册到PCI总线。那么nvme_driver是如何注册的呢?
当驱动被加载时就会调用 nvme_init
( drivers/nvme/host/pci.c
4 ) 函数,如下所示,
static int __init nvme_init(void)
{BUILD_BUG_ON(sizeof(struct nvme_create_cq) != 64);BUILD_BUG_ON(sizeof(struct nvme_create_sq) != 64);BUILD_BUG_ON(sizeof(struct nvme_delete_queue) != 64);BUILD_BUG_ON(IRQ_AFFINITY_MAX_SETS < 2);BUILD_BUG_ON(DIV_ROUND_UP(nvme_pci_npages_prp(), NVME_CTRL_PAGE_SIZE) >S8_MAX);return pci_register_driver(&nvme_driver);
}
在这个函数中,调用了 kernel
的函数 pci_register_driver
,注册 nvme_driver
。这样 PCI bus
上就多了一个 pci_driver nvme_driver
。
static struct pci_driver nvme_driver = {.name = "nvme",.id_table = nvme_id_table,.probe = nvme_probe,.remove = nvme_remove,.shutdown = nvme_shutdown,
#ifdef CONFIG_PM_SLEEP.driver = {.pm = &nvme_dev_pm_ops,},
#endif.sriov_configure = pci_sriov_configure_simple,.err_handler = &nvme_err_handler,
};
当读到一个设备的 class code
是 010802h
时,就会调用这个 nvme_driver
结构体的 probe
函数, 也就是说当设备和驱动匹配了之后,驱动的 probe
函数就会被调用,来实现驱动的加载。
Probe
函数主要完成四个工作:
- 映射设备的
BAR
空间到内存虚拟地址空间; - 设置
admin queue
; - 添加
nvme namespace
设备; - 添加
nvme Controller
,提供ioctl
接口。
PCIe
的 Header
空间和 BAR
空间是 PCIe
的关键特性。Header
空间是 PCIe
设备的通有属性,所有的 PCIe Spec
功能和规范都在这里实现;BAR
空间则是设备差异化的具体体现,BAR
空间的定义决定了这个设备是网卡,SSD
还是虚拟设备。BAR
空间是 Host
和 PCIe
设备进行信息交互的重要介质,BAR
空间的数据实际存储在 PCIe
设备上。Host
这边给 PCIe
设备分配的地址资源,并不占用 Host
的内存资源。当读写 BAR
空间时,都需要通过 PCIe
接口(通过PCI TLP
消息)进行实际的数据传输。
接着来看下 nvme_driver
结构体中的 .probe
函数 nvme_probe
,
static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{int node, result = -ENOMEM;struct nvme_dev *dev;unsigned long quirks = id->driver_data;size_t alloc_size;node = dev_to_node(&pdev->dev);if (node == NUMA_NO_NODE)set_dev_node(&pdev->dev, first_memory_node);dev = kzalloc_node(sizeof(*dev), GFP_KERNEL, node);if (!dev)return -ENOMEM;dev->nr_write_queues = write_queues;dev->nr_poll_queues = poll_queues;dev->nr_allocated_queues = nvme_max_io_queues(dev) + 1;dev->queues = kcalloc_node(dev->nr_allocated_queues,sizeof(struct nvme_queue), GFP_KERNEL, node);if (!dev->queues)goto free;dev->dev = get_device(&pdev->dev);pci_set_drvdata(pdev, dev);result = nvme_dev_map(dev);if (result)goto put_pci;INIT_WORK(&dev->ctrl.reset_work, nvme_reset_work);INIT_WORK(&dev->remove_work, nvme_remove_dead_ctrl_work);mutex_init(&dev->shutdown_lock);result = nvme_setup_prp_pools(dev);if (result)goto unmap;quirks |= check_vendor_combination_bug(pdev);if (!noacpi && acpi_storage_d3(&pdev->dev)) {/** Some systems use a bios work around to ask for D3 on* platforms that support kernel managed suspend.*/dev_info(&pdev->dev,"platform quirk: setting simple suspend\n");quirks |= NVME_QUIRK_SIMPLE_SUSPEND;}/** Double check that our mempool alloc size will cover the biggest* command we support.*/alloc_size = nvme_pci_iod_alloc_size();WARN_ON_ONCE(alloc_size > PAGE_SIZE);dev->iod_mempool = mempool_create_node(1, mempool_kmalloc,mempool_kfree,(void *) alloc_size,GFP_KERNEL, node);if (!dev->iod_mempool) {result = -ENOMEM;goto release_pools;}result = nvme_init_ctrl(&dev->ctrl, &pdev->dev, &nvme_pci_ctrl_ops,quirks);if (result)goto release_mempool;dev_info(dev->ctrl.device, "pci function %s\n", dev_name(&pdev->dev));nvme_reset_ctrl(&dev->ctrl);async_schedule(nvme_async_probe, dev);return 0;
nvme_probe
函数会通过 nvme_dev_map
函数 (层层调用之后) 映射设备的 BAR
空间到内核的虚拟地址空间当中, PCI
协议里规定了 PCI
设备的配置空间里有 6
个 32
位的 BAR
寄存器,代表了 PCI
设备上的一段内存空间,可以通过writel
,readl
这类函数直接读写寄存器,并分配设备数据结构 nvme_dev
,队列 nvme_queue
等。
nvme_dev
结构体如下,
/** Represents an NVM Express device. Each nvme_dev is a PCI function.*/
struct nvme_dev {struct nvme_queue *queues;struct blk_mq_tag_set tagset;struct blk_mq_tag_set admin_tagset;u32 __iomem *dbs;struct device *dev;struct dma_pool *prp_page_pool;struct dma_pool *prp_small_pool;unsigned online_queues;unsigned max_qid;unsigned io_queues[HCTX_MAX_TYPES];unsigned int num_vecs;u32 q_depth;int io_sqes;u32 db_stride;void __iomem *bar;unsigned long bar_mapped_size;struct work_struct remove_work;struct mutex shutdown_lock;bool subsystem;u64 cmb_size;bool cmb_use_sqes;u32 cmbsz;u32 cmbloc;struct nvme_ctrl ctrl;u32 last_ps;bool hmb;mempool_t *iod_mempool;/* shadow doorbell buffer support: */u32 *dbbuf_dbs;dma_addr_t dbbuf_dbs_dma_addr;u32 *dbbuf_eis;dma_addr_t dbbuf_eis_dma_addr;/* host memory buffer support: */u64 host_mem_size;u32 nr_host_mem_descs;dma_addr_t host_mem_descs_dma;struct nvme_host_mem_buf_desc *host_mem_descs;void **host_mem_desc_bufs;unsigned int nr_allocated_queues;unsigned int nr_write_queues;unsigned int nr_poll_queues;bool attrs_added;
};
每个设备至少两个队列,一个是 admin
管理命令,一个是给 I / O
命令,这个队列概念和之前介绍块驱动中的磁盘队列一个道理,只是那个驱动比较基础,所以命令和IO并不区分队列,nvme_queue
具体结构体如下,
/** An NVM Express queue. Each device has at least two (one for admin* commands and one for I/O commands).*/
struct nvme_queue {struct nvme_dev *dev;spinlock_t sq_lock;void *sq_cmds;/* only used for poll queues: */spinlock_t cq_poll_lock ____cacheline_aligned_in_smp;struct nvme_completion *cqes;dma_addr_t sq_dma_addr;dma_addr_t cq_dma_addr;u32 __iomem *q_db;u32 q_depth;u16 cq_vector;u16 sq_tail;u16 last_sq_tail;u16 cq_head;u16 qid;u8 cq_phase;u8 sqes;unsigned long flags;
#define NVMEQ_ENABLED 0
#define NVMEQ_SQ_CMB 1
#define NVMEQ_DELETE_ERROR 2
#define NVMEQ_POLLED 3u32 *dbbuf_sq_db;u32 *dbbuf_cq_db;u32 *dbbuf_sq_ei;u32 *dbbuf_cq_ei;struct completion delete_done;
};
继续说 nvme_probe
函数,nvme_setup_prp_pools
主要是创建 dma pool
,后面可以通过 dma
函数从 dma pool
中获得memory
。主要是为了给 4k
和 128k
的不同 IO
来做优化。
nvme_init_ctrl
函数会创建 NVMe
控制器结构体,这样在后后续 probe
阶段时候用初始化过的结构,其传入的操作函数集是 nvme_pci_ctrl_ops
,如下所示,
static const struct nvme_ctrl_ops nvme_pci_ctrl_ops = {.name = "pcie",.module = THIS_MODULE,.flags = NVME_F_METADATA_SUPPORTED,.reg_read32 = nvme_pci_reg_read32,.reg_write32 = nvme_pci_reg_write32,.reg_read64 = nvme_pci_reg_read64,.free_ctrl = nvme_pci_free_ctrl,.submit_async_event = nvme_pci_submit_async_event,.get_address = nvme_pci_get_address,.print_device_info = nvme_pci_print_device_info,.supports_pci_p2pdma = nvme_pci_supports_pci_p2pdma,
};
另外 NVMe
磁盘的操作函数集,例如打开,释放等属于 block_device_operations
( drivers/nvme/host/core.c
5 ),其结构体如下,
static const struct block_device_operations nvme_bdev_ops = {.owner = THIS_MODULE,.ioctl = nvme_ioctl,.compat_ioctl = blkdev_compat_ptr_ioctl,.open = nvme_open,.release = nvme_release,.getgeo = nvme_getgeo,.report_zones = nvme_report_zones,.pr_ops = &nvme_pr_ops,
};
创建 NVMe 块设备
参考这里 3
对于 Linux
的块设备来说,其主要的是通过调用 device_add_disk
或者 add_disk
函数(后者是对前者的简单包装)在 /dev
目录下创建块设备,来实现向操作系统添加一个设备实例。
NVMe
本身也是块设备,自然也不会跳出这个大框架。
NVMe
块设备文件操作集合会在 add_disk
时通过 block_device_operations
( drivers/nvme/host/multipath.c
6 ) 进行声明,代码如下:
const struct block_device_operations nvme_ns_head_ops = {.owner = THIS_MODULE,.submit_bio = nvme_ns_head_submit_bio,.open = nvme_ns_head_open,.release = nvme_ns_head_release,.ioctl = nvme_ns_head_ioctl,.compat_ioctl = blkdev_compat_ptr_ioctl,.getgeo = nvme_getgeo,.report_zones = nvme_ns_head_report_zones,.pr_ops = &nvme_pr_ops,
};
其中 ownder
表示该 nvme_ns_head_ops
的所有者是 NVMe
块设备驱动,而 ioctl
和 compat_ioctl
分别是用户调用 ioctl
的两种方式。
进入 nvme_ns_head_ioctl
(如下所示)接口,
int nvme_ns_head_ioctl(struct block_device *bdev, fmode_t mode,unsigned int cmd, unsigned long arg)
{struct nvme_ns_head *head = bdev->bd_disk->private_data;void __user *argp = (void __user *)arg;struct nvme_ns *ns;int srcu_idx, ret = -EWOULDBLOCK;srcu_idx = srcu_read_lock(&head->srcu);ns = nvme_find_path(head);if (!ns)goto out_unlock;/** Handle ioctls that apply to the controller instead of the namespace* seperately and drop the ns SRCU reference early. This avoids a* deadlock when deleting namespaces using the passthrough interface.*/if (is_ctrl_ioctl(cmd))return nvme_ns_head_ctrl_ioctl(ns, cmd, argp, head, srcu_idx);ret = nvme_ns_ioctl(ns, cmd, argp);
out_unlock:srcu_read_unlock(&head->srcu, srcu_idx);return ret;
}
硬件层面
首先从硬件层面上,我们知道任何设备必须通过某个总线与 CPU
向连接,NVMe
则正是通过PCIe
总线与 CPU
相连,如下所示:
当然,目前 NVMe
除了可以通过 PCIe
总线与 CPU
相连外,还可以通过其它通道连接,比如FC
或者 IB
。后者则是一种将 NVMe
设备从计算节点独立出来的方式,也就是此时 NVMe
就不再是一个卡设备,而是一个独立机箱的设备。无论何种方式相连接,其本质是一样的。
软件层面
硬件的连通性是基础,当硬件已经连通后,就可以在 Linux
内核层面发现设备,并进行初始化了。
软件层面的初始化有两种情况:
- 计算机启动的时候,操作系统会扫描总线上的设备,并完成初始化;
- 设备在系统启动后连接的,此时需要手动触发扫描的过程。
无论是系统启动也好,还是手动触发扫描也好,NVMe
发现设备的核心流程是一样的,如下所示:
与其它块设备类似,NVMe
设备初始化完成后会在 /dev
目录下出现一个文件。NVMe
设备会出现一个形如 nvmeXnY
的设备文件。
如下图所示,红色方框中的为一个 NVMe
块设备,
上面我们简要的介绍了初始化的主流程。
在上面初始化流程中需要重点关注的是 nvme_alloc_ns
函数的流程。该函数完成了块设备创建基本信息填充和块设备注册到内核等工作。
在整个初始化流程中比较关键的是对请求队列( request_queue
)中请求处理函数指针(make_request_fn
)的初始化及多队列函数集( mq_ops
)的初始化。因为,这里的函数正是NVMe
区别于 SCSI
等类型设备数据处理流程的地方。
NVMe 设备的 IO 流程
参考这里 1’ 2
机械硬盘时代,由于其随机访问性能差,内核开发者主要放在缓存 I / O
、合并 I / O
等方面,并没有考虑多队列的设计。
而 Flash
的出现,性能出现了戏剧性的反转,因为单个 CPU
每秒发出 IO
请求数量是有限的,所以促进了 IO
多队列开发。
驱动中的队列创建,通过函数 kcalloc_node
( drivers/nvme/host/pci.c
4 ) 如下,
dev->queues = kcalloc_node(dev->nr_allocated_queues,sizeof(struct nvme_queue), GFP_KERNEL, node);if (!dev->queues)goto free;
Queue
有的概念,那就是队列深度,表示其能够放多少个成员。在 NVMe
中,这个队列深度是由 NVMe SSD
决定的,存储在 NVMe
设备的 BAR
空间里。
队列用来存放 NVMe Command
,NVMe Command
是主机与 SSD
控制器交流的基本单元,应用的 I/O
请求也要转化成NVMe Command
。
不过需要注意的是,就算有很多 CPU
发送请求,但是 Block
层并不能保证都能处理完,将来可能要绕过 IO
栈的块层,不然瓶颈就是操作系统本身了。
当前 Linux
内核提供了 blk_queue_make_request
函数,调用这个函数注册自定义的队列处理方法,可以绕过 IO
调度和 io
队列,从而缩短 io
延时。Block
层收到上层发送的 IO
请求,就会选择该方法处理。
为了便于理解 NVMe
的处理流程,我们给出了传统 SCSI
及 NVMe
数据处理的对比流程,如下图所示,
整个流程是从通用块层的接口( submit_bio
)开始的。
对于 NVMe
设备来说,在初始化的时候初始化函数指针 make_request_fn
为 nvme_queue_rq
,该函数就是 NVMe
驱动程序的请求处理接口。该函数最终会将请求写入 NVMe
中的 SQ
队列当中,并通知控制器处理请求。
相对于 SCSI
设备来说,NVMe
设备的驱动还是非常简单的。
DMA
参考这里 1
PCIe
有个寄存器位 Bus Master Enable
,这个 bit
置 1
后,PCIe
设备就可以向 Host
发送 DMA Read Memory
和 DMA Write Memory
请求。
当 Host
的 driver
需要跟 PCIe
设备传输数据的时候,只需要告诉 PCIe
设备存放数据的地址就可以。
NVMe Command
占用 64
个字节,另外其 PCIe BAR
空间被映射到虚拟内存空间(其中包括用来通知 NVMe SSD Controller
读取 Command
的 Doorbell
寄存器)。
NVMe
数据传输都是通过 NVMe Command
,而 NVMe Command
则存放在 NVMe Queue
中,其配置如下图,
其中队列中有 Submission Queue
,Completion Queue
两个。
参考链接
#TODO
nvme驱动分析
Linux中nvme驱动详解 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
NVMe的Linux内核驱动分析 ↩︎ ↩︎
linux NVMe驱动总结 ↩︎ ↩︎
linux/drivers/nvme/host/pci.c ↩︎ ↩︎
linux/drivers/nvme/host/core.c ↩︎
linux/drivers/nvme/host/multipath.c ↩︎
驱动 | Linux | NVMe - 1. 内核驱动相关推荐
- 驱动 | Linux | NVMe | 2. nvme_probe
本文主要参考这里 1' 2 的解析和 linux 源码 3. 此处推荐一个可以便捷查看 linux 源码的网站 bootlin 4. 更新:2022 / 02 / 19 驱动 | Linux | NV ...
- Linux应用层与内核驱动层3种交互方式
本文主要是总结出应用层与内核驱动层的主要交互方式,并提供示例代码分析交互过程.但不涉及更细节的内核代码的分析. 应用层与内核驱动层交互的方式多种多样,这里只写出了我目前理解到的3种方式,至于其它等以后 ...
- linux命令查看驱动,Linux下查看网卡驱动和版本信息
Linux下查看网卡驱动和版本信息 查看网卡生产厂商和信号 查看基本信息:lspci 查看详细信息:lspci -vvv # 3个小写的v 查看网卡信息:lspci | grep Ethernet ...
- linux查看网卡驱动,Linux操作系统查看网卡驱动,你需要学习了
操作方法 01 如果有一天,你想查看自己linux系统的网卡驱动版本,以此决定是否升级驱动,那你该怎么查看哪? 首先,用dmesg找驱动模块名称,下文中的eth0卡没启用,所以搜不到有用信息,而eth ...
- linux下的rtc设备驱动,linux下测试RTC驱动相关的命令date和hwclock常见用法讲解
之前对Linux下面时间相关的内容,一无所知,第一次见到hwclock,不知为何物,也没找到解释清楚的帖子.故此整理一下,简单介绍Linux下验证rtc驱动是否工作正常,相关的的命令:date和hwc ...
- linux蓝牙声卡驱动,Linux下安装声卡驱动总结
Linux下安装声卡驱动总结 发布时间:2008-03-27 01:18:34来源:红联作者:verytow 装了几次Linux OS,当然也装了几次声卡驱动,一般来说都是安装ALSA(Adcance ...
- mv150us无线网卡驱动linux,水星mw150us无线网卡驱动官方下载-v90最新版
水星mw150us无线网卡驱动是一款是被用于该品牌下的无线网卡驱动工具.mercury无线网卡驱动可帮助用户解决因驱动出现的问题,让你的无线网卡硬件可正常工作,如用户使用的是该品牌的网卡搭配该品牌的驱 ...
- kali linux查看网卡_CentOS7.6安装无线网卡驱动|Linux如何安装网卡驱动|Linux如何让配置网卡...
此前提到,Thinkpad E490安装CentOS7.6遇到内核崩溃的问题,解决之后,安装CentOS7.6操作系统成功. 安装时发现,系统能够检测到有线网卡,但无法检测到无线网卡,说明CentOS ...
- linux dd来加载驱动,linux 加载raid驱动
Centos5.5安装 上步完成后将U盘插到服务器上,插入光盘开始安装,在进入到安装界面的时候按F2 (Centos版本6是按ESC键就会出现boot:) CentOS安装RAID卡驱动总结 首先先介 ...
- linux如何终端安装网卡驱动,linux如何安装网卡驱动
很多初学者都觉得能够在自己的笔记本上安装linux系统是一件很酷的事情,结果两个小时安装好linux系统,却发现缺少各种驱动,当年笔者安装网卡驱动硬是一个月才弄好. 为了让各位少走一些弯路,小编在这里 ...
最新文章
- 圈钱跑路 ERC20 Token 合约代码分析
- C# 获取枚举的描述属性
- Android软件测试Monkey测试工具
- hook NSArray 方法在debug模式下会崩溃, 在release模式下会返回nil
- 【Ubuntu】VMware下Ubuntu和主机的共享文件夹
- 总结django form
- 表字段identity
- css hsla和rgba的区别
- volatile和原子操作
- Effective C++ 读书笔记之Part4.Design and Declarations
- Android客户端和服务器端数据交互的第一种方法
- 人脸识别 特征值脸_你的脸值多少钱?
- Designing With Web Standard(一)
- AUTOSAR工程师,年薪50W?
- Excel如何批量添加邮箱后缀
- Python 列表转为字典
- Win10开机时怎么跳过磁盘检查?
- 公众号怎么做意见反馈菜单_公众号菜单怎么添加意见反馈表
- Vue2.x动态组件的使用实现组件整合大屏展示
- B-样条曲线——动机 Motivation
热门文章
- Matlab2017b配置C++/C/Fortan编译器的问题
- web-----简单小游戏项目
- (附源码)计算机毕业设计SSM餐厅订餐系统
- opencv:VS无法导入源文件(环境配置不全解决100%有效)
- 汉诺塔C语言实现(纯代码)
- 计算机网络——网络安全基础笔记
- python数据处理源代码_python数据分析与应用源数据和代码
- prinect pdf toolbox 2021中文版
- sap系统搭建教程_SAP基础教程
- java并发编程源码_Java并发编程实战 PDF+源码