编辑
2025-01-22
记录知识
0

根据vDSO--什么是vDSO的实验,我们知道了啥是vDSO,本文基于内核的实现,简单介绍一下vDSO的内核原理。

一、vdso的初始化

在代码arch/arm64/kernel/vdso.c中,我们可以看到vdso的初始化如下:

static struct vm_special_mapping aarch64_vdso_maps[] __ro_after_init = { [AA64_MAP_VVAR] = { .name = "[vvar]", .fault = vvar_fault, .mremap = vvar_mremap, }, [AA64_MAP_VDSO] = { .name = "[vdso]", .mremap = vdso_mremap, }, }; static int __init vdso_init(void) { vdso_info[VDSO_ABI_AA64].dm = &aarch64_vdso_maps[AA64_MAP_VVAR]; vdso_info[VDSO_ABI_AA64].cm = &aarch64_vdso_maps[AA64_MAP_VDSO]; return __vdso_init(VDSO_ABI_AA64); } arch_initcall(vdso_init);

可以看到vdso默认通过arch_initcall拉起来,然后,默认初始化了两个特殊页映射的结构体aarch64_vdso_maps,我们关注__vdso_init如下

static int __vdso_init(enum vdso_abi abi) { int i; struct page **vdso_pagelist; unsigned long pfn; if (memcmp(vdso_info[abi].vdso_code_start, "\177ELF", 4)) { pr_err("vDSO is not a valid ELF object!\n"); return -EINVAL; } vdso_info[abi].vdso_pages = ( vdso_info[abi].vdso_code_end - vdso_info[abi].vdso_code_start) >> PAGE_SHIFT; vdso_pagelist = kcalloc(vdso_info[abi].vdso_pages, sizeof(struct page *), GFP_KERNEL); if (vdso_pagelist == NULL) return -ENOMEM; /* Grab the vDSO code pages. */ pfn = sym_to_pfn(vdso_info[abi].vdso_code_start); for (i = 0; i < vdso_info[abi].vdso_pages; i++) vdso_pagelist[i] = pfn_to_page(pfn + i); vdso_info[abi].cm->pages = vdso_pagelist; return 0; }

这里看到计算了vdso的代码所需页数,然后为其kcalloc申请了页,然后通过页地址找到页帧号,然后再找到物理的页地址。

这里我们完成了vdso的整个初始化过程

二、vdso插入用户内存空间

首先我们留意到一个函数:

arch_setup_additional_pages

此时我们关注fs/binfmt_elf.c的如下函数

static int load_elf_binary(struct linux_binprm *bprm)

它有如下代码:

#ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES retval = arch_setup_additional_pages(bprm, !!interpreter); if (retval < 0) goto out; #endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES */

这里就清楚了,当我们执行一个elf文件的时候,或通过load_elf_binary来解析elf,在这个过程中,我们调用arch_setup_additional_pages将其安插在用户的内存空间布局中。

主要操作如下:

ret = _install_special_mapping(mm, vdso_base, VVAR_NR_PAGES * PAGE_SIZE, VM_READ|VM_MAYREAD|VM_PFNMAP, vdso_info[abi].dm); ret = _install_special_mapping(mm, vdso_base, vdso_text_len, VM_READ|VM_EXEC|gp_flags| VM_MAYREAD|VM_MAYWRITE|VM_MAYEXEC, vdso_info[abi].cm);

这里和进程maps对应上了,如下:

7f93766000-7f93768000 r--p 00000000 00:00 0 [vvar] 7f93768000-7f93769000 r-xp 00000000 00:00 0 [vdso]

三、程序使用vdso

根据上面的信息,我们知道了vdso的初始化,vdso在elf加载的时候默认map到程序内存空间,但是具体的,我们需要知道vdso如何优化syscall的调用的,首先,我们得知道如下图:

image.png 这里以gettimeofday为例,我们需要先关注链接脚本文件vdso.lds.S

