【好文推荐】

浅析linux内核网络协议栈--linux bridge

深入理解SR-IOV和IO虚拟化

virtio-net 实现机制【一】(图文并茂)

驱动的加载

驱动加载实际就是module的加载,而module加载时会对整个module进行初始化,nvme驱动的module初始化函数为nvme_init(),如下:

static struct pci_driver nvme_driver = {.name        = "nvme",.id_table    = nvme_id_table,.probe        = nvme_probe,.remove        = nvme_remove,.shutdown    = nvme_shutdown,.driver        = {.pm    = &nvme_dev_pm_ops,},.err_handler    = &nvme_err_handler,
};static int __init nvme_init(void)
{int result;/* 初始化等待队列nvme_kthread_wait,此等待队列用于创建nvme_kthread(只允许单进程创建nvme_kthread) */init_waitqueue_head(&nvme_kthread_wait);/* 创建一个workqueue叫nvme */nvme_workq = create_singlethread_workqueue("nvme");if (!nvme_workq)return -ENOMEM;/* 在内核中注册新的一类块设备驱动,名字叫nvme,注意这里只是注册,表示kernel支持了nvme类的块设备,返回一个major,之后所有的nvme设备的major都是此值 */result = register_blkdev(nvme_major, "nvme");if (result < 0)goto kill_workq;else if (result > 0)nvme_major = result;/* 注册一些通知信息 */nvme_nb.notifier_call = &nvme_cpu_notify;result = register_hotcpu_notifier(&nvme_nb);if (result)goto unregister_blkdev;/* 注册pci nvme驱动 */result = pci_register_driver(&nvme_driver);if (result)goto unregister_hotcpu;return 0;unregister_hotcpu:unregister_hotcpu_notifier(&nvme_nb);unregister_blkdev:unregister_blkdev(nvme_major, "nvme");kill_workq:destroy_workqueue(nvme_workq);return result;
}

这里面其实最重要的就是做了两件事,一件事是register_blkdev,注册nvme这类块设备,返回一个major,另一件事是注册了nvme_driver,注册了nvme_driver后,当有nvme设备插入后系统后,系统会自动调用nvme_driver->nvme_probe去初始化这个nvme设备.这时候可能会有疑问,系统是如何知道插入的设备是nvme设备的呢,注意看struct pci_driver nvme_driver这个结构体,里面有一个nvme_id_table,其内容如下:

/* Move to pci_ids.h later */
#define PCI_CLASS_STORAGE_EXPRESS    0x010802static const struct pci_device_id nvme_id_table[] = {{ PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff) },{ 0, }
};

再看看PCI_DEVICE_CLASS宏是如何定义的

#define PCI_DEVICE_CLASS(dev_class,dev_class_mask) \.class = (dev_class), .class_mask = (dev_class_mask), \.vendor = PCI_ANY_ID, .device = PCI_ANY_ID, \.subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID

也就是当pci class为PCI_CLASS_STORAGE_EXPRESS时,就表示是nvme设备,并且这个是写在设备里的,当设备插入host时,pci driver(并不是nvme driver)回去读取这个值,然后判断它需要哪个驱动去做处理.

nvme数据结构

现在假设nvme.ko已经加载完了(注册了nvme类块设备,并且注册了nvme driver),这时候如果有nvme盘插入pcie插槽,pci会自动识别到,并交给nvme driver去处理,而nvme driver就是调用nvme_probe去处理这个新加入的设备.

在说nvme_probe之前,先说一下nvme设备的数据结构,首先,内核使用一个nvme_dev结构体来描述一个nvme设备, 一个nvme设备对应一个nvme_dev,nvme_dev如下:

/* nvme设备描述符,描述一个nvme设备 */
struct nvme_dev {struct list_head node;/* 设备的queue,一个nvme设备至少有2个queue,一个admin queue,一个io queue,实际情况一般都是一个admin queue,多个io queue,并且io queue会与CPU做绑定 */struct nvme_queue __rcu **queues;/* unsigned short的数组,每个CPU占一个,主要用于存放CPU上绑定的io queue的qid,一个CPU绑定一个queues,一个queues绑定到1到多个CPU上 */unsigned short __percpu *io_queue;/* ((void __iomem *)dev->bar) + 4096 */u32 __iomem *dbs;/* 此nvme设备对应的pci dev */struct pci_dev *pci_dev;/* dma池,主要是以4k为大小的dma块,用于dma分配 */struct dma_pool *prp_page_pool;/* 也是dma池,但是不是以4k为大小的,是小于4k时使用 */struct dma_pool *prp_small_pool;/* 实例的id,第一个加入的nvme dev,它的instance为0,第二个加入的nvme,instance为1,也用于做/dev/nvme%d的显示,%d实际就是instance的数值 */int instance;/* queue的数量, 等于admin queue + io queue */unsigned queue_count;/* 在线可以使用的queue数量,跟online cpu有关 */unsigned online_queues;/* 最大的queue id */unsigned max_qid;/* nvme queue支持的最大cmd数量,为((bar->cap) & 0xffff)或者1024的最小值 */int q_depth;/* 1 << (((bar->cap) >> 32) & 0xf),应该是每个io queue占用的bar空间 */u32 db_stride;/*    初始化设置的值*    dev->ctrl_config = NVME_CC_ENABLE | NVME_CC_CSS_NVM;*    dev->ctrl_config |= (PAGE_SHIFT - 12) << NVME_CC_MPS_SHIFT;*    dev->ctrl_config |= NVME_CC_ARB_RR | NVME_CC_SHN_NONE;*    dev->ctrl_config |= NVME_CC_IOSQES | NVME_CC_IOCQES;*/u32 ctrl_config;/* msix中断所使用的entry,指针表示会使用多个msix中断,使用的中断的个数与io queue对等,多少个io queue就会申请多少个中断* 并且让每个io queue的中断尽量分到不同的CPU上运行*/struct msix_entry *entry;/* bar的映射地址,默认是映射8192,当io queue过多时,有可能会大于8192 */struct nvme_bar __iomem *bar;/* 其实就是块设备,一张nvme卡有可能会有多个块设备 */struct list_head namespaces;/* 对应的在/sys下的结构 */struct kref kref;/* 对应的字符设备,用于ioctl操作 */struct miscdevice miscdev;/* 2个work,暂时还不知道什么用 */work_func_t reset_workfn;struct work_struct reset_work;struct work_struct cpu_work;/* 这个nvme设备的名字,为nvme%d */char name[12];/* SN号 */char serial[20];char model[40];char firmware_rev[8];/* 这些值都是从nvme盘上获取 */u32 max_hw_sectors;u32 stripe_size;u16 oncs;u16 abort_limit;u8 vwc;u8 initialized;
};

