BPF CO-RE 示例代码解析

BPF的可移植性和CO-RE一文的末尾提到了一个名为runqslower的工具,该工具用于展示在CPU run队列中停留的时间大于某一值的任务。现在以该工具来展示如何使用BPF CO-RE。

环境

本地测试的话,建议采用Ubuntu,其内核本身已经开启了BTF选项,无需再对内核进行编译。我用的是Ubuntu 20.10,内核版本5.8.0

  1. # cat /boot//config-$(uname -r)|grep BTF
  2. CONFIG_VIDEO_SONY_BTF_MPX=m
  3. CONFIG_DEBUG_INFO_BTF=y

编译

仅需要在runqslower目录下执行make即可。如果用的是自己生成的vmlinux,则需要在Makefile中增加对VMLINUX_BTF 的定义,值为本地编译的vmlinux的路径,如:

  1. VMLINUX_BTF := /root/linux-5.10.5/vmlinux

BCC和libbpf的转换一文中可以了解到,BPF CO-RE的基本步骤如下,:

  1. 生成包含所有内核类型的头文件vmlinux.h
  2. 使用Clang(版本10或更新版本)将BPF程序的源代码编译为.o对象文件;
  3. 从编译好的BPF对象文件中生成BPF skeleton 头文件(对应runqslower的BPF对象文件为runqslower.bpf.o,也可以通过bpftool gen skeleton runqslower.bpf.o生成skeleton头文件) ;
  4. 在用户空间代码中包含生成的BPF skeleton 头文件(BPF skeleton 头文件是给用户空间使用的);
  5. 最后,编译用户空间代码,这样会嵌入BPF对象代码,后续就不用发布单独的文件。

其中第1、3步分别使用bpftool btf dump filebpftool gen skeleton来生成vmliunx.h和skeleton 头文件。具体使用方式可以参见runqslowerMakefile文件。

运行

直接看下最终的效果,运行如下,可以看到该BPF应用其实就是一个普通的ELF可执行文件(无需独立发布BPF程序和用户侧程序),大小仅为1M左右,如果要在另一台机器运行,直接拷贝过去即可(前提是目标内核开启了CONFIG_DEBUG_INFO_BTF选项)。

  1. # ./runqslower 200
  2. Tracing run queue latency higher than 200 us
  3. TIME COMM PID LAT(us)
  4. 16:45:16 kworker/u256:1 6007 209
  5. 16:45:16 kworker/1:2 6045 1222
  6. 16:45:16 sshd 6045 331
  7. 16:45:16 swapper/0 6045 2120

使用bpftool prog -p可以查看安装的bpf程序:

  1. {
  2. "id": 157,
  3. "type": "tracing",
  4. "name": "handle__sched_w",
  5. "tag": "4eadb7a05d79f434",
  6. "gpl_compatible": true,
  7. "loaded_at": 1611822519,
  8. "uid": 0,
  9. "bytes_xlated": 176,
  10. "jited": true,
  11. "bytes_jited": 121,
  12. "bytes_memlock": 4096,
  13. "map_ids": [71,69
  14. ],
  15. "btf_id": 65,
  16. "pids": [{
  17. "pid": 6012,
  18. "comm": "runqslower"
  19. }
  20. ]
  21. },{
  22. "id": 158,
  23. "type": "tracing",
  24. "name": "handle__sched_s",
  25. "tag": "36ab461bac5b3a97",
  26. "gpl_compatible": true,
  27. "loaded_at": 1611822519,
  28. "uid": 0,
  29. "bytes_xlated": 584,
  30. "jited": true,
  31. "bytes_jited": 354,
  32. "bytes_memlock": 4096,
  33. "map_ids": [71,69,70
  34. ],
  35. "btf_id": 65,
  36. "pids": [{
  37. "pid": 6012,
  38. "comm": "runqslower"
  39. }
  40. ]
  41. }

代码解析

按照上述编译中设计的顺序,首选应该编写BFP层的代码,然后再编写用户空间的代码。BPF CO-RE的处理逻辑基本与BCC保持一致。当触发相关事件时会运行内核空间代码,然后在用户空间接收内核代码传递的信息。

下面以代码注释的方式解析BPF CO-RE的一些使用规范,最后会做一个总结。

