nginx HTTP Filter 模块

过滤模块基本概念 普通的 HTTP 模块和 HTTP filter 模块有很大的不同。普通模块,例如上篇博客提到的 hello world 模块,可以介入 nginx http 框架的 7 个处理阶段,绝大多数情况下介入 NGX_HTTP_CONTENT_PHASE 阶段,特点是一旦介入了,那么一个 http 请求在这个阶段将只有这个模块处理。 http filter 模块则不同,一个请求可以被任意个 http 过滤模块处理,而且过滤模块 仅处理服务器发出的 HTTP 响应 header 和 body,不处理客户端发来的请求。 过滤链表的顺序 编译 nginx 的第一步是执行 configure 脚本生成 objs/ngx_modules.c 文件,这个文件中的 ngx_modules 数组会保存所有的 nginx 模块,包括普通的 http 模块和本文介绍的 http filter 模块。 nginx 在启动时初始化模块的顺序就是 nginx_modules 数组成员的顺序。因此,只要看 configure 命令生成的 ngx_modules.c 文件就可以知道所有 http 过滤模块的顺序。 对于 http 过滤模块来说,在 ngx_modules 数组中的位置越靠后,实际执行请求时就越先执行。因为在初始化 http 过滤模块时,每个过滤模块都是将自己插入到整个链表的首部。 开发一个 HTTP 过滤模块 一个简单的过滤模块实现这样的功能:在返回的 http response 中,先检查 header,如果是 200 OK,则在 body 中插入一段字符。如下所示:...

2018-11-19 · Me

nginx 模块开发入门

编写模块 想要学习如何开发一个 nginx 模块,最快速简单的方法莫过于写一个 Hello World 模块,没错,还真有这么一个 nginx-hello-world-module,而且 nginx.org 官网还介绍了这个模块。 首先,对于所有的 nginx 模块来说,都需要实现一个 ngx_module_t 结构体,如下所示,需要特别注意的是,如果去看 module 结构体的定义,它与下面的代码并不是一一对应的,这是因为 NGX_MODULE_V1 宏把其他变量都赋值了,帮我们屏蔽了一些细节。 总而言之,开发一个 nginx 模块,我们跟着这个套路走就行了。 ngx_module_t ngx_http_hello_world_module = { NGX_MODULE_V1, &ngx_http_hello_world_module_ctx, /* module context */ ngx_http_hello_world_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; 其中第一个变量和最后一个变量都是固定的,我们不需要关心。如果开发的是 HTTP 模块,那么 module type 那儿写上 HTTP 的宏就行了,都是固定死的操作。...

2018-11-18 · Me

Wireshark/tcpdump 抓到的数据包可信吗?

table {:toc} 问题的表面现象 问题的背景是这样的: 一个应用程序监听某端口的 UDP 包,发送者发送的 UDP 包比较大,有 65535 个字节。显然,发送者的 UDP 包在经过 IP 层时,会被拆分层多个 1460 字节长度的 IP 分片,经过网络传输之后到达接收方,接收方的网卡收到包后,在内核协议栈的 IP 层又将分片重新组合,生成大的 UDP 包给应用程序。 正常情况下,应用程序能准确无误的收到大 UDP 包,偶尔系统网络流量十分巨大的时候,会丢个别 UDP 包 ——这都在允许范围内。 突然有一天,即便网络流量不是很大的时候,UDP 包的丢包十分严重,应用程序几乎收不到任何数据包。 开始 debug 遇到这样的问题,第一反应有两种可能: 网络不通,数据包没送到网卡。 个别 IP 分片在传输中丢失了,导致接收方无法重组成完整的 UDP 包。 于是用 tcpdump 在 interface eth0 上抓包,出乎意料的是,抓到的 pcap 有完整的 IP 分片。用 wireshark 打开 pcap,wireshark 会自动把 IP 分片重组成 UDP 数据包,检查这个 UDP 包,数据完整无误。 那么现在能断定是应用程序自己出了问题吗? 因为一直以来一个根深蒂固的想法是:既然抓到的 pcap 准确无误,所以数据包已经送到了接收方了,linux 内核只要把分片重组一下交给应用层就可以了,这个过程一般不会出错,所以应用程序没收到只能怪它自己咯? 实际导致问题的原因 最终查明的原因是这台 linux 系统上有两个参数被修改了,当 IP 分片数量过大时,内核中分配给重组的缓冲区已满,导致之后的分片都被丢弃了。...

2018-10-24 · Me

nginx 文件锁、自旋锁的实现

在上一篇博客 Linux 共享内存以及 nginx 中的实现的示例中,我们看到每次多个进程同时对共享内存中的 count 加一,导致每次运行结果都不一样,那么解决的方法就是对临界区加锁了,所以本文就来研究一下 nginx 中的几种加锁方式。 文件锁 文件锁的原理就是在磁盘上创建一个文件(操作系统创建一个文件描述符),然后多个进程竞争去获取这个文件的访问权限,因此同一时刻只有一个进程能够访问临界区。 可以看出,进程并不会真正在这个文件中写什么东西,我们只是想要一个文件描述符 FD 而已,因此 nginx 会在创建了文件后把这个文件删除,只留下文件描述符。 多个进程打开同一个文件,各个进程看到的文件描述 FD 值可能会不一样。例如文件 test.txt 在 进程1 中是 101, 而在进程2中是 201 使用文件锁举例 使用文件锁主要用到两个 libc 提供的结构体和函数。 struct flock; 提供一些锁的基本信息,比如读锁 F_RDLCK, 还是写锁 F_WRLCK fcntl(): 对文件描述符进行操作的库函数。 那么如何用这个函数来实现锁的功能呢? 先看一个加锁的代码: void mtx_file_lock(struct fdmutex *m) { struct flock fl; memset(&fl, 0, sizeof(struct flock)); fl.l_type = F_WRLCK; fl.l_whence = SEEK_SET; if (fcntl(m->fd, F_SETLKW, &fl) == -1) { printf("[-] PID %d, lock failed (%s)....

2018-09-18 · Me

Linux 共享内存以及 nginx 中的实现

共享内存方法简介 Linux/Unix系统中,共享内存可以通过两个系统调用来获得,mmap 和 shmget/shm_open,其中 shmget 和 shm_open 分别属于不同的标准: POSIX 共享内存(shm_open()、shm_unlink()) System V 共享内存(shmget()、shmat()、shmdt()) shmget 和 shm_open 类似的地方在于都是创建一个共享内存,挂载到 /dev/shm 目录下,并且返回一个文件描述符,fd。 区别是 POSIX 没有提供将 fd 映射到进程地址空间的方法,而 System V 方式则直接提供了 shmat(),之后再 nginx 的实现中会再次看到。 mmap 语义上比 shmget 更通用,因为它最一般的做法,是将一个打开的实体文件,映射到一段连续的内存中,各个进程可以根据各自的权限对该段内存进行相应的读写操作,其他进程则可以看到其他进程写入的结果。 而 shmget 在语义上相当于是匿名的 mmap,即不关注实体文件,直接在内存中开辟这块共享区域,mmap 通过设置调用时的参数,也可达到这种效果,一种方法是映射/dev/zero 设备,另一种是使用MAP_ANON选项。 mmap() 的函数原型如下,具体参数含义在最后的参考资料中给出。 void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); nginx 中的实现 nginx 中是怎么实现的呢? 我们看一下源码 src/os/unix/ngx_shmem.c。 一目了然,简单粗暴有木有! 分三种情况 如果mmap系统调用支持 MAP_ANON选项,则使用 MAP_ANON 如果1不满足,如果mmap系统调用支持映射/dev/zero设备,则映射/dev/zero来实现。 如果1和2都不满足,且如果支持shmget的话,则使用该shmget来实现。 #if (NGX_HAVE_MAP_ANON) ngx_int_t ngx_shm_alloc(ngx_shm_t *shm) { shm->addr = (u_char *) mmap(NULL, shm->size, PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0); return NGX_OK; } #elif (NGX_HAVE_MAP_DEVZERO) ngx_int_t ngx_shm_alloc(ngx_shm_t *shm) { fd = open("/dev/zero", O_RDWR); shm->addr = (u_char *) mmap(NULL, shm->size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); return (shm->addr == MAP_FAILED) ?...

2018-09-04 · Me

使用 socketpair 实现进程间通信

socketpair 牛刀小试 int socketpair(int d, int type, int protocol, int sv[2]); 第1个参数d,表示协议族,只能为 AF_LOCAL 或者 AF_UNIX; 第2个参数 type,表示类型,只能为0。 第3个参数 protocol,表示协议,可以是 SOCK_STREAM 或者 SOCK_DGRAM AF_UNIX 指的就是 Unix Domain socket,那么它与通常网络编程里面的 TCP socket 有什么区别呢? 查阅了资料后发现: Unix Domain socket 是同一台机器上不同进程间的通信机制。 IP(TCP/IP) socket 是网络上不同主机之间进程的通讯机制。 socketpair() 只支持 AF_LOCAL 或者 AF_UNIX,不支持 TCP/IP,也就是 AF_INET, 所以用 socketpair() 的话无法进行跨主机的进程间通信。 先看一个简单的示例: int main() { int fd[2], retpid; int pid , status; char input[MAX_LEN]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0) { printf("call socketpair() failed, exit\n"); return -1; } pid = fork(); if (pid) { /* parent */ printf("Parent process, pid = %d\n", getpid()); while (1) { fgets(input, MAX_LEN, stdin); write(fd[0], input, MAX_LEN); } } else { /* child */ printf("Child process, pid = %d\n", getpid()); int nread = 0; while (1) { nread = read(fd[1], input, MAX_LEN); input[nread] = '\0'; printf("Child: nread = %d, data = %s\n", nread, input); } } retpid = wait(&status); if (retpid) { printf("Parent: reap child process pid = %d\n", retpid); } return 0; } 编译后运行,可以看到每次在终端输入信息,子进程都会回显到屏幕上。...

2018-08-28 · Me

BPF -- Linux 中的 DTrace

记得 5 年前刚接触 perf 的时候,还特意调研了一下不同系统上的动态和静态追踪工具,知道了 Linux 上的 SystemTap,perf。Solaris 上的 DTrace。看到绝大多数资料都说 DTrace 多么的强大好用,但是 Linux 却没有与之相提并论的工具。 最近看到 BPF 这三个字被提及的很频繁,搜索了一下发现它号称 “Linux 中的 DTrace”, 于是试着玩了一下。 BPF 全称是 “Berkeley Packet Filter”,字面意思是包过滤器,那么问题来了:我一个包过滤器,怎么就成了追踪调试工具呢? 这主要是因为一些历史的进程:原先开发 BPF 的目的是在内核重定向数据包,接着增加了对事件的追踪功能,然后又增加了基于时间的采样,于是久而久之 BPF 就成了一个功能强大的调试工具。 安装 首先,内核版本最好大于 4.9 , 可以用 uname -a 命令查看。 其次,查看一下内核在编译的时候是否开启了 BPF 选项,一般在 /boot/ 目录下有对应内核版本的 config 文件,比如在我的机器上是 /boot/config-4.15.0-30-generic。 如果看到 CONFIG_BPF_SYSCALL=y 说明可以用 BPF 的基本功能。 前面提到 BPF 号称 Linux 中的 DTrace,为什么呢? 因为 DTrace 包含了一个类似脚本语言的 D 语言,用户可以用简单的几句 D 语言完成复杂的调试追踪任务,这一点是 perf 做不到,而 BPF 做到了。 确认了内核支持 BPF 之后,我们可以安装一个叫做 bcc 的工具,通过它可以方便的使用 BPF。...

2018-08-09 · Me

Aho–Corasick 算法,AC 自动机

AHO 算法,或者叫 AC 自动机、又或者叫 Aho–Corasick string matching algorithm,是一个高效的多模式匹配算法,它的特点是可以同时匹配多个模式串。 最常见的应用就是病毒扫描 : 把所有病毒的特征码(类似一段字符串)构造成一个 AC 自动机,把用户的文件或者网络数据流作为输入文本,只要在输入文本中找到了任何一个特征码,那么就表示有病毒存在。 对于这样的一个应用场景,我们最希望的功能就是只扫描一遍输入文本,找出所有的病毒,而 AC 自动机恰恰就有这样的能力。 一般使用 AC 自动机需要以下三步: 根据待匹配的字符串 P1, P2, Pn 建立 Trie 给 Trie 添加失败路径,实际上是生成了一个自动机。 将输入文本 str 的字符逐个通过自动机,在 O(n) 的时间复杂度内找出 P1, P2 … Pn 是否存在于 str 内。 举例 假设我们有模式字符串 { fat, fare, hat, are }, 输入的文本为 “fatehatfare”。 显然,所有的模式串都出现在了我们的输入文本中。我们的目标就是只扫描一遍文本串,找出所有的模式串。 建立 Trie 首先根据模式串建立起一个 trie,一般来说每个节点的结构大概是这样: Node { Node * children[26]; /* 指向子节点的指针 */ Node * parent; /* 指向父节点的指针 */ Node * fail; /* 失败指针 */ bool terminate; /* 是否是一个终结节点,即一个串的最后一个字符 */ } Trie 建立好之后大概是这个样子:...

2018-07-14 · Me

SO_REUSEPORT 和 epoll 的 Thundering Herd

SO_REUSEPORT 顾名思义就是重用端口,是指不同的 socket 可以 bind 到同一个端口上。 Linux 内核 3.9 版本引入了这个新特性,有兴趣的同学可以移步到这个链接查看更加详细的内容。 https://lwn.net/Articles/542629/ Reuse Port 我们先通过一段简单的代码来看看怎么使用这个选项(完整的代码在这里下载)。 int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 一定要在 bind() 函数之前设定好 SO_REUSEPORT setsockopt(serv_sock, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(int)); bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); listen(serv_sock, 20); accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); 将上面的代码编译生成两个可执行文件,分别启动运行,并监听相同的端口。 ./port_reuse1 127.0.0.1 1234 再用 telnet/nc 等工具发送请求到 1234 端口上,多重复几次,会看到两个进程轮流的处理客户端发来的请求。 这里说一个题外话,上面的例子是手动启动两个进程。而我发现如果是进程自动 fork() 生成 2 个进程的话,似乎不用设置 SO_REUSEPORT 也能自动监听同一个端口。这是为什么? Thundering Herd / 惊群现象 The thundering herd problem occurs when a large number of processes waiting for an event are awoken when that event occurs, but only one process is able to proceed at a time....

2018-06-23 · Me

如何拦截库函数调用 ?

LD_PRELOAD 环境变量 : 直接作用在可执行文件上 (准确的说是拦截库函数) 。 ptrace() : 拦截子进程的系统调用。 1. LD_PRELOAD LD_PRELOAD 的优势: 使用简单。 不需要修改被拦截程序的源码。 例如我想拦截程序 A 所有调用 malloc() 的地方,那么程序 A 不需要任何修改,只要准备好自己的 malloc() 函数,编译成动态链接库 .so 文件,然后在运行 A 之前先用 LD_PRELOAD 设定好环境变量就可以了。 LD_PRELOAD 的原理就是链接器在动态链接的时刻,优先链接 LD_PRELOAD 指定的函数。准确的说 LD_PRELOAD 拦截的是动态库中的函数,但是一般我们写的应用程序都是通过库函数来调用系统调用 API,所以 LD_PRELOAD 也间接的拦截了系统调用。 说到这里,LD_PRELOAD 的缺点也非常明显,它只能作用于动态链接库,要是静态链接的就没戏了。 腾讯的 C++ 协程库 libco,以及 tcmalloc 的 TC_MALLOC 都用到了这种方式。 2. ptrace() ptrace 是 linux 内核原生提供的一个功能,因此功能比 LD_PRELOAD 强大的多。它最初的目的是用来 debug,例如大名鼎鼎的 gdb 就是依赖于 ptrace。 要使用 ptrace 拦截程序 A 的系统调用,有两种方法: ptrace 一个新进程:在代码中 fork 一个子进程,子进程执行 ptrace(PTRACE_TRACEME, 0, 0, 0)函数,然后通过 execv() 调用程序 A。 attach 到已运行的程序 A :执行ptrace(PTRACE_ATTACH, pid, 0, 0)。 以上两种方式,ptrace 都会拦截发送到 A 进程的所有信号(除 SIGKILL 外),然后我们需要自己选择哪些系统调用需要拦截,并在拦截后转到我们自己的处理函数。...

2018-06-03 · Me