在nvme_dev结构中,最最重要的数据就是nvme_queue,struct nvme_queue用来表示一个nvme的queue,每一个nvme_queue会申请自己的中断,也有自己的中断处理函数,也就是每个nvme_queue在驱动层面是完全独立的.nvme_queue有两种,一种是admin queue,一种是io queue,这两种queue都用struct nvme_queue来描述,而这两种queue的区别如下:

  • admin queue: 用于发送控制命令的queue,所有非io命令都会通过此queue发送给nvme设备,一个nvme设备只有一个admin queue,在nvme_dev中,使用queues[0]来描述.
  • io queue: 用于发送io命令的queue,所有io命令都是通过此queue发送给nvme设备,简单来说读/写操作都是通过io queue发送给nvme设备的,一个nvme设备有一个或多个io queue,每个io queue的中断会绑定到不同的一个或多个CPU上.在nvme_dev中,使用queues[1~N]来描述.

以上说的io命令和非io命令都是nvme命令,比如快层下发一个写request,nvme驱动就会根据此request构造出一个写命令,将这个写命令放入某个io queue中,当controller完成了这个写命令后,会通过此io queue的中断返回完成信息,驱动再将此完成信息返回给块层.明白了两种队列的作用,我们看看具体的数据结构struct nvme_queue

/* nvme的命令队列,其中包括sq和cq。一个nvme设备至少包含两个命令队列* 一个是控制命令队列,一个是IO命令队列*/
struct nvme_queue {struct rcu_head r_head;struct device *q_dmadev;/* 所属的nvme_dev */struct nvme_dev *dev;/* 中断名字,名字格式为nvme%dq%d,在proc/interrupts可以查看到 */char irqname[24];    /* nvme4294967295-65535\0 *//* queue的锁,当操作nvme_queue时,需要占用此锁 */spinlock_t q_lock;/* sq的虚拟地址空间,主机需要发给设备的命令就存在这里面 */struct nvme_command *sq_cmds;/* cq的虚拟地址空间,设备返回的命令就存在这里面 */volatile struct nvme_completion *cqes;/* 实际就是sq_cmds的dma地址 */dma_addr_t sq_dma_addr;/* cq的dma地址,实际就是cqes对应的dma地址,用于dma传输 */dma_addr_t cq_dma_addr;/* 等待队列,当sq满时,进程会加到此等待队列,等待有空闲的cmd区域 */wait_queue_head_t sq_full;/* wait queue的一个entry,主要是当cmdinfo满时,会将它放入sq_full,而sq_full最后会通过它,唤醒nvme_thread */wait_queue_t sq_cong_wait;struct bio_list sq_cong;/* iod是读写请求的封装,可以看成是一个bio的封装,此链表有可能为空,比如admin queue就为空 */struct list_head iod_bio;/* 当前sq_tail位置,是nvme设备上的一个寄存器,告知设备最新的发送命令存在哪,存在于bar空间中 */u32 __iomem *q_db;/* cq和sq最大能够存放的command数量 */u16 q_depth;/* 如果是admin queue,那么为0,之后的io queue按分配顺序依次增加,主要用于获取对应的irq entry,因为所有的queue的irq entry是一个数组 */u16 cq_vector;/* 当完成命令时会更新,当sq_head == sq_tail时表示cmd queue为空 */u16 sq_head;/* 当有新的命令存放到sq时,sq_tail++,如果sq_tail == q_depth,那么sq_tail会被重新设置为0,并且cq_phase翻转 * 实际上就是一个环*/u16 sq_tail;/* 驱动已经处理完成的cmd位置,当cq_head == sq_tail时,表示cmd队列为空,当sq_tail == cq_head - 1时表示cmd队列已满 */u16 cq_head;/* 此nvme queue在此nvme设备中的queue id * 0: 控制命令队列*/u16 qid;/* 初始设为1,主要用于判断命令是否完成,当cqe.status & 1 != cq_phase时,表示命令还没有完成* 当每次sq_tail == q_depth时,此值会取反*/u8 cq_phase;u8 cqe_seen;/* 初始设为1 */u8 q_suspended;/* CPU亲和性,用于设置此nvme queue能够在哪些CPU上做中断和中断处理 */cpumask_var_t cpu_mask;struct async_cmd_info cmdinfo;/* 实际就是cmdinfo,此包含d_depth个cmdinfo,一个cmdid表示一个cmdinfo,当对应的bit为0时,表示此槽位空闲,为1时表示此槽位存有cmd * 空闲的cmdinfo的默认完成回调函数都是special_completion* 其内存结构如下*                      d_depth bits                                       d_depth cmdinfo*   (每个bit一个cmdid,用于表示此cmdinfo是空闲还是被占用)              (d_depth个struct nvme_cmd_info)* |                                                      |                                                   |*/unsigned long cmdid_data[];
};

nvme_queue是nvme驱动最核心的数据结构,它是nvme驱动和nvme设备通信的桥梁,重点也要围绕nvme_queue来说,之前也说过,一个nvme设备有多个nvme_queue(一个admin queue,至少一个io queue),每个nvme_queue是独立的,它们有

  • 自己对应的中断(irq)
  • 自己的submission queue(sq),用于将struct nvme command发送给nvme设备,并且最多能存dev->d_depth个nvme command
  • 自己的completion queue(cq),用于nvme设备将完成的命令信息(struct nvme_completion)发送给host,并且最多能存dev->d_depth个nvme_completion.
  • 自己的cmdinfo,用于描述一个nvme command.(struct nvme_cmd_info)

可以把sq想象成一个struct nvme_command sq[dev->d_depth]的数组,而cq为struct nvme_completion cq[dev->d_depth]的数组.

struct nvme_command主要用于存储一个nvme命令,包括io命令,或者控制命令,当初始化好一个struct nvme_command后,直接将其下发给nvme设备,nvme设备就会根据它来执行对应操作,其结构如下:

struct nvme_command {union {struct nvme_common_command common;struct nvme_rw_command rw;struct nvme_identify identify;struct nvme_features features;struct nvme_create_cq create_cq;struct nvme_create_sq create_sq;struct nvme_delete_queue delete_queue;struct nvme_download_firmware dlfw;struct nvme_format_cmd format;struct nvme_dsm_cmd dsm;struct nvme_abort_cmd abort;};
};struct nvme_format_cmd {__u8            opcode;__u8            flags;__u16            command_id;  __le32            nsid;__u64            rsvd2[4];__le32            cdw10;__u32            rsvd11[5];
};

联合体里面就是nvme支持的所有种类的命令,我随便取了一个nvme_format_cmd,可以看看里面的变量,只要将这些变量设置正确,传给nvme设备,nvme是能够执行这个命令的.

再看看struct nvme_completion,它用于描述完成的命令