代码链接

内核空间(BPF)代码

内核空间代码通常包含如下头文件:

  1. #include "vmlinux.h" /* all kernel types */
  2. #include <bpf/bpf_helpers.h> /* most used helpers: SEC, __always_inline, etc */
  3. #include <bpf/bpf_core_read.h> /* for BPF CO-RE helpers */

内核空间的BPF代码如下(假设生成的.o文件名为runqslower.bpf.o):

  1. // SPDX-License-Identifier: GPL-2.0
  2. // Copyright (c) 2019 Facebook
  3. /* BPF程序包含的头文件,可以看到内容想相当简洁 */
  4. #include "vmlinux.h"
  5. #include <bpf/bpf_helpers.h>
  6. #include "runqslower.h"
  7. #define TASK_RUNNING 0
  8. #define BPF_F_CURRENT_CPU 0xffffffffULL
  9. /* 在BPF代码侧,可以使用一个 const volatile 声明只读的全局变量,只读的全局变量,变量最后会存在于runqslower.bpf.o的.rodata只读段,用户侧可以在BPF程序加载前读取或修改该只读段的参数【1】 */
  10. const volatile __u64 min_us = 0;
  11. const volatile pid_t targ_pid = 0;
  12. /* 定义名为 start 的map,类型为 BPF_MAP_TYPE_HASH。容量为10240,key类型为u32,value类型为u64。可以在【1】中查看BPF程序解析出来的.maps段【2】 */
  13. struct {
  14. __uint(type, BPF_MAP_TYPE_HASH);
  15. __uint(max_entries, 10240);
  16. __type(key, u32);
  17. __type(value, u64);
  18. } start SEC(".maps");
  19. /* 由于 PERF_EVENT_ARRAY, STACK_TRACE 和其他特殊的maps(DEVMAP, CPUMAP, etc) 尚不支持key/value类型的BTF类型,因此需要直接指定 key_size/value_size */
  20. struct {
  21. __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
  22. __uint(key_size, sizeof(u32));
  23. __uint(value_size, sizeof(u32));
  24. } events SEC(".maps");
  25. /* record enqueue timestamp */
  26. /* 自定义的辅助函数必须标记为 static __always_inline。该函数用于保存唤醒的任务事件,key为pid,value为唤醒的时间点 */
  27. __always_inline
  28. static int trace_enqueue(u32 tgid, u32 pid)
  29. {
  30. u64 ts;
  31. if (!pid || (targ_pid && targ_pid != pid))
  32. return 0;
  33. ts = bpf_ktime_get_ns();
  34. bpf_map_update_elem(&start, &pid, &ts, 0);
  35. return 0;
  36. }
  37. /* 所有BPF程序提供的功能都需要通过 SEC() (来自 bpf_helpers.h )宏来自定义section名称【3】。可以在【1】中查看BPF程序解析出来的自定义函数 */
  38. /* 唤醒一个任务,并保存当前时间 */
  39. SEC("tp_btf/sched_wakeup")
  40. int handle__sched_wakeup(u64 *ctx)
  41. {
  42. /* TP_PROTO(struct task_struct *p) */
  43. struct task_struct *p = (void *)ctx[0];
  44. return trace_enqueue(p->tgid, p->pid);
  45. }
  46. /* 唤醒一个新创建的任务,并保存当前时间。BPF的上下文为一个task_struct*结构体 */
  47. SEC("tp_btf/sched_wakeup_new")
  48. int handle__sched_wakeup_new(u64 *ctx)
  49. {
  50. /* TP_PROTO(struct task_struct *p) */
  51. struct task_struct *p = (void *)ctx[0];
  52. return trace_enqueue(p->tgid, p->pid);
  53. }
  54. /* 计算一个任务入run队列到出队列的时间 */
  55. SEC("tp_btf/sched_switch")
  56. int handle__sched_switch(u64 *ctx)
  57. {
  58. /* TP_PROTO(bool preempt, struct task_struct *prev,
  59. * struct task_struct *next)
  60. */
  61. struct task_struct *prev = (struct task_struct *)ctx[1];
  62. struct task_struct *next = (struct task_struct *)ctx[2];
  63. struct event event = {};
  64. u64 *tsp, delta_us;
  65. long state;
  66. u32 pid;
  67. /* ivcsw: treat like an enqueue event and store timestamp */
  68. /* 如果被切换的任务的状态仍然是TASK_RUNNING,说明其又重新进入run队列,更新入队列的时间 */
  69. if (prev->state == TASK_RUNNING)
  70. trace_enqueue(prev->tgid, prev->pid);
  71. /* 获取下一个任务的PID */
  72. pid = next->pid;
  73. /* fetch timestamp and calculate delta */
  74. /* 如果该任务并没有被唤醒,则无法正常进行任务切换,返回0即可 */
  75. tsp = bpf_map_lookup_elem(&start, &pid);
  76. if (!tsp)
  77. return 0; /* missed enqueue */
  78. /* 当前切换时间减去该任务的入队列时间,计算进入run队列到真正调度的毫秒级时间 */
  79. delta_us = (bpf_ktime_get_ns() - *tsp) / 1000;
  80. if (min_us && delta_us <= min_us)
  81. return 0;
  82. /* 更新events section,以便用户侧读取 */
  83. event.pid = pid;
  84. event.delta_us = delta_us;
  85. bpf_get_current_comm(&event.task, sizeof(event.task));
  86. /* output */
  87. bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
  88. &event, sizeof(event));
  89. /* 该任务已经出队列,删除map */
  90. bpf_map_delete_elem(&start, &pid);
  91. return 0;
  92. }
  93. char LICENSE[] SEC("license") = "GPL";

