冰澜

  • 首页

  • 标签

  • 归档

  • 搜索

link-load-library

发表于 2019-11-29 | | 阅读次数:

引子

过度优化 - volatile

  • 编译器优化

    1
    2
    3
    4
    5
    x= 0
    Thread1 Thread2
    lock(); lock;
    x++; x++;
    unlock(); unlock();

    变量缓存在寄存器而不写回

  • cpu 动态调度换序

    1
    2
    3
    4
    x = y = 0;
    Thread1 Thread2
    x= 1; y = 1;
    r1 = y; r2 = x;
    1
    2
    3
    4
    x = y = 0;
    Thread1 Thread2
    r1 = y; y = 1;
    x1 = 1; r2 = x;

    编译器指令顺序交换


过度优化 - barrier

1
2
3
4
5
6
7
8
9
10
11
12
volatile T* pInst = 0;

T* getInstance() {
if (pInst == NULL) {
lock();
if (pInst == NULL) {
pInst == new T;
}
unlock();
}
return pInst;
}

c++ 的 new 对象是分 2 个步骤:

  • 分配内存
  • 调用构造函数

pInst = new T 包含了 3 个步骤:

  • 分配内存
  • 在分配内存上调用构造函数
  • 将内存地址复制给 pInst

而步骤 2 和步骤 3 是可以交换的,导致的问题是分配的内存尚未调用构造函数就已经被分配出去


使用 lwsync 指令,防止编译指令换序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define barrier() __asm__ volatile ("lwsync")

volatile T* pInst = 0;

T* getInstance() {
if (pInst != NULL) {
lock();
if (pInst != NULL) {
T* temp = new T;
barrier();
pInst == temp;
}
unlock();
}
return pInst;
}

内存与装载

64 位内核虚拟地址和程序布局空间关系

:scale 100%


可执行文件,虚拟地址空间和物理地址空间的映射关系

:scale 100%


进程堆管理

两种堆分配形式

  • brk 系统调用,设置进程数据段的结束地址,向高地址移动,扩大的部分空间可以被程序使用(sbrk 对 brk 的包装)
  • mmap 向操作系统申请一段虚拟地址空间,可以映射到某个文件,不映射文件时为匿名空间(用于 malloc)

调用惯例

函数调用方和被调用的约定

  • 函数参数的传递顺序和方式(栈传递,寄存器传递;压栈顺序(左到右,右到左))
  • 栈的维护方式,出栈由调用方还是被调用方
  • 名字修饰策略
调用惯例 出栈方 参数传递 名字修饰
cdecl 函数调用方 从右至左 下划线+函数名
stdcall 函数本身 从右至左压栈 下划线+函数名+@+参数字节数,如 int func(int a, double b) -> _func@12
fastcall 函数本身 头两个字节放入寄存器,其他从右至左压栈 @+函数名+@+参数字节数
pascal 函数本身 从左至右压入栈 较复杂

编译

  • 预编译
  • 编译
  • 汇编
  • 链接

预编译

预编译工作:宏展开,删除注释,生成行号和文件标识

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