SECTIONS { PROVIDE(_vdso_data = . - __VVAR_PAGES * PAGE_SIZE); #ifdef CONFIG_TIME_NS PROVIDE(_timens_data = _vdso_data + PAGE_SIZE); #endif . = VDSO_LBASE + SIZEOF_HEADERS; .hash : { *(.hash) } :text .gnu.hash : { *(.gnu.hash) } .dynsym : { *(.dynsym) } .dynstr : { *(.dynstr) } .gnu.version : { *(.gnu.version) } .gnu.version_d : { *(.gnu.version_d) } .gnu.version_r : { *(.gnu.version_r) } /* * Discard .note.gnu.property sections which are unused and have * different alignment requirement from vDSO note sections. */ /DISCARD/ : { *(.note.GNU-stack .note.gnu.property) } .note : { *(.note.*) } :text :note . = ALIGN(16); .text : { *(.text*) } :text =0xd503201f PROVIDE (__etext = .); PROVIDE (_etext = .); PROVIDE (etext = .); .eh_frame_hdr : { *(.eh_frame_hdr) } :text :eh_frame_hdr .eh_frame : { KEEP (*(.eh_frame)) } :text .dynamic : { *(.dynamic) } :text :dynamic .rodata : { *(.rodata*) } :text _end = .; PROVIDE(end = .); /DISCARD/ : { *(.data .data.* .gnu.linkonce.d.* .sdata*) *(.bss .sbss .dynbss .dynsbss) } }

此时我们查看导出符号

VERSION { LINUX_2.6.39 { global: __kernel_rt_sigreturn; __kernel_gettimeofday; __kernel_clock_gettime; __kernel_clock_getres; local: *; }; }

这里我们知道,用户想要调用gettimeofday,实际上vdso是调用实现的__kernel_gettimeofday,我们追踪此程序的实现:

arch/arm64/kernel/vdso/vgettimeofday.c

int __kernel_gettimeofday(struct __kernel_old_timeval *tv, struct timezone *tz) { return __cvdso_gettimeofday(tv, tz); }

然后我们找到`__cvdso_gettimeofday`的实现在:

lib/vdso/gettimeofday.c

static __maybe_unused int __cvdso_gettimeofday_data(const struct vdso_data *vd, struct __kernel_old_timeval *tv, struct timezone *tz) { if (likely(tv != NULL)) { struct __kernel_timespec ts; if (do_hres(&vd[CS_HRES_COARSE], CLOCK_REALTIME, &ts)) return gettimeofday_fallback(tv, tz); tv->tv_sec = ts.tv_sec; tv->tv_usec = (u32)ts.tv_nsec / NSEC_PER_USEC; } if (unlikely(tz != NULL)) { if (IS_ENABLED(CONFIG_TIME_NS) && vd->clock_mode == VDSO_CLOCKMODE_TIMENS) vd = __arch_get_timens_vdso_data(); tz->tz_minuteswest = vd[CS_HRES_COARSE].tz_minuteswest; tz->tz_dsttime = vd[CS_HRES_COARSE].tz_dsttime; } return 0; } static __maybe_unused int __cvdso_gettimeofday(struct __kernel_old_timeval *tv, struct timezone *tz) { return __cvdso_gettimeofday_data(__arch_get_vdso_data(), tv, tz); }

这里我们关注函数do_hres,其实现如下:

路径:lib/vdso/gettimeofday.c
ts->tv_sec = sec + __iter_div_u64_rem(ns, NSEC_PER_SEC, &ns); ts->tv_nsec = ns;

这里直接给ts赋值即可,如果vdso的实现失效,则切回syscall,如下:

arch/arm64/include/asm/vdso/gettimeofday.h
static __always_inline int gettimeofday_fallback(struct __kernel_old_timeval *_tv, struct timezone *_tz) { register struct timezone *tz asm("x1") = _tz; register struct __kernel_old_timeval *tv asm("x0") = _tv; register long ret asm ("x0"); register long nr asm("x8") = __NR_gettimeofday; asm volatile( " svc #0\n" : "=r" (ret) : "r" (tv), "r" (tz), "r" (nr) : "memory"); return ret; }

这里就存在一个疑问点,我们直接赋值的数据从哪里来。

3.1 vdso的数据vvar

我们已经知道了代码通过vdso下发到直接去数据,我们稍微留意一下就知道这个数据是

__cvdso_gettimeofday_data(__arch_get_vdso_data(), tv, tz);

也就是

static __always_inline const struct vdso_data *__arch_get_vdso_data(void) { return _vdso_data; }

也就是

/* * The vDSO data page. */ static union { struct vdso_data data[CS_BASES]; u8 page[PAGE_SIZE]; } vdso_data_store __page_aligned_data; struct vdso_data *vdso_data = vdso_data_store.data;

这里可以知道了,这个数据来源vvar里面,但是数据如何更新的呢

3.2 vvar的数据更新

对于gettimeofday的函数的实现,我们需要关注timer的核心函数timekeeping_update,代码位置如下:

kernel/time/timekeeping.c

我们关心这句话

update_vsyscall(tk);

其实现在如下:

void update_vsyscall(struct timekeeper *tk) { struct vdso_data *vdata = __arch_get_k_vdso_data(); struct vdso_timestamp *vdso_ts; s32 clock_mode; u64 nsec; /* copy vsyscall data */ vdso_write_begin(vdata); clock_mode = tk->tkr_mono.clock->vdso_clock_mode; vdata[CS_HRES_COARSE].clock_mode = clock_mode; vdata[CS_RAW].clock_mode = clock_mode; /* CLOCK_REALTIME also required for time() */ vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_REALTIME]; vdso_ts->sec = tk->xtime_sec; vdso_ts->nsec = tk->tkr_mono.xtime_nsec; /* CLOCK_REALTIME_COARSE */ vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_REALTIME_COARSE]; vdso_ts->sec = tk->xtime_sec; vdso_ts->nsec = tk->tkr_mono.xtime_nsec >> tk->tkr_mono.shift; /* CLOCK_MONOTONIC_COARSE */ vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_MONOTONIC_COARSE]; vdso_ts->sec = tk->xtime_sec + tk->wall_to_monotonic.tv_sec; nsec = tk->tkr_mono.xtime_nsec >> tk->tkr_mono.shift; nsec = nsec + tk->wall_to_monotonic.tv_nsec; vdso_ts->sec += __iter_div_u64_rem(nsec, NSEC_PER_SEC, &vdso_ts->nsec); /* * Read without the seqlock held by clock_getres(). * Note: No need to have a second copy. */ WRITE_ONCE(vdata[CS_HRES_COARSE].hrtimer_res, hrtimer_resolution); /* * If the current clocksource is not VDSO capable, then spare the * update of the high reolution parts. */ if (clock_mode != VDSO_CLOCKMODE_NONE) update_vdso_data(vdata, tk); __arch_update_vsyscall(vdata, tk); vdso_write_end(vdata); __arch_sync_vdso_data(vdata); }