【1】:

  • 用户空间可以且只能通过BPF skeletob方式来访问和更新全局变量,更新后的变量会立即反应到BPF侧。需要注意的是,全局变量只是BPF侧的变量,用户空间其实是通过.rodata间接来操作这类变量,意味着如果用户侧也定义了一个相同的变量,则会被视为两个独立的变量。

  • 用户空间操作全局变量的一般操作如下:

    1. struct <name> *skel = <name>__open();
    2. if (!skel)
    3. /* handle errors */
    4. skel->rodata->my_cfg.feature_enabled = true;
    5. skel->rodata->my_cfg.pid_to_filter = 123;
    6. if (<name>__load(skel))
    7. /* handle errors */
  • 从下面解析的ELF文件的内容可以看到,使用const volatile声明的全局变量min_ustarg_pid位于.rodata(read-only)段,用户空间的应用可以在加载BPF程序前读取或更新BPF侧的全局变量,runqslower通过这种方式设置了min_us的值。

    1. # llvm-objdump -t runqslower.bpf.o
    2. runqslower.bpf.o: file format elf64-bpf
    3. SYMBOL TABLE:
    4. 0000000000000050 l .text 0000000000000000 LBB0_3
    5. 00000000000000a0 l .text 0000000000000000 LBB0_4
    6. 0000000000000100 l .text 0000000000000000 LBB1_3
    7. 0000000000000150 l .text 0000000000000000 LBB1_4
    8. 00000000000001f8 l .text 0000000000000000 LBB2_4
    9. 0000000000000248 l .text 0000000000000000 LBB2_5
    10. 00000000000002e0 l .text 0000000000000000 LBB2_8
    11. 0000000000000388 l .text 0000000000000000 LBB2_9
    12. 0000000000000000 l d .text 0000000000000000 .text
    13. 0000000000000000 g O license 0000000000000004 LICENSE
    14. 0000000000000020 g O .maps 0000000000000018 events #名为 events 的 maps
    15. 0000000000000160 g F .text 0000000000000238 handle__sched_switch #handle__sched_switch 代码段
    16. 0000000000000000 g F .text 00000000000000b0 handle__sched_wakeup #handle__sched_wakeup 代码段
    17. 00000000000000b0 g F .text 00000000000000b0 handle__sched_wakeup_new #handle__sched_wakeup_new 代码段
    18. 0000000000000000 g O .rodata 0000000000000008 min_us #全局变量 min_us
    19. 0000000000000000 g O .maps 0000000000000020 start #名为 start 的 maps
    20. 0000000000000008 g O .rodata 0000000000000004 targ_pid #全局变量 targ_pid
    • skel->rodata 用于只读变量;
    • skel->bss 用于初始值为0的可变量;
    • skel->data 用于初始值非0的可变量。