struct nvme_completion {__le32    result;        /* Used by admin commands to return data */__u32    rsvd;__le16    sq_head;    /* how much of this queue may be reclaimed */__le16    sq_id;        /* submission queue that generated this entry */__u16    command_id;    /* of the command which completed */__le16    status;        /* did the command fail, and if so, why? */
};

按之前说的,我们把sq和cq想象成两个数组,比如驱动之前将一个nvme_format_cmd放到了sq[10]中,设备对这个nvme_format_cmd命令做了处理,这时候设备就会返回一个nvme_completion,并且把这个nvme_completion放入到cq[6](这里的index为6是假设,实际上我认为一个nvme_command对应一个nvme_completion,如果这个假设成立的话,正常情况这里应该也是为10),并且产生一个中断,在nvme queue的中断处理中,会获取到这个nvme_completion,并通过nvme_completion->sq_head就能够获取到sq[10]中的nvme_format_cmd.这样sq和cq就能够完全联系起来了.

对于驱动来说,一个命令应该是由两部分组成:

  1. 命令的格式,要通过怎样的格式发送给硬件,硬件能够识别.
  2. 命令的额外信息.

对于第一点,实际上就是nvme_command来做,而对于第二点,就需要用nvme_cmd_info来保存了,nvme_cmd_info也是一个数组,根据d_depth来分配长度(因为sq和cq都是根据d_depth来分配长度),并且nvme_queue还会维护一个nvme_cmd_info的used_bitmap,用来表示哪个nvme_cmd_info数组中哪个cmd_info已经被占用,nvme_cmd_info如下:

struct nvme_cmd_info {nvme_completion_fn fn;    // 命令完成后的回调函数void *ctx;                // 命令的信息,不同命令使用不同结构来描述,所以这里只提供一个指针unsigned long timeout;    // 命令允许的超时时间int aborted;            // 命令是否作废
};

现在来说说nvme驱动怎么把nvme_command,nvme_completion和nvme_cmd_info联系起来,以上面的nvme_format_cmd为例,假设nvme驱动要发送一个nvme_format_cmd命令,那么先会从nvme_cmd_info的used_bitmap中获取一个空闲的nvme_cmd_info(包括这个cmd_info对应的index,实际就是nvme_cmd_info的数组下标,也称为cmdid),然后根据nvme_format_cmd驱动需要做的事情和信息,来初始化这个nvme_cmd_info,将nvme_format_cmd中的command_id设置为cmdid,发送nvme_format_cmd给nvme设备,nvme设备处理完毕后,发送nvme_format_cmd对应的nvme_completion给host,host获取到此nvme_comletion,从command_id中获取到cmdid,根据获取到的cmdid就能够获取到对应的nvme_cmd_info了.也就是说,在将命令发送给nvme设备时,要将cmd_info对应的cmd_id也一并传下去,之后命令返回时,nvme设备也会将这个cmd_id传回来,这样就能够将三者对应联系起来了.

nvme设备初始化

之前也说了,nvme驱动加载好后,如果有新的nvme设备加入,那么会通过nvme_probe来初始化这个nvme设备,我们先看看nvme_probe这个函数.

/* 当插入一个nvme设备时,会通过此函数进行nvme设备的初始化 */
static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{int result = -ENOMEM;/* nvme设备描述符 */struct nvme_dev *dev;dev = kzalloc(sizeof(*dev), GFP_KERNEL);if (!dev)return -ENOMEM;/* nvme用的是msi/msix中断,这里应该是是按numa内的CPU个数来分配entry数量,entry是msix_entry */dev->entry = kcalloc(num_possible_cpus(), sizeof(*dev->entry),GFP_KERNEL);if (!dev->entry)goto free;/* struct nvme_queue,数量是numa内的CPU个数+1 */dev->queues = kcalloc(num_possible_cpus() + 1, sizeof(void *),GFP_KERNEL);if (!dev->queues)goto free;/* unsigned short的数组,每个CPU占一个 */dev->io_queue = alloc_percpu(unsigned short);if (!dev->io_queue)goto free;/* 初始化namespace链表 */INIT_LIST_HEAD(&dev->namespaces);/* reset work的调用函数 */dev->reset_workfn = nvme_reset_failed_dev;INIT_WORK(&dev->reset_work, nvme_reset_workfn);INIT_WORK(&dev->cpu_work, nvme_cpu_workfn);dev->pci_dev = pdev;pci_set_drvdata(pdev, dev);/* 分配一个ID,保存到dev->instance里,实际上第一个加入的nvme设备,它的instance为0,第二个加入的nvme设备,instance为1,以此类推 */result = nvme_set_instance(dev);if (result)goto free;/* 主要创建两个dma pool,一个是4k大小(prp list page),一个是256B大小(prp list 256) */result = nvme_setup_prp_pools(dev);if (result)goto release;kref_init(&dev->kref);/* 1.做bar空间的映射,映射地址存放到nvme_dev->bar * 2.当此设备是系统中第一个加载的nvme设备或者nvme_thread没有启动时,就会启动一个nvme_thread* 3.初始化nvme的io queue(主要)*/result = nvme_dev_start(dev);if (result) {if (result == -EBUSY)goto create_cdev;goto release_pools;}/* 分配request queue和disk,执行完此函数后,在/dev/下就有此nvme设备了 */result = nvme_dev_add(dev);if (result)goto shutdown;create_cdev:/* 这里开始分配一个对应的混杂设备,可以理解为字符设备,主要用于应用层用ioctl接口来操作此nvme设备 * 这个字符设备的名字为nvme%d*/scnprintf(dev->name, sizeof(dev->name), "nvme%d", dev->instance);dev->miscdev.minor = MISC_DYNAMIC_MINOR;dev->miscdev.parent = &pdev->dev;dev->miscdev.name = dev->name;dev->miscdev.fops = &nvme_dev_fops;result = misc_register(&dev->miscdev);if (result)goto remove;dev->initialized = 1;return 0;remove:nvme_dev_remove(dev);nvme_free_namespaces(dev);shutdown:nvme_dev_shutdown(dev);release_pools:nvme_free_queues(dev, 0);nvme_release_prp_pools(dev);release:nvme_release_instance(dev);free:free_percpu(dev->io_queue);kfree(dev->queues);kfree(dev->entry);kfree(dev);return result;
}

nvme_probe函数主要做如下几件事情:

  1. 为中断创建msi/msix的entry,按CPU的数量进行entry的分配,为什么要按照CPU数量进行分配,因为每个io queue会占用一个.而整个系统io queue最大值也就是possible_cpus.
  2. 分配possible个cpus+1的queue结构体,possible应该是系统最大能够插入的cpu核个数,其不等于online_cpus,注意这里是possible_cpus+1,而中断的msi/msix的entry个数为possible_cpus,而每个queue会用一个entry,这样不是就会导致有一个queue是没有entry用的吗?实际上admin queue和第一个io queue会共用entry0.
  3. 分配instance,实际上就是一个nvme id,从0开始依次递增.
  4. 分配两个dma pool,一个pool中的元素大小为4k,一个是256B,这两个pool都是用于数据传输时做dma分配用的.
  5. 调用nvme_dev_start和nvme_dev_add,这两个是主要函数,之后重点看这两个函数.

