Senlin's Blog


  • 分类

  • 归档

  • 标签

  • 关于

谈谈 gRPC 的 C++ 异步编程

发表于 2017-08-10   |   分类于 网络编程   |   阅读次数

  这篇文章我们会介绍 gRPC 的异步编程,如果读者没有使用过 gRPC 的经验,可以先阅读我之前写过的 gRPC 编程指南。

异步 Client

  gRPC 支持同步和异步两种编程方式。同步编程理解起来比较容易,譬如说,当一个同步的 client 调用 server 的某个方法时,它会一直处于阻塞状态,等待 server 发送回响应。
  同步的代码编写起来也很简单,和一般的函数调用差不多:

1
2
// 调用 server 的 SayHello 方法,阻塞中
Status status = stub_->SayHello(&context, request, &reply);

  然而对于同步的 client 来说,由于调用远程方法时会阻塞当前线程,所以它无法同时发送多个请求。如果需要同时发送多个请求,那么只能每次发送请求时都在新的线程中发送,而频繁地创建线程会带来不小的开销。
  异步 client 成功地解决了这两个问题:它允许同时发送多个请求,并且每次发送请求时都不需要另外创建线程。异步 client 的解决思路很简单:

  • gRPC 本身提供了异步 API,譬如说我们想调用 server 的SayHello()方法,我们可以调用它的异步版本AsyncSayHello(),调用之后会立即返回,不会阻塞。
  • 当方法调用成功时,我们可以让 gRPC 自动将返回结果放到一个CompletionQueue的队列中(CompletionQueue是一个阻塞队列,并且是线程安全的)。
  • 程序可以启动一个线程,这个线程要做的事,就是不断地从队列中取出结果并处理。
阅读全文 »

Linux 的 OOM Killer 机制分析

发表于 2017-07-03   |   分类于 网络编程   |   阅读次数

按需分配物理页面

  很多情况下,一个进程会申请一块很大的内存,但只是用到其中的一小部分。为了避免内存的浪费,在分配页面时,Linux 采用的是按需分配物理页面的方式。譬如说,某个进程调用malloc()申请了一块小内存,这时内核会分配一个虚拟页面,但这个页面不会映射到实际的物理页面。


  从图中可以看到,当程序首次访问这个虚拟页面时,会触发一个缺页异常 (page fault)。这时内核会分配一个物理页面,让虚拟页面映射到这个物理页面,同时更新进程的页表 (page table)。

Memory Overcommit

  这种按需分配物理页面的方式,可以大大节省物理内存的使用,但有时会导致 Memory Overcommit。所谓 Memory Overcommit,也就是说,所有进程使用的虚拟内存超过了系统的物理内存和交换空间的总和。默认情况下,Linux 是允许 Memory Overcommit 的。并且在大多数情况下,Memory Overcommit 也是安全的,因为很多进程只是申请了很多内存,但实际使用到的内存并不多。
  但万一很多进程都使用了申请来的大部分内存,就可能导致物理内存和交换空间不够用了,这时内核的 OOM Killer 就会出马,它会选择杀掉一个或多个进程,这样就能腾出一些内存给其它进程使用。
  在 Linux 中,可以通过内核参数vm.overcommit_memory去控制是否允许 overcommit:

  • 默认值是 0,在这种情况下,只允许轻微的 overcommit,而比较明显的 overcommit 将不被允许。
  • 如果设置为 1,表示总是允许 overcommit。
  • 如果设置为 2,则表示总是禁止 overcommit。也就是说,如果某个申请内存的操作将导致 overcommit,那么这个操作将不会得逞。

  那么对内核来说,怎样才算 overcommit 呢?Linux 设定了一个阈值,叫做 CommitLimit,如果所有进程申请的总内存超过了 CommitLimit,那就算是 overcommit 了。在/proc/meminfo中可以看到 CommitLimit 的大小:

1
2
$ cat /proc/meminfo | grep CommitLimit
CommitLimit: 3829768 kB

  CommitLimit 的值是这样计算的:

1
CommitLimit = [swap size] + [RAM size] * vm.overcommit_ratio / 100

  其中的vm.overcommit_ratio也是内核参数,它的默认值是 50。

OOM Killer

  当物理内存和交换空间不够用时,OOM Killer 就会选择杀死进程,那么它是怎样知道要先杀死哪个进程呢?其实 Linux 的每个进程都有一个 oom_score (位于/proc/<pid>/oom_score),这个值越大,就越有可能被 OOM Killer 选中。oom_score 的值是由很多因素共同决定的,这里列举几个因素:

  • 如果进程消耗的内存越大,它的 oom_score 通常也会越大。
  • 如果进程运行了很长时间,并且消耗很多 CPU 时间,那么通常它的 oom_score 会偏小。
  • 如果进程以 superuser 的身份运行,那么它的 oom_score 也会偏小。

  如何才能尽量防止某个重要的进程被杀死呢?Linux 每个进程都有一个 oom_adj (位于/proc/<pid>/oom_adj),这个值的范围是 [-17, +15],进程的 oom_adj 会影响 oom_score 的计算,也就是说,我们可以通过调小进程的 oom_adj 从而降低进程的 oom_score。对于一些比较重要的进程,例如 MySQL,我们想尽量避免它被 OOM Killer 杀死,这时候就可以调低它的 oom_adj 的值,例如:

1
$ sudo echo -10 > /proc/$(pidof mysqld)/oom_adj

阅读全文 »

浅谈 Linux 的内存管理

发表于 2017-07-02   |   分类于 内存管理   |   阅读次数

匿名内存映射

  在 Linux 中,如果使用malloc()分配一块很大的内存 (大于128KB),这时malloc()不会直接从 heap 上分配内存,而是会调用mmap()创建匿名的内存映射,那么使用mmap()分配内存有何优缺点呢?

  • 使用mmap()分配内存时,不会产生内存碎片。因为当这块内存不需要时,可以直接调用munmap()释放它,这样内存就会直接返回给操作系统。
  • 在 Linux 中,一个页面的大小是 4KB,所以mmap()分配的内存会按照 4KB 的大小对齐。通常来说,分配小块内存并不会用到mmap(),因为mmap()分配的内存是 4KB 的整数倍,如果不能充分利用,就会造成内存的浪费。
  • 和在 heap 上分配内存相比,使用mmap()分配内存时将带来比较大的开销,所以mmap()不适合频繁地调用,通常只有当分配的内存比较大时,才会使用到mmap()。

  使用mmap()分配内存有两种形式,一种是私有的内存映射,另一种是共享的内存映射。如果创建的是私有的内存映射,那么在fork()之后,尽管子进程也可以访问这块内存,但是 copy-on-write 会保证父进程和子进程对这块内存的修改不会被彼此看见:

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
47
48
49
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/mman.h>
int main()
{
// 创建私有的内存映射, 512 KB
size_t sz = 512 * 1024;
void *p = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED)
{
perror("mmap");
exit(EXIT_FAILURE);
}
int *addr = p;
int n = fork();
if (n == 0)
{
*addr = 100;
printf("Child set value to %d\n", *addr);
if (munmap(p, sz) == -1)
{
perror("munmap");
exit(EXIT_FAILURE);
}
}
else if (n > 0)
{
wait(NULL); // 等待子进程退出
printf("Parent get value %d\n", *addr);
if (munmap(p, sz) == -1)
{
perror("munmap");
exit(EXIT_FAILURE);
}
}
else
{
perror("fork");
exit(EXIT_FAILURE);
}
return 0;
}

  从程序的输出可以看到,子进程对内存的修改不会被父进程看到:

1
2
Child set value to 100
Parent get value 0

阅读全文 »

Linux 系统参数调优

发表于 2017-07-01   |   分类于 网络编程   |   阅读次数

