PCIe学习笔记之pcie初始化枚举和资源分配流程代码分析
本文主要是对PCIe的初始化枚举、资源分配流程进行分析。代码对应的是linux 4.19, 平台是arm64.
文章首发于这里
1. PCIe architecture
1.1 pcie的拓扑结构
在分析PCIe初始化枚举流程之前,先描述下pcie的拓扑结构。
如下图所示:
整个PCIe是一个树形的拓扑:
• Root Complex是树的根,它一般实现了一个主桥设备(host bridge), 一条内部PCIe总线(BUS 0),以及通过若干个PCI bridge扩展出一些root port。host bridge可以完成CPU地址到PCI域地址的转换,pci bridge用于系统的扩展,没有地址转换功能;
• Swich是转接器设备,目的是扩展PCIe总线。switch中有一个upstream port和若干个downstream port, 每一个端口都相当于一个pci bridge
• PCIe ep device是叶子节点设备,比如pcie网卡,显卡,nvme卡等。
每个PCIe设备,包括host bridge、pci bridge和ep设备都有一个4k的配置空间。arm使用ecam的方式访问pcie配置空间。
1.2 PCIe的软件层次
PCIe模块涉及到的代码文件很多,在分析pcie的代码前,先对pcie的涉及到的代码梳理下。
这里以arm64架构为例,pcie代码主要分散在三个目录:
drivers/pci/*
driver/acpi/pci*
arch/arm64/kernel/pci.c
将pcie代码按如下层次划分:
|-->+ pcie hp service driver +|-->+ pcie aer service driver +|-->+ pcie pme service driver +|-->+ pcie dpc service driver +|
+---------------------+ +----------------+
| pcie port bus driver| | pcie ep driver |
+---------------------+ +----------------+
+------------------------------------------+
| pcie core driver |
+------------------------------------------+
+------------------+ +-------------------+
| arch pcie driver | | acpi pcie driver |
+------------------+ +-------------------+
____________________________________________
+------------------------------------------+
| pcie hardware |
+------------------------------------------+
arch pcie driver:放一些和架构强相关的pcie的函数实现,对应arch/arm64/kernel/pci.c
acpi pcie driver: apci扫描时所涉及到的pcie代码,包括host bridge的解析初始化,pcie bus的创建,ecam的映射等, 对应drivers/acpi/pci*.c
pcie core driver: pcie的子系统代码,包括pcie的枚举流程,资源分配流程,中断流程等,主要对应drivers/pci/
pcie port bus driver: 是pcie port的四个service代码的整合, 四个service主要指的是pcie dpc/pme/hotplug/aer,对应的是drivers/pci/pcie/*
pcie ep driver:是叶子节点的设备驱动,比如显卡,网卡,nvme等。
Linux内核实现
2.1 pcie初始化流程
pcie的代码文件这么多,初始化涉及的调用也很多,从哪一个开始看呢?
内核通过initcore的level决定模块的启动顺序
cat System.map | grep pci | grep initcall
再结合Makefile中object定义的先后顺序
obj-y += access.o bus.o probe.o remove.o pci.o quirks.o /pci-driver.o search.o pci-sysfs.o rom.o setup-res.o
可以看出关键symbol的调用顺序如下:
|-->pcibus_class_init() /* postcore_initcall(pcibus_class_init) */
|
|-->pci_driver_init() /* postcore_initcall(pci_driver_init) */
|
|-->acpi_pci_init() /* arch_initcall(acpi_pci_init) */
|
|-->acpi_init() /* subsys_initcall(acpi_init) */
pcibus_class_init(): 注册pci_bus class,完成后创建了/sys/class/pci_bus目录。
pci_driver_init(): 注册pci_bus_type, 完成后创建了/sys/bus/pci目录。
acpi_pci_init(): 注册acpi_pci_bus, 并设置电源管理相应的操作。
acpi_init(): apci启动所涉及到的初始化流程,PCIe基于acpi的启动流程从该接口进入。
在linux/Documentation/acpi/namespace.txt中定义了acpi解析的流程
+---------+ +-------+ +--------+ +------------------------+| RSDP | +->| XSDT | +->| FADT | | +-------------------+ |+---------+ | +-------+ | +--------+ +-|->| DSDT | || Pointer | | | Entry |-+ | ...... | | | +-------------------+ |+---------+ | +-------+ | X_DSDT |--+ | | Definition Blocks | || Pointer |-+ | ..... | | ...... | | +-------------------+ |+---------+ +-------+ +--------+ | +-------------------+ || Entry |------------------|->| SSDT | |+- - - -+ | +-------------------| || Entry | - - - - - - - -+ | | Definition Blocks | |+- - - -+ | | +-------------------+ || | +- - - - - - - - - -+ |+-|->| SSDT | || +-------------------+ || | Definition Blocks | || +- - - - - - - - - -+ |+------------------------+|OSPM Loading |\|/+----------------+| ACPI Namespace |+----------------+
ACPI Namespace就是表示系统上所有可枚举的ACPI设备的层次结构。
现在对acpi_init()流程展开,主要找和pci初始化相关的调用:
acpi_init() /* subsys_initcall(acpi_init) */+-> mmcfg_late_init()+-> acpi_scan_init()+-> acpi_pci_root_init()+-> acpi_scan_add_handler_with_hotplug(&pci_root_handler, "pci_root");+-> .attach = acpi_pci_root_add/** register pci_link_handler to list: acpi_scan_handlers_list.* this handler has relationship with PCI IRQ.*/+-> acpi_pci_link_init()/* we facus on PCI-ACPI, ignore other handlers' init */...+-> acpi_bus_scan()/* create struct acpi_devices for all device in this system */--> acpi_walk_namespace()--> acpi_bus_attach()--> acpi_scan_attach_handler()--> acpi_scan_match_handler()--> handler->attach /* attach is acpi_pci_root_add */
mmcfg_late_init(), acpi先扫描MCFG表,MCFG表定义了ecam的相关资源。
acpi_pci_root_init(),定义pcie host bridge device的attach函数, ACPI的Definition Block中使用PNP0A03表示一个PCI Host Bridge。
acpi_pci_link_init(), 注册pci_link_handler, 主要和pcie IRQ相关。
acpi_bus_scan(), 会通过acpi_walk_namespace()会遍历system中所有的device,并为这些acpi device创建数据结构,执行对应device的attatch函数。根据ACPI spec定义,pcie host bridge device定义在DSDT表中,acpi在扫描过程中扫描DSDT,如果发现了pcie host bridge, 就会执行device对应的attach函数,调用到acpi_pci_root_add()。
acpi_pci_root_add的函数很长,完整代码就不贴了, 它主要做了几个动作
(1)通过ACPI的_SEG参数, 获取host bridge使用的segment号, segment指的就是pcie domain, 主要目的是为了突破pcie最大256条bus的限制。
(2)通过ACPI的_CRS里的BusRange类型资源取得该Host Bridge的Secondary总线范围,保存在root->secondary这个resource中
(3)通过ACPI的_BNN参数获取host bridge的根总线号。
执行到这里如果没有返回失败,硬件设备上会有如下打印:
pr_info(PREFIX "%s [%s](domain %04x %pR)\n",acpi_device_name(device), acpi_device_bid(device),root->segment, &root->secondary);
...
ACPI: PCI Root Bridge [PCI0](domain 0000 [bus 00-7f])
(3) pci_acpi_scan_root, pcie枚举流程的入口
2.2 pcie枚举流程
166 struct pci_bus *pci_acpi_scan_root(struct acpi_pci_root *root)
167 {168 int node = acpi_get_node(root->device->handle);
169 struct acpi_pci_generic_root_info *ri;
170 struct pci_bus *bus, *child;
171 struct acpi_pci_root_ops *root_ops;
172
173 ri = kzalloc_node(sizeof(*ri), GFP_KERNEL, node);
174 if (!ri)
175 return NULL;
176
177 root_ops = kzalloc_node(sizeof(*root_ops), GFP_KERNEL, node);
178 if (!root_ops) {179 kfree(ri);
180 return NULL;
181 }
182
183 ri->cfg = pci_acpi_setup_ecam_mapping(root); -------(1)
184 if (!ri->cfg) {185 kfree(ri);
186 kfree(root_ops);
187 return NULL;
188 }
189
190 root_ops->release_info = pci_acpi_generic_release_info;
191 root_ops->prepare_resources = pci_acpi_root_prepare_resources;
192 root_ops->pci_ops = &ri->cfg->ops->pci_ops; ----- (2)
193 bus = acpi_pci_root_create(root, root_ops, &ri->common, ri->cfg); ---- (3)
194 if (!bus)
195 return NULL;
196....
202
203 return bus;
204 }
(1) pci_acpi_setup_ecam_mapping(), 建立ecam映射。 arm64上访问pcie的配置空间都是通过ecam机制进行访问,将ecam的空间进行映射,这样cpu就可以通过访问内存访问到相应设备的配置空间。
118 static struct pci_config_window *
119 pci_acpi_setup_ecam_mapping(struct acpi_pci_root *root)
120 {121 struct device *dev = &root->device->dev;
122 struct resource *bus_res = &root->secondary;
123 u16 seg = root->segment;
124 struct pci_ecam_ops *ecam_ops;
125 struct resource cfgres;
126 struct acpi_device *adev;
127 struct pci_config_window *cfg;
128 int ret;
129
130 ret = pci_mcfg_lookup(root, &cfgres, &ecam_ops);
131 if (ret) {132 dev_err(dev, "%04x:%pR ECAM region not found\n", seg, bus_res);
133 return NULL;
134 }
135
136 adev = acpi_resource_consumer(&cfgres);
137 if (adev)
138 dev_info(dev, "ECAM area %pR reserved by %s\n", &cfgres,
139 dev_name(&adev->dev));
140 else
141 dev_warn(dev, FW_BUG "ECAM area %pR not reserved in ACPI namespace\n",
142 &cfgres);
143
144 cfg = pci_ecam_create(dev, &cfgres, bus_res, ecam_ops);
145 if (IS_ERR(cfg)) {146 dev_err(dev, "%04x:%pR error %ld mapping ECAM\n", seg, bus_res,
147 PTR_ERR(cfg));
148 return NULL;
149 }
150
151 return cfg;
152 }
130行:pci_mcfg_lookup(), 通过该接口可以获取ecam的资源以及访问配置空间的操作ecam_ops.
ecam_ops默认是pci_generic_ecam_ops, 定义在drivers/pci/ecam.c中,但也可以由厂商自定义,厂商自定义的ecam_ops实现在drivers/pci/controller/目录下, 比如hisi_pcie_ops和ali_pcie_ops,厂商会依据实际的硬件对ecam进行限制。
107 struct pci_ecam_ops ali_pcie_ops = {108 .bus_shift = 20,
109 .init = ali_pcie_init,
110 .pci_ops = {111 .map_bus = ali_pcie_map_bus,
112 .read = ali_pcie_rd_conf,
113 .write = ali_pcie_wr_conf,
114 }
115 };
144行: pci_ecam_create(), 对ecam的地址进行ioremap,如果定义了ecam_ops->init,还会执行到相应的初始化函数中
(2) 设置root_ops的pci_ops, 这里的pci_ops就是对应上面说的ecam_ops->pci_ops, 即配置空间的访问接口
(3) bus = acpi_pci_root_create(root, root_ops, &ri->common, ri->cfg);
struct pci_bus *acpi_pci_root_create(struct acpi_pci_root *root,
878 struct acpi_pci_root_ops *ops,
879 struct acpi_pci_root_info *info,
880 void *sysdata)
881 {882 int ret, busnum = root->secondary.start;
883 struct acpi_device *device = root->device;
884 int node = acpi_get_node(device->handle);
885 struct pci_bus *bus;
886 struct pci_host_bridge *host_bridge;
887...
906 bus = pci_create_root_bus(NULL, busnum, ops->pci_ops,
907 sysdata, &info->resources);
908 if (!bus)
909 goto out_release_info;
910
911 host_bridge = to_pci_host_bridge(bus->bridge);...
923 pci_scan_child_bus(bus);
924 pci_set_host_bridge_release(host_bridge, acpi_pci_root_release_info,
925 info);
926 if (node != NUMA_NO_NODE)
927 dev_printk(KERN_DEBUG, &bus->dev, "on NUMA node %d\n", node);
928 return bus;
929
930 out_release_info:
931 __acpi_pci_root_release_info(info);
932 return NULL;
933 }
906行: pci_create_root_bus()用来创建该{segment: busnr}下的根总线。传递的参数: NULL是host bridge设备的parent节点; busnum是总线号; ops->pci_ops对应的是ecam->pci_ops,即配置空间的操作接口; sysdata私有数据,对应的是pcie_create_ecam()所返回的pci_cfg_window, 包括ecam的地址范围,映射地址等; info->resource是一个resource_list, 用来保存总线号,I/O空间,mem空间等信息。
2914 struct pci_bus *pci_create_root_bus(struct device *parent, int bus,
2915 struct pci_ops *ops, void *sysdata, struct list_head *resources)
2916 {2917 int error;
2918 struct pci_host_bridge *bridge;
2919
2920 bridge = pci_alloc_host_bridge(0);
2921 if (!bridge)
2922 return NULL;
2923
2924 bridge->dev.parent = parent;
2925
2926 list_splice_init(resources, &bridge->windows);
2927 bridge->sysdata = sysdata;
2928 bridge->busnr = bus;
2929 bridge->ops = ops;
2930
2931 error = pci_register_host_bridge(bridge);
2932 if (error < 0)
2933 goto err_out;
2934
2935 return bridge->bus;
2936
2937 err_out:
2938 kfree(bridge);
2939 return NULL;
2940 }
2920行: 分配struct pci_host_bridge, 一个pci_host_bridge对应一个pci host bridge设备。
2924行:设置该bridge的parent为NULL, 说明host bridge device是最上层设备。
2931行: pci_register_host_bridge()。 注册host bridge device。 该函数比较长,就不贴具体实现了,主要是为host bridge数据结构注册对应的设备,创建了一个根总线pci_bus, 也为该pci_bus数据结构注册一个设备并填充初始化的数据。
dev_set_name(&bridge->dev, "pci%04x:%02x", pci_domain_nr(bus),bridge->busnr); err = device_register(&bridge->dev);dev_set_name(&bus->dev, "%04x:%02x", pci_domain_nr(bus), bus->number);err = device_register(&bus->dev);
2935行:返回root_bus, 即pci_host_bridge的bus成员。
到该函数结束我们已经有了一个root_bus device, 一个host bridge device, 也知道了他们的关系:
回到上面acpi_pci_root_create()函数中,看923行 pci_scan_child_bus(), 现在开始遍历host bridge主桥下的所有pci设备
函数也比较长,列一些关键的函数调用:
pci_scan_child_bus()+-> pci_scan_child_bus_extend()+-> for dev range(0, 256)pci_scan_slot()+-> pci_scan_single_device()+-> pci_scan_device()+-> pci_bus_read_dev_vendor_id()+-> pci_alloc_dev()+-> pci_setup_device()+-> pci_add_device()+-> for each pci bridge+-> pci_scan_bridge_extend()
pci_scan_slot(): 一条pcie总线最多32个设备,每个设备最多8个function, 所以这里pci_scan_child_bus枚举了所有的pcie function, 调用了pci_scan_slot 256次, pci_scan_slot调用pci_scan_single_device()配置当前总线下的所有pci设备。
pci_scan_single_device(): 进一步调用pci_scan_device()和pci_add_device()。 pci_scan_device先去通过配置空间访问接口读取设备的vendor id, 如果60s没读到,说明没有找到该设备。 如果找到该设备,则通过pci_alloc_dev创建pci_dev数据结构,并对pci的配置空间进行一些配置。pci_add_device,软件将pci dev添加到设备list中。
pci_setup_device(): 获取pci设备信息,中断号,BAR地址和大小(使用pci_read_bases, 就是往BAR地址写1来计算的),并保存到pci_dev->resources中。
现在我们已经扫描完了host bridge下的bus和dev, 现在开始扫描bridge, 一个bridge也对应一个pci_dev。比如switch中的每一个port对应一个pci bridge。
pci_scan_bridge_extend()就是用于扫描pci桥和pci桥下的所有设备, 这个函数会被调用2次,第一次是处理BIOS已经配置好的pci桥, 这个是为了兼容各个架构所做的妥协。通过2次调用pci_scan_bridge_extend函数,完成所有的pci桥的处理。
1061 static int pci_scan_bridge_extend(struct pci_bus *bus, struct pci_dev *dev,
1062 int max, unsigned int available_buses,
1063 int pass)
1064 {1065 struct pci_bus *child;......
1078 pci_read_config_dword(dev, PCI_PRIMARY_BUS, &buses);
1079 primary = buses & 0xFF;
1080 secondary = (buses >> 8) & 0xFF;
1081 subordinate = (buses >> 16) & 0xFF;
1082
1083 pci_dbg(dev, "scanning [bus %02x-%02x] behind bridge, pass %d\n",
1084 secondary, subordinate, pass);
1085......
1110 if ((secondary || subordinate) && !pcibios_assign_all_busses() &&
1111 !is_cardbus && !broken) {.......
1145 } else {1146
1151 if (!pass) {1152 if (pcibios_assign_all_busses() || broken || is_cardbus)
1153
1162 pci_write_config_dword(dev, PCI_PRIMARY_BUS,
1163 buses & ~0xffffff);
1164 goto out;
1165 }
1166
1167 /* Clear errors */
1168 pci_write_config_word(dev, PCI_STATUS, 0xffff);
1169
1175 child = pci_find_bus(pci_domain_nr(bus), max+1);
1176 if (!child) {1177 child = pci_add_new_bus(bus, dev, max+1);
1178 if (!child)
1179 goto out;
1180 pci_bus_insert_busn_res(child, max+1,
1181 bus->busn_res.end);
1182 }
1183 max++;
1184 if (available_buses)
1185 available_buses--;
1186
1187 buses = (buses & 0xff000000)
1188 | ((unsigned int)(child->primary) << 0)
1189 | ((unsigned int)(child->busn_res.start) << 8)
1190 | ((unsigned int)(child->busn_res.end) << 16);
1191......
1200
1201 /* We need to blast all three values with a single write */
1202 pci_write_config_dword(dev, PCI_PRIMARY_BUS, buses);
1203
1204 if (!is_cardbus) {1205 child->bridge_ctl = bctl;
1206 max = pci_scan_child_bus_extend(child, available_buses);
1207 } else {.......
1239 }
1240
1241 /* Set subordinate bus number to its real value */
1242 pci_bus_update_busn_res_end(child, max);
1243 pci_write_config_byte(dev, PCI_SUBORDINATE_BUS, max);
1244 }
1245 .......
1268 return max;
1269 }
1078行:一开始读取pci bridge的主bus号,是因为有的体系结构可能已经在BIOS中对这些做过配置。如果需要在kernel中进行scan bridge,就不会进1110行的那个if 分支。
1151行: 第一次pci_scan_bridge的pass参数为0,在这里直接返回。
1187行:生成新的BUS号,准备写入pci配置空间。
1202行: 将该pci bridge的primary bus, secondary bus, subordinate bus写入配置空间。
1206行: 这里又递归调用了pci_scan_child_bus, 扫描该子总线下所有设备。
1242行: 比较关键, 每次递归结束把实际的subordinate bus写入pci桥的配置空间。subordinate bus表示该pci桥下最大的总线号。
最后,在PCIe总线树枚举完成后,返回PCIe总线树中的最后一个pci总线号,PCIe的枚举流程至此结束。
总的来说,枚举流程分为3步:
发现主桥设备和根总线;
发现主桥设备下的所有pci设备
如果主桥下面的设备是pci bridge, 那么再次遍历这个pci bridge桥下的所有pci设备,并以此递归,直到将当前的pci总线树遍历完毕,并且返回host bridge的subordinate总线号。
2.3 pcie的资源分配
pcie枚举完成后,pci总线号已经分配,pcie ecam的映射、 pcie设备信息、BAR的个数及大小等也已经ready, 但此时并没有给各个pci device的BAR, pci bridge的mem, I/O, prefetch mem的base/limit寄存器分配资源。
这时就需要走到pcie的资源分配流程,整个资源分配的过程就是从系统的总资源里给每个pci device的bar分配资源,给每个pci桥的base, limit的寄存器分配资源。
pcie的资源分配流程整体比较复杂,主要介绍下总体的流程,对关键的函数再做展开。
pcie资源分配的入口在pci_acpi_scan_root()->pci_bus_assign_resources()
在调用pci_bus_assign_resources()之前,先调用pci_bus_size_bridges()
pci_bus_size_bridges(): 用深度优先递归确定各级pci桥上base/limit的大小,会记录在pci_dev->resource[PCI_BRIDGE_RESOURCES]中。
再进行资源分配pci_bus_assign_resources():
1376 void __pci_bus_assign_resources(const struct pci_bus *bus,
1377 struct list_head *realloc_head,
1378 struct list_head *fail_head)
1379 {1380 struct pci_bus *b;
1381 struct pci_dev *dev;
1382
1383 pbus_assign_resources_sorted(bus, realloc_head, fail_head);
1384
1385 list_for_each_entry(dev, &bus->devices, bus_list) {1386 pdev_assign_fixed_resources(dev);
1387
1388 b = dev->subordinate;
1389 if (!b)
1390 continue;
1391
1392 __pci_bus_assign_resources(b, realloc_head, fail_head);
1393
1394 switch (dev->class >> 8) {1395 case PCI_CLASS_BRIDGE_PCI:
1396 if (!pci_is_enabled(dev))
1397 pci_setup_bridge(b);
1398 break;
1399
1400 case PCI_CLASS_BRIDGE_CARDBUS:
1401 pci_setup_cardbus(b);
1402 break;
1403
1404 default:
1405 pci_info(dev, "not setting up bridge for bus %04x:%02x\n",
1406 pci_domain_nr(b), b->number);
1407 break;
1408 }
1409 }
1410 }
1383行: pbus_assign_resources_sorted, 这个函数先对当前总线下设备请求的资源进行排序
+-> pbus_assign_resources_sorted()+-> list_for_each_entry(dev, &bus->devices, bus_list)__dev_sort_resources(dev, &head);
+-> __assign_resources_sorted(&head, realloc_head, fail_head);+-> assign_requested_resources_sorted(head, fail_head);+-> list_for_each_entry(dev_res, head, list)pci_assign_resource(dev_res->dev, idx)+-> pci_bus_alloc_resource()+-> allocate_resource()+-> find_resource()+-> request_resource()+->pci_update_resource()
__dev_sort_resources将pci设备使用的资源进行对齐和排序,然后加入到head流程中。
__assign_resources_sorted中先调用find_resource()获取上游pci bridge的所管理的空间资源范围。 再调用request_resource()为当前pci设备分配pcie地址空间,最后调用pci_update_resource()将初始化pcie bar寄存器,将更新的资源区间写到寄存器。
1392行: 和枚举流程一样,这里也是用深度优先遍历的方法,依次分配各个pcie ep设备的bar资源。
1397行: pci_setup_bridge(),某个总线下所有设备BAR空间分配之后,将初始化该总线桥的配置空间中的memory base寄存器(该总线子树下所有设备使用的PCI总线域地址空间的基地址)和memory limit寄存器(总线子树使用的总地址空间的大小)。
总而言之,pcie的资源枚举过程可以概况如下:
获取上游pci 桥设备所管理的系统资源范围
使用DFS对所有的pci ep device进行bar资源的分配
使用DFS对当前pci桥设备的base和limit的值,并对这些寄存器进行更新。
至此,pci树中所有pci设备的BAR寄存器,以及pci桥的base、limit寄存器都已经初始化完毕。
参考资料
PCI Express体系结构导读
PCIe学习笔记之pcie初始化枚举和资源分配流程代码分析相关推荐
- PCIe初始化枚举和资源分配流程分析
本文主要是对PCIe的初始化枚举.资源分配流程进行分析,代码对应的是alikernel-4.19,平台是arm64 1. PCIe architecture 1.1 pcie的拓扑结构 在分析PCIe ...
- PCIe学习笔记之pcie结构和配置空间
PCIe概述 PCI Express,是计算机总线PCI的一种,它沿用现有的PCI编程概念及通信标准,但建基于更快的串行通信系统. PCIE总线使用的是高速差分总线,并采用端到端的连接方式, 现在的高 ...
- RocketMQ学习笔记二之【DefaultMQPushConsumer使用与流程原理分析】
版本: <dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-c ...
- RocketMQ学习笔记四之【DefaultMQPullConsumer使用与流程简单分析】
我们首先看下DefaultMQPullConsumer使用例子: package com.swk.springboot.rocketmq;import java.util.HashMap; impor ...
- PCIe学习笔记之Max payload size
本文基于linux 5.7.0, 平台是arm64 在平时设备的使用过程中,有可能遇到过数据通信错误(malformed tlp), 或者网卡/磁盘在进行数据读写时性能没有达到预期,这些都可能和pci ...
- 学习笔记(一)(x264编码流程)
学习笔记(一)(x264编码流程) 作者 张士辉 11月 2, 2007 <script type=text/javascript></script> <script s ...
- 吴恩达《机器学习》学习笔记七——逻辑回归(二分类)代码
吴恩达<机器学习>学习笔记七--逻辑回归(二分类)代码 一.无正则项的逻辑回归 1.问题描述 2.导入模块 3.准备数据 4.假设函数 5.代价函数 6.梯度下降 7.拟合参数 8.用训练 ...
- 面向对象的编程思想写单片机程序——(3)学习笔记 之 程序分层、数据产生流程
系列文章目录 面向对象的编程思想写单片机程序--(1)学习笔记 之 程序设计 面向对象的编程思想写单片机程序--(2)学习笔记 之 怎么抽象出结构体 面向对象的编程思想写单片机程序--(3)学习笔记 ...
- 软件调试学习笔记(四)—— 异常的处理流程
软件调试学习笔记(四)-- 异常的处理流程 要点回顾 异常的处理流程 实验1:理解调试器与异常的关系 未处理异常:最后一道防线 实验2:理解UnhandledExceptionFilter执行流程 实 ...
最新文章
- 使用字节缓冲流在文件中写内容
- mysql dba系统学习(16)mysql的mysqldump备份 mysql dba系统学习(17)mysql的备份和恢复的完整实践
- eclipse 提交git失败_简单10步教你使用eclipse整合gitee码云实现共享开发
- 那些35岁的程序员都去哪了
- 涉嫌内幕交易?美国SEC对马斯克展开调查
- 程序员小哥月入5万,却被丈母娘拒绝,丈母娘一番话让小哥很尴尬
- java实验报告13答案_(完整版)Java程序设计实验报告
- mysql alter table_mysql alter table 修改表命令详细介绍
- 在Gilt使用Scala、Docker和AWS演化微服务
- 写软件开发需求你必须掌握的规格
- XCode应该是从11.4开始支持Sandbox
- 百度SEO之-权重与排名(含工具网站)
- java:数据结构面试题
- 标准误(Standard Error)
- android进入recovery模式,Android关机重启至recovery安卓进入Recovery模式模式
- WIN7 鼠标右键反应慢如何处理
- Oracle 历史数据表迁移方案
- 源码安装禅道11.0总结
- Android开发新手入门总结(1)
- 解决微信内置浏览器屏蔽下载链接问题