nvme_dev_start和nvme_dev_add是负责不同的初始化,简单点说,nvme_dev_start是将硬件和驱动的联系进行初始化,当nvme_dev_start执行完成后,此nvme设备实际已经能够通过驱动正常使用了,但实际操作系统还是无法使用此设备,原因是需要nvme_dev_add函数将此设备注册到操作系统中,实际就是注册对应的gendisk和request queue,这样在/dev/和操作系统中都能过对此nvme设备进行操作.

nvme_dev_start

nvme_dev_start函数主要是做硬件方面与驱动方面的传输通道的初始化和硬件的一些初始化,实际主要就是建立admin queue和io queue,并且为这些queue绑定到各自的irq上.

/* 1.做bar空间的映射,映射地址存放到nvme_dev->bar * 2.当此设备是系统中第一个加载的nvme设备或者nvme_thread没有启动时,就会启动一个nvme_thread* 3.初始化nvme的io queue*/
static int nvme_dev_start(struct nvme_dev *dev)
{int result;bool start_thread = false;/* 主要做bar空间的映射,映射地址存放到nvme_dev->bar,并且从bar空间获取nvme设备的d_queue,d_queue是queue中允许的最大cmd数量 */result = nvme_dev_map(dev);if (result)return result;/* 初始化控制命令队列,中断处理函数为nvme_irq */result = nvme_configure_admin_queue(dev);if (result)goto unmap;spin_lock(&dev_list_lock);/* 当此设备是系统中第一个加载的nvme设备或者nvme_thread没有启动时,就会启动一个nvme_thread */if (list_empty(&dev_list) && IS_ERR_OR_NULL(nvme_thread)) {start_thread = true;nvme_thread = NULL;}list_add(&dev->node, &dev_list);spin_unlock(&dev_list_lock);if (start_thread) {/* 在此nvme设备的加载上下文中创建nvme_thread */nvme_thread = kthread_run(nvme_kthread, NULL, "nvme");wake_up(&nvme_kthread_wait);} else/* 非创建nvme_thread的nvme设备就会在这里等待nvme_thread创建完成 */wait_event_killable(nvme_kthread_wait, nvme_thread);if (IS_ERR_OR_NULL(nvme_thread)) {result = nvme_thread ? PTR_ERR(nvme_thread) : -EINTR;goto disable;}/* 初始化nvme的io queue,此为nvme_queue,一个nvme设备至少一个admin queue,一个io queue */result = nvme_setup_io_queues(dev);if (result && result != -EBUSY)goto disable;return result;disable:nvme_disable_queue(dev, 0);nvme_dev_list_remove(dev);unmap:nvme_dev_unmap(dev);return result;
}

需要注意,d_queue默认是1024,驱动会通过此nvme设备的pci bar空间获取到设备支持的d_queue,并取两者的最小值作为此设备所有queue的d_queue,d_queue是queue中允许存放的cmd数量最大值.

d_queue获取到后,第一件事情是初始化admin queue,使用nvme_configure_admin_queue:

/* 初始化控制命令队列,中断处理函数为nvme_irq */
static int nvme_configure_admin_queue(struct nvme_dev *dev)
{int result;u32 aqa;u64 cap = readq(&dev->bar->cap);struct nvme_queue *nvmeq;/* 应该是告诉nvme设备禁止操作 * 实现方法是对bar空间的NVME_CC_ENABLEbit做操作,因为当前还没有做irq分配和使用,只能通过寄存器的方法做设置*/result = nvme_disable_ctrl(dev, cap);if (result < 0)return result;/* 获取qid为0的nvme queue,实际上就是admin queue */nvmeq = raw_nvmeq(dev, 0);/* 如果不存在,则分配一个nvme queue的内存空间用于admin queue(qid 0) *//* 主要分配cq和sq的dma空间,大小为depth*(struct nvme_completion),depth*(struct nvme_command) * 注意sq和cq的dma空间都必须使用dma_alloc_coherent来分配*/if (!nvmeq) {nvmeq = nvme_alloc_queue(dev, 0, 64, 0);if (!nvmeq)return -ENOMEM;}aqa = nvmeq->q_depth - 1;aqa |= aqa << 16;dev->ctrl_config = NVME_CC_ENABLE | NVME_CC_CSS_NVM;dev->ctrl_config |= (PAGE_SHIFT - 12) << NVME_CC_MPS_SHIFT;dev->ctrl_config |= NVME_CC_ARB_RR | NVME_CC_SHN_NONE;dev->ctrl_config |= NVME_CC_IOSQES | NVME_CC_IOCQES;/* 初始化sq和cq */writel(aqa, &dev->bar->aqa);writeq(nvmeq->sq_dma_addr, &dev->bar->asq);writeq(nvmeq->cq_dma_addr, &dev->bar->acq);writel(dev->ctrl_config, &dev->bar->cc);/* 应该是告诉nvme设备使能操作 */result = nvme_enable_ctrl(dev, cap);if (result)return result;/* 分配中断,这里主要分配cq的中断,中断处理函数为nvme_irq */result = queue_request_irq(dev, nvmeq, nvmeq->irqname);if (result)return result;spin_lock_irq(&nvmeq->q_lock);/* 初始化cq和sq */nvme_init_queue(nvmeq, 0);spin_unlock_irq(&nvmeq->q_lock);return result;
}/* 分配cq和sq的dma空间,大小为depth*(struct nvme_completion),depth*(struct nvme_command) */
static struct nvme_queue *nvme_alloc_queue(struct nvme_dev *dev, int qid,int depth, int vector)
{struct device *dmadev = &dev->pci_dev->dev;unsigned extra = nvme_queue_extra(depth);struct nvme_queue *nvmeq = kzalloc(sizeof(*nvmeq) + extra, GFP_KERNEL);if (!nvmeq)return NULL;/* cq的dma区域,存放completion cmd的地方 */nvmeq->cqes = dma_alloc_coherent(dmadev, CQ_SIZE(depth),&nvmeq->cq_dma_addr, GFP_KERNEL);if (!nvmeq->cqes)goto free_nvmeq;memset((void *)nvmeq->cqes, 0, CQ_SIZE(depth));/* sq的dma区域,存放submission cmd的地方 */nvmeq->sq_cmds = dma_alloc_coherent(dmadev, SQ_SIZE(depth),&nvmeq->sq_dma_addr, GFP_KERNEL);if (!nvmeq->sq_cmds)goto free_cqdma;if (qid && !zalloc_cpumask_var(&nvmeq->cpu_mask, GFP_KERNEL))goto free_sqdma;nvmeq->q_dmadev = dmadev;nvmeq->dev = dev;snprintf(nvmeq->irqname, sizeof(nvmeq->irqname), "nvme%dq%d",dev->instance, qid);spin_lock_init(&nvmeq->q_lock);nvmeq->cq_head = 0;nvmeq->cq_phase = 1;/* 当sq中的cmdinfo满时,会将进程加入到此waitqueue做等待 */init_waitqueue_head(&nvmeq->sq_full);/* sq_cong_wait是用于加入到sq_full,当sq_full唤醒sq_cong_wait时,实际上是唤醒了nvme_thread */init_waitqueue_entry(&nvmeq->sq_cong_wait, nvme_thread);bio_list_init(&nvmeq->sq_cong);INIT_LIST_HEAD(&nvmeq->iod_bio);/* 当前sq_tail位置,是nvme设备上的一个寄存器,存在于bar空间中 * 发送命令流程: cmd放入sq_cmds,sq_head++,更新sq_head到此q_db,nvme设置会感知到,然后dma sq cmds,并处理sq cmd.*/nvmeq->q_db = &dev->dbs[qid * 2 * dev->db_stride];/* 1024或者nvme设备支持的最大值 */nvmeq->q_depth = depth;/* admin queue为0,io queue从0~io queue count */nvmeq->cq_vector = vector;/* queue id, admin queue为0, io queue为1~ io_queue_count+1 */nvmeq->qid = qid;nvmeq->q_suspended = 1;/* nvme设备的queue_count++ */dev->queue_count++;rcu_assign_pointer(dev->queues[qid], nvmeq);return nvmeq;free_sqdma:dma_free_coherent(dmadev, SQ_SIZE(depth), (void *)nvmeq->sq_cmds,nvmeq->sq_dma_addr);free_cqdma:dma_free_coherent(dmadev, CQ_SIZE(depth), (void *)nvmeq->cqes,nvmeq->cq_dma_addr);free_nvmeq:kfree(nvmeq);return NULL;
}/* 初始化cq和sq */
static void nvme_init_queue(struct nvme_queue *nvmeq, u16 qid)
{struct nvme_dev *dev = nvmeq->dev;/* 大部分情况都是0 */unsigned extra = nvme_queue_extra(nvmeq->q_depth);nvmeq->sq_tail = 0;nvmeq->cq_head = 0;nvmeq->cq_phase = 1;nvmeq->q_db = &dev->dbs[qid * 2 * dev->db_stride];memset(nvmeq->cmdid_data, 0, extra);memset((void *)nvmeq->cqes, 0, CQ_SIZE(nvmeq->q_depth));/* 告诉设备取消处理当前设备中的io请求 */nvme_cancel_ios(nvmeq, false);nvmeq->q_suspended = 0;dev->online_queues++;
}