这里一目了然vdso_ts就是vdata的成员,结构体如下:

struct vdso_data { u32 seq; s32 clock_mode; u64 cycle_last; u64 mask; u32 mult; u32 shift; union { struct vdso_timestamp basetime[VDSO_BASES]; struct timens_offset offset[VDSO_BASES]; }; s32 tz_minuteswest; s32 tz_dsttime; u32 hrtimer_res; u32 __unused; struct arch_vdso_data arch_data; };

所以数据存放在vvar区域,我们定义了一个数据结构,在内核中,我们直接利用vvar区域的数据赋值给vdso的代码调用,也就避免了系统调用。

四、总结

至此,我们从内核的所有方面了解到了vdso的实现原理,相当于内核直接实现了一段代码,作为动态链接放在每个程序上运行,这样就避免了syscall带来的性能问题。

编辑
2025-01-22
记录知识
0

linux系统中有一个很有意思的共享库,名字为linux-vdso.so.1,这个库我们在rootfs中找不到实体,但是每个elf文件都需要链接它。之前和同事讨论的时候,同事想要了解elf的运行原理,我顺便提出了vDSO的这个东西,elf不必多说,相信大家都清楚,本文本着普及了解vDSO的目的,介绍一下什么是vDSO,以及深入了解vDSO。

一、什么是vDSO

vDSO是virtual dynamic shared object,也就是虚拟的动态链接库。

关于vDSO的解释,第一次看到的时候是如下文章,讲解的很仔细,可以看看:

https://www.kernel.org/doc/Documentation/ABI/stable/vdso

对于更详细的文章,可以看如下:

https://lwn.net/Articles/615809/

根据链接的意思,对于每个应用程序,会主动加载vDSO程序到进程空间,这样提供高度优化的syscall方案,也就是加快了系统的syscall的调用性能。

关于arm的实现,我们可以查看如下ppt

image.png

二、系统的vDSO

对于系统中的vDSO,我们两个地方可以查看。以systemd为例

2.1 ldd查看

# ldd /usr/bin/systemd linux-vdso.so.1 (0x0000007f8888a000)

这里我们看到程序未运行时,默认有一个linux-vdso.so.1加载地址

2.2 maps查看

# cat /proc/1/maps | grep "vdso\|vvar" 7f93766000-7f93768000 r--p 00000000 00:00 0 [vvar] 7f93768000-7f93769000 r-xp 00000000 00:00 0 [vdso]

可以看到systemd运行的时候,实际的重定向后的地址是0x7f93768000,这个地址小于ld-2.31,大于其他动态链接库。

7f93736000-7f93737000 rw-p 0000b000 b3:04 143061 /usr/lib/aarch64-linux-gnu/libdrm-cursor.so.1.0.0 7f93737000-7f93738000 rw-p 00000000 00:00 0 7f93738000-7f93759000 r-xp 00000000 b3:04 141832 /usr/lib/aarch64-linux-gnu/ld-2.31.so 7f93759000-7f93765000 rw-p 00000000 00:00 0 7f93766000-7f93768000 r--p 00000000 00:00 0 [vvar] 7f93768000-7f93769000 r-xp 00000000 00:00 0 [vdso] 7f93769000-7f9376a000 r--p 00021000 b3:04 141832 /usr/lib/aarch64-linux-gnu/ld-2.31.so 7f9376a000-7f9376c000 rw-p 00022000 b3:04 141832 /usr/lib/aarch64-linux-gnu/ld-2.31.so

所以这里我们可以获取两个信息:

  • vdso是一个常规动态链接库
  • vdso是ld加载后主动加载的动态库

三、vDSO的位置

带着上面的结论,我们可以知道,这个文件应该是在内核的,所以其位置如下:

arch/arm64/kernel/vdso/vdso.so

因为其是动态链接文件,所以是标准的elf文件,我们可以如下查看:

# readelf -l vdso.so Elf 文件类型为 DYN (共享目标文件) Entry point 0x320 There are 4 program headers, starting at offset 64 程序头: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000990 0x0000000000000990 R E 0x10 DYNAMIC 0x0000000000000860 0x0000000000000860 0x0000000000000860 0x0000000000000110 0x0000000000000110 R 0x8 NOTE 0x00000000000002c8 0x00000000000002c8 0x00000000000002c8 0x0000000000000054 0x0000000000000054 R 0x4 GNU_EH_FRAME 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x8 Section to Segment mapping: 段节... 00 .hash .dynsym .dynstr .gnu.version .gnu.version_d .note .text .eh_frame .dynamic .got .got.plt 01 .dynamic 02 .note 03

我们看看其中的符号,如下

# readelf -s vdso.so Symbol table '.dynsym' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS LINUX_2.6.39 2: 0000000000000780 108 FUNC GLOBAL DEFAULT 7 __kernel_clock_getres@@LINUX_2.6.39 3: 00000000000007f0 8 NOTYPE GLOBAL DEFAULT 7 __kernel_rt_sigreturn@@LINUX_2.6.39 4: 00000000000005c0 424 FUNC GLOBAL DEFAULT 7 __kernel_gettimeofday@@LINUX_2.6.39 5: 0000000000000320 664 FUNC GLOBAL DEFAULT 7 __kernel_clock_gettime@@LINUX_2.6.39

可以发现,这个so就提供了四个函数符号。

也就是说,如果程序调用这四个符号,则默认优先调用vdso,而不是直接系统调用

四、程序实验

为了测试验证vDSO的功能,我们以gettimeofday为例,编写程序,用于测试vDSO,如下是代码

#include <sys/syscall.h> #include <sys/time.h> #include <sys/auxv.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main(int argc, char *argv[]) { struct timeval tv; int i; unsigned int loop; unsigned long sysinfo_ehdr = getauxval(AT_SYSINFO_EHDR); if(argc==1 || (strcmp(argv[1], "--help")==0)){ printf("Usage:\n"); printf("\t %s %s %s %s\n", argv[0], "vdso|syscall", "count", "loop"); return 0; } if (argc == 3) loop = atoi(argv[2]); else { loop = 1000; } printf("pid=%d sysinfo_ehdr(vdso_addr)=%#lx \n", getpid(), sysinfo_ehdr); if (strcmp(argv[1], "vdso") == 0) { int (*ptr)(struct timeval *, void *) = gettimeofday; printf("gettimeofday addr=%p \n", ptr); for (i = 0; i < loop; i++){ gettimeofday(&tv, NULL); } } else if (strcmp(argv[1], "syscall") == 0){ for (i = 0; i < loop; i++) syscall(__NR_gettimeofday, &tv, NULL); } if(argc == 4 && (strcmp(argv[3], "loop")==0)){ while(1){ sleep(60); } } return 0; }

默认此程序如下提示:

# ./test_vdso Usage: ./test_vdso vdso|syscall count loop

我们可以进行两项基准测试:

vdso syscall

count代表循环的次数,loop代表是否进入死循环。

4.1 vdso测试

我们以1次的vdso测试,如下:

# ./test_vdso vdso 1 loop pid=53329 sysinfo_ehdr(vdso_addr)=0x7f96689000 gettimeofday addr=0x7f966895c0

我们拿到了两个地址,一个是vdso_addr=0x7f96689000,一个是函数符号地址 gettimeofday=0x7f966895c0

此时我们可以查看maps,如下:

# cat /proc/$(pidof test_vdso)/maps | grep "\[vdso\]" 7f96689000-7f9668a000 r-xp 00000000 00:00 0 这里看到0x7f96689000能够对应上AT_SYSINFO_EHDR

此时我们查看gettimeofday的符号地址如下:

00000000000005c0 424 FUNC GLOBAL DEFAULT 7 __kernel_gettimeofday@@LINUX_2.6.39

可以发现其计算如下:

0x7f966895c0 = 0x7f96689000 + 00000000000005c0

我们使用ltrace定位如下:

# ltrace ./test_vdso vdso 1 __libc_start_main(0x55756a0a9c, 3, 0x7fddd9a658, 0x55756a0cc0 <unfinished ...> getauxval(33, 0, 0x7fddd9a678, 0x55756a0a9c) = 0x7f80bba000 strcmp("vdso", "--help") = 73 atoi(0x7fddd9b4f9, 0x55756a0d61, 118, 45) = 1 getpid() = 59791 printf("pid=%d sysinfo_ehdr(vdso_addr)=%"..., 59791, 0x7f80bba000pid=59791 sysinfo_ehdr(vdso_addr)=0x7f80bba000 ) = 48 strcmp("vdso", "vdso") = 0 printf("gettimeofday addr=%p \n", 0x7f80bba5c0gettimeofday addr=0x7f80bba5c0 ) = 32 gettimeofday(0x7fddd9a4e8, 0) = 0 __cxa_finalize(0x55756b2008, 0x55756a0a50, 0x11d20, 1) = 0x7f80b4ec70 +++ exited (status 0) +++

这里看到ltrace调用能够定位到其调用了动态库的gettimeofday函数。我们strace查看调用如下:

strace ./test_vdso vdso 1 2>&1 | grep "gettimeofday("

可以发现vdso的时候,调用gettimeofday并不会产生系统调用。

至此,我们可以知道,代码里gettimeofday(&tv, NULL);的调用就是调用的vdso.so里面的__kernel_gettimeofday@@LINUX_2.6.39

