Nouveau源码分析(四):NVIDIA设备初始化之nouveau_drm_load (1)
2015-09-01 16:38
501 查看
Nouveau源码分析(四)
probe函数成功返回之后,DRM模块就会调用struct drm_driver的load函数,对应nouveau的nouveau_drm_load.这个函数虽然看起来不是特别长,但每一个调用的函数展开后就会变得非常长了!
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/nouveau_drm.c
364 static int
365 nouveau_drm_load(struct drm_device *dev, unsigned long flags)
366 {
367 struct pci_dev *pdev = dev->pdev;
368 struct nouveau_drm *drm;
369 int ret;
370
371 ret = nouveau_cli_create(nouveau_name(dev), "DRM", sizeof(*drm),
372 (void **)&drm);
373 if (ret)
374 return ret;
375
376 dev->dev_private = drm;
377 drm->dev = dev;
378 nvkm_client(&drm->client.base)->debug =
379 nouveau_dbgopt(nouveau_debug, "DRM");
380
381 INIT_LIST_HEAD(&drm->clients);
382 spin_lock_init(&drm->tile.lock);
383
384 nouveau_get_hdmi_dev(drm);
385
386 /* make sure AGP controller is in a consistent state before we
387 * (possibly) execute vbios init tables (see nouveau_agp.h)
388 */
389 if (pdev && drm_pci_device_is_agp(dev) && dev->agp) {
390 const u64 enables = NV_DEVICE_V0_DISABLE_IDENTIFY |
391 NV_DEVICE_V0_DISABLE_MMIO;
392 /* dummy device object, doesn't init anything, but allows
393 * agp code access to registers
394 */
395 ret = nvif_device_init(&drm->client.base.base, NULL,
396 NVDRM_DEVICE, NV_DEVICE,
397 &(struct nv_device_v0) {
398 .device = ~0,
399 .disable = ~enables,
400 .debug0 = ~0,
401 }, sizeof(struct nv_device_v0),
402 &drm->device);
403 if (ret)
404 goto fail_device;
405
406 nouveau_agp_reset(drm);
407 nvif_device_fini(&drm->device);
408 }
409
410 ret = nvif_device_init(&drm->client.base.base, NULL, NVDRM_DEVICE,
411 NV_DEVICE,
412 &(struct nv_device_v0) {
413 .device = ~0,
414 .disable = 0,
415 .debug0 = 0,
416 }, sizeof(struct nv_device_v0),
417 &drm->device);
418 if (ret)
419 goto fail_device;
420
421 dev->irq_enabled = true;
422
423 /* workaround an odd issue on nvc1 by disabling the device's
424 * nosnoop capability. hopefully won't cause issues until a
425 * better fix is found - assuming there is one...
426 */
427 if (drm->device.info.chipset == 0xc1)
428 nvif_mask(&drm->device, 0x00088080, 0x00000800, 0x00000000);
429
430 nouveau_vga_init(drm);
431 nouveau_agp_init(drm);
432
433 if (drm->device.info.family >= NV_DEVICE_INFO_V0_TESLA) {
434 ret = nouveau_vm_new(nvkm_device(&drm->device), 0, (1ULL << 40),
435 0x1000, &drm->client.vm);
436 if (ret)
437 goto fail_device;
438
439 nvkm_client(&drm->client.base)->vm = drm->client.vm;
440 }
441
442 ret = nouveau_ttm_init(drm);
443 if (ret)
444 goto fail_ttm;
445
446 ret = nouveau_bios_init(dev);
447 if (ret)
448 goto fail_bios;
449
450 ret = nouveau_display_create(dev);
451 if (ret)
452 goto fail_dispctor;
453
454 if (dev->mode_config.num_crtc) {
455 ret = nouveau_display_init(dev);
456 if (ret)
457 goto fail_dispinit;
458 }
459
460 nouveau_sysfs_init(dev);
461 nouveau_hwmon_init(dev);
462 nouveau_accel_init(drm);
463 nouveau_fbcon_init(dev);
464
465 if (nouveau_runtime_pm != 0) {
466 pm_runtime_use_autosuspend(dev->dev);
467 pm_runtime_set_autosuspend_delay(dev->dev, 5000);
468 pm_runtime_set_active(dev->dev);
469 pm_runtime_allow(dev->dev);
470 pm_runtime_mark_last_busy(dev->dev);
471 pm_runtime_put(dev->dev);
472 }
473 return 0;
474
475 fail_dispinit:
476 nouveau_display_destroy(dev);
477 fail_dispctor:
478 nouveau_bios_takedown(dev);
479 fail_bios:
480 nouveau_ttm_fini(drm);
481 fail_ttm:
482 nouveau_agp_fini(drm);
483 nouveau_vga_fini(drm);
484 fail_device:
485 nvif_device_fini(&drm->device);
486 nouveau_cli_destroy(&drm->client);
487 return ret;
488 }
第371行,创建一个nouveau_drm结构体. 这里,我们重新梳理一下nouveau中结构体的线索.
第一条线索 struct nouveau_xxx,这个我们在上一篇中已经见到了很多了,比如nouveau_device nouveau_client nouveau_object [以下简称nv结构体/nv对象]
另一条线索 struct nvif_xxx/struct nouveau_cli,这个我们从这一节会碰到,比如nouveau_cli nvif_client nvif_object nvif_device[以下简称nvif结构体/nvif对象]
每一个nvif对象都会有对应一个nv对象,可以通过ioctl来访问. 比如对一个nvif对象执行ioctl_rd32,就可以执行对应nv对象oclass里的rd32方法.
仔细阅读下面这个图片:
方块里的是类型名称,箭头上的是成员名称 [带*号的表示指针].
我们现在要创建的就是处于最顶层的nouveau_drm,可以看到如果要和上一节创建的nouveau_device对应起来,还有一段路要走.
因为nouveau_drm这个结构体成员包含的面很广,从物理显存、虚拟映射显存,到channel,dma,以及硬件加速移动显存,还有显示模式切换都会有所涉及,所以在此处先不分析这个结构体的每一个成员,先跟着代码走下去吧.
因为struct nouveau_drm第一个成员就是struct nouveau_cli client; 所以可以把nouveau_cli看作是它的base,因此,第371行创建一个struct nouveau_cli,大小是sizeof(struct nouveau_drm) .
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/nouveau_drm.c
102 static int
103 nouveau_cli_create(u64 name, const char *sname,
104 int size, void **pcli)
105 {
106 struct nouveau_cli *cli = *pcli = kzalloc(size, GFP_KERNEL);
107 if (cli) {
108 int ret = nvif_client_init(NULL, NULL, sname, name,
109 nouveau_config, nouveau_debug,
110 &cli->base);
111 if (ret == 0) {
112 mutex_init(&cli->mutex);
113 usif_client_init(cli);
114 }
115 return ret;
116 }
117 return -ENOMEM;
118 }
第106行,kzalloc分配内存.
第108行,因为nouveau_cli的base是nvif_client,所以这里调用nvif_client的init函数.
第112行,如果nvif_client初始化成功的话,就初始化一个mutex .
第113行,初始化usif. 这是个什么东西呢? 其实就是nvif对user空间的一个接口. 让运行在用户态下的程序能创建usif对象,然后进行读写什么的.
这个初始化usif的函数很简单,就是初始化两个链表.
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/nouveau_usif.c
379 void
380 usif_client_init(struct nouveau_cli *cli)
381 {
382 INIT_LIST_HEAD(&cli->objects);
383 INIT_LIST_HEAD(&cli->notifys);
384 }
再来看刚才的nvif_client_init函数:
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/nvif/client.c
69 int
70 nvif_client_init(void (*dtor)(struct nvif_client *), const char *driver,
71 const char *name, u64 device, const char *cfg, const char *dbg,
72 struct nvif_client *client)
73 {
74 int ret, i;
75
76 ret = nvif_object_init(NULL, (void*)dtor, 0, 0, NULL, 0, &client->base);
77 if (ret)
78 return ret;
79
80 client->base.parent = &client->base;
81 client->base.handle = ~0;
82 client->object = &client->base;
83 client->super = true;
84
85 for (i = 0, ret = -EINVAL; (client->driver = nvif_drivers[i]); i++) {
86 if (!driver || !strcmp(client->driver->name, driver)) {
87 ret = client->driver->init(name, device, cfg, dbg,
88 &client->base.priv);
89 if (!ret || driver)
90 break;
91 }
92 }
93
94 if (ret)
95 nvif_client_fini(client);
96 return ret;
97 }
第76行,首先初始化nvif_object:
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/nvif/object.c
217 int
218 nvif_object_init(struct nvif_object *parent, void (*dtor)(struct nvif_object *),
219 u32 handle, u32 oclass, void *data, u32 size,
220 struct nvif_object *object)
221 {
222 struct ctor *ctor;
223 int ret = 0;
224
225 object->parent = NULL;
226 object->object = object;
227 nvif_object_ref(parent, &object->parent);
228 kref_init(&object->refcount);
229 object->handle = handle;
230 object->oclass = oclass;
231 object->data = NULL;
232 object->size = 0;
233 object->dtor = dtor;
234 object->map.ptr = NULL;
235 object->map.size = 0;
236
237 if (object->parent) {
238 if (!(ctor = kmalloc(sizeof(*ctor) + size, GFP_KERNEL))) {
239 nvif_object_fini(object);
240 return -ENOMEM;
241 }
242 object->data = ctor->new.data;
243 object->size = size;
244 memcpy(object->data, data, size);
245
246 ctor->ioctl.version = 0;
247 ctor->ioctl.type = NVIF_IOCTL_V0_NEW;
248 ctor->new.version = 0;
249 ctor->new.route = NVIF_IOCTL_V0_ROUTE_NVIF;
250 ctor->new.token = (unsigned long)(void *)object;
251 ctor->new.handle = handle;
252 ctor->new.oclass = oclass;
253
254 ret = nvif_object_ioctl(parent, ctor, sizeof(*ctor) +
255 object->size, &object->priv);
256 }
257
258 if (ret)
259 nvif_object_fini(object);
260 return ret;
261 }
首先各种填充字段:
这里的oclass是创建nouveau_object的时候用的.
这里的handle是nouveau_object和nvif_object一一对应用的.
然后dtor是析构函数指针,object是nvif_object()用的,其实就是nvif对象间的转换.
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/nvif/object.h
37 #define nvif_object(a) (a)->object
前面说过,每一个nvif_object会对应一个nouveau_object,但对于这个例子来说,创建nouveau_object的代码实际上在第254行,由于object->parent为0,根本执行不到,所以并不会使用这个oclass和handle. [当然,这个nvif_object还是有对应的nouveau_object对象的,只不过不在这里创建了,等会儿会讲到.]
那个if语句的代码也暂时不展开,等到以后真正执行到再回来说. 于是这个函数就这么返回了.
先回到nvif_client_init,下面还有一个循环语句,遍历nvif_drivers:
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/nvif/client.c
58 const struct nvif_driver *
59 nvif_drivers[] = {
60 #ifdef __KERNEL__
61 &nvif_driver_nvkm,
62 #else
63 &nvif_driver_drm,
64 &nvif_driver_lib,
65 #endif
66 NULL
67 };
很明显这是内核里的代码,因此只存在一个nvif_driver,那就是nvif_driver_nvkm.
再看看这个for循环,很明显我们将会调用nvif_driver_nvkm.init():
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/nouveau_nvif.c
125 const struct nvif_driver
126 nvif_driver_nvkm = {
127 .name = "nvkm",
128 .init = nvkm_client_init,
129 .fini = nvkm_client_fini,
130 .suspend = nvkm_client_suspend,
131 .resume = nvkm_client_resume,
132 .ioctl = nvkm_client_ioctl,
133 .map = nvkm_client_map,
134 .unmap = nvkm_client_unmap,
135 .keep = false,
136 };
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/nouveau_nvif.c
109 static int
110 nvkm_client_init(const char *name, u64 device, const char *cfg,
111 const char *dbg, void **ppriv)
112 {
113 struct nouveau_client *client;
114 int ret;
115
116 ret = nouveau_client_create(name, device, cfg, dbg, &client);
117 *ppriv = client;
118 if (ret)
119 return ret;
120
121 client->ntfy = nvkm_client_ntfy;
122 return 0;
123 }
首先创建一个nouveau_client,这个其实就是一个nouveau_object,也就是说在这里创建了这个nvif_object对应的nouveau_object .
第121行,一个有关notify的函数. notify貌似是一个回调工具,创建一个里面包含函数指针,数据什么的;当发生某个条件后,可以触发这个notify,然后就会调用里面的函数.
再来看nouveau_client_create:
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/core/include/core/client.h
39 #define nouveau_client_create(n,c,oc,od,d) \
40 nouveau_client_create_((n), (c), (oc), (od), sizeof(**d), (void **)d)
41
42 int nouveau_client_create_(const char *name, u64 device, const char *cfg,
43 const char *dbg, int, void **);
又一次见到了这个东西,直接去看nouveau_client_create_
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/core/core/client.c
203 int
204 nouveau_client_create_(const char *name, u64 devname, const char *cfg,
205 const char *dbg, int length, void **pobject)
206 {
207 struct nouveau_object *device;
208 struct nouveau_client *client;
209 int ret;
210
211 device = (void *)nouveau_device_find(devname);
212 if (!device)
213 return -ENODEV;
214
215 ret = nouveau_namedb_create_(NULL, NULL, &nouveau_client_oclass,
216 NV_CLIENT_CLASS, NULL,
217 (1ULL << NVDEV_ENGINE_DEVICE),
218 length, pobject);
219 client = *pobject;
220 if (ret)
221 return ret;
222
223 ret = nouveau_handle_create(nv_object(client), ~0, ~0,
224 nv_object(client), &client->root);
225 if (ret)
226 return ret;
227
228 /* prevent init/fini being called, os in in charge of this */
229 atomic_set(&nv_object(client)->usecount, 2);
230
231 nouveau_object_ref(device, &client->device);
232 snprintf(client->name, sizeof(client->name), "%s", name);
233 client->debug = nouveau_dbgopt(dbg, "CLIENT");
234 return 0;
235 }
第211行,首先寻找对应的nouveau_device.
第215行,创建namedb,通过看最开始的结构体,应该也能想象出是个什么东西:
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/core/core/namedb.c
166 int
167 nouveau_namedb_create_(struct nouveau_object *parent,
168 struct nouveau_object *engine,
169 struct nouveau_oclass *oclass, u32 pclass,
170 struct nouveau_oclass *sclass, u64 engcls,
171 int length, void **pobject)
172 {
173 struct nouveau_namedb *namedb;
174 int ret;
175
176 ret = nouveau_parent_create_(parent, engine, oclass, pclass |
177 NV_NAMEDB_CLASS, sclass, engcls,
178 length, pobject);
179 namedb = *pobject;
180 if (ret)
181 return ret;
182
183 rwlock_init(&namedb->lock);
184 INIT_LIST_HEAD(&namedb->list);
185 return 0;
186 }
先创建一个nouveau_parent,这个结构体和nouveau_engine都能把u32 oclass转换成对应的nouveau_oclass *oclass .
接下来初始化namedb的list链表,这个链表和nouveau_handle的node链表相连,表示储存在namedb里的nouveau_handle对象 .
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/core/core/parent.c
110 int
111 nouveau_parent_create_(struct nouveau_object *parent,
112 struct nouveau_object *engine,
113 struct nouveau_oclass *oclass, u32 pclass,
114 struct nouveau_oclass *sclass, u64 engcls,
115 int size, void **pobject)
116 {
117 struct nouveau_parent *object;
118 struct nouveau_sclass *nclass;
119 int ret;
120
121 ret = nouveau_object_create_(parent, engine, oclass, pclass |
122 NV_PARENT_CLASS, size, pobject);
123 object = *pobject;
124 if (ret)
125 return ret;
126
127 while (sclass && sclass->ofuncs) {
128 nclass = kzalloc(sizeof(*nclass), GFP_KERNEL);
129 if (!nclass)
130 return -ENOMEM;
131
132 nclass->sclass = object->sclass;
133 object->sclass = nclass;
134 nclass->engine = engine ? nv_engine(engine) : NULL;
135 nclass->oclass = sclass;
136 sclass++;
137 }
138
139 object->engine = engcls;
140 return 0;
141 }
首先创建一个nouveau_object对象.
然后下面这个while语句,把sclass连成了一个链表放在了object->sclass中.
然后engcls表示可以用来完成u32 oclass转换的engines.
如果要通过一个nouveau_parent对象来完成u32 oclass的转换,会先从object->sclass中查找.
如果没找到,那么就查看object->engine中指示可用的engine,再从这些engine中查找.
此处的sclass实际上是0,所以object->sclass为0. 而且因为这个结构体实际上是nouveau_client,转换u32 oclass的时候还有一个特别的处理,会直接使用client->device,也就是nouveau_device,到时候再具体说.
回到nouveau_client_create_,接下来创建一个nouveau_handle *root.
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/core/core/handle.c
99 int
100 nouveau_handle_create(struct nouveau_object *parent, u32 _parent, u32 _handle,
101 struct nouveau_object *object,
102 struct nouveau_handle **phandle)
103 {
104 struct nouveau_object *namedb;
105 struct nouveau_handle *handle;
106 int ret;
107
108 namedb = parent;
109 while (!nv_iclass(namedb, NV_NAMEDB_CLASS))
110 namedb = namedb->parent;
111
112 handle = kzalloc(sizeof(*handle), GFP_KERNEL);
113 if (!handle)
114 return -ENOMEM;
115
116 INIT_LIST_HEAD(&handle->head);
117 INIT_LIST_HEAD(&handle->tree);
118 handle->name = _handle;
119 handle->priv = ~0;
120
121 ret = nouveau_namedb_insert(nv_namedb(namedb), _handle, object, handle);
122 if (ret) {
123 kfree(handle);
124 return ret;
125 }
126
127 if (nv_parent(parent)->object_attach) {
128 ret = nv_parent(parent)->object_attach(parent, object, _handle);
129 if (ret < 0) {
130 nouveau_handle_destroy(handle);
131 return ret;
132 }
133
134 handle->priv = ret;
135 }
136
137 if (object != namedb) {
138 while (!nv_iclass(namedb, NV_CLIENT_CLASS))
139 namedb = namedb->parent;
140
141 handle->parent = nouveau_namedb_get(nv_namedb(namedb), _parent);
142 if (handle->parent) {
143 list_add(&handle->head, &handle->parent->tree);
144 nouveau_namedb_put(handle->parent);
145 }
146 }
147
148 hprintk(handle, TRACE, "created\n");
149 *phandle = handle;
150 return 0;
151 }
首先把parent向上查找,直到找到一个nouveau_namedb,对于这个例子,parent本来就是一个nouveau_namedb,所以parent == namedb.
接着初始化handle的一些字段,紧接着使用nouveau_namedb_insert把这个插入到namedb中. handle的node链表和namedb的list链表相连.
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/core/core/namedb.c
87 int
88 nouveau_namedb_insert(struct nouveau_namedb *namedb, u32 name,
89 struct nouveau_object *object,
90 struct nouveau_handle *handle)
91 {
92 int ret = -EEXIST;
93 write_lock_irq(&namedb->lock);
94 if (!nouveau_namedb_lookup(namedb, name)) {
95 nouveau_object_ref(object, &handle->object);
96 handle->namedb = namedb;
97 list_add(&handle->node, &namedb->list);
98 ret = 0;
99 }
100 write_unlock_irq(&namedb->lock);
101 return ret;
102 }
这个insert函数首先查找是否有重复的,有就返回-EEXIST,然后用链表连起来,返回.
第127行,检查parent有没有一个关联handle和object的函数,对于这个例子,内存用kzalloc分配,之后也没有对这个进行初始化. 所以不会进入这个if语句.
第137行,如果object和namedb不一样,那么寻找_parent对应的handle,然后把当前handle的head链表加入到parent handle的tree链表中.
对于这个例子object和namedb相等,因此不会执行进去.
这个地方可以顺便看一下nouveau_namedb_get和nouveau_namedb_put:
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/core/core/namedb.c
115 struct nouveau_handle *
116 nouveau_namedb_get(struct nouveau_namedb *namedb, u32 name)
117 {
118 struct nouveau_handle *handle;
119 read_lock(&namedb->lock);
120 handle = nouveau_namedb_lookup(namedb, name);
121 if (handle == NULL)
122 read_unlock(&namedb->lock);
123 return handle;
124 }
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/core/core/namedb.c
159 void
160 nouveau_namedb_put(struct nouveau_handle *handle)
161 {
162 if (handle)
163 read_unlock(&handle->namedb->lock);
164 }
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/core/core/namedb.c
30 static struct nouveau_handle *
31 nouveau_namedb_lookup(struct nouveau_namedb *namedb, u32 name)
32 {
33 struct nouveau_handle *handle;
34
35 list_for_each_entry(handle, &namedb->list, node) {
36 if (handle->name == name)
37 return handle;
38 }
39
40 return NULL;
41 }
代码都很容易理解,自己尝试阅读一下,不多说了. 回到刚才那个函数:
第149行,把phandle赋值为handle,返回.
回到nouveau_client_create_,第229行,初始化几个字段,比如把device字段储存上对应的nouveau_device,这个也就是我们在上一篇中用nouveau_device_create创建的那个.
返回! 至此我们终于可以回到nouveau_drm_load了!
第376行,把新创建的nouveau_drm放进drm提供的结构体里.
第377行,把drm的结构体放进nouveau_drm里.
第378行,一个debug配置信息. 这里的nvkm_xxx就是获得nvif_object所对应的nouveau_object,并把它转换成nouveau_xxx .
第381行,初始化clients链表. 对nouveau设备文件进行open操作时就会创建一个nouveau_cli并加入这个链表. 里面包含的是每个文件特有的东西,比如前面提到的usif .
第384行,获取hdmi设备. 不知道hdmi是什么的可以去百度谷歌一下.
[cpp] view
plaincopyprint?
// /drivers/gpu/drm/nouveau/nouveau_drm.c
336 static void
337 nouveau_get_hdmi_dev(struct nouveau_drm *drm)
338 {
339 struct pci_dev *pdev = drm->dev->pdev;
340
341 if (!pdev) {
342 DRM_INFO("not a PCI device; no HDMI\n");
343 drm->hdmi_device = NULL;
344 return;
345 }
346
347 /* subfunction one is a hdmi audio device? */
348 drm->hdmi_device = pci_get_bus_and_slot((unsigned int)pdev->bus->number,
349 PCI_DEVFN(PCI_SLOT(pdev->devfn), 1));
350
351 if (!drm->hdmi_device) {
352 NV_DEBUG(drm, "hdmi device not found %d %d %d\n", pdev->bus->number, PCI_SLOT(pdev->devfn), 1);
353 return;
354 }
355
356 if ((drm->hdmi_device->class >> 8) != PCI_CLASS_MULTIMEDIA_HD_AUDIO) {
357 NV_DEBUG(drm, "possible hdmi device not audio %d\n", drm->hdmi_device->class);
358 pci_dev_put(drm->hdmi_device);
359 drm->hdmi_device = NULL;
360 return;
361 }
362 }
第341行,检查是否是PCI设备.
第348行,使用PCI模块的函数获取HDMI对应的PCI设备.
第356行,检查获取的HDMI设备的class信息.
接下来的nvif_device_init又是一个大函数,暂且先到这. 下一篇再讨论nvif_device_init函数.
相关文章推荐
- 避免python Popen阻塞
- 线程通信方法
- ActiveMQ的插件开发介绍
- try、finally问题讨论
- python 切片(slice)
- Nouveau源码分析(二):Nouveau结构体的基本框架
- MySQL的biglog文件操作
- 订机票
- MyBatis学习笔记(七)Clob与Blob数据类型与多参数传入
- handlebars-----由于if的条件不足引出的helper
- JS设计模式——命令模式
- Linux匿名管道操作
- Vbs脚本执行不同版本的Office激活任务(MAK&KMS激活)
- oracle imp,exp使用详解
- deque双向队列
- Linux shell 交互式编程、TCL/TK 和 Expect 编译与安装、expect 编程
- linux 安装gtest 单元测试
- uva1584 - Circular Sequence
- [LeetCode169]Majority Element
- Hive相关随笔