到这里admin queue已经初始化完成,可以通过对admin queue发送nvme控制命令来操作nvme设置.admin queue初始化完成后的结果如下:

  • qid为0就是admin queue,并且nvme_dev->queues[0]就是admin queue.
  • nvme_dev->entrys[0]是admin queue使用的.

admin queue初始化完成后,创建nvme_thread,此内核线程不会在初始化流程中使用,暂时先不看,接下来就是初始化io queue了.

初始化io queue是nvme_setup_io_queue函数

/* 初始化nvme设备的所有io queue */
static int nvme_setup_io_queues(struct nvme_dev *dev)
{struct nvme_queue *adminq = raw_nvmeq(dev, 0);struct pci_dev *pdev = dev->pci_dev;int result, i, vecs, nr_io_queues, size;/* 以CPU个数来分配io queue */nr_io_queues = num_possible_cpus();/* 此函数用于设置controller支持的io queue数量(通过发送NVME_FEAT_NUM_QUEUES命令),nvme driver最优的结果是cpus个数个io queue* 在服务器上nvme设备肯定不会支持那么多io queue,所以设置时controller最多只会设置自己支持的io queue,并返回自己支持的io queue个数* 最后我们选择最小的那个数作为io queue个数,因为也有可能CPU很少,controller支持的io queue很多*/result = set_queue_count(dev, nr_io_queues);if (result < 0)return result;if (result < nr_io_queues)nr_io_queues = result;/* 4096 + ((nr_io_queues + 1) * 8 * dev->db_stride) */size = db_bar_size(dev, nr_io_queues);/* size过大,重新映射bar空间 */if (size > 8192) {iounmap(dev->bar);do {dev->bar = ioremap(pci_resource_start(pdev, 0), size);if (dev->bar)break;if (!--nr_io_queues)return -ENOMEM;size = db_bar_size(dev, nr_io_queues);} while (1);dev->dbs = ((void __iomem *)dev->bar) + 4096;adminq->q_db = dev->dbs;}/* Deregister the admin queue's interrupt *//* 释放admin queue的irq */free_irq(dev->entry[0].vector, adminq);for (i = 0; i < nr_io_queues; i++)dev->entry[i].entry = i;/* 每个io queue申请一个msix,如果不支持msix,则用msi */vecs = pci_enable_msix_range(pdev, dev->entry, 1, nr_io_queues);if (vecs < 0) {vecs = pci_enable_msi_range(pdev, 1, min(nr_io_queues, 32));if (vecs < 0) {vecs = 1;} else {for (i = 0; i < vecs; i++)dev->entry[i].vector = i + pdev->irq;}}/** Should investigate if there's a performance win from allocating* more queues than interrupt vectors; it might allow the submission* path to scale better, even if the receive path is limited by the* number of interrupts.*/nr_io_queues = vecs;dev->max_qid = nr_io_queues;/* 重新分配admin queue的irq */result = queue_request_irq(dev, adminq, adminq->irqname);if (result) {adminq->q_suspended = 1;goto free_queues;}/* Free previously allocated queues that are no longer usable *//* 释放多余的io queue */nvme_free_queues(dev, nr_io_queues + 1);/* 分配io queue需要的内存,并且分配对应的irq,最后设置CPU亲和性 */nvme_assign_io_queues(dev);return 0;free_queues:nvme_free_queues(dev, 1);return result;
}/* 分配一个nvme queue,包括其需要的CQ和SQ空间和DMA地址 */
/* 通过admin queue告知nvme设备创建cq和sq,并且分配对应的irq */
static void nvme_create_io_queues(struct nvme_dev *dev)
{unsigned i, max;max = min(dev->max_qid, num_online_cpus());for (i = dev->queue_count; i <= max; i++)/* 分配一个nvme queue,包括其需要的CQ和SQ空间和DMA地址,注意这里第一个io queue使用的entry是0,也就是和admin queue共用 */if (!nvme_alloc_queue(dev, i, dev->q_depth, i - 1))break;max = min(dev->queue_count - 1, num_online_cpus());for (i = dev->online_queues; i <= max; i++)/* 通过admin queue告知nvme设备创建cq和sq,并且分配对应的irq */if (nvme_create_queue(raw_nvmeq(dev, i), i))break;
}static int nvme_create_queue(struct nvme_queue *nvmeq, int qid)
{struct nvme_dev *dev = nvmeq->dev;int result;/* 通过admin queue将nvme_admin_create_cq命令发送给nvme设备,主要将当前queue的cq_dma地址和qid传给nvme设备,这样就能将cq关联起来 */result = adapter_alloc_cq(dev, qid, nvmeq);if (result < 0)return result;/* 通过admin queue将nvme_admin_create_sq命令发送给nvme设备,主要将当前queue的sq_dma地址和qid传给nvme设备,这样就能将sq关联起来 */result = adapter_alloc_sq(dev, qid, nvmeq);if (result < 0)goto release_cq;/* 为此queue创建一个irq */result = queue_request_irq(dev, nvmeq, nvmeq->irqname);if (result < 0)goto release_sq;spin_lock_irq(&nvmeq->q_lock);nvme_init_queue(nvmeq, qid);spin_unlock_irq(&nvmeq->q_lock);return result;release_sq:adapter_delete_sq(dev, qid);release_cq:adapter_delete_cq(dev, qid);return result;
}/* 分配io queue需要的内存,并且分配对应的irq,最后设置CPU亲和性 */
static void nvme_assign_io_queues(struct nvme_dev *dev)
{unsigned cpu, cpus_per_queue, queues, remainder, i;cpumask_var_t unassigned_cpus;/* 分配一个nvme queue,包括其需要的CQ和SQ空间和DMA地址 *//* 告知nvme设备创建cq和sq,并且分配对应的irq */nvme_create_io_queues(dev);/* 获取queue的数量,其至少<=CPUS */queues = min(dev->online_queues - 1, num_online_cpus());if (!queues)return;/* 计算每个io queue的中断可以绑定到多少个CPU上,结果>=1 */cpus_per_queue = num_online_cpus() / queues;/* 剩余的CPU个数,因为queues <= cpus,当queues < cpus时,那么必然有一些queues绑定的cpus比其他的少一个,具体看下面的代码 */remainder = queues - (num_online_cpus() - queues * cpus_per_queue);if (!alloc_cpumask_var(&unassigned_cpus, GFP_KERNEL))return;/* 将所有可用的CPU的mask复制到unassigned_cpus */cpumask_copy(unassigned_cpus, cpu_online_mask);/* 获取第一个可用的CPU */cpu = cpumask_first(unassigned_cpus);/* 遍历所有的io queue,从1开始是因为0是admin queue */for (i = 1; i <= queues; i++) {/* 根据获取对应的io queue */struct nvme_queue *nvmeq = lock_nvmeq(dev, i);cpumask_t mask;/* 清除此io queue的cpumask */cpumask_clear(nvmeq->cpu_mask);/* 如果unassigned_cpus为0,表示没有CPU可以使用,则退出,之后会初始化nvme dev失败 */if (!cpumask_weight(unassigned_cpus)) {unlock_nvmeq(nvmeq);break;}/* 根据CPU ID.获取此CPU的cpumask */mask = *get_cpu_mask(cpu);/* 设置此io queue使用此CPU */nvme_set_queue_cpus(&mask, nvmeq, cpus_per_queue);/* 如果绑定的CPU个数少于cpus_per_queue,那么先绑定此CPU对应的超线程的其他CPU */if (cpus_weight(mask) < cpus_per_queue)nvme_add_cpus(&mask, unassigned_cpus,topology_thread_cpumask(cpu),nvmeq, cpus_per_queue);/* 如果绑定的CPU个数还少于cpus_per_queue,那么绑定此CPU对应的同一个socket上其他CPU */if (cpus_weight(mask) < cpus_per_queue)nvme_add_cpus(&mask, unassigned_cpus,topology_core_cpumask(cpu),nvmeq, cpus_per_queue);/* 如果绑定的CPU个数还少于cpus_per_queue,那么绑定此CPU对应的node上的所有CPU */if (cpus_weight(mask) < cpus_per_queue)nvme_add_cpus(&mask, unassigned_cpus,cpumask_of_node(cpu_to_node(cpu)),nvmeq, cpus_per_queue);/* 如果绑定的CPU个数还少于cpus_per_queue,那么绑定此CPU对应的node最近的node上的所有CPU */if (cpus_weight(mask) < cpus_per_queue)nvme_add_cpus(&mask, unassigned_cpus,cpumask_of_node(nvme_find_closest_node(cpu_to_node(cpu))),nvmeq, cpus_per_queue);/* 如果绑定的CPU个数还少于cpus_per_queue,那么绑定所有可用的CPU */if (cpus_weight(mask) < cpus_per_queue)nvme_add_cpus(&mask, unassigned_cpus,unassigned_cpus,nvmeq, cpus_per_queue);WARN(cpumask_weight(nvmeq->cpu_mask) != cpus_per_queue,"nvme%d qid:%d mis-matched queue-to-cpu assignment\n",dev->instance, i);/* 到这里,已经获取到了此queue对应绑定的CPU的cpumask,并且哪个CPU绑定哪个queue,已经写到nvme_dev->io_queue *//* 根据cpumask,设置中断的亲和性 */irq_set_affinity_hint(dev->entry[nvmeq->cq_vector].vector,nvmeq->cpu_mask);/* 将这些绑定的CPU从unassigned_cpus中删除 */cpumask_andnot(unassigned_cpus, unassigned_cpus,nvmeq->cpu_mask);/* cpu += 1 */cpu = cpumask_next(cpu, unassigned_cpus);/* 如果此时remainder为0了,那么从下一个queue开始,它绑定的cpus+1,也就是下一个及其之后的queue,绑定的CPUS都比之前的多1 */if (remainder && !--remainder)cpus_per_queue++;unlock_nvmeq(nvmeq);}WARN(cpumask_weight(unassigned_cpus), "nvme%d unassigned online cpus\n",dev->instance);i = 0;cpumask_andnot(unassigned_cpus, cpu_possible_mask, cpu_online_mask);/* 如果还有剩余的可用CPU的情况,那么就从queue1开始依次绑到剩余这些CPU上 */for_each_cpu(cpu, unassigned_cpus)*per_cpu_ptr(dev->io_queue, cpu) = (i++ % queues) + 1;free_cpumask_var(unassigned_cpus);
}