打开文件数

  在 Linux 中,文件描述符是一种资源,为了控制对资源的合理使用,Linux 会限制所有进程所能打开的文件描述符总数。可以通过下面的命令查看:

1
2
$ cat /proc/sys/fs/file-max
99736

  通常来说,如果机器的内存越大,那么file-max的默认值也会越大。当然,也可以手动调大它:

1
2
3
$ sudo vi /etc/sysctl.conf
fs.file-max = 100000
$ sudo sysctl -p # 使改动生效

  那么要怎样才能知道系统当前打开了多少文件描述符呢?可以用下面的命令:

1
2
$ cat /proc/sys/fs/file-nr
704 0 100000

  输出结果的第一个值表示系统当前打开了 704 个文件描述符,在 Linux 2.6 之后,第二个值总是 0,第三个值等于/proc/sys/fs/file-max的值。


  然而fs.file-max这个参数是系统级别的限制,除此之外,Linux 还会限制某个用户所能打开的文件描述符数量,这个值默认是 1024,可以用下面命令查看:

1
2
$ ulimit -n
1024

  从上面的命令可以看到,当前用户最多只能打开 1024 个文件描述符。有时在运行高并发服务器的时候,经常会出现文件描述符不够用的错误,这时候就需要调高这个用户所能打开的文件描述符数量了,可以通过下面的命令修改:

1
2
3
$ sudo vi /etc/security/limits.conf
www-data soft nofile 10240
www-data hard nofile 20480

  上面的命令为www-data用户设置所能打开的文件描述符数量,其中软限制为 10240,而硬限制为 20480。那么软限制和硬限制的区别是什么呢?其实进程在运行的时候可以修改软限制的值,但要保证这个值不能超过硬限制。然而进程却无法修改硬限制的值,除非以 superuser 的身份运行。

阅读全文 »

OpenMP 并行编程指南

发表于 2017-06-25   |   分类于 并行计算   |   阅读次数

并行计算

  对于 CPU 密集型的程序来说,可以考虑使用 OpenMP 加快程序的计算速度。OpenMP 是跨平台的,大部分现代的 C/C++ 编译器都支持 OpenMP。OpenMP 在一定程度上对并行算法进行了抽象,因此它使用起来很方便,程序员可以简单地通过编译器指令#pragma omp去控制程序的行为。
  OpenMP 的语法很简单,它看起来是这样的:

1
#pragma omp <directive> [clause[[,] clause] ...]

  最常见的指令应该算是parallel指令了,紧接在parallel指令后面的那个代码块将会并行地执行:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
int main()
{
#pragma omp parallel
{
std::cout << "Hello, World!" << std::endl;
}
return 0;
}

  程序会启用 N 个线程去执行parallel指令后面的那个代码块 (N 等同于 CPU 的核心数),执行完这个代码块之后,程序又会变回单线程。编译时只需要提供-fopenmp参数,就可以让编译器启用 OpenMP。例如,下面在双核的机器上运行这个程序,将会打印两条消息:

1
2
3
4
$ g++ -std=c++11 -fopenmp -o main main.cpp
$ ./main
Hello, World!
Hello, World!


  OpenMP 还提供了parallel for指令,它的作用就是将 for 循环拆分给 N 个线程去执行,这样每个线程都只需要执行整个 for 循环的其中一部分,所以可以实现并行计算。例如下面的例子,需要计算数组中每个元素的平方,我们可以将它拆分给 N 个线程去执行 (N 等同于 CPU 的核心数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <vector>
#include <cstdint>
using Int = uint64_t;
int main()
{
constexpr Int size = 10 * 1024 * 1024;
std::vector<Int> squares(size, 0);
#pragma omp parallel for
for (Int i = 0; i < size; ++i)
{
squares[i] = i * i;
}
return 0;
}

阅读全文 »
1…567…13

高性能

61 日志
13 分类
14 标签
GitHub 知乎
© 2015 - 2022
由 Hexo 强力驱动
主题 - NexT.Mist
  |   总访问量: