锁竞争分析工具

一、概述

当很多线程争抢同一把锁时,一些线程无法立刻获得锁,而必须睡眠直到某个线程退出临界区。这个争抢过程我们称之为contention。在多核机器上,当多个线程需要操作同一个资源却被一把锁挡住时,便无法充分发挥多个核心的并发能力。现代OS通过提供比锁更底层的同步原语,使得无竞争锁完全不需要系统调用,只是一两条 wait-free,耗时 10-20ns 的原子操作,非常快。而锁一旦发生竞争,一些线程就要陷入睡眠,再次醒来触发了OS的调度代码,代价至少为 3-5us 。所以让锁尽量无竞争,让所有线程“一起起飞”是高性能服务器的永恒话题。

我们当前的 profiler 工具,可以分析在等待锁上花费的时间。等待过程中线程是休眠状态,不会占用 CPU,所以 contention profiler 中的时间并不是 cpu 时间,也不会出现在 cpu profiler 中。cpu profiler 可以抓到特别频繁的锁,因为他花费了很多 CPU,但耗时真正巨大的临界区往往不是那么频繁,而无法被 cpu profiler 发现。

因此,cpu profiler 和 contention profiler 是一种互补关系,前者分析忙碌时间,后者分析被动的等待时间。还有一类是由用户基于 condition(条件)或 sleep 发起的主动等待时间,无需分析。

注意,无竞争的锁不会被采集,我们采集的是在锁上花费的所有等待时间。

目前我们支持 pthread_mutex_t 锁,开启后每秒默认最多采集 1000 个竞争锁。如果一秒内竞争锁的次数超过了 1000,那么每把锁会有 1000/N 的概率被采集。在各类测试场景中,暂没有发现被采集程序的性能有明显变化。

查看更多

生成二进制文件的过程

生成二进制文件的过程

当我们踏入计算机学习的时候,大多数的同学应该都是从如下一个简单的“hello world”,生成一个 a.out 的可执行文件,然后执行他,屏幕上输出一行 “hello world”。

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello world\n");
return 0;
}

然后进行编译,运行。

1
2
3
4
# 编译 main.c 这个文件,会生成一个 a.out 的文件
gcc main.c
# 执行 a.out,会输出 "hello world"
./a.out

查看更多

二进制的运行过程

二进制的运行过程

在上一篇文章中,我们知道了 Linux 下如何生成二进制文件的过程。我们知道首先需要用高级语言写代码,然后预处理器、编译器、汇编器、链接器这些处理完之后,才得到了一个可执行文件。

这个可执行文件运行后,输出 “hello world”。那么这个过程中又发生了什么呢?下面我们一起来学习

一、装载可执行文件

当我们运行一个可执行文件时,操作系统会根据可执行文件(ELF 格式)中的 ELF 头信息确定可执行文件的入口点,然后在内存中为该可执行文件分配一块虚拟地址空间,并将可执行文件装载到该空间中。在装载过程中,操作系统会完成一系列的操作,包括对可执行文件中各个段的访问权限和映射关系进行调整,将代码段、数据段和 BSS 段等映射到适当的虚拟地址空间中,等等。

好,如上很快速的叙述完了,现在来细化看看。首先来看进程虚拟地址空间。

进程虚拟地址空间

查看更多

认识 ELF 文件格式(一)

认识 ELF 文件格式(一)

前面用了两篇文章做引子,简单说明了二进制的生成过程,以及二进制的运行过程。我们接下来进行我们的重点话题,即 ELF 文件的格式,接下来我们一起来了解 ELF 文件的内容格式。

ELF 文件格式不仅针对于可执行文件,还有可重定位文件、共享库、以及核心转储(Core 文件)。

我们主要分享 64 位 ELF 文件格式,他是 32 位的 ELF 文件格式是比较相似的,主要区别也就是某些头部字段和其他数据结构的大小和顺序。

ELF 文件格式实际上可以划分为 4 个模块:ELF 头部、多个程序头、多个节、每个节对应的节头。如下:

查看更多

认识 ELF 文件格式(三)

认识 ELF 文件格式(三)

本篇文章我们介绍 ELF 文件中的程序头

一、程序头

程序头提供了 ELF 文件的段视图,segment 我们将其翻译成段。与节头提供的节视图不一样。节视图仅适用于静态链接。而段视图是在将 ELF 文件加载到进程并执行的时候,定位相关代码和数据,并确定加载到虚拟内存中的内容时,操作系统和动态链接器会用到段视图。

ELF 的段包含零个或多个节,实际上就是把多个节捆绑成单个段。段提供的可执行视图,只有 ELF 二进制文件才会用到他们,而非二进制文件,比如可重定位对象,则用不到他们。

如下则是程序头以及段和节之间的映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# readelf --wide --segments main 

Elf file type is DYN (Shared object file)
Entry point 0x400
There are 10 program headers, starting at offset 52

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00000034 0x00000034 0x00140 0x00140 R 0x4
INTERP 0x000174 0x00000174 0x00000174 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x00000000 0x00000000 0x0074c 0x0074c R E 0x1000
LOAD 0x000ed8 0x00001ed8 0x00001ed8 0x00130 0x00134 RW 0x1000
DYNAMIC 0x000ee0 0x00001ee0 0x00001ee0 0x000f8 0x000f8 RW 0x4
NOTE 0x000188 0x00000188 0x00000188 0x00044 0x00044 R 0x4
TLS 0x000ed8 0x00001ed8 0x00001ed8 0x00000 0x00004 R 0x4
GNU_EH_FRAME 0x000614 0x00000614 0x00000614 0x0003c 0x0003c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
GNU_RELRO 0x000ed8 0x00001ed8 0x00001ed8 0x00128 0x00128 R 0x1

Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .dynamic .got .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .tbss
07 .eh_frame_hdr
08
09 .init_array .fini_array .dynamic .got

查看更多

动态链接

动态链接

一、为什么需要动态链接

静态链接有缺陷

  • 内存占用大。如果是静态库,每个运行的程序都需要使用公共的静态库(例如 glibc.a),那么如果一个静态库 1MB,100 个进程就需要浪费 100MB 的内存
  • 磁盘占用大,如果是静态库,每个二进制都包含了公共的静态库(例如 glibc.a),那么如果一个静态库 1MB,系统上 1000 个二进制,就占用 1GB 的磁盘空间
  • 如果使用静态库,程序的开发、更新、部署、发布都比较困难。一个模块的改动需要整个二进制重新编译。
查看更多

静态链接

本篇内容主要分析静态链接的概念、原理,以及对应的节,以及这些节的内容。

一、链接的由来

比如我们有一个”hello world” 程序,如下:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello world\n");
return 0;
}

我们进行编译:gcc main_03.c -fno-builtin -c -o main_03.o 生成 main_03.o 这个 ELF 文件。 使用 -fno-builtin 是为了禁用内联函数的优化,避免 printf 被优化成 puts。

查看更多

认识 ELF 文件格式(二)

认识 ELF 文件格式(二)

接下来,本篇文章我们来说一下 ELF 文件的节头、以及对应的各个节;他们所存储的数据内容。

一、节头

ELF 文件中的代码和数据在逻辑上被分为连续的非重叠块,称为节(Section)。节没有固定的结构体,节的结构取决于节的内容。每个节都有一个节头,这个节头中存储着这个节的属性、信息。节头的格式都是一致的。

需要注意的是,节只为链接器提供视图,而有些 ELF 文件不需要链接,也就不需要节,同时不需要节头。

如下,我们看看节头的格式,被定义在 /usr/include/elf.h 中,我们只看 64 位的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;

查看更多