一个nvme设备会有多个io queue,每个io queue会有自己的中断,并且nvme设备会将每个io queue的中断绑定到不同的CPU上(实际上并不是真正的做了绑定,注意irq_set_affinity_hint这个函数,它实际上是告知使用者,这个irq更适合在哪些CPU上做处理,但是kernel还是有可能将这个IRQ放到不属于这些CPU中的CPU去处理,不过如果在用户层使用了irqbalance命令,那么irqbalance会将这个IRQ放到这个函数设置的CPU上去处理.具体可以看/proc/irq中的值就能明白了,它改变的是smp_affinity_hint值,而非smp_affinity),就有了上面的代码.一般情况应该是一个io queue绑定到多个CPU上,比如CPU有16个,io queue有8个,那么io queue[0]绑定到cpu0,1上,io queue[1]绑定到cpu2,3上,依次类推.当io queue初始化完成后,一些需要注意的细节如下:

  • io queue使用的entry是从0开始的,也就是io queue0会与admin queue共用一个entry.
  • nvme_dev->queues是从1开始保存io queue.
  • queue的sq_dma,cq_dma和qid通过admin queue发送给nvme设备,nvme设备会将其做绑定.并且注意,在nvme_alloc_queue时,queue->q_db指向的位置是通过qid计算的,所以实际上,sq_dma,cq_dma,qid和q_db都能过联系起来了.