int main() {
printf("hellow world!\n");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
~ gcc -E hello.c -o hello.i
~ cat hello.i
...
extern char# 840 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 868 "/usr/include/stdio.h" 3 4
# 2 "hello.c" 2
# 3 "hello.c"
int main() {
printf("hellow world!\n");
return 0;
}

编译

  • 词法分析
  • 语法分析
  • 语义分析

词法分析

1
array[index] = (index + 4) * (2 + 6)
symbol array [ index ] = ( index + 4 ) * ( 2 + 6 )
type 标识符 左方括号 标识符 右方括号 赋值 左圆括号 标识符 加号 数字 右圆括号 乘号 左圆括号 数字 加号 数字 右圆括号

语法分析

:scale 100%


语义分析

:scale 100%


汇编

汇编代码转换成机器指令(可理解为 2 进制)

链接

目标文件链接成可执行文件

  • 地址和空间分配 - Address and Storage Allocation
  • 符号决议 - Symbol Resolution
  • 重定位 - Relocation

目标文件

目标文件类型

目标文件类型 含义 示例
Relocatable File 包含代码和数据,可链接为可执行文件或者共享目标文件,静态文件也属于这一类 *.o
Executable File 可执行文件 /bin/bash
Shared Object File 可以和其他重新可定位文件链接成新的共享文件;作为运行进程的映象一部分 *.so
Core Dump File 进程意外终止是的堆栈信息 core dump

目标内容

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int printf(const char* format, ...);

int global_init_var = 84;
int global_uniit_var;

void func1(int i) {
printf("%d\n", i);
}

int main(void) {
static int static_var = 85;
static int static_var2;

int a = 1;
int b;
func1(static_var + static_var2 + a + b);

return a;
}

查看示例代码的 Sections 情况,因为还是目标文件,VMA 和 LMA 地址都是 0,需要在链接的时候确定,objdump 目标文件的一些常见 Section

1
2
3
4
5
6
7
8
9
10
11
12
13
~ objdump -w -h SimpleSection.o

SimpleSection.o: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn Flags
0 .text 00000057 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 00000098 2**2 CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a0 2**2 ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a0 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000012 0000000000000000 0000000000000000 000000a4 2**0 CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000b6 2**0 CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000b8 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

Sections 常见字段含义

字段 含义
.text 代码段
.data 数据段
.bss 未初始化的全局变量和静态变量
.rodata 只读数据段(只读字符串等, eg: “hello %s”)
.comment 编译器版本信息
.note.GNU-stack 堆栈提示段
.eh_frame -

目标文件段分布

使用 readelf 查看目标文件中的所有 Section

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
vagrant@onepiece:~/tmp   readelf -S SimpleSection.o
There are 13 section headers, starting at offset 0x438:

Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040 0000000000000057 0000000000000000 AX 0 0 1
[ 3] .data PROGBITS 0000000000000000 00000098 0000000000000008 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000a0 0000000000000004 0000000000000000 A 0 0 1
[ 4] .bss NOBITS 0000000000000000 000000a0 0000000000000004 0000000000000000 WA 0 0 4
[ 6] .comment PROGBITS 0000000000000000 000000a4 0000000000000012 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000b6 0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000b8 0000000000000058 0000000000000000 A 0 0 8
[10] .symtab SYMTAB 0000000000000000 00000110 0000000000000198 0000000000000018 11 11 8
[11] .strtab STRTAB 0000000000000000 000002a8 000000000000007b 0000000000000000 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000328 0000000000000078 0000000000000018 I 10 1 8
[ 9] .rela.eh_frame RELA 0000000000000000 000003a0 0000000000000030 0000000000000018 I 10 8 8
[12] .shstrtab STRTAB 0000000000000000 000003d0 0000000000000061 0000000000000000 0 0 1

Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

静态链接

静态链接过程

1
2
3
~ gcc -c a.c
~ gcc -c b.c
~ ld a.o b.o -e main -o ab
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* a.c */
extern int shared;

int main()
{
int a = 100;
swap(&a, &shared);
}

/* b.c */
int shared = 1;
void swap(int* a, int *b) {
*a ^= *b ^= *a ^= *b;
}

段合并

合并多个目标文件中的相同 Section, 分配内存和虚拟地址
:scale 80%

符号地址确定

单个目标文件中,每个 Section 的位置已确定,每条指令的 offset 也是固定的,在合并多个目标文件后,
将多个相同 Section 合并,调整对应 Section 中的 offset 得到 Section 合并后的 offset

另外所有的 VMA 地址未初始化,VMA 的值为 64 linux kernel 的用户态虚拟空间的起始地址 0000000000400000
加上入口函数(这里先看做是 main 函数)的偏移量(按页对齐)

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
~ objdump -w -h a.o
Idx Name Size VMA LMA File off Algn Flags
0 .text 0000002e 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 0000006e 2**0 CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 0000006e 2**0 ALLOC
3 .comment 00000012 0000000000000000 0000000000000000 0000006e 2**0 CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000080 2**0 CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 00000080 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
------------------------------------------------------------------------------------------------------------------------------
~ objdump -w -h b.o
Idx Name Size VMA LMA File off Algn Flags
0 .text 0000004b 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 0000008c 2**2 CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000090 2**0 ALLOC
3 .comment 00000012 0000000000000000 0000000000000000 00000090 2**0 CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000a2 2**0 CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 000000a8 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
------------------------------------------------------------------------------------------------------------------------------
~ objdump -w -h ab
Idx Name Size VMA LMA File off Algn Flags
0 .text 00000079 0000000000401000 0000000000401000 00001000 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000058 0000000000402000 0000000000402000 00002000 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000004 0000000000404000 0000000000404000 00003000 2**2 CONTENTS, ALLOC, LOAD, DATA
3 .comment 00000011 0000000000000000 0000000000000000 00003004 2**0 CONTENTS, READONLY
------------------------------------------------------------------------------------------------------------------------------

重定位(指令修正)

可执行文件中需要重定位的符号的指令地址尚未初始化,先查看需要充定位的符号(UND 类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~ readelf -s a.o

Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 46 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared // UND 表示未定义的符号,需要重定位
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap // 同上

查看需要重定位的符号

1
2
3
4
5
6
7
8
9
~ readelf -r a.o
Relocation section '.rela.text' at offset 0x208 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000016 000900000002 R_X86_64_PC32 0000000000000000 shared - 4 // R_X86_64_PC32 相对地址修正
000000000023 000b00000004 R_X86_64_PLT32 0000000000000000 swap - 4 // R_X86_64_PLT32 相对地址修正 + 延迟绑定

Relocation section '.rela.eh_frame' at offset 0x238 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0

查看 a.o 的汇编代码,看看未重定位前的指令地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
~ objdump -d a.o
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 1a <main+0x1a> // shared 变量引用, (00 00 00 00) 0x0 未赋值,
1a: 48 89 c7 mov %rax,%rdi // 0x1a 是下一条指令偏移,lea 指令压入下一条指令地址
1d: b8 00 00 00 00 mov $0x0,%eax
22: e8 00 00 00 00 callq 27 <main+0x27> // callq 调用 swap 函数,0x27 是下一条指令地址
27: b8 00 00 00 00 mov $0x0,%eax
2c: c9 leaveq
2d: c3 retq

查看链接完成后的指令地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
~ objdump -d ab
0000000000401000 <main>:
401000: 55 push %rbp
401001: 48 89 e5 mov %rsp,%rbp
401004: 48 83 ec 10 sub $0x10,%rsp
401008: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
40100f: 48 8d 45 fc lea -0x4(%rbp),%rax
401013: 48 8d 35 e6 2f 00 00 lea 0x2fe6(%rip),%rsi # 404000 <shared> // 0x2fe6 = 0x404000 - 0x40101a
40101a: 48 89 c7 mov %rax,%rdi
40101d: b8 00 00 00 00 mov $0x0,%eax
401022: e8 07 00 00 00 callq 40102e <swap> // e8 00 00 00 00 -> e8 07 00 00 00; 0x7= 0x40102e - 0x401027
401027: b8 00 00 00 00 mov $0x0,%eax
40102c: c9 leaveq
40102d: c3 retq

000000000040102e <swap>:
40102e: 55 push %rbp
40102f: 48 89 e5 mov %rsp,%rbp
401032: 48 89 7d f8 mov %rdi,-0x8(%rbp)
401036: 48 89 75 f0 mov %rsi,-0x10(%rbp)

common块之强弱符号

强符号:函数和已初始化的全局变量为;弱符号:未初始化的全局变量

1
2
3
4
5
6
7
8
9
extern int ext;

int weak; // 弱符号
int string = 1; // 强符号
__attribute__((weak)) weak2 = 2; // 弱符号

int main() { // 强符号
return 0;
}

多个文件中出现对同一个文件的定义,比如

1
2
/* a. c */  |   /* b.c */
int a; | long a;
  • 多个强符号冲突,编译失败
  • 强符号和弱符号同时定义,选强符号
  • 多个弱符号,选类型最大的弱符号
  • 定义弱引用,编译时不会报错,运行时报错,可用于链接库函数(比如程序是否支持多线程版本 ‘-lphread’)
  • 使用 ‘-fno-common’ 禁用弱引用

全局构造和析构

main 函数只是编写代码的入口,实际程序运行的入口是 _start,c++ 使用特殊的 Section 控制全局构造和析构

  • .init 构成进程的初始化代码,在 main 函数之前调用
  • .fini 构成进程的终止代码,在 main 函数退出后执行

静态库链接

静态库是一组目标文件的集合,基本上其中的一个目标文件只包含一个函数,链接时从库中找到具体的函数实现链接成可执行文件

1
2
3
4
5
6
7
8
9
~ ar -t /usr/lib/libc.a | grep printf
vfprintf.o
vprintf.o
printf_fp.o
reg-printf.o
printf-prs.o
printf_fphex.o
printf_size.o
fprintf.o

如何不使用 # include <stdio.h> 实现 hello.c 中对标准库中 printf 函数的引用

1
2
3
/* hello.c */
int main() {
}
1
2
~ ar -x /usr/lib/libc.a  -> 得到 printf.o 目标文件
~ ld hello.o printf.o -> 实际上也会失败,因为 printf 还依赖其他目标文件(vfpintf.o),但可以按照层次去解决

链接成可执行文件需要的库和目标文件

  • crt1.o
  • crti.o
  • crtbeginT.o
  • libgcc.a
  • libgcc_eh.a
  • libc.a
  • crtend.o
  • crtn.o

crt1.o、crti.o 和 crtn.o 均是 glibc 运行库启动文件的一部分,运行库部分讲解!!

静态链接示例

静态链接的一个例子,代码和分布

1
2
3
4
5
6
7
8
9
#include <stdlib.h>
#include <unistd.h>

int main() {
while (1) {
sleep(1000);
}
return 0;
}
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
33
34
35
36
37
38
39
40
~ readelf -W -S SectionMapping.elf | column
There are 31 section headers, starting at offset 0xbc330:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-id NOTE 0000000000400200 000200 000024 00 A 0 0 4
[ 2] .note.ABI-tag NOTE 0000000000400224 000224 000020 00 A 0 0 4
[ 3] .rela.plt RELA 0000000000400248 000248 000240 18 AI 0 19 8
[ 4] .init PROGBITS 0000000000401000 001000 00001b 00 AX 0 0 4
[ 5] .plt PROGBITS 0000000000401020 001020 0000c0 00 AX 0 0 8
[ 6] .text PROGBITS 00000000004010e0 0010e0 07e1c0 00 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 000000000047f2a0 07f2a0 000aa0 00 AX 0 0 16
[ 8] .fini PROGBITS 000000000047fd40 07fd40 00000d 00 AX 0 0 4
[ 9] .rodata PROGBITS 0000000000480000 080000 01aa8c 00 A 0 0 32
[10] .stapsdt.base PROGBITS 000000000049aa8c 09aa8c 000001 00 A 0 0 1
[11] .eh_frame PROGBITS 000000000049aa90 09aa90 00a1ac 00 A 0 0 8
[12] .gcc_except_table PROGBITS 00000000004a4c3c 0a4c3c 0000b1 00 A 0 0 1
[13] .tdata PROGBITS 00000000004a6140 0a5140 000020 00 WAT 0 0 8
[14] .tbss NOBITS 00000000004a6160 0a5160 000040 00 WAT 0 0 8
[15] .init_array INIT_ARRAY 00000000004a6160 0a5160 000010 08 WA 0 0 8
[16] .fini_array FINI_ARRAY 00000000004a6170 0a5170 000010 08 WA 0 0 8
[17] .data.rel.ro PROGBITS 00000000004a6180 0a5180 002d74 00 WA 0 0 32
[18] .got PROGBITS 00000000004a8ef8 0a7ef8 0000f0 00 WA 0 0 8
[19] .got.plt PROGBITS 00000000004a9000 0a8000 0000d8 08 WA 0 0 8
[20] .data PROGBITS 00000000004a90e0 0a80e0 001a50 00 WA 0 0 32
[21] __libc_subfreeres PROGBITS 00000000004aab30 0a9b30 000048 00 WA 0 0 8
[22] __libc_IO_vtables PROGBITS 00000000004aab80 0a9b80 0006a8 00 WA 0 0 32
[23] __libc_atexit PROGBITS 00000000004ab228 0aa228 000008 00 WA 0 0 8
[24] .bss NOBITS 00000000004ab240 0aa230 001758 00 WA 0 0 32
[25] __libc_freeres_ptrs NOBITS 00000000004ac998 0aa230 000028 00 WA 0 0 8
[26] .comment PROGBITS 0000000000000000 0aa230 000011 01 MS 0 0 1
[27] .note.stapsdt NOTE 0000000000000000 0aa244 000048 00 0 0 4
[28] .symtab SYMTAB 0000000000000000 0aa290 00b0a0 18 29 752 8
[29] .strtab STRTAB 0000000000000000 0b5330 006eb5 00 0 0 1
[30] .shstrtab STRTAB 0000000000000000 0bc1e5 000144 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

静态链接内存映射

查看编译好的 elf 文件的 Segment(将 Section 合并得到 Segment) 信息

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
~ readelf -W -l SectionMapping.elf

Elf file type is EXEC (Executable file)
Entry point 0x401ac0
There are 8 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x000488 0x000488 R 0x1000
LOAD 0x001000 0x0000000000401000 0x0000000000401000 0x07ed4d 0x07ed4d R E 0x1000
LOAD 0x080000 0x0000000000480000 0x0000000000480000 0x024ced 0x024ced R 0x1000
LOAD 0x0a5140 0x00000000004a6140 0x00000000004a6140 0x0050f0 0x006880 RW 0x1000
NOTE 0x000200 0x0000000000400200 0x0000000000400200 0x000044 0x000044 R 0x4
TLS 0x0a5140 0x00000000004a6140 0x00000000004a6140 0x000020 0x000060 R 0x8
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x0a5140 0x00000000004a6140 0x00000000004a6140 0x002ec0 0x002ec0 R 0x1

Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .note.ABI-tag .rela.plt
01 .init .plt .text __libc_freeres_fn .fini
02 .rodata .stapsdt.base .eh_frame .gcc_except_table
03 .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data __libc_subfreeres __libc_IO_vtables __libc_atexit .bss __libc_freeres_ptrs
04 .note.gnu.build-id .note.ABI-tag
05 .tdata .tbss
06
07 .tdata .init_array .fini_array .data.rel.ro .got

程序运行起来后的实际内存映射情况

1
2
3
4
5
6
7
8
9
10
11
12
13
~ ./SectionMapping.elf &
[1] 106421
~ cat /proc/106421/maps
00400000-00401000 r--p 00000000 08:02 9571400 /home/vagrant/compile-link-load/SectionMapping.elf
00401000-00480000 r-xp 00001000 08:02 9571400 /home/vagrant/compile-link-load/SectionMapping.elf
00480000-004a5000 r--p 00080000 08:02 9571400 /home/vagrant/compile-link-load/SectionMapping.elf
004a6000-004ac000 rw-p 000a5000 08:02 9571400 /home/vagrant/compile-link-load/SectionMapping.elf
004ac000-004ad000 rw-p 00000000 00:00 0
01539000-0155c000 rw-p 00000000 00:00 0 [heap]
7ffffa319000-7ffffa33a000 rw-p 00000000 00:00 0 [stack]
7ffffa367000-7ffffa36a000 r--p 00000000 00:00 0 [vvar]
7ffffa36a000-7ffffa36b000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

动态链接

什么是动态链接

为什么需要动态链接?

  • 静态链接浪费内存空间,对任何公共库函数每个函数都要链接进可执行文件
  • 不利于程序的开发和发布,任务静态库更新后,可执行文件需要重新编译

什么是动态链接?

  • 程序的模块分隔开,形成独立的文件,不再将他们链接在一起;等程序运行时链接需要的模块
  • program1 和 program2 都依赖 lib.so
  • program1 运行时发现依赖 lib.so,操作系统将 lib.so 加载至内存,开始链接过程(符号解析、地址定位等
  • 运行 program2,加载 program2 是发现其依赖 lib.so,而此时内存中已经有 lib.so 的 副本,则不需要重新加载,只需要链接 program2 和 lib.so

动态链接优势?

  • 节省内存
  • 减少物理页面的换入和换出
  • 增加 cpu 缓存命中率
  • 动态的加载各种程序模块(插件开发)

缺点?

  • 动态链接的版本不一致,api 接口变动等,导致程序不能运行 (DLL Hell)
  • 程序加载时需要代码和数据的重定位(GOT 定位),加上间接寻址,导致程序的运行速度变慢(启动速度)

地址无关代码

编译时指定 -fPIC 可让动态库编译成地址无关(-fpic 和 -fPIC 功能一致,但包小,会出现兼容问题,一般使用 -fPIC);
从静态链接的链接过程中,我们也可以按照静态链接的方式链接动态库,对动态链接中的绝对地址做基址重置;但做基址重置时基址是调用该库的程序的基址,
比如 program1 调用库的需要基址重置修改库中的指令地址,而 program2 调用库也需要修改指令地址;
这样 program1 和 program2 需要自己维护各自的库指令和数据,失去了动态链接的意义。

地址无关:把指令中需要被修改的部分分离出来,跟数据块放在一起,指令部分可以保持不变,数据部分可以再进程中拥有自己的副本。

共享库中地址引用的方式:

  • 模块内部调用和跳转:调用函数和调用者在同一个模块,位置相对固定,使用相对地址调用或寄存器相对调用
  • 模块内部数据访问:.text 和 .data 的相对位置也是固定的,也可以使用相对地址调用
  • 模块间数据访问和模块间调用和跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
static int a;
extern int b; // b 在其他动态模块中
extern void ext(); // ext 函数在其他模块中

void bar() {
a = 1;
b = 2;
}

void foo() {
bar();
ext();
}

代码中 b 和 ext 的目标地址需要等到装载时才能决定,引入 GOT 表( GLOBAL Offset Table),GOT 表存储在 .data 字段,那么 GOT 表和代码段的相对位置可以固定
GOT 表存存储需要访问的外部变量的地址,则通过一次间接寻址可以获取 b 的实际地址;但编译时是 GOT 表中的对应项是 0,在程序加载时确定外部模块的地址后才能由动态链接器填入具体值

GOT 实现原理

:scale 80%


延迟绑定 PLT

动态链接相对灵活,却牺牲一部分性能

  • 全局和静态数据的间接寻址
  • 模块间调用的GOT重定位; 动态链接是运行时完成,寻找并装共享对象,进行符号地址重定位

在程序执行前,动态链接会耗费时间解决模块之间的函数引用符号的查找和重定位,然后很多函数可能很少用到,如错误处理函数。 ELF 采用延迟绑定(lazing binding)
, 当函数第一次被用到是才进行绑定,如果没用用到就不绑定。ELF 使用 PLT (procedure linkage table)来实现;在 GOT 实现中,外部变量的引用的间接寻址的值由链接器加载时填入,使用 PLT 可以延迟这个过程。

:scale 100%

动态链接结构

  • .interp 共享库使用的解释器,一般为 /lib/ld-linux.so
  • .dynampic 保存动态链接需要的用到的信息;动态链接符号地址,动态链接字符串地址,重定位表入口等
  • .dynsym 保存于动态链接相关的符号
  • .symtab 保存所有的符号
  • .rela.dyn 对数据引用修正,修正位置位于.got
  • .rela.plt 对函数引用修正,修正位置位于 .got.plt

动态链接过程

  • ld-linux.so 自举
  • 装载共享对象
  • 重定位和初始化

显示运行时链接

  • dlopen
  • dlsym
  • dlerror
  • dlclose

共享库的组织

命名规范

1
libname.so.x.y.z

lib 是前缀,中间是库名字

  • x 主版本号 重大升级,不同主版本号不兼容
  • y 次版本号,增量升级,增加新接口,保持原来符号不变
  • z 发布版本号,错误修正,性能改进,不添加任何新接口

so-name 机制

每个共享库都有一个 so-name, 去掉次版本号和发布版本号,共享库 /lib/libfoo.so.2.6.1 对应的 so-name 为 /lib/libfoo.so.2 的软链接

环境变量

  • LD_LIBRARY_PATH 设置搜索路径,调试动态库
  • LD_PRELOAD 预先加载覆盖后加载的共享库,用于测试
  • LD_DEBUG 打印调试信息,打印装载过程

实战

  • segfault dmseg
  • 错误线索
  • 定位步骤

segfault dmesg

戳我!
segfault 产生时会在系统的日志中记录错误信息,可以用 dmesg 查看

1
2
~ dmesg | grep testp
testp[19288]: segfault at 0 ip 0000000000401271 sp 00007fff2ce4d210 error 4 in testp[400000+98000]
  • testp[19288] 是发错段错误的 PID
  • segfault at 0 表示程序访问地址 0 是发生了段错误;地址 0 可能访问了空指针。
  • ip 0000000000401271 是指令之指针(instruction pointer),在 64-bit x86 下对应的寄存器为 %rip
  • sp 00007fff2ce4d210 是栈指针(stack pointer), 在 64-bit x86 下对应的寄存器为 %rsp
  • error 4 是 16 进制的错误码; 一般情况下至少为 4 表示是用户态的错误;4 表示读一个未映射的内存(unmapped area),6(4+2)表示写一个未映射的内存
  • in testp[400000+98000] 段错误的程序是 testp, 其 ip 所在的虚拟地址范围为 0x400000 ~ 0x400000+0x98000, 0x98000 表示是该程序映射的大小

错误线索

  • curl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    vagrant@archlinux:~/nos-openresty  master ✗ curl -v 'http://127.0.0.1:1989?ip=121.193.184.2'
    * Rebuilt URL to: http://127.0.0.1:1989/?ip=121.193.184.2
    * Trying 127.0.0.1...
    * TCP_NODELAY set
    * Connected to 127.0.0.1 (127.0.0.1) port 1989 (#0)
    > GET /?ip=121.193.184.2 HTTP/1.1
    > Host: 127.0.0.1:1989
    > User-Agent: curl/7.59.0
    > Accept: */*
    >
    * Empty reply from server
    * Connection #0 to host 127.0.0.1 left intact
    curl: (52) Empty reply from server
  • error log

    1
    2
    3
    2019/11/21 09:57:43 [notice] 14393#14393: signal 17 (SIGCHLD) received from 14394
    2019/11/21 09:57:43 [alert] 14393#14393: worker process 14394 exited on signal 11 (core dumped)
    2019/11/21 09:57:43 [notice] 14393#14393: start worker process 14566
  • demsg

    1
    [37568.563121] nginx[14394]: segfault at a ip 00007f10ef0973fd sp 00007ffc1688f480 error 6 in lipip.so[7f10ef096000+2000]

    这里的 lipip.so[7f10ef096000+2000] 已经表明了是 lipip.so 中的异常,如果没有标识出是 lipip 库的异常,可以根据 maps 看是共享库还是程序本身的异常。7f10ef096000+2000 中
    7f10ef096000 是基址偏移量, +2000 标识这个 map 对应的大小。

定位步骤

  • 先看下 lipip.so 有没有 dwarf

    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
    vagrant@archlinux:~/reading/lipip  develop ✗ readelf -W -S lipip.so
    There are 35 section headers, starting at offset 0x67d0:

    Section Headers:
    [Nr] Name Type Address Off Size ES Flg Lk Inf Al
    [ 0] NULL 0000000000000000 000000 000000 00 0 0 0
    [ 1] .note.gnu.build-id NOTE 00000000000001c8 0001c8 000024 00 A 0 0 4
    [ 2] .gnu.hash GNU_HASH 00000000000001f0 0001f0 000050 00 A 3 0 8

    ......

    [11] .text PROGBITS 00000000000010e0 0010e0 0008b2 00 AX 0 0 16

    ......

    [20] .got PROGBITS 0000000000201fe0 001fe0 000020 08 WA 0 0 8
    [21] .got.plt PROGBITS 0000000000202000 002000 000138 08 WA 0 0 8
    [22] .data PROGBITS 0000000000202140 002140 000060 00 WA 0 0 32
    [23] .bss NOBITS 00000000002021a0 0021a0 000008 00 WA 0 0 1
    [24] .comment PROGBITS 0000000000000000 0021a0 00001a 01 MS 0 0 1
    [25] .debug_aranges PROGBITS 0000000000000000 0021ba 000090 00 0 0 1
    [26] .debug_info PROGBITS 0000000000000000 00224a 001830 00 0 0 1
    [27] .debug_abbrev PROGBITS 0000000000000000 003a7a 000503 00 0 0 1
    [28] .debug_line PROGBITS 0000000000000000 003f7d 00046f 00 0 0 1
    [29] .debug_str PROGBITS 0000000000000000 0043ec 0005a6 01 MS 0 0 1
    [30] .debug_loc PROGBITS 0000000000000000 004992 000ec9 00 0 0 1
    [31] .debug_ranges PROGBITS 0000000000000000 00585b 000060 00 0 0 1
    [32] .symtab SYMTAB 0000000000000000 0058c0 0009a8 18 33 57 8
    [33] .strtab STRTAB 0000000000000000 006268 00040e 00 0 0 1
    [34] .shstrtab STRTAB 0000000000000000 006676 000153 00 0 0 1

    如果没有 .debug 开头的 section,说明没有 dwarf 符号,需要到编译机 -g 重新编译。

  • 看程序段的地址分配

    1
    [11] .text             PROGBITS        00000000000010e0 0010e0 0008b2 00  AX  0   0 16

    范围为 0x00000000000010e0 ~ 0000000000001992(0x00000000000010e0+0x0008b2)

  • 查看 segfault 错误信息

    1
    segfault at a ip 00007f10ef0973fd sp 00007ffc1688f480 error 6 in lipip.so[7f10ef096000+2000]

    用 ip 值减去基址偏移:

    1
    0x00007f10ef0973fd - 0x7f10ef096000 = 0x13fd
  • 用 addr2line 工具定位错误行号

    1
    vagrant@archlinux:~/reading/lipip  develop ✗ addr2line -e lipip.so 0x13fd
  • 查看代码行号

    1
    2
    3
    4
    5
    6
    7
    8
     96     ...
    97 if (s <= e) {
    98 lua_pushstring(L, s);
    99 lua_rawseti(L, -2, i);
    100 }
    101
    102 int *p = 10;
    103 *p = 0; // 修改栈上值

运行库

启动函数

main 函数并非程序的起点: _start -> _libc_start_main (.init -> rtld_fini -> .fini)-> main -> exit

  • crt1.o 是程序的真正入口函数 _start,由它调用 _libc_start_main,包含基本的启动、退出代码。开始叫 crt.0,为强调是链接输入第一个文件更名为 crt0.o, 后来为了兼容 .init 和 .fint 又升级为 crt1.o
  • 由于c++ 出现和 elf 改进,必须在 main 函数之前全局/静态对象的构造,glibc 库在每个目标文件中引入了 .init 和 .fint 段; crti.o 和 crtn.o 帮助 .init 和 .fint 完成构造和清理相关工作
  • crti.o 和 crtn.o 提供了 .init 和 .finit 的机制, 实际完成构造和析构的是 crtbeginT.o 和 crtend.o

运行库

运行库(runtime library), C 运行库 CRT

  • 启动与退出
  • 标准函数(标准输入、输出;文件;字符;字符串..)
  • I/O
  • 堆 / 特殊实现 / 调试

标准函数变长实现

1
2
3
4
#define va_list char*
#define va_start(ap,arg) (ap=(va_list)&arg+sizeof(arg))
#define va_arg(ap, t) (*(t*)(ap+=sizeof(t))-sizeof(t))
#define va_end(ap) (ap=(va_list)0)
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
#include <stdio.h>
#include <stdarg.h>

void
foo(char *fmt, ...)
{
va_list ap;
int d;
char c, *s;

va_start(ap, fmt);
while (*fmt)
switch (*fmt++) {
case 's': /* string */
s = va_arg(ap, char *);
printf("string %s\n", s);
break;
case 'd': /* int */
d = va_arg(ap, int);
printf("int %d\n", d);
break;
case 'c': /* char */
c = (char) va_arg(ap, int);
printf("char %c\n", c);
break;
}
va_end(ap);
}

为什么只有 cdcel 可以实现变长参数,而 stdcall 不能实现变长参数?

CRT 与多线程

  • 多线程版本运行库中,线程不安全函数会自动加锁,包括 malloc, printf 等

  • errno # define errno (*__errno_location()) 不同的 __errno_location 返回地址不同

  • 线程特有的只是栈(出入栈频繁,不可控)和寄存器(数量少),使用 Thread Local Storage

    1
    __thread int number`

系统调用与API

什么是系统调用?
程序运行时,其本身是没有多少权利去访问系统资源(文件、网络、io 等)的,因系统有限的资源可能被多个不同的程序同时访问,需要系统级的保护和协调。

linux 系统调用
使用通用寄存器传递参数,EAX 寄存器传递系统调用号(例如0x80),EAX=1(exit); EAX=2(fork); EAX=3(IO read); EAX=4(IO write);
系统调用可以绕过 glibc 直接使用(性能考虑)

linux 为了系统的稳定性和安全性分为两种特权级别:用户态和内核态,系统调用运行在内核态,而要使用系统调用时必须有用户态切换到内核态,切换的过程是通过中断实现。

中断

中断是一个硬件或软件发出的请求,要求 cpu 暂停当前的工作去处理其他事情;
中断有两个属性,中断号和中断处理程序,而中断号是有限的,所以 linux 采用中断号和系统调用号组合来实现不通的系统调用

:scale 90%

  • 触发中断
  • 切换堆栈(用户态和内核态使用不同的栈,切换至当前栈(ESP 寄存器的值)至内核态的栈)
  • 中断处理程序

OpenResty Best Practice

发表于 2018-10-31 | | 阅读次数:

目录

  • OpenResty 发展起源
  • OpenResty 之 lua 编程
  • OpenResty 模块编写
  • OpenResty 核心原理
  • OpenResty hooks
  • OpenResty 开发常见陷阱
  • OpenResty 编程优化
  • OpenResty 易混易错配置解析
  • nginx 维护与更新

OpenResty 发展起源

OpenResty(也称为 ngx_openresty)是一个全功能的 Web 应用服务器。它打包了标准的 nginx 核心,很多的常用的第三方模块,以及它们的大多数依赖项。
通过揉和众多设计良好的 nginx 模块,OpenResty 有效地把 nginx 服务器转变为一个强大的 Web 应用服务器,基于它开发人员可以使用 lua 编程语言对 nginx 核心以及现有的各种 nginx C 模块进行脚本编程,构建出可以处理一万以上并发请求的极端高性能的 Web 应用。

OpenResty 致力于将你的服务器端应用完全运行于 nginx 服务器中,充分利用 nginx 的事件模型来进行非阻塞 I/O 通信。不仅仅是和 HTTP 客户端间的网络通信是非阻塞的,与 MySQL、PostgreSQL、Memcached 以及 Redis 等众多后端之间的网络通信也是非阻塞的。
因为 OpenResty 软件包的维护者也是其中打包的许多 nginx 模块的作者,所以 OpenResty 可以确保所包含的所有组件可以可靠地协同工作。

OpenResty 最早是雅虎中国的一个公司项目,起步于 2007 年 10 月。当时兴起了 OpenAPI 的热潮,用于满足各种 Web Service 的需求,基于 Perl 和 Haskell 实现;
2009 章亦春在加入淘宝数据部门的量子团队,决定对 OpenResty 进行重新设计和彻底重写,并把应用重点放在支持像量子统计这样的 Web 产品上面,这是第二代的 OpenResty,基于 nginx 和 lua 进行开发。

为什么要取 OpenResty 这个名字呢?OpenResty 最早是顺应 OpenAPI 的潮流做的,所以 Open 取自“开放”之意,而 Resty 便是 REST 风格的意思。虽然后来也可以基于 ngx_openresty 实现任何形式的 Web service 或者传统的 Web 应用。

也就是说 nginx 不再是一个简单的静态网页服务器,也不再是一个简单的反向代理了,OpenResty 致力于通过一系列 nginx 模块,把 nginx 扩展为全功能的 Web 应用服务器,目前有两大应用目标:

  1. 通用目的的 Web 应用服务器。在这个目标下,现有的 Web 应用技术都可以算是和 OpenResty 或多或少有些类似,比如 Nodejs,PHP 等等,但 OpenResty 的性能更加出色。
  2. nginx 的脚本扩展编程,为构建灵活的 Web 应用网关和 Web 应用防火墙等功能提供了极大的便利性。

OpenResty 特性概括如下:

  • 基于 nginx 的 Web 服务器
  • 打包 nginx 核心、常用的第三方模块及依赖项
  • 使用 lua 对 nginx 进行脚本编程
  • 充分利用 nginx 的事件模型进行非阻塞 I/O 通信
  • 使用 lua 以同步方式进行异步编程
  • 拓展后端通信方式

综合 OpenResty 的特性,它不仅具备 nginx 的负载均衡、反向代理及传统 http server 等功能,还可以利用 lua 脚本编程实现路由网关,实现访问认证、流量控制、路由控制及日志处理等多种功能;同时利用 cosocket 拓展和后端(mysql、redis、kafaka)通信后,更可开发通用的 restful api 程序。

OpenResty 之 lua 编程

lua 简介

1993 年在巴西里约热内卢天主教大学诞生了一门编程语言,他们给这门语言取了个浪漫的名字 — lua,在葡萄牙语里代表美丽的月亮。事实证明他们没有糟蹋这个优美的单词,lua 语言正如它名字所预示的那样成长为一门简洁、优雅且富有乐趣的语言。

lua 从一开始就是作为一门方便嵌入(其它应用程序)并可扩展的轻量级脚本语言来设计,因此她一直遵从着简单、小巧、可移植、快速的原则,官方实现完全采用 ANSI C 编写,能以 C 程序库的形式嵌入到宿主程序中。luaJIT 2 和标准 lua 5.1 解释器采用的是著名的 MIT 许可协议。正由于上述特点,所以 lua 在游戏开发、机器人控制、分布式应用、图像处理、生物信息学等各种各样的领域中得到了越来越广泛的应用。其中尤以游戏开发为最,许多著名的游戏,比如 World of Warcraft、大话西游,都采用了 lua 来配合引擎完成数据描述、配置管理和逻辑控制等任务。即使像 Redis 这样中性的内存键值数据库也提供了内嵌用户 lua 脚本的官方支持。

作为一门过程型动态语言,lua 有着如下的特性:

  1. 变量名没有类型,值才有类型,变量名在运行时可与任何类型的值绑定;
  2. 语言只提供唯一一种数据结构,称为表(table),它混合了数组、哈希,可以用任何类型的值作为 key 和 value。提供了一致且富有表达力的表构造语法,使得 lua 很适合描述复杂的数据;
  3. 函数是一等类型,支持匿名函数和正则尾递归(proper tail recursion);
  4. 支持词法定界(lexical scoping)和闭包(closure);
  5. 提供 thread 类型和结构化的协程(coroutine)机制,在此基础上可方便实现协作式多任务;
  6. 运行期能编译字符串形式的程序文本并载入虚拟机执行;
  7. 通过元表(metatable)和元方法(metamethod)提供动态元机制(dynamic meta-mechanism),从而允许程序运行时根据需要改变或扩充语法设施的内定语义;
  8. 能方便地利用表和动态元机制实现基于原型(prototype-based)的面向对象模型;
  9. 从 5.1 版开始提供了完善的模块机制,从而更好地支持开发大型的应用程序;

lua 基础数据类型

1
2
3
4
5
print(type("hello world")) --> output:string
print(type(print)) --> output:function
print(type(true)) --> output:boolean
print(type(360.0)) --> output:number
print(type(nil)) --> output:nil

nil

nil 是一种类型,lua 将 nil 用于表示“无效值”。一个变量在第一次赋值前的默认值是 nil,将 nil 赋予给一个全局变量就等同于删除它。

1
2
3
4
5
local num
print(num) --> output:nil

num = 100
print(num) --> output:100

boolean (true/false)

布尔类型,可选值 true/false;lua 中 nil 和 false 为“假”,其它所有值均为“真”,比如 0 和空字符串就是“真”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
local a = true
local b = 0
local c = nil

if a then
print("a") --> output:a
else
print("not a") -- 这个没有执行
end

if b then
print("b") --> output:b
else
print("not b") -- 这个没有执行
end

if c then
print("c") -- 这个没有执行
else
print("not c") --> output:not c
end

number

Number 类型用于表示实数,和 C/C++ 里面的 double 类型很类似。可以使用数学函数 math.floor(向下取整)和 math.ceil(向上取整)进行取整操作。

1
2
3
4
local order = 3.99
local score = 98.01
print(math.floor(order)) --> output:3
print(math.ceil(score)) --> output:99

string

和其他语言 string 大同小异

1
2
3
4
5
6
7
8
9
local str1 = 'hello world'
local str2 = "hello lua"
local str3 = [["add\name",'hello']]
local str4 = [=[string have a [[]].]=]

print(str1) --> output:hello world
print(str2) --> output:hello lua
print(str3) --> output:"add\name",'hello'
print(str4) --> output:string have a [[]].

table (数组、字典)

Table 类型实现了一种抽象的“关联数组”。“关联数组”是一种具有特殊索引方式的数组,索引通常是字符串(string)或者 number 类型,但也可以是除 nil 以外的任意类型的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local corp = {
web = "www.google.com", -- 索引为字符串,key = "web",
-- value = "www.google.com"
telephone = "12345678", -- 索引为字符串
staff = {"Jack", "Scott", "Gary"}, -- 索引为字符串,值也是一个表
100876, -- 相当于 [1] = 100876,此时索引为数字
-- key = 1, value = 100876
100191, -- 相当于 [2] = 100191,此时索引为数字
[10] = 360, -- 直接把数字索引给出
["city"] = "Beijing" -- 索引为字符串
}

print(corp.web) --> output:www.google.com
print(corp["telephone"]) --> output:12345678
print(corp[2]) --> output:100191
print(corp["city"]) --> output:"Beijing"
print(corp.staff[1]) --> output:Jack
print(corp[10]) --> output:360

在内部实现上,table 通常实现为一个哈希表、一个数组、或者两者的混合。具体的实现为何种形式,动态依赖于具体的 table 的键分布特点。

function

在 lua 中,函数也是一种数据类型,函数可以存储在变量中,可以通过参数传递给其他函数,还可以作为其他函数的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local function foo()
print("in the function")
-- dosomething()
local x = 10
local y = 20
return x + y
end

local a = foo -- 把函数赋给变量

print(a())

-- output:
in the function
30

lua 表达式

算术运算符 说明 关系运算符 说明 逻辑运算符 说明
+ 加法 < 小于 and 逻辑与
- 减法 > 大于 or 逻辑或
* 乘法 <= 小于等于 not 逻辑非
/ 除法 >= 大于等于 - -
^ 指数 ~= 不等于 - -
% 取模 - - - -

note: lua 中的不等于用 ~= 表示, 和其他语言的 != 不一致

lua 流程控制

lua 的流程控制结构和 python 类似,有几个特例:

  • lua 中的 elseif 需要连写,中间不能有空行;python 中写法是 elif
  • lua 中没有 continue 流控

if/else/elseif

1
2
3
4
5
6
7
if a = 1 then
print("1")
elseif a == 2 then
print("2")
else
print("3")
end

while

1
2
3
4
5
6
while a > 1 do
if a == 5 then
break
end
a = a + 1
end

repeat

1
2
3
4
5
6
7
local i = 0
repeat
print(i)
if i == 5 then
break
end
until true

for/break

1
2
3
4
5
6
7
8
9
10
11
12
13
local t = { a = 1, b = 2}
for k, v in pairs(t) do -- 遍历字典
print(k, v)
end

local t = {1, 2}
for k, v in ipairs(t) do -- 遍历整型数组
print(k, v)
end

for i = 1, 10 do -- range 循环
print(i)
end

return

1
2
3
4
5
6
7
local function foo(arg)
if arg == "" then
return nil
end

return "bar"
end

OpenResty 模块编写

编写一个 access.lua 模块,源码如下:

1
2
3
4
5
6
7
8
9
local _M = {}

_M.check = function()
if ngx.var.http_host == "foo.bar.com" then
ngx.exit(403)
end
end

return _M -- 注意 return _M,返回 table 表示的模块

在 access_by_lua 的 nginx hook 中调用 access 模块:

1
2
3
4
access_by_lua_block {
local rule = require "access" -- require 中不需要加 `.lua` 后缀
rule.check()
}

OpenResty 核心原理

nginx 进程模型

nginx 是一个 master + 多个 worker 进程模型;master 进程负责管理和监控 worker 进程,如加载和解析配置文件,重启 worker 进程,更新二进制文件等。 worker 进程负责处理请求,每个 worker 地位和功能相同,内部按照 epoll + callback 方式实现并发连接处理;整体架构图如下:
nginx 架构模型

nginx 请求处理流程

每个 worker 进程都分阶段处理 http 请求,简单概括为初始化请求 -> 处理请求行 -> 后端交互 -> 响应头处理 -> 响应包体处理 -> 打印日志等几个阶段。其中处理响应体阶段又可以挂载多个不同的 filter。具体的请求阶段可以参见
nginx Phase, nginx 请求处理流程如下图:
nginx请求处理流程

nginx 事件机制

nginx 的事件驱动机制是对 epoll 驱动的封装,但其本质还是 epoll + callback 方式:
nginx事件机制

lua 协程

函数 描述
coroutine.create() 创建 coroutine,返回 coroutine,参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用
coroutine.resume() 重启 coroutine,和 create 配合使用
coroutine.yield() 挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果
coroutine.status() 查看 coroutine 的状态。注:coroutine 的状态有四种:dead,suspend,running,normal

coroutine.create(f)

创建一个主体函数为 f 的新协程。f 必须是一个 lua 的函数。返回这个新协程,它是一个类型为 “thread” 的对象,创建后并不会启动该协程。

coroutine.resume(co, [, val1, …])

开始或继续协程 co 的运行。当第一次执行一个协程时,他会从主函数处开始运行。val1, … 这些值会以参数形式传入主体函数。如果该协程被挂起,resume 会重新启动它;val1, … 这些参数会作为挂起点的返回值。如果协程运行起来没有错误,resume 返回 true 加上传给 yield 的所有值 (当协程挂起),或是主体函数的所有返回值(当协程中止)。

coroutine.yield(…)

挂起正在调用的协程的执行。 传递给 yield 的参数都会转为 resume 的额外返回值。

coroutine.status(co)

以字符串形式返回协程 co 的状态:

  • 当协程正在运行(它就是调用 status 的那个) ,返回 “running”;
  • 如果协程调用 yield 挂起或是还没有开始运行,返回 “suspended”;
  • 如果协程是活动的,都并不在运行(即它正在延续其它协程),返回 “normal”;
  • 如果协程运行完主体函数或因错误停止,返回 “dead”。

协程实例(生产者消费者)

使用协程实现生产者消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

local function produce()
while true do
local x = io.read()
coroutine.yield(x) -- 挂起协程
end
end

local producer = coroutine.create(produce) -- 创建协程

local function receive()
local status, value = coroutine.resume(producer) -- 执行协程
return value
end

local function consumer()
while true do
local x = receive()
io.write(x, "\n")
end
end

consumer() -- loop

lua 与 c 堆栈交互

lua 虚拟机常嵌入 C 程序中运行,对于 C 程序来说,lua 虚拟机就是一个子进程。lua 将所有状态都保存在 lua_State 类型中,所有的 C API 都要求传入一个指向该结构的指针。我们根据这个指针来获取 lua 虚拟机(也就是子进程)的状态。

虚拟机内部与外部的 C 程序发生数据交换主要是通过一个公用栈实现的,也就是说 lua 虚拟机和 C 程序公用一个栈,双方都可以压栈或读取数据。一方压入,另一方弹出就能实现数据的交换。

在 c 中,lua 堆栈就是一个 struct,堆栈索引方式可能是正数也可能是负数,区别是:正数索引 1 永远表示栈底,负数索引 -1 永远表示栈顶。
堆栈的默认大小是 20,可以用 lua_checkstack 修改,用 lua_gettop 则可以获得栈里的元素数目。

C 调用 lua

  • 在 C 中创建 lua 虚拟机

    1
    lua_State *luaL_newstate (void)
  • 加载 lua 的库函数

    1
    void luaL_openlibs (lua_State *L);
  • 加载 lua 文件,使用接口

    1
    int luaL_dofile (lua_State *L, const char *filename);
  • 开始交互,lua 定义一个函数

    1
    function test_func_add(a, b) return a + b end
  • 如果你的 lua_State 是全局变量,那么每次对堆栈有新操作时务必使用lua_settop(lua_State, -1)将偏移重新置到栈顶

  • 去lua文件中取得test_func_add方法

    1
    void lua_getglobal (lua_State *L, const char *name);
  • 参数压栈

    1
    lua_pushnumber
  • 通过 pcall 调用

    1
    int lua_pcall (lua_State *L, int nargs, int nresults, int msg);

完整示例,先编写一个 foo.lua 文件,在文件中实现 test_func_add 方法

1
2
3
function test_func_add(a, b)
return a + b
end

接下来在 C 代码中调用 foo.lua:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
lua_State* init_lua()
{
lua_State* s_lua = luaL_newstate();
if (!s_lua) {
printf("luaL_newstate failed!\n");
exit(-1);
}
luaL_openlibs(s_lua);

return s_lua;
}

bool load_lua_file(lua_State* s_lua, const char* lua_file)
{
if (luaL_dofile(s_lua, lua_file) != 0) {
printf("LOAD LUA %s %s\n", lua_file, BOOT_FAIL);
return false;
}
printf("LOAD LUA %s %s\n", lua_file, BOOT_OK);
return true;
}


int proc_add_operation(lua_State* s_lua, int a, int b)
{
lua_settop(s_lua, -1);
lua_getglobal(s_lua, "test_func_add");
lua_pushnumber(s_lua, a);
lua_pushnumber(s_lua, b);

int val = lua_pcall(s_lua, 2, 1, 0);
if (val) {
printf("lua_pcall_error %d\n", val);
}

return (int)lua_tonumber(s_lua, -1);
}

int main() {
lua_State* s_lua =init_lua();
if (!load_lua_file(s_lua, "foo")) {
return -1;
}

proc_add_operation(s_lua, 1, 2);
}

lua 调用 c

  • 定义谁先实现 C 接口

    1
    2
    3
    4
    5
    6
    7
    8
    #define target 300
    static int l_test_check_value(lua_State * l)
    {
    int num = lua_tointeger(l, -1);
    bool check = (num == target);
    lua_pushboolean(l, check);
    return 1;
    }
  • lua 虚拟启动时候,注册加载 C 接口

    1
    lua_register(s_lua, "test_check_value", l_test_check_value);
  • 在 lua 代码中调用注册的 C 接口

    1
    2
    3
    4
    function test_func_check(a)
    local val = test_check_value(a)
    return val
    end

lua 协程与 nginx 事件机制结合

文章前部分用大量篇幅阐述了 lua 和 nginx 的相关知识,包括 nginx 的进程架构,nginx 的事件循环机制,lua 协程,lua 协程如何与 C 实现交互;在了解这些知识之后,本节阐述 lua 协程是如何和 nginx 的事件机制协同工作。

从 nginx 的架构和事件驱动机制来看, nginx 的并发处理模型概括为:单 worker + 多连接 + epoll + callback。
即每个 nginx worker 同时处理了大量连接,每个连接对应一个 http 请求,一个 http 请求对应 nignx 中的一个结构体(ngx_http_request_t):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ngx_http_request_s {
uint32_t signature; /* "HTTP" */

ngx_connection_t *connection;

void **ctx;
void **main_conf;
void **srv_conf;
void **loc_conf;

ngx_http_event_handler_pt read_event_handler;
ngx_http_event_handler_pt write_event_handler;

....
}

结构体中的核心成员为 ngx_connection_t *connection,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ngx_connection_s {
void *data;
ngx_event_t *read; // epoll 读事件对应的结构体成员
ngx_event_t *write; // epoll 写事件对应的结构体成员

ngx_socket_t fd; // tcp 对应的 socket fd

ngx_recv_pt recv;
ngx_send_pt send;
ngx_recv_chain_pt recv_chain;
ngx_send_chain_pt send_chain;

ngx_listening_t *listening;

...
}

从如上结构体可知,每个请求中对应的 ngx_connection_t 中的读写事件和 epoll 关联;nginx epoll 的事件处理核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
   ...

events = epoll_wait(ep, event_list, (int) nevents, timer);
for (i = 0; i < events; i++) {
c = event_list[i].data.ptr;

instance = (uintptr_t) c & 1;
c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1); // epoll 获取激活事件,将事件转换成 ngx_connection_t

...

rev = c->read;
rev->handler(rev);

...

wev = c->write;
wev->handler(ev);

...
}

nginx epoll loop 中调用 epoll_wait 获取 epoll 接管的激活事件,并通过 c 的指针强转,得到 ngx_connection_t 获取对应的连接和连接上对应的读写事件的回调函数,即通过 C 结构体变量成员之间的相关关联来串联请求和事件驱动,实现请求的并发处理;这里其实和高级语言的面向对象的写法如出一辙,只是模块和成员变量之间的获取方式的差异。

如果引入 lua 的协程机制,在 lua 代码中出现阻塞的时候,主动调用 coroutine.yield 将自身挂起,待阻塞操作恢复时,再将挂起的协程调用 coroutine.resume 恢复则可以避免在 lua 代码中写回调;而何时恢复协程可以交由 c 层面的 epoll 机制来实现,则可以实现事件驱动和协程之间的关联。现在我们只需要考虑,如何将 lua_State 封装的 lua land 和 C land 中的 epoll 机制融合在一起。

事实上 lua-nginx-module 确实是按照这种方式来处理协程与 nginx 事件驱动之间的关系,lua-nginx-module 为每个 nginx worker 生成了一个 lua_state 虚拟机,即每个 worker 绑定一个 lua 虚拟机,当需要 lua 脚本介入请求处理流程时,基于 worker 绑定的虚拟机创建 lua_coroutine 来处理逻辑,当阻塞发生、需要挂起时或者处理逻辑完成时挂起自己,等待下次 epoll 调度时再次唤醒协程执行。如下是 rewrite_by_lua 核心代码部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
tatic ngx_int_t
ngx_http_lua_rewrite_by_chunk(lua_State *L, ngx_http_request_t *r)
{
co = ngx_http_lua_new_thread(r, L, &co_ref);

lua_xmove(L, co, 1);
ngx_http_lua_get_globals_table(co);
lua_setfenv(co, -2);

ngx_http_lua_set_req(co, r); // 此处设置协程与 ngx_http_request_t 之间的关系

...

rc = ngx_http_lua_run_thread(L, r, ctx, 0); // 运行 lua 脚本处理 rewrite 逻辑

if (rc == NGX_ERROR || rc > NGX_OK) {
return rc;
}

...
}

从上述代码片段中我们看到了协程与 ngx 请求之间的绑定关系,那么只要在 ngx_http_lua_run_thread 函数中(实际上是在 lua 脚本中)处理何时挂起 lua 的执行即可。大部分时候我们在 lua 中的脚本工作类型分两种,一种是基于请求信息的逻辑改写,一种是基于 tcp 连接的后端交互。逻辑改写往往不会发生 io 阻塞,即当前脚本很快执行完成后回到 C land,不需要挂起再唤醒的流程。而对于方式二,lua-nginx-module 提供了 cosocket api,
它封装了 tcp api,并且会在合适的时候(coroutine.yield 的调用发生在 IO 异常,读取包体完毕,或者 proxy_buffers 已满等情形,具体的实现读者可以参考 ngx_http_lua_socket_tcp.c 源码)调用 coroutine.yield 方法 。
lua-corotine

综上所述,结合lua 协程和 nginx 事件驱动机制,使用 OpenResty 可以使用 lua 脚本方便的扩展 nignx 的功能。

OpenResty hooks (编程钩子)

lua-resty-phase

init_by_lua

该阶段主要用于预加载一些 lua 模块, 如加载全局 json 模块:require 'cjson.safe';设置全局的 lua_share_dict 等,并且可以利用操作系统的 copy-on-write 机制;reload nginx 会重新加载该阶段的代码。

init_worker_by_lua

该阶段可用于为每个 worker 设置独立的定时器,设置心跳检查等。

rewrite_by_lua

实际场景中应用最多的一个 hooks 之一,可用于请求重定向相关的逻辑,如改写 host 头,改写请求参数和请求路径等

access_by_lua

该阶段可用于实现访问控制相关的逻辑,如动态限流、限速,防盗链等

content_by_lua

该阶段用于生成 http 请求的内容,和 proxy_pass 指令冲突;二者在同一个阶段只能用一个。该阶段可用于动态的后端交互,如 mysql、redis、kafaka 等;也可用于动态的 http 内容生成,如使用 lua 实现 c 的 slice 功能,完成大文件的分片切割。

banalce_by_lua

该阶段可用于动态的设置 proxy_pass 的上游地址,例如用 lua 实现一个带监控检测机制的一致性 hash 轮序后端算法,根据上游的响应动态设置该地址是否可用。

body_filter_by_lua

用于过滤和加工响应包体,如对 chunk 模式的包体进行 gzip; 也可以根据包体的大小来动态设置 ngx.var.limit_rate.

header_filter_by_lua

调整发送给 client 端的响应头,也是最常用的 hooks 之一;比如设置响应的 server 头,修缓存头 cache-control 等。

log_by_lua

一方面可以设置 nginx 日志输出的字段值,另一方面我们也可以用 cosocket 将日志信息发送到指定的 http server;因响应头和响应体已发送给客户端,该阶段的操作不会影响到客户端的响应速度。

OpenResty 之 lua 编写常见陷阱

  • elseif,区别于 else if;
  • and & or,不支持问号表达式;lua 中 0 表示 true;
  • no continue,lua 中不支持 continue 语法;需要用 if 和 else 语句实现;
  • . & :,lua 中 object.method 和 object:method 行为不同,object:method 为语法糖,会扩展成第一个参数为 self
  • forgot return _M,在编写模块的时候如果最后忘记 return _M, 调用时会提示尝试对 string 调用方法的异常

OpenResty 编程优化

  • do local statement,尽量使用 local 化的变量声明,加速变量索引速度的同时避免全局命名空间的污染;
  • do not use blocked api,不要调用会阻塞 lua 协程的 api,比如 lua 原生的 socket,会造成 nginx worker block;
  • use ngx.ctx instead of ngx.var,ngx.var 会调用 ngx.var 的变量索引系统,比 ngx.ctx 低效很多;
  • decrease table resize,避免 lua table 表的 resize 操作,可以用 luajit 事先声明指定大小的 table。比如频繁的 lua 字符串相加的 .. 操作,当 lua 预分配内存不够时,会重新动态扩容(和 c++ vector 类型),会造成低效;
  • use lua-resty-core,使用 lua-resty-core api,该部分 api 用 luajit 的 ffi 实现比直接的 C 和 lua 交互高效;
  • use jit support function,少用不可 jit 加速的函数,那些函数不能 jit 支持,可以参看 luajit 文档。
  • ffi,对自己实现的 C 接口,也建议用 ffi 暴露出接口给 lua 调用。

nginx 易混易错配置说明

so_keepalive

用于 listen 中,探测连接保活; 采用TCP连接的C/S模式软件,连接的双方在连接空闲状态时,如果任意一方意外崩溃、当机、网线断开或路由器故障,另一方无法得知TCP连接已经失效,除非继续在此连接上发送数据导致错误返回。很多时候,这不是我们需要的。我们希望服务器端和客户端都能及时有效地检测到连接失效,然后优雅地完成一些清理工作并把错误报告给用户。

如何及时有效地检测到一方的非正常断开,一直有两种技术可以运用。一种是由TCP协议层实现的Keepalive,另一种是由应用层自己实现的心跳包。

TCP默认并不开启Keepalive功能,因为开启 Keepalive 功能需要消耗额外的宽带和流量,尽管这微不足道,但在按流量计费的环境下增加了费用,另一方面,Keepalive设置不合理时可能会因为短暂的网络波动而断开健康的TCP连接。并且,默认的Keepalive超时需要7,200,000 milliseconds,即2小时,探测次数为 5 次。系统默认的 keepalive 配置如下:

1
2
3
net.ipv4.tcpkeepaliveintvl = 75
net.ipv4.tcpkeepaliveprobes = 5
net.ipv4.tcpkeepalivetime = 7200

如果在 listen 的时候不设置 so_keepalive 则使用了系统默认的 keepalive 探测保活机制,需要 2 小时才能清理掉这种异常连接;如果在 listen 指令中加入

1
so_keepalive=30m::10

可设置如果连接空闲了半个小时后每 75s 探测一次,如果超过 10 次 探测失败,则释放该连接。

sendfile/directio

sendfile

copies data between one file descriptor and another. Because this copying is done within the kernel, sendfile() is more efficient than the combination of read(2) and write(2), which would require transferring data to and from user space.

从 Linux 的文档中可以看出,当 nginx 有磁盘缓存文件时候,可以利用 sendfile 特性将磁盘内容直接发送到网卡避免了用户态的读写操作。

directio

Enables the use of the O_DIRECT flag (FreeBSD, Linux), the F_NOCACHE flag (macOS), or the directio() function (Solaris), when reading files that are larger than or equal to the specified size. The directive automatically disables (0.7.15) the use of sendfile for a given request

写文件时不经过 Linux 的文件缓存系统,不写 pagecache, 直接写磁盘扇区。启用aio时会自动启用directio, 小于directio定义的大小的文件则采用 sendfile 进行发送,超过或等于 directio 定义的大小的文件,将采用 aio 线程池进行发送,也就是说 aio 和 directio 适合大文件下载。因为大文件不适合进入操作系统的 buffers/cache,这样会浪费内存,而且 Linux AIO(异步磁盘IO) 也要求使用directio的形式。

proxy_request_buffering

控制处理客户端包体的行为,如果设置为 on, 则 nginx 会接收完 client 的整个包体后处理。如 nginx 作为反向代理服务处理客户端的上传操作,则先接收完包体再转发给上游,这样上游异常的时候,nginx 可以多次重试上传,但有个问题是如果包体过大,nginx 端如果负载较重话,会有大量的写磁盘操作,同时对磁盘的容量也有较高要求。如果设置为 off, 则传输变成流式处理,一个 chunk 一个 chunk
传输,传输出错更多需要 client 端重试。

proxy_buffer_size

Sets the size of the buffer used for reading the first part of the response received from the proxied server. This part usually contains a small response header. By default, the buffer size is equal to one memory page. This is either 4K or 8K, depending on a platform.

proxy_buffers

Sets the number and size of the buffers used for reading a response from the proxied server, for a single connection. By default, the buffer size is equal to one memory page. This is either 4K or 8K, depending on a platform.

proxy_buffering

Enables or disables buffering of responses from the proxied server.

When buffering is enabled, nginx receives a response from the proxied server as soon as possible, saving it into the buffers set by the proxy_buffer_size and proxy_buffers directives. If the whole response does not fit into memory, a part of it can be saved to a temporary file on the disk. Writing to temporary files is controlled by the proxy_max_temp_file_size and proxy_temp_file_write_size directives.

When buffering is disabled, the response is passed to a client synchronously, immediately as it is received. nginx will not try to read the whole response from the proxied server. The maximum size of the data that nginx can receive from the server at a time is set by the proxy_buffer_size directive.

当 proxy_buffering on 时处理上游的响应可以使用 proxy_buffer_size 和 proxy_buffers 两个缓冲区;而设置 proxy_buffering off 时,只能使用proxy_buffer_size 一个缓冲区。

proxy_busy_size

When buffering of responses from the proxied server is enabled, limits the total size of buffers that can be busy sending a response to the client while the response is not yet fully read. In the meantime, the rest of the buffers can be used for reading the response and, if needed, buffering part of the response to a temporary file. By default, size is limited by the size of two buffers set by the proxy_buffer_size and proxy_buffers directives.

当接收上游的响应发送给 client 端时,也需要一个缓存区,即发送给客户端而未确认的部分,这个 buffer 也是从 proxy_buffers 中分配,该指令限定能从 proxy_buffers 中分配的大小。

keepalive

该指令可作用于 nginx.conf 和 upstream 的 server 中;当作用于 nginx.conf 中时,表示作为 http server 端回复客户端响应后,不关闭该连接,让该连接保持 ESTAB 状态,即 keepalive。 当该指令作用于 upstrem 块中时,表示发送给上游的 http 请求加入 connection: keepalive, 让服务端保活该连接。值得注意的是服务端和客户端均需要设置 keepalive 才能实现长连接。 同时 keepalive指令需要和 如下两个指令配合使用:

1
2
keepalive_requests 100;
keepalive_timeout 65;

keepalive_requests 表示一个长连接可以复用的次数,keepalive_timeout 表示长连接在空闲多久后可以关闭。
keepalive_timeout 如果设置过大会造成 nginx 服务端 ESTAB 状态的连接数增多。

nginx 维护与更新

nginx 信号集和 nginx 操作之间的对应关系如下:

nginx operation signal
reload SIGHUP
reload SIGUSR1
stop SIGTERM
quit SIGQUIT
hot update SIGUSR2 & SIGWINCH & SIGQUIT

stop vs quit

stop 发送 SIGTERM 信号,表示要求强制退出,quit 发送 SIGQUIT,表示优雅地退出。 具体区别在于,worker 进程在收到 SIGQUIT 消息(注意不是直接发送信号,所以这里用消息替代)后,会关闭监听的套接字,关闭当前空闲的连接(可以被抢占的连接),然后提前处理所有的定时器事件,最后退出。没有特殊情况,都应该使用 quit 而不是 stop。

reload

master 进程收到 SIGHUP 后,会重新进行配置文件解析、共享内存申请,等一系列其他的工作,然后产生一批新的 worker 进程,最后向旧的 worker 进程发送 SIGQUIT 对应的消息,最终无缝实现了重启操作。 再 master 进程重新解析配置文件过程中,如果解析失败则会回滚使用原来的配置文件,即 reload 失败,此时工作的还是老的 worker。

reopen

master 进程收到 SIGUSR1 后,会重新打开所有已经打开的文件(比如日志),然后向每个 worker 进程发送 SIGUSR1 信息,worker 进程收到信号后,会执行同样的操作。reopen 可用于日志切割,比如 nginx 官方就提供了一个方案:

1
2
3
4
$ mv access.log access.log.0
$ kill -USR1 `cat master.nginx.pid`
$ sleep 1
$ gzip access.log.0 # do something with access.log.0

这里 sleep 1 是必须的,因为在 master 进程向 worker 进程发送 SIGUSR1 消息到 worker 进程真正重新打开 access.log 之间,有一段时间窗口,此时 worker 进程还是向文件 access.log.0 里写入日志的。通过 sleep 1s,保证了 access.log.0 日志信息的完整性(如果没有 sleep 而直接进行压缩,很有可能出现日志丢失的情况)。

hot update

某些时候我们需要进行二进制热更新,nginx 在设计的时候就包含了这种功能,不过无法通过 nginx 提供的命令行完成,我们需要手动发送信号。

首先需要给当前的 master 进程发送 SIGUSR2,之后 master 会重命名 nginx.pid 到 nginx.pid.oldbin,然后 fork 一个新的进程,新进程会通过 execve 这个系统调用,使用新的 nginx ELF 文件替换当前的进程映像,成为新的 master 进程。新 master 进程起来之后,就会进行配置文件解析等操作,然后 fork 出新的 worker 进程开始工作。

接着我们向旧的 master 发送 SIGWINCH 信号,然后旧的 master 进程则会向它的 worker 进程发送 SIGQUIT 信息,从而使得 worker 进程退出。向 master 进程发送 SIGWINCH 和 SIGQUIT 都会使得 worker 进程退出,但是前者不会使得 master 进程也退出。

最后,如果我们觉得旧的 master 进程使命完成,就可以向它发送 SIGQUIT 信号,让其退出了。

引用

  • OpenResty 最佳实践
  • lua-nginx-module Readme
  • nginx doc
  • lua 与 c 进行交互
  • 浅谈 nginx 信号集

Linux 系统优化指令杂记

发表于 2018-09-24 | 更新于 2018-10-05 | | 阅读次数:

系统优化的前 5 分钟

使用如下指令了解机器的整体运行情况, 此处有指令的中文解释

  • uptime 检查系统是否宕机重启
  • dmesg | tail 查看 dmesg 是否有系统层面错误日志
  • vmstat 1 检查 cpu 的负载情况, 是否存在 cpu 饱和
  • mpstat -P ALL 1 检查CPU是否存在负载不均衡; 单个过于忙碌的CPU可能意味着整个应用只有单个线程在工作
  • pidstat 1 观察随时间变化的 cpu、内存等信息
  • iostat -xz 1 弄清块设备(磁盘)的状况, 包括工作负载和处理性能
  • free -m 查看剩余内存, 文件和 IO 缓存大小
  • sar -n DEV 1 检查网络流量的工作负载:rxkB/s和txkB/s, 以及它是否达到限额了
  • sar -n TCP,ETCP 1 查看系统网络负载, 过多重传可能是服务器过载开始丢包了
  • top 没什么好多, 大家都会用的

top 各个指标含义

  • Cpu(s):表示这一行显示CPU总体信息
  • 0.0%us:用户态进程占用CPU时间百分比, 不包含renice值为负的任务占用的CPU的时间。
  • 0.7%sy:内核占用CPU时间百分比
  • 0.0%ni:改变过优先级的进程占用CPU的百分比
  • 99.3%id:空闲CPU时间百分比
  • 0.0%wa:等待I/O的CPU时间百分比
  • 0.0%hi: CPU硬中断时间百分比
  • 0.0%si: CPU软中断时间百分比
  • 0.0%st: 超线程开启时候, 线程等待另外一个虚拟核完成任务的时间百分比

note:这里显示数据是所有cpu的平均值, 如果想看每一个cpu的处理情况, 按1即可;折叠, 再次按1

阅读全文 »

linux 命令助记

发表于 2018-09-24 | 更新于 2018-10-05 | | 阅读次数:

print all system information

1
uname -a

kernel version:

1
uname -r

see hardware info.

1
cat /proc/meminfo cat /proc/cpuinfo

count line

1
find . -name '\*.c' | xargs wc -l {}\;

yum remove without dependency

1
sudo rpm -e --nodeps vim-common-7.4.160-1.el7.x86_64
阅读全文 »

updns QPS 异常问题排查

发表于 2018-09-24 | | 阅读次数:

环境问题排查

  • 系统: 系统版本, 内核版本
  • cpu: 物理核心数, 物理核数,逻辑核数
  • 内存: 内存大小
  • 网卡: 网卡是否是万兆, 驱动型号

updns 版本确认(排查是否最近代码导致)

  • 以前压测过的版本回退 v0.03
  • 使用最近版本测试

系统各种参数优化确认

  • sendmmsg recvmmsg
  • 多线程发包
  • so_reuseport
  • 绑定网卡亲缘性
  • 系统参数调优化
  • 查看软件终端, 查看 cpu 绑定是否生效

使用系统工具确认压测机器是否正常

  • iperf 网络ok, 吞吐量OK, QPS: 60s 400W QPS, 800Mb/s
  • udp sender 查看发包 80w/s, 系统占用低
  • perf spin_irq_lock 60%
  • 查看网卡的收包, 接包, 丢包情况

被测端和压测端的情况

nginx server locaton 匹配顺序

发表于 2018-09-24 | | 阅读次数:

server name

  1. 完全匹配
  2. 通配符在前面,*.tesetwb.com
  3. 通配符在后面,www.testwb.*
  4. 正则表达式匹配 (.*)?anyfeel.cn

location

  1. 完全匹配,= or /
  2. 大小写敏感,~
  3. 大小写不敏感,~*
  4. 前半部分匹配,^~
  5. @location, 表示只能用于 nginx 内部跳转

Nginx 模块初始化过程

发表于 2018-09-24 | 更新于 2018-10-05 | | 阅读次数:

初始化核心函数

ngx_init_cycle()(ngx_cycle.c:275) -> ngx_conf_parse()(ngx_conf_file.c:) -> ngx_conf_handler()

初始化步骤

  • modules 和序号绑定关系
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    ngx_int_t
    ngx_preinit_modules(void)
    {
    ngx_uint_t i;

    for (i = 0; ngx_modules[i]; i++) {
    ngx_modules[i]->index = i;
    ngx_modules[i]->name = ngx_module_names[i];
    }

    ngx_modules_n = i;
    ngx_max_module = ngx_modules_n + NGX_MAX_DYNAMIC_MODULES;

    return NGX_OK;
    }
阅读全文 »

TCP 连接状态整理

发表于 2018-09-24 | 更新于 2018-10-05 | | 阅读次数:

要点

  1. 五层架构与七层架构
  2. 链路层作用 ARP,以太网包格式
  3. IP/TCP 格式
  4. TCP 状态机及常用的错误码,time_wait 相关系,SO_RESUSEADDR 作用。
  5. NAGEL 算法(TCP_NODELAY关闭NAGEL)滑动窗口拥塞避免(慢启动),快速重传与快速恢复,选择重传
  6. 四种定时器的作用 ACK 定时器,persist 定时器,keep-alive 定时器,2MSL 定时器
  7. 使用 TCPDUMP 和应用程序分析 TCP 状态机

note:so_linger 作用: 发送 RST 包来断开连接,而不是发送 FIN.

connect resty by peer

含义

本端向对端发送数据,但对端无法识别该连接,返回一个 RST 强制关闭连接

阅读全文 »

Linux 磁盘调整

发表于 2018-09-23 | | 阅读次数:

磁盘工具

  • parted/fdisk/gdisk/ blkid

note:

  • 注意区分 PARTUUID 和 UUID
  • gdisk sort 可以调整分区序号

磁盘格式化

  • EFI 分区格式化
    1
    mkfs.fat -F32 /dev/sdxY -> EFI 分区格式化

如果没有 mkfs.fat 则安装 dosfstools

  • 分区格式化
    1
    mkfs.ext4 /dev/sda1

磁盘启动管理

  • 安装 bootctl

    1
    bootctl —path=/mnt/boot install
  • 增加磁盘启动选项

    1
    /boot/loader/entries/arch.conf

这里写入的是 root 分区的 PARTUUID

磁盘分区大小调整(resize)

  • 先删除原有分区,然后再重新建立(启动区块号不能变,否则会丢失数据)
  • 使用 resize2fs /dev/sda1 来重新调整分区大小
  • 最后需要解决 EFI loader 中 PARTUUID,PARTUUID 可能会有变更

使用 pyenv 构建线上 python 环境

发表于 2018-09-23 | | 阅读次数:

使用 pyenv 部署 python 线上环境

当生产服务器因权限问题或者系统版本阉割出现 python 依赖问题时,可以使用 pyenv

安装方法

  1. 下载 pyenv.

    1
    $ git clone https://github.com/yyuu/pyenv.git ~/.pyenv
  2. 设置PYENV_ROOT 为 pyenv 安装路径, 添加 $PYENV_ROOT/bin 至环境变量

    1
    2
    $ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
    $ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
  3. 开启 pyenv shims 和自动补全

    1
    $ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
  4. 重启 shell 让 pyenv 生效

    1
    $ exec $SHELL
  5. 安装需要的 python 版本 至 $PYENV_ROOT/versions 目录

    1
    $ pyenv install 2.7.8

pyenv 常用命令

1
2
3
4
5
pyenv install 2.7.5
pyenv versions
pyenv version
pyenv local 2.7.5
pyenv shell 2.7.5
123
anyfeel

anyfeel

我就是我,是颜色不一样的烟花

22 日志
7 标签
© 2015 – 2019 anyfeel
由 Hexo 强力驱动 v3.7.1
|
主题 – NexT.Pisces v6.4.2