此时我们将count放大为1亿次调用,统计时间如下:

# time ./test_vdso vdso 100000000 pid=58465 sysinfo_ehdr(vdso_addr)=0x7f8e1b8000 gettimeofday addr=0x7f8e1b85c0 real 0m3.946s user 0m3.940s sys 0m0.007s 可以发现,用时3.946s

4.2 syscall测试

我们以1次的syscall测试,通过strace查看系统调用,如下:

# strace ./test_vdso syscall 1 2>&1 | grep "gettimeofday(" gettimeofday({tv_sec=1734333991, tv_usec=512630}, NULL) = 0

可以发现其通过syscall下发的gettimeofday。

我们尝试看看ltrace的信息

# ltrace ./test_vdso syscall 1 2>&1 __libc_start_main(0x558d780a9c, 3, 0x7fe207c958, 0x558d780cc0 <unfinished ...> getauxval(33, 0, 0x7fe207c978, 0x558d780a9c) = 0x7fb95b3000 strcmp("syscall", "--help") = 70 atoi(0x7fe207d4f9, 0x558d780d61, 115, 45) = 1 getpid() = 63596 printf("pid=%d sysinfo_ehdr(vdso_addr)=%"..., 63596, 0x7fb95b3000pid=63596 sysinfo_ehdr(vdso_addr)=0x7fb95b3000 ) = 48 strcmp("syscall", "vdso") = -3 strcmp("syscall", "syscall") = 0 syscall(169, 0x7fe207c7e8, 0, 0x11b033b440000) = 0 __cxa_finalize(0x558d792008, 0x558d780a50, 0x11d20, 1) = 0x7fb9547c70 +++ exited (status 0) +++

可以发现ltrace这里没有gettimeofday。

此时我们将count放大为1亿次调用,统计时间如下:

# time ./test_vdso syscall 100000000 pid=64134 sysinfo_ehdr(vdso_addr)=0x7fb55c0000 real 0m16.279s user 0m3.927s sys 0m12.352s

可以发现,用时16.279s,主要耗时在syscall上。

五、结论

我们可以发现,对于vDSO而言,linux设计了一个动态库,使其默认通过vDSO的共享地址调用函数,而不需要使用系统调用,其在1亿次为基准的情况下能够是syscall的5-6倍的性能提升。

编辑
2025-01-22
记录知识
0

tracepoint是驱动编程必学的小技巧,它依附于ftrace系统,本文介绍一个最简单的tracepoint,方便大家在编写subsystem/driver时,为自己的程序安插tracepoint,后面同样会介绍linux中预置的trace event的使用方法。

一、tracepoint举例

我们之前聊过blk_update_request是块设备层的经典函数,在文件系统中,如果读取一个文件,它会先submit_bio来提交到block层,此后,blk_update_request用于更新block的request信息后续直接处理IO。

我们以blk_update_request为例,测试一下其本身的tracepoint,如下:

1.1 查看tracepoint代码

vim block/blk-core.c

先查看头文件

#define CREATE_TRACE_POINTS #include <trace/events/block.h>

此时找到这个h文件,查看如下:

#undef TRACE_SYSTEM #define TRACE_SYSTEM block

然后查看具体函数代码:

bool blk_update_request(struct request *req, blk_status_t error, unsigned int nr_bytes) { int total_bytes; trace_block_rq_complete(req, blk_status_to_errno(error), nr_bytes); if (!req->bio) return false;

我们留意trace_block_rq_complete(req, blk_status_to_errno(error), nr_bytes);此时我们知道tracepoint是block下的block_rq_complete

1.2 查看trace event

根据上面的信息我们找一下路径如下,我们直接进入:

cd /sys/kernel/tracing/events/block/block_rq_complete/

此时我们正常enable即可,

echo 1 > enable

此时我们监听ftrace的日志即可

cat /sys/kernel/debug/tracing/trace_pipe

我们可以看到日志如下:

# cat /sys/kernel/debug/tracing/trace_pipe <idle>-0 [000] ..s. 104.385029: block_rq_complete: 179,0 WS () 13430392 + 96 [0] kworker/3:2H-610 [003] .... 104.385386: block_rq_complete: 179,0 FF () 18446744073709551615 + 0 [0] sshd-3368 [000] ..s. 104.385710: block_rq_complete: 179,0 WFS () 13430488 + 8 [0] sshd-3368 [000] d.s. 104.385733: block_rq_complete: 179,0 WFS () 13430488 + 0 [0] <idle>-0 [000] ..s. 105.877799: block_rq_complete: 179,0 W () 17460720 + 8 [0]

至此tracepoint的演示完成了。其他文章会更详细的演示linux已有的tracepoint

二、写一个tracepoint

为了在自己的驱动写tracepoint,我们需要注意如下几点:

  • 定义一个trace的头文件
  • 为c文件定义一个CREATE_TRACE_POINTS
  • 在代码安插tracepoint

不清楚的可以参考如下:

https://www.kernel.org/doc/html/latest/trace/tracepoints.html

2.1 编写驱动

我们可以编写一个最简单的驱动,使用kthread拉起函数,如下:

#include <linux/init.h> #include <linux/module.h> #include <linux/kthread.h> #include <linux/delay.h> #define CREATE_TRACE_POINTS #include "trace/events/kylin.h" static struct task_struct *thread1; static void test_tracepoint(void) { pr_info("%s starting...\n", __func__); trace_test_kylin("test"); } static int thread_1(void *arg) { while(!kthread_should_stop()){ schedule_timeout_interruptible(msecs_to_jiffies(1000)); test_tracepoint(); } return 0; } static void start_test(void) { thread1 = kthread_run(thread_1, "Thread", "tracepoint-test"); return ; } static int __init test_init(void) { start_test(); return 0; } static void __exit test_exit(void) { kthread_stop(thread1); return; } module_init(test_init); module_exit(test_exit); MODULE_AUTHOR("tangfeng <tangfeng@kylinos.cn>"); MODULE_DESCRIPTION("Test tracepoint"); MODULE_LICENSE("GPL");

这里细节后面可以介绍,先简单看看即可。

2.2 定义头文件

vim include/trace/events/kylin.h
#undef TRACE_SYSTEM #define TRACE_SYSTEM kylin #if !defined(_TRACE_KYLIN_H) || defined(TRACE_HEADER_MULTI_READ) #define _TRACE_KYLIN_H #include <linux/tracepoint.h> TRACE_EVENT(test_kylin, TP_PROTO(const char *name), TP_ARGS(name), TP_STRUCT__entry( __field(const char *, name) ), TP_fast_assign( __entry->name = name ), TP_printk("kylin: %s", __entry->name) ); #endif #include <trace/define_trace.h>

我们需要注意以下几点:

  • 为trace system定义一个名字,例如:#define TRACE_SYSTEM kylin
  • 定义一个TRACE_EVENT
  • 头文件末尾定义添加#include <trace/define_trace.h>

关于TRACE_EVENT的编写,参考如下:

https://lwn.net/Articles/379903/ https://lwn.net/Articles/381064/ https://lwn.net/Articles/383362/

我们只需要关注TRACE_EVENT五要素:

  • name:给到ftrace的name字段
  • prototype:callback的函数声明类型
  • args:输入的参数
  • struct:构造的结构体
  • assign:用参数给构造的结构体赋值
  • print:ftrace输出格式

对于我们的代码,如下定义:

TRACE_EVENT(test_kylin, TP_PROTO(const char *name), TP_ARGS(name), TP_STRUCT__entry( __field(const char *, name) ), TP_fast_assign( __entry->name = name ), TP_printk("kylin: %s", __entry->name) );

对于prototype:我们使用TP_PROTO的宏定义,传入需要跟踪的函数声明即可,我们打算跟踪的是带字符串的trace如下:

trace_test_kylin("test");

所以我们只需要一个const char* 即可

对于args:我们使用TP_ARGS的宏定义,这里传入参数name

对于struct:我们需要新增一个const char*的成员,故通过TP_STRUCT__entry 定义一个__field即可。

对于assign:我们使用TP_fast_assign来将参数赋值给结构体成员变量

对于print:我们使用TP_printk直接按照printk的方式打印结构体的成员变量

2.3 存放头文件

为了使得头文件能够生效,我们定义了TRACE_SYSTEM 为kylin

#undef TRACE_SYSTEM #define TRACE_SYSTEM kylin

这里就要求了头文件的存放位置为include/trace/events,且名字是kylin

include/trace/events/kylin.h

2.4 创建tracepoint

为了在驱动中使用tracepoint,我们需要在include kylin.h之前添加CREATE_TRACE_POINTS的定义,如下:

#define CREATE_TRACE_POINTS #include "trace/events/kylin.h"

2.5 使用tracepoint

接下来我们就直接使用tracepoint即可,在函数直接安插:

static void test_tracepoint(void) { trace_test_kylin("test"); }

三、测试tracepoint

我们写了一个tracepoint,接下来是测试这个tracepoint,当ko加载时,我们可以发现ftrace的events下存在如下文件

# find /sys/kernel/debug/tracing/events/kylin/ /sys/kernel/debug/tracing/events/kylin/ /sys/kernel/debug/tracing/events/kylin/test_kylin /sys/kernel/debug/tracing/events/kylin/test_kylin/format /sys/kernel/debug/tracing/events/kylin/test_kylin/trigger /sys/kernel/debug/tracing/events/kylin/test_kylin/filter /sys/kernel/debug/tracing/events/kylin/test_kylin/id /sys/kernel/debug/tracing/events/kylin/test_kylin/enable /sys/kernel/debug/tracing/events/kylin/enable /sys/kernel/debug/tracing/events/kylin/filter

然后我们准备获取trace的内容,如下:

cat /sys/kernel/debug/tracing/trace_pipe

我们使得我们自己的tracepoint如下:

echo 1 > /sys/kernel/debug/tracing/events/kylin/enable

这样我们trace_pipe可以看到如下日志

# cat /sys/kernel/debug/tracing/trace_pipe | grep test_ thread-141765 [002] .... 9598.458509: test_kylin: kylin: test thread-141765 [002] .... 9599.471840: test_kylin: kylin: test

至此,一个tracepoint的演示完全完成。想必能够有效的帮助大家去编写driver

编辑
2025-01-22
记录知识
0

系统中存在很多linux内核默认预设的trace events,其目的是方便大家查相同问题时能够复用他们的tracepoint,基于此,本文章根据linux内核现成的trace event做分享,主要聊聊如何使用人家定义好的event,从而方便大家调试问题

一、events目录

trace events的目录如下:

# ls /sys/kernel/debug/tracing/events/ alarmtimer fib6 namei sched android_fs filelock napi scmi asoc filemap neigh signal avc ftrace net skb binder gadget nfs smbus block gpio nfs4 sock bpf_test_run header_event nfsd spi bpf_trace header_page nvme sunrpc bridge hwmon oom swiotlb btrfs i2c pagefault sync_trace cfg80211 initcall page_isolation task cgroup iomap pagemap tcp cifs iommu page_pool thermal clk io_uring percpu thermal_ipa_power cma ipi power thermal_power_allocator compaction irq printk timer cpufreq_interactive jbd2 pwm udp cpuhp kmem qdisc v4l2 devfreq kvm ras vb2 dma_fence kyber raw_syscalls virtio_gpu drm mac80211 rcu vmscan dwc3 mali regmap workqueue emulation mdio regulator writeback enable migrate rpcgss xdp error_report mmap rpm xfs ext4 mmc rseq xhci-hcd fib module rtc

我们可以看到linux提供了107个trace system,我们在查问题的时候可以先考虑这些trace system

这里还是以经典函数block_rq_complete为例,可以看到其内容如下:

# find /sys/kernel/debug/tracing/events/block/block_rq_complete/ /sys/kernel/debug/tracing/events/block/block_rq_complete/ /sys/kernel/debug/tracing/events/block/block_rq_complete/format /sys/kernel/debug/tracing/events/block/block_rq_complete/trigger /sys/kernel/debug/tracing/events/block/block_rq_complete/filter /sys/kernel/debug/tracing/events/block/block_rq_complete/id /sys/kernel/debug/tracing/events/block/block_rq_complete/enable

二、示例使用

关于trace evemt的文章,可以查看内核官方文档如下,这里就不按照文档重复讨论了:

https://www.kernel.org/doc/html/latest/trace/events.html

本章主要聚焦在下面这几个文件的使用上:

enable filter format id trigger

2.1 enable

顾名思义,这个就是打开此event的事件,方法如下:

echo 1 > enable

这里和tracepoint基础编程介绍有点重复,就无需展开了。

2.2 format

这里能够查看这个event的格式,我们在tracepoint基础编程介绍可以知道定义一个TRACE_EVENT需要包含五要素:name/prototype/args/struct/assign/print,这里是具体展现。如下示例:

# cat format name: block_rq_complete ID: 1350 format: field:unsigned short common_type; offset:0; size:2; signed:0; field:unsigned char common_flags; offset:2; size:1; signed:0; field:unsigned char common_preempt_count; offset:3; size:1; signed:0; field:int common_pid; offset:4; size:4; signed:1; field:dev_t dev; offset:8; size:4; signed:0; field:sector_t sector; offset:16; size:8; signed:0; field:unsigned int nr_sector; offset:24; size:4; signed:0; field:int error; offset:28; size:4; signed:1; field:char rwbs[8]; offset:32; size:8; signed:0; field:__data_loc char[] cmd; offset:40; size:4; signed:0; print fmt: "%d,%d %s (%s) %llu + %u [%d]", ((unsigned int) ((REC->dev) >> 20)), ((unsigned int) ((REC->dev) & ((1U << 20) - 1))), REC->rwbs, __get_str(cmd), (unsigned long long)REC->sector, REC->nr_sector, REC->error

这里我们知道如下信息:

这个trace里面,结构体提供了从common_type到cmd的所有字段域 提供了这个struct的offset,也就是具体位置 提供了打印格式

2.3 id

这里提供了id,这个id代表这个trace的id,我们可以从format上看到,如下:

ID: 1350

这个id是只读的,所以我们只能读到这个id,用作filter过滤

# cat id 1350

2.4 filter

这个用作过滤,我们可以知道,如果直接enable,我们没办法进行过滤,所以可以在这里添加多个过滤条件,如下:

假设我们只想看idle进程的信息,则如下:

echo "common_pid==0" > filter

如果想要关闭filter,直接echo 0即可

echo 0 > filter

注意,这里的filter只能是struct的成员,如果不是,就会失效,所以提前需要cat format看一下struct哪些成员可以过滤

2.5 trigger

trigger是trace默认提供的触发类型,我们可以通过cat获取可以使用的trigger类型,如下:

# cat trigger # Available triggers: # traceon traceoff stacktrace enable_event disable_event

这里提供了6个trigger,我们如果想要echo trigger,这里需要指定的语法格式,如下:

echo 'command[:count] [if filter]' > trigger

这里可以看到,我们需要填入command: trigger类型,count

次数 if filter 过滤条件。

2.5.1 traceon

这里是检测过滤条件满足的时候,一定次数下直接打开此trace,如下示例

echo 'traceon:5 if common_pid==0' > trigger

这里意思是如果pid为idle下触发blk_update_request达到5次,则主动打开此trace event

2.5.2 traceoff

echo 'traceoff:5 if common_pid==0' > trigger

这里意思是如果pid为idle下触发blk_update_request达到5次,则主动关闭此trace event

2.5.3 stacktrace

这里是打印此函数的堆栈,可以示例如下:

echo "stacktrace:1 if common_pid==0" > trigger

这里是当pid是idle的时候,打印一次堆栈,这样日志如下:

<idle>-0 [000] .Ns. 3780.543922: <stack trace> => trace_event_buffer_commit => trace_event_raw_event_block_rq_complete => blk_update_request => mmc_blk_cqe_complete_rq => mmc_blk_mq_complete => blk_done_softirq => __do_softirq => irq_exit => __handle_domain_irq => gic_handle_irq => el1_irq => cpuidle_enter_state => cpuidle_enter => call_cpuidle => do_idle => cpu_startup_entry => rest_init => arch_call_rest_init => start_kernel

2.5.4 enable_event

这是打开对于的event的trigger,如下:

echo "enable_event:block:block_rq_complete:2" > trigger

当block_rq_complete被触发两次时,打开event

2.5.5 disable_event

对于我们有关闭event的trigger,如下:

echo 'disable_event:block:block_rq_complete:2' > trigger

当block_rq_complete被触发两次时,关闭event

至此,我们简单了解了trace event的使用。

编辑
2025-01-22
记录知识
0

自己经常使用windows平台,通过工具putty和mtputty已经能够做到很好的切屏和操作,最近为了在调试openharmony的时候,经常需要编译,而putty放在后台的编译需要自己手拖,这不是很方便,最近同事分享了tmux,原来虽然自己了解过tmux,但是鉴于第一次学习时觉得比较复杂,就没有使用下来,最近恰巧网上系统的学习了tmux的技巧,编写了适用于自己的tmux配置,目前感觉tmux还是有一定的优势的,故分享自己的tmux配置和用途。

一、安装

安装tmux比较简单,如下

apt install tmux

二、配置tmux文件

为了让tmux在我的环境良好的工作,我自己按照自己习惯设计了如下配置,将其填入

~/.tmux.conf

即可

# 参考:https://louiszhai.github.io/2017/09/30/tmux/ # ctrl+b alt+方向键 调整切屏大小 # 设置第二个prefix为` set-option -g prefix2 ` # 重载配置文件 unbind r bind r source-file ~/.tmux.conf \; display "Configuration Reloaded!" # 开启256 colors set -g default-terminal "screen-256color" # 按竖分割 unbind % bind | split-window -h # 按行分割 unbind '"' bind - split-window -v # 按竖分割,这里默认左边只有40 bind v split-window -v -l 40 # 按行分割,这里默认上面只有120 bind h split-window -h -l 120 # 状态栏左对齐 set -g status-justify left # 非当前窗口有内容更新时在状态栏通知 setw -g monitor-activity on # 关闭rename机制,提高性能 setw -g automatic-rename off setw -g allow-rename off # 绑定C为清空缓冲区 bind C send-keys -R \; send-keys C-l \; clear-history \; send-keys

三、介绍

3.1 工具前缀

tmux默认使用ctrl+b作为命令前缀,可以设置第二个命令前缀为`,这样敲快捷键的时候,就不用一直敲ctrl+b那么麻烦了

set-option -g prefix2 `

3.2 重新加载配置文件

原始配置重新加载需要

ctrl+b : source-file ~/.tmux.conf

比较麻烦,这里使用r键作为快捷键,只需要

`+r

即可,如下是配置详情

unbind r bind r source-file ~/.tmux.conf \; display "Configuration Reloaded!"

3.3 开启256 colors

通常来说,我们的terminal带点颜色好看的

3.4 按竖分割

为了快捷按键按竖分割,这里设置了|,所以我们可以如下

`+|

实现按竖分割

效果如下

image.png 如果我们是看代码,可以按照代码行数分割,如下

image.png

此时命令是

`+h

3.5 按行分割

为了快捷按键按行分割,这里设置了-,所以我们可以如下

`+-

效果如下

image.png 如果我们是在运行编译命令,则可以将编译命令放上面,如下

`+v

效果如下

image.png

这样上面编译代码,下面看代码就行了

3.6 多窗口下切换

当我们开了多窗口下,我们还是需要切换窗口,可以使用如下命令

`+alt+(上下左右)

3.7 多个tmux切换

如果我们开了多个tmux,我们可以使用n来切换,如下

`+n

3.8 清空缓冲区

通过C命令可以清空缓冲区,如下

`+C

3.9 搜索缓冲区内容

如果我们需要检索缓冲区的内容,可以通过如下

`+[ # 进入查看模式 `+s # 开始搜索

四、参考文献

https://louiszhai.github.io/2017/09/30/tmux/