到这里,admin queue和io queue都初始化完成了,之后就是在块层注册设备的操作.

nvme_add_dev

static int nvme_dev_add(struct nvme_dev *dev)
{struct pci_dev *pdev = dev->pci_dev;int res;unsigned nn, i;struct nvme_ns *ns;struct nvme_id_ctrl *ctrl;struct nvme_id_ns *id_ns;void *mem;dma_addr_t dma_addr;int shift = NVME_CAP_MPSMIN(readq(&dev->bar->cap)) + 12;/* 分配一个一致性dma区域,注意大小是8192B,前4096B放盘的信息,后面4096B空闲,之后会使用 */mem = dma_alloc_coherent(&pdev->dev, 8192, &dma_addr, GFP_KERNEL);if (!mem)return -ENOMEM;/* 向controller发送一个identify命令,此命令会让controller将nvme卡的信息保存到mem这块一致性dma区域中 */res = nvme_identify(dev, 0, 1, dma_addr);if (res) {dev_err(&pdev->dev, "Identify Controller failed (%d)\n", res);res = -EIO;goto out;}/* 已经获取到信息,包括sn号,model,fw版本,用户可用容量等信息,注意,nn是表示此nvme物理盘生成多少个块设备 */ctrl = mem;/* 决定了生成多少个块设备 */nn = le32_to_cpup(&ctrl->nn);dev->oncs = le16_to_cpup(&ctrl->oncs);dev->abort_limit = ctrl->acl + 1;dev->vwc = ctrl->vwc;memcpy(dev->serial, ctrl->sn, sizeof(ctrl->sn));memcpy(dev->model, ctrl->mn, sizeof(ctrl->mn));memcpy(dev->firmware_rev, ctrl->fr, sizeof(ctrl->fr));if (ctrl->mdts)dev->max_hw_sectors = 1 << (ctrl->mdts + shift - 9);if ((pdev->vendor == PCI_VENDOR_ID_INTEL) &&(pdev->device == 0x0953) && ctrl->vs[3])dev->stripe_size = 1 << (ctrl->vs[3] + shift);id_ns = mem;for (i = 1; i <= nn; i++) {res = nvme_identify(dev, i, 0, dma_addr);if (res)continue;if (id_ns->ncap == 0)continue;/* 通过admin queue获取设备盘容量,lba大小等信息,存放到mem的后4096B中 */res = nvme_get_features(dev, NVME_FEAT_LBA_RANGE, i,dma_addr + 4096, NULL);if (res)memset(mem + 4096, 0, 4096);/* 分配disk和request queue,一个块设备就是一个namespace */ns = nvme_alloc_ns(dev, i, mem, mem + 4096);/* 加入到nvme_dev->namespace链表中 */if (ns)list_add_tail(&ns->list, &dev->namespaces);}/* 将disk添加到系统中,这样用户就能在/dev/下面看到了 */list_for_each_entry(ns, &dev->namespaces, list)add_disk(ns->disk);res = 0;out:dma_free_coherent(&dev->pci_dev->dev, 8192, mem, dma_addr);return res;
}

此函数主要做几件事情:

  1. 获取nvme设备的信息.
  2. 根据nvme设备的信息,创建对应的namespace,一个namespace实际就是一个块设备
  3. 将创建的namespace加入到系统中的块设备中.

主要是通过nvme_alloc_ns函数来初始化一个namespace,一个namespace是一个块设备,一个块设备主要初始化两个结构,一个是gendisk,一个是request queue,当两个结构都初始化好后,调用add_disk()函数,这个块设备就会正式加入到系统中的块设备中.

static struct nvme_ns *nvme_alloc_ns(struct nvme_dev *dev, unsigned nsid,struct nvme_id_ns *id, struct nvme_lba_range_type *rt)
{struct nvme_ns *ns;struct gendisk *disk;int lbaf;if (rt->attributes & NVME_LBART_ATTRIB_HIDE)return NULL;ns = kzalloc(sizeof(*ns), GFP_KERNEL);if (!ns)return NULL;/* 分配一个request queue */ns->queue = blk_alloc_queue(GFP_KERNEL);if (!ns->queue)goto out_free_ns;ns->queue->queue_flags = QUEUE_FLAG_DEFAULT;/* 禁止合并操作,包括bio合并到request操作,两个request合并操作 */queue_flag_set_unlocked(QUEUE_FLAG_NOMERGES, ns->queue);/* 表示是一个ssd设备 */queue_flag_set_unlocked(QUEUE_FLAG_NONROT, ns->queue);queue_flag_clear_unlocked(QUEUE_FLAG_ADD_RANDOM, ns->queue);/* 绑定request queue的make_request_fn函数到nvme_make_request */blk_queue_make_request(ns->queue, nvme_make_request);ns->dev = dev;ns->queue->queuedata = ns;/* 分配一个gendisk结构,gendisk用于描述一个块设备 */disk = alloc_disk(0);if (!disk)goto out_free_queue;ns->ns_id = nsid;ns->disk = disk;lbaf = id->flbas & 0xf;ns->lba_shift = id->lbaf[lbaf].ds;ns->ms = le16_to_cpu(id->lbaf[lbaf].ms);/* 物理sector的大小,用户看到的逻辑sector大小一般是512B,而物理sector大小不同厂商不同定义,可能跟一个nand flash page一样,也可能小于一个nand flash page */blk_queue_logical_block_size(ns->queue, 1 << ns->lba_shift);/* 设备允许的一次request支持最大sector数量,request中的sector数量不能超过此值 */if (dev->max_hw_sectors)blk_queue_max_hw_sectors(ns->queue, dev->max_hw_sectors);if (dev->vwc & NVME_CTRL_VWC_PRESENT)blk_queue_flush(ns->queue, REQ_FLUSH | REQ_FUA);disk->major = nvme_major;disk->first_minor = 0;/* 此块设备的操作函数 */disk->fops = &nvme_fops;disk->private_data = ns;/* 将上面初始化好的request queue与gendisk联系一起 */disk->queue = ns->queue;disk->driverfs_dev = &dev->pci_dev->dev;/* 标记为允许扩展的设备,暂时不清楚什么意思 */disk->flags = GENHD_FL_EXT_DEVT;/* 在/dev/下显示的名字 */sprintf(disk->disk_name, "nvme%dn%d", dev->instance, nsid);/* 设置用户可用容量 */set_capacity(disk, le64_to_cpup(&id->nsze) << (ns->lba_shift - 9));/* 如果此nvme盘支持discard操作,则设置discard的一些初始参数,如discard必须以物理sector大小对齐 */if (dev->oncs & NVME_CTRL_ONCS_DSM)nvme_config_discard(ns);return ns;out_free_queue:blk_cleanup_queue(ns->queue);out_free_ns:kfree(ns);return NULL;
}