【2】:

  • 通常一个map具有如下属性:

    • 类型
    • 最大元素数目
    • key的字节大小
    • value的字节大小

    可以使用如下接口对maps进行操作:

    1. bpf_map_operation_elem(&some_map, some, args);

    一般常见的接口如下,可以在内核/用户空间对maps中的元素进行增删改查操作:

    1. bpf_map_lookup_elem
    2. bpf_map_update_elem
    3. bpf_map_delete_elem
    4. bpf_map_push_elem
    5. bpf_map_pop_elem
    6. bpf_map_peek_elem

【3】:

  • 约定的SEC的命名方式如下,libbpf可以根据SEC字段自动检测BPF程序类型,然后关联特定的BPF程序类型,不同的程序类型决定了BPF程序的第一个入参关联的上下文。使用bpftool feature可以查看支持不同程序类型的BPF辅助函数。更多参见section_defs

    • tp/<category>/<name> 用于Tracepoints;
    • kprobe/<func_name> 用于kprobe ,kretprobe/<func_name> 用于kretprobe;
    • raw_tp/<name> 用于原始Tracepoint;
    • cgroup_skb/ingress, cgroup_skb/egress,以及整个cgroup/<subtype> 程序族。
  • tp_btf/sched_wakeuptp_btf/sched_wakeup_newtp_btf/sched_switch跟踪了系统任务上下文切换相关的事件,可以在/sys/kernel/debug/tracing/events/sched下找到对应的事件定义。

  • int handle__sched_wakeup(u64 *ctx)这样的用法仍然属于BCC的使用方式,BPF支持使用BPF_KPROBE/BPF_KRETPROBE来像内核函数一样给BPF程序传参,主要用于tp_btf/fentry/fexit BPF程序。用法如下(更多方式,参见这里):

    1. SEC("kprobe/xfs_file_open")
    2. int BPF_KPROBE(xfs_file_open, struct inode *inode, struct file *file)
    3. {
    4. .......
    5. }

    使用BPF_KPROBE时需要保证,第一个参数必须是一个系统调用,由于tp_btf/sched_wakeuptp_btf/sched_wakeup_newtp_btf/sched_switch并不是系统调用,而是跟踪事件,因此不能使用BPF_KPROBE

用户空间代码

用户侧代码通常包含如下头文件:

  1. #include <bpf/bpf.h>
  2. #include <bpf/libbpf.h>
  3. #include "path/to/your/skeleton.skel.h"