这里主要初始化gendisk和request queue,gendisk用于描述一个块设备,也就是当gendisk初始化好后,并调用add_disk(),就会在/dev/下出现一个此gendisk->name的块设备.而request_queue有什么用呢,注意看gendisk初始化时,会将gendisk->queue设置为一个初始化好的request_queue.对于request_queue,最重要的是初始化一个make_request_fn的函数指针,当有进程对此gendisk对应的块设备进行读写时,最终都会调用到gendisk的request_queue的make_request_fn所指的函数.在nvme驱动中,主要将request_queue的make_request_fn初始化为了nvme_make_request()函数,未来在说nvme设备的读写流程时,会详细说明此函数.

linux内核源码分析 - nvme设备的初始化相关推荐

  1. linux内核源码分析之设备驱动

    目录 一.I/O 体系结构 二.与外设的交互 1.I/O端口: 2.I/O内存映射 3.轮询和中断 三.驱动分类 四.注册 五.与文件系统关联 六.资源管理 七.结构体总结 一.I/O 体系结构 与外 ...

  2. Linux内核源码分析--内核启动之(4)Image内核启动(setup_arch函数)(Linux-3.0 ARMv7)【转】...

    原文地址:Linux内核源码分析--内核启动之(4)Image内核启动(setup_arch函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.c ...

  3. Linux内核源码分析《进程管理》

    Linux内核源码分析<进程管理> 前言 1. Linux 内核源码分析架构 2. 进程原理分析 2.1 进程基础知识 2.2 Linux进程四要素 2.3 进程描述符 task_stru ...

  4. iostat IO统计原理linux内核源码分析----基于单通道SATA盘

    iostat IO统计原理linux内核源码分析----基于单通道SATA盘 先上一个IO发送submit_bio流程图,本文基本就是围绕该流程讲解. 内核版本 3.10.96 详细的源码注释:htt ...

  5. Linux内核源码分析—从用户空间复制数据到内核空间

    Linux内核源码分析-从用户空间复制数据到内核空间 本文主要参考<深入理解Linux内核>,结合2.6.11.1版的内核代码,分析从用户空间复制数据到内核空间函数. 1.不描述内核同步. ...

  6. v67.03 鸿蒙内核源码分析(字符设备) | 绝大多数设备都是这类 | 百篇博客分析OpenHarmony源码

    曾子曰:"君子以文会友,以友辅仁." <论语>:颜渊篇 百篇博客系列篇.本篇为: v67.xx 鸿蒙内核源码分析(字符设备篇) | 绝大多数设备都是这类 文件系统相关篇 ...

  7. Linux内核源码分析之内存管理

    本文站的角度更底层,基本都是从Linux内核出发,会更深入.所以当你都读完,然后再次审视这些功能的实现和设计时,我相信你会有种豁然开朗的感觉. 1.页 内核把物理页作为内存管理的基本单元. 尽管处理器 ...

  8. Linux内核源码分析方法—程序员进阶必备

    一.内核源码之我见 Linux内核代码的庞大令不少人"望而生畏",也正因为如此,使得人们对Linux的了解仅处于泛泛的层次.如果想透析Linux,深入操作系统的本质,阅读内核源码是 ...

  9. 【技术分享篇】Linux内核——手把手带你实现一个Linux内核文件系统丨Linux内核源码分析

    手把手带你实现一个Linux内核文件系统 1. 内核文件系统架构分析 2. 行行珠玑,代码实现 [技术分享篇]Linux内核--手把手带你实现一个Linux内核文件系统丨Linux内核源码分析 更多L ...

最新文章

  1. 谈谈Tomcat连接器
  2. 36岁自学python_Python语言基础
  3. python编程培训-马哥教育官网-专业Linux培训班,Python培训机构
  4. Bayesian Neural Network for regression (PRML)
  5. Netflix Curator 使用 Zookeeper 编程
  6. java.lang.Instrument 代理Agent使用
  7. Leetcode12. 整数转罗马数字(C++)
  8. centos7中安装RocketMQ4.7版本
  9. android 帧动画旋转,安卓动画实现
  10. 模拟消耗CPU之shell脚本
  11. xml方式实现aop-切点表达式的抽取
  12. Git ----fatal: unable to access ‘https://gitee.com/***.git/‘: SSL certificate problem: unable
  13. [.NET跨平台]Jexus独立版本的便利与过程中的一些坑
  14. JAVA捕捉输入格式异常_Java学习(四).异常处理
  15. MySQL基本操作指令
  16. python opencv —— io(帧、图像、视频的读取与保存)
  17. 【电路设计】Altium Designer 20 PCB设计
  18. qt编译出错 /usr/bin/ld 找不到 -lGL cannot find -lGL
  19. 【微博简易爬虫】Python获取指定微博用户的发布文本
  20. android+下落+动画,完美起航-Android面试题之动画+事件处理篇

热门文章

  1. 根据梯形图写指令和根据指令写梯形图
  2. (推荐)2008元旦祝福语集锦
  3. 别再抱怨缺乏算法实践场景,不妨来挑战这场百万奖池的比赛!
  4. ironpython是什么意思_Py交易是什么意思?Py交易是什么梗?
  5. 学Java软件开发,就选动力节点软件开发培训学校
  6. IM要做手机扫码登录?先看看微信的扫码登录功能技术原理
  7. Python网络爬虫,Appuim+夜神模拟器爬取得到APP课程数据
  8. 从一部大学生自拍DV看当代大学生的价值取向
  9. 引导好评弹窗怎么做才能让用户体验更好?
  10. Postgres-XL介绍