用户侧的主要代码如下:

  1. int libbpf_print_fn(enum libbpf_print_level level,
  2. const char *format, va_list args)
  3. {
  4. if (level == LIBBPF_DEBUG && !env.verbose)
  5. return 0;
  6. return vfprintf(stderr, format, args);
  7. }
  8. static int bump_memlock_rlimit(void)
  9. {
  10. struct rlimit rlim_new = {
  11. .rlim_cur = RLIM_INFINITY,
  12. .rlim_max = RLIM_INFINITY,
  13. };
  14. return setrlimit(RLIMIT_MEMLOCK, &rlim_new);
  15. }
  16. void handle_event(void *ctx, int cpu, void *data, __u32 data_sz)
  17. {
  18. const struct event *e = data;
  19. struct tm *tm;
  20. char ts[32];
  21. time_t t;
  22. time(&t);
  23. tm = localtime(&t);
  24. strftime(ts, sizeof(ts), "%H:%M:%S", tm);
  25. printf("%-8s %-16s %-6d %14llu\n", ts, e->task, e->pid, e->delta_us);
  26. }
  27. void handle_lost_events(void *ctx, int cpu, __u64 lost_cnt)
  28. {
  29. printf("Lost %llu events on CPU #%d!\n", lost_cnt, cpu);
  30. }
  31. int main(int argc, char **argv)
  32. {
  33. static const struct argp argp = {
  34. .options = opts,
  35. .parser = parse_arg,
  36. .doc = argp_program_doc,
  37. };
  38. struct perf_buffer_opts pb_opts;
  39. struct perf_buffer *pb = NULL;
  40. struct runqslower_bpf *obj;
  41. int err;
  42. err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
  43. if (err)
  44. return err;
  45. /* 设置libbpf的日志打印 */
  46. libbpf_set_print(libbpf_print_fn);
  47. /* BPF的BPF maps以及其他内容使用了locked类型的内存, libbpf不会自动设置该值,因此必须手动指定 */
  48. err = bump_memlock_rlimit();
  49. if (err) {
  50. fprintf(stderr, "failed to increase rlimit: %d", err);
  51. return 1;
  52. }
  53. /* 获取BPF对象,程序被编码到了bpf_object_skeleton.data中【1】 */
  54. obj = runqslower_bpf__open();
  55. if (!obj) {
  56. fprintf(stderr, "failed to open and/or load BPF object\n");
  57. return 1;
  58. }
  59. /* initialize global data (filtering options) */
  60. /* 通过.rodata段修改全局变量,注意此时并没有加载BPF程序 */
  61. obj->rodata->targ_pid = env.pid;
  62. obj->rodata->min_us = env.min_us;
  63. /* 将BPF程序(使用mmap方式)加载到内存中 */
  64. err = runqslower_bpf__load(obj);
  65. if (err) {
  66. fprintf(stderr, "failed to load BPF object: %d\n", err);
  67. goto cleanup;
  68. }
  69. /* 附加BPF程序,此时runqslower_bpf.links生效【2】 */
  70. err = runqslower_bpf__attach(obj);
  71. if (err) {
  72. fprintf(stderr, "failed to attach BPF programs\n");
  73. goto cleanup;
  74. }
  75. printf("Tracing run queue latency higher than %llu us\n", env.min_us);
  76. printf("%-8s %-16s %-6s %14s\n", "TIME", "COMM", "PID", "LAT(us)");
  77. pb_opts.sample_cb = handle_event;
  78. pb_opts.lost_cb = handle_lost_events;
  79. pb = perf_buffer__new(bpf_map__fd(obj->maps.events), 64, &pb_opts);
  80. err = libbpf_get_error(pb);
  81. if (err) {
  82. pb = NULL;
  83. fprintf(stderr, "failed to open perf buffer: %d\n", err);
  84. goto cleanup;
  85. }
  86. /* 轮询event事件,并通过挂载的perf钩子打印输出 */
  87. while ((err = perf_buffer__poll(pb, 100)) >= 0)
  88. ;
  89. printf("Error polling perf buffer: %d\n", err);
  90. cleanup:
  91. perf_buffer__free(pb);
  92. runqslower_bpf__destroy(obj);
  93. return err != 0;
  94. }

【1】

  • 用户空间需要接收内核空间传递过来的信息,使用生成的skeleton头文件的如下函数操作内核程序:

    • <name>__open() – 创建并打开 BPF 应用(例如的runqslowerrunqslower_bpf__open()函数);
    • <name>__load() – 初始化,加载和校验BPF 应用部分;
    • <name>__attach() – 附加所有可附加的BPF程序 (可选,可以直接使用libbpf API作更多控制);
    • <name>__destroy() – 分离BPF 程序并使用其使用的所有资源。
  • obj = runqslower_bpf__open();,其中obj的结构体位于runqslower.skel.h,是根据BPF程序自动生成的,内容如下:

    1. struct runqslower_bpf {
    2. struct bpf_object_skeleton *skeleton;
    3. struct bpf_object *obj;
    4. struct {
    5. struct bpf_map *start;
    6. struct bpf_map *events;
    7. struct bpf_map *rodata;
    8. } maps; /* 对应BPF程序中定义的两个.maps以及一个全局只读section .rodata */
    9. struct {
    10. struct bpf_program *handle__sched_wakeup;
    11. struct bpf_program *handle__sched_wakeup_new;
    12. struct bpf_program *handle__sched_switch;
    13. } progs; /* 对应BPF程序使用SEC()定义的3个BPF程序 */
    14. struct {
    15. struct bpf_link *handle__sched_wakeup;
    16. struct bpf_link *handle__sched_wakeup_new;
    17. struct bpf_link *handle__sched_switch;
    18. } links; /* 链接到BPF程序的link,可以使用bpftool link命令查看,可以显示链接的BPF程序,进程等信息 */
    19. struct runqslower_bpf__rodata {
    20. __u64 min_us;
    21. pid_t targ_pid;
    22. } *rodata; /* 对应BPF程序的.rodata section */
    23. };
  • 其实整个处理过程简单归结为:创建runqslower_bpf.skeleton对象,赋值runqslow的信息(maps,progs,links,rodata),其中skeleton->data编码了BPF程序,后续会被解析为Efile对象;然后加载BPF程序,进行初始化和校验;然后attach之后,BPF程序开始正式运行。

【2】

  • Skeleton 可以用于大部分场景,但有一个例外:perf events。这种情况下,不能使用struct <name>__bpf中的links,而应该自定义一个struct bpf_link *links[],原因是perf_event需要在每个CPU上进行操作。例如llcstat.c

    1. static int open_and_attach_perf_event(__u64 config, int period,
    2. struct bpf_program *prog,
    3. struct bpf_link *links[])
    4. {
    5. struct perf_event_attr attr = {
    6. .type = PERF_TYPE_HARDWARE,
    7. .freq = 0,
    8. .sample_period = period,
    9. .config = config,
    10. };
    11. int i, fd;
    12. for (i = 0; i < nr_cpus; i++) {
    13. fd = syscall(__NR_perf_event_open, &attr, -1, i, -1, 0);
    14. if (fd < 0) {
    15. fprintf(stderr, "failed to init perf sampling: %s\n",
    16. strerror(errno));
    17. return -1;
    18. }
    19. links[i] = bpf_program__attach_perf_event(prog, fd);
    20. if (libbpf_get_error(links[i])) {
    21. fprintf(stderr, "failed to attach perf event on cpu: "
    22. "%d\n", i);
    23. links[i] = NULL;
    24. close(fd);
    25. return -1;
    26. }
    27. }
    28. return 0;
    29. }

TIPs

  • 非内核5.3以上的版本中的循环都必须添加#pragma unroll标志

    1. #pragma unroll
    2. for (i = 0; i < 10; i++) { ... }
  • bpf_printk 调试,仅适用于非生产环境

    1. char comm[16];
    2. u64 ts = bpf_ktime_get_ns();
    3. u32 pid = bpf_get_current_pid_tgid();
    4. bpf_get_current_comm(&comm, sizeof(comm));
    5. bpf_printk("ts: %lu, comm: %s, pid: %d\n", ts, comm, pid);
  • BPF涉及到的主要头文件有:

    • libbpf.h: 定义了通用的ebpf ELF对象的加载操作
    • libbpf/include/uapi/linux/bpf.h: 定义了BPF的各种类型(prog_type,map_type,attach_type以及设计的结构体定义等)
    • libbpf/src/bpf.h: 定义了通用的eBPF ELF操作
    • bpf_core_read.h: 定义了读取内核结构的方法
    • bpf_helpers.h: 定义了BPF程序用到的宏SEC()

总结

  • 首先编写BPF程序,定义BPF的maps和sections;
  • 编译BPF程序,然后根据编译出来的.o文件生成对应的skeleton头文件
  • 用户空间的程序包含skeleton头文件,可以通过const volatile定义的全局变量(在加载BPF程序前)给BPF程序传递参数。需要注意的是,全局变量在BPF程序加载后是不可变的,如果要在加载之后给BPF程序传递数据,可以使用map(全局变量就是为了节省在给BPF程序传递常量的情况下存在的,节省查找map的开销);
  • 用户空间执行open->load->attach->destroy来控制BPF程序的生命周期。

下一篇将使用BPF CO-RE方式重写一个XDP程序。

参考

BPF CO-RE 示例代码解析的更多相关文章

  1. 【Android应用开发】 Universal Image Loader ( 使用简介 | 示例代码解析 )

    作者 : 韩曙亮 转载请注明出处 : http://blog.csdn.net/shulianghan/article/details/50824912 相关地址介绍 : -- Universal I ...

  2. pyspider示例代码:解析JSON数据

    pyspider示例代码官方网站是http://demo.pyspider.org/.上面的示例代码太多,无从下手.因此本人找出一下比较经典的示例进行简单讲解,希望对新手有一些帮助. 示例说明: py ...

  3. pyspider示例代码三:用PyQuery解析页面数据

    本系列文章主要记录和讲解pyspider的示例代码,希望能抛砖引玉.pyspider示例代码官方网站是http://demo.pyspider.org/.上面的示例代码太多,无从下手.因此本人找出一些 ...

  4. pyspider示例代码二:解析JSON数据

    本系列文章主要记录和讲解pyspider的示例代码,希望能抛砖引玉.pyspider示例代码官方网站是http://demo.pyspider.org/.上面的示例代码太多,无从下手.因此本人找出一下 ...

  5. VBA常用代码解析

    031 删除工作表中的空行 如果需要删除工作表中所有的空行,可以使用下面的代码. Sub DelBlankRow() DimrRow As Long DimLRow As Long Dimi As L ...

  6. osg 示例程序解析之osgdelaunay

    osg 示例程序解析之osgdelaunay 转自:http://lzchenheng.blog.163.com/blog/static/838335362010821103038928/ 本示例程序 ...

  7. C# WebSocket 服务端示例代码 + HTML5客户端示例代码

    WebSocket服务端 C#示例代码 using System; using System.Collections.Generic; using System.Linq; using System. ...

  8. Swift常用语法示例代码(二)

    此篇文章整理自我以前学习Swift时的一些练习代码,其存在的意义多是可以通过看示例代码更快地回忆Swift的主要语法. 如果你想系统学习Swift或者是Swift的初学者请绕路,感谢Github上Th ...

  9. Swift常用语法示例代码(一)

    此篇文章整理自我以前学习Swift时的一些练习代码,其存在的意义多是可以通过看示例代码更快地回忆Swift的主要语法. 如果你想系统学习Swift或者是Swift的初学者请绕路,感谢Github上Th ...

随机推荐

  1. AjaxControlToolKit CalendarExtender(日历扩展控件)的使用方法

    设置CalendarExtender的TargetControlID为需要显示日期的TextBox的ID,textBox控件的readOnly属性设置为 false ,这样就可以点击textbox控件 ...

  2. C#扫盲篇(二)依赖倒置•控制反转•依赖注入•面向接口编程--满腹经纶的说

    扫盲系列的文章收到了广大粉丝朋友的支持,十分感谢,你们的支持就是我最大动力. 我的扫盲系列还会继续输出,本人也是一线码农,有什么问题大家可以一起讨论.也可以私信或者留言您想要了解的知识点,我们一起进步 ...

  3. java之volatile

    一.谈谈对volatile的理解 volatile是java虚拟机提供的轻量级的同步机制 保证可见性.不保证原子性.禁止指令重排 1.可见性理解:所有线程存放都是主内存的副本(比如某个变量值为25), ...

  4. 快速了解JavaScript的基础知识

    注释 单行注释: // 单行注释 多行注释: /* 多行 注释 */ 历史上 JavaScript 可以兼容 HTML 注释,因此 <!-- 和 --> 也可以是单行注释. x = 1; ...

  5. Spark学习进度10-DS&DF基础操作

    有类型操作 flatMap 通过 flatMap 可以将一条数据转为一个数组, 后再展开这个数组放入 Dataset val ds1=Seq("hello spark"," ...

  6. Tomcat-8 安装和配置

    JDK 安装: # 选择版本: yum list all | grep jdk # 安装openjdk-1.8.0: yum install java-1.8.0-openjdk.x86_64 -y ...

  7. python学习笔记 | strftime()格式化输出时间

    time模块 import time t = time.strftime("%Y-%m-%d %H:%M:%S") print(t) datetime模块 import datet ...

  8. kubernets之向外部应用暴露应用

    一  通过NodePort来暴露服务 前面已经介绍的服务的一些作用,例如将集群内部的应用暴露给集群内部的pod使用,将外部的应用通过服务暴露给内部应用使用,但是服务最大的作用不仅仅是这些 而是将集群内 ...

  9. win10打开IIS服务并发布网站

    1.打开控制面板 win+x后点击控制面板 2.点击程序集下边的解除安装程式 3.点击开启或关闭windows功能 4.找到Internet information services并勾选前面的复选框 ...

  10. 【JAVA并发第三篇】线程间通信

    线程间的通信 JVM在运行时会将自己管理的内存区域,划分为不同的数据区,称为运行时数据区.每个线程都有自己私有的内存空间,如下图示: Java线程按照自己虚拟机栈中的方法代码一步一步的执行下去,在这一 ...