cross compile

cross compile

arm gcc

又一次要进行 cross compile. 记录一下问题.

这一次是直接使用的 friendlyarm 的 gcc.

git clone https://github.com/friendlyarm/prebuilts.git
sudo mkdir -p /opt/FriendlyARM/toolchain
sudo tar xf prebuilts/gcc-x64/arm-cortexa9-linux-gnueabihf-4.9.3.tar.xz -C /opt/FriendlyARM/toolchain/

glibc

出现的问题在于 libc 的版本问题. 这个包里的工具链是依赖于 libc-2.14 的, 但是 centos 6.7 的系统里的
只有 2.12 libc.

所以第一步是安装一个 libc

wget http://ftp.gnu.org/gnu/glibc/glibc-2.14.tar.gz

tar zxvf glibc-2.14.tar.gz

cd glibc-2.14

mkdir build

cd build

../configure --prefix=/opt/glibc-2.14

make -j4

sudo make install

export LD_LIBRARY_PATH=/opt/glibc-2.14/lib

nginx

nginx 正常的情况下要进行一些分析探测当前的机器是不是支持一些功能, 如 epoll, int 大小之类的.
但是在 cross compile 的情况下, 进行探测的结果并不是最后运行的机器的环境. 所以这些探测可能都会失败.

所以要做一些调整, 以及手动开启一些功能.

  • 修改 auto/cc/name@21 的 exit 1, 注释掉.
  • 修改 auto/types/sizeof@48 直接设置 ngx_size 为 4.
  • 修改 auto/modules@43 开启 epoll 模块

增加如下的 flags.

FLAGS='-D NGX_HAVE_GCC_ATOMIC -DNGX_SYS_NERR=132 -DNGX_PTR_SIZE=4 -DNGX_HAVE_SYSVSHM=1 \
-DNGX_HAVE_EPOLL -DNGX_HAVE_EPOLLRDHUP=1 -DNGX_TEST_BUILD_EPOLL=0 -DNGX_HAVE_EPOLLEXCLUSIVE=0'

一些环境变量的分析会出现问题:

  • NGX_HAVE_GCC_ATOMIC: 使用 gcc 的 atomic, nginx 支持几个 atomic 没有 arm.
  • NGX_PTR_SIZE: arm 的机器是 32 位的.
  • NGX_HAVE_SYSVSHM: 对于 shm 的探测好像也出现了问题.
  • NGX_HAVE_EPOLL: 使用 epoll
  • NGX_HAVE_EPOLLRDHUP: 使用 epoll rdhup 事件
  • NGX_TEST_BUILD_EPOLL: 关闭 epoll 测试代码
  • NGX_HAVE_EPOLLEXCLUSIVE: 关闭 EPOLLEXCLUSIVE 功能

configure:

./configure --prefix=/opt/root/ \
           --with-cc=/opt/Friendly_ARMv7/4.9.3/bin/arm-linux-gcc \
           --without-http_rewrite_module \
           --without-http_gzip_module \
           --with-cc-opt="'$FLAGS'"
make
make install

这样可以正常生成最后的 nginx.

其实如果有一个 arm 的系统, 直接编译 nginx 也是非常简单的, 一般情况下也不用
去交叉编译这些上层的程序.

新 Blog 系统

新 Blog 系统

之前的 blog 系统是分成两部分的.

  • blog web: 这个是静态的, 核心的展示与 render 是依赖于 js 在前端完成的
  • api: 这个增加, 修改 文章的 api 接口. 使用 vim 作为客户端. 服务端运行一个 python 后台.

问题在于:
python 后台, 我不是怎么管理, 时间长了之后, 处理不可以维护的状态, 只有我一个人在用的小程度, 作为服务不是一个好的选择.

我想把整个发布体系变成一些更加稳定的方案. 这个时候其实就想到了之前, 那个使用 github 作为博客的方案了. 虽然我不喜欢, 但是
思路是可以用的.

另一个问题在于 blog 只有首页是被 google 索引了, 原因是由于首页对于其它的文章的链接是通过 js 跳转的, 这样就导致了
google 没有办法得到其它的文章的地址, 把所有的文章的地址通过 html a 进行了链接就解决了问题. 大概 12h 内可以从日志里看到
google 的请求, 之后大于 1-2天的时间完成了索引工作. 可以在 google 搜索到所有的文章了.
文章的页面其实也是 js 的, 都是通过同一个 html 页面打开, 通过参数 js 得到文章的内容还有其它信息, 但是这种程序上的并不影响
google 的解析.

新的 blog 通过 git 进行文件维护, 方便保存到 github 上, 不怕文件丢失了. 也方便直接推送到我的服务器上, 使用 git hook
进行发布. 这样服务器上就是一个完全静态的网站了.

本地的操作:

  • make create: 创建新的 store 下的目录, 打开 vim 进行编辑
  • make list: 列出当前修改, 新增加 的文章.
  • make render: render index.html 文件
  • make post: 进行 render, 并提交到 github 与 服务器
  • 修改文件, 直接使用 vim 编辑相应 id 下的 index.mkiv 文件

tcp timestamps

tcp timestamps

用于开启是否使用 timestamps.

在发起请求的时候在如下的位置会设置 OPTIONS_TS.


/* Compute TCP options for SYN packets. This is not the final
 * network wire format yet.
 */
static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
				struct tcp_out_options *opts,
				struct tcp_md5sig_key **md5)
{

......
	if (likely(sysctl_tcp_timestamps && !*md5)) {
		opts->options |= OPTION_TS;
		opts->tsval = tcp_skb_timestamp(skb) + tp->tsoffset;
		opts->tsecr = tp->rx_opt.ts_recent;
		remaining -= TCPOLEN_TSTAMP_ALIGNED;
	}
......
}

在收到的 syn 返回 synack 的情况下, 这个时候也就是一个协商的过程,
所以这里会检查收到的 syn 的选项是不是包含有 OPTION_TS.
这里的在 syn 的选项在解析到 OPTION_TS 的时间,
也只有 sysctl_tcp_timestamps 为 true 的时候可以正确识别.

tcp_make_synack –> tcp_options_write

static void tcp_options_write(__be32 *ptr, struct tcp_sock *tp,
			      struct tcp_out_options *opts)
{
	u16 options = opts->options;	/* mungable copy */
.......

	if (likely(OPTION_TS & options)) {
		if (unlikely(OPTION_SACK_ADVERTISE & options)) {
			*ptr++ = htonl((TCPOPT_SACK_PERM << 24) |
				       (TCPOLEN_SACK_PERM << 16) |
				       (TCPOPT_TIMESTAMP << 8) |
				       TCPOLEN_TIMESTAMP);
			options &= ~OPTION_SACK_ADVERTISE;
		} else {
			*ptr++ = htonl((TCPOPT_NOP << 24) |
				       (TCPOPT_NOP << 16) |
				       (TCPOPT_TIMESTAMP << 8) |
				       TCPOLEN_TIMESTAMP);
		}
		*ptr++ = htonl(opts->tsval);
		*ptr++ = htonl(opts->tsecr);
	}
.......
}

另补充一个有意思的事情, 我们知道在 timestamp 开启的时候, 如果服务器开启了
tw_recycle 如果机器在 net 后面, 就有可能出错. 我在测试的时候发现.
virtualbox 的 net 会删除 tcp 的 timestamp 选项.

tcp recycle and reuse

tcp recycle and reuse

关于以下的三个选项的真正的含义, 以及相互之间的关系, 网络上真的是众说纷纭.

  • tw_recycle
  • tw_reuse
  • timestamps

tw_recycle & timestamps

一个核心的点在于, tw_recycle 依赖是对端的 TS, 与本机的 timestamps 选项之间
并没有直接的关系.

当然了, 一般的情况下, 如果本机关闭了 timestamps, 那么在协商的时候, 对端也
不会开启就是了.
就算对端总在报文里带有 TS 信息, 在本机 timestamps 选项关闭的情况下, 也不会识别.
所以 tw_recycle 也可以说与本机的 timestamps 选项有关了.

tw_reuse & timestamps

这两者之间的关系, 大体是没有什么问题的, tw_reuse 是依赖于 timestamps.

相同四元组

比如 peer 使用相同的地址端口再次发起了请求. 这个时候, 如果服务器的之前那个连接的 socket 还在 tw 状态.
这个时候是如何处理的.

	/* Out of window segment.

	   All the segments are ACKed immediately.

	   The only exception is new SYN. We accept it, if it is
	   not old duplicate and we are not in danger to be killed
	   by delayed old duplicates. RFC check is that it has
	   newer sequence number works at rates <40Mbit/sec.
	   However, if paws works, it is reliable AND even more,
	   we even may relax silly seq space cutoff.

	   RED-PEN: we violate main RFC requirement, if this SYN will appear
	   old duplicate (i.e. we receive RST in reply to SYN-ACK),
	   we must return socket to time-wait state. It is not good,
	   but not fatal yet.
	 */

	if (th->syn && !th->rst && !th->ack && !paws_reject &&
	    (after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) ||
	     (tmp_opt.saw_tstamp &&
	      (s32)(tcptw->tw_ts_recent - tmp_opt.rcv_tsval) < 0))) {
		u32 isn = tcptw->tw_snd_nxt + 65535 + 2;
		if (isn == 0)
			isn++;
		TCP_SKB_CB(skb)->tcp_tw_isn = isn;
		return TCP_TW_SYN;
	}

可以看到如果是正常的 syn, 并且符合以下条件之一就可以了.

  • seq 增加了
  • peer 提供了 timestamps

回到 tw 的意义, 一个重要的意义就是等所有旧的包消失. 如果新的连接的 seq
比之前的大, 那么在底速网络中, 可以保证旧的所有的包并不会对于新的连接有影响.

另一个条件看起来好像和 tw_recycle 有些相似, 只要新的连接有 timestamp, 并且 timestamp
在增加. timestamp 机制会保证所有在这个连接上的旧的会都会被丢掉.

那么这里会不会引发那个 recycle 的 net 问题呢. 其实是会的, 但是只是一次连接失败,
并不影响什么. 因为只要你的 seq 是小的, 那么也一样会有问题. 实际上的 recycle 对于
timestamps 的利用就是要解决 seq 可能是小的情况. recycle 的net 问题的关键并不在这里.

recycle

recycle 机制是对于 timestamp 的利用. 一个核心的想法就是, 如果不符合 timestamp 的包
都会被丢掉, 那么我们为什么要等 2MSL 呢. 只要等一个 rto 保证, 最后 ack 传递成功, 就可以了.
不用等网络上的包都丢失了. 这样就实现了 tw 的快速加收.

// tcp_time_wait() net/ipv4/tcp_minisocks.c  line 267
...
// ts_recent_stamp依赖于timestamp选项的开启,可进tcp_minisocks.c验证
if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
    recycle_ok = tcp_remember_stamp(s);
...
// 如果能够recycle,则使用更短的rto作为timeout,从而更快回收TIME-WAIT
if (timeo < rto)
    timeo = rto;
if (recycle_ok) {
    tw->tw_timeout = rto;
} else {
    tw->tw_timeout = TCP_TIMEWAIT_LEN;
    if (state == TCP_TIME_WAIT)
        timeo = TCP_TIMEWAIT_LEN;
}
inet_twsh_schedule(tw, &tcp_death_row, timeo, TCP_TIMEWAIT_LEN);

// tcp_timewait_state_process() net/ipv4/tcp_minisocks.c line 94
// 另一条进入time-wait的路线有类似的代码
if (tcp_death_row.sysctl_tw_recycle &&
    tcptw->tw_ts_recent_stamp &&
    tcp_tw_remember_stamp(tw))
        inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,
                           TCP_TIMEWAIT_LEN);
else
        inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                           TCP_TIMEWAIT_LEN);

如果是这样, 好像也是没有问题的. 那 recycle 的net 问题是如何出现的呢?

在收到 syn 的情况下, 由于 tw 状态的 socket 已经快速回收了, 那这个收到的 syn
有没有可能是一个旧的又重新出现的 syn 呢?
那在使用 recycle 的时候就要对于新出现的 syn 进行 timestamps 检查了.
由于 tw 的 socket 已经不存在了, 我们只能记录所有快速回收的 socket 的最后一次 timestamps.
并且这个值, 绑定到对端的 ip 上. 为什么不绑定到 ip:port 上, 我想, 如果这样, 纪录的东西太多了,
这样 tw 回收意义就没有了.

// tcp_v4_conn_request(), net/ipv4/tcp_ipv4.c line 1551
if (tmp_opt.saw_tstamp &&      // 是否见到过tcp_timestamp选项
    tcp_death_row.sysctl_tw_recycle &&   // 接着判断是否开启recycle
    (dst = inet_csk_route_req(sk, &fl4, req)) != NULL &&    // 最终判断saddr是否有相关记录在route表中
    fl4.daddr == saffr) {
    if (!tcp_peer_is_proven(req, dst, true)) {  // 如果这个建连请求不能被proven,则会被丢弃
        NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
        goto drop_and_release;
    }
}

这里的核心就是 ip. 由于是绑定在 ip 上, 那么使用同一个 ip 的所有的机器都要保证 timestamps 的增加是
一致的, 这在 net 网络里, 当然是不行的了. 所以就有可能, 导致 net 后的一些机器, 总是
无法进行连接. 由于这些机器的 timestamps 不正确. 与之前那个相同四元组的情况相比,
这种情况更加严重得多. 这些也是 recycle 机制与 net 冲突的核心所在.

reuse

从名字也可以看出区别, reuse 只是重用 tw socket. recycle 是可以回收 socket, 这个价值在于, 可以影响更多的 socket.

reuse 在连接出去的时候, 可以复用 socket.

int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
	const struct tcp_timewait_sock *tcptw = tcp_twsk(sktw);
	struct tcp_sock *tp = tcp_sk(sk);

	/* With PAWS, it is safe from the viewpoint
	   of data integrity. Even without PAWS it is safe provided sequence
	   spaces do not overlap i.e. at data rates <= 80Mbit/sec.

	   Actually, the idea is close to VJ's one, only timestamp cache is
	   held not per host, but per port pair and TW bucket is used as state
	   holder.

	   If TW bucket has been already destroyed we fall back to VJ's scheme
	   and use initial timestamp retrieved from peer table.
	 */
	if (tcptw->tw_ts_recent_stamp &&
	    (!twp || (sysctl_tcp_tw_reuse &&
			     get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
		tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2;
		if (tp->write_seq == 0)
			tp->write_seq = 1;
		tp->rx_opt.ts_recent	   = tcptw->tw_ts_recent;
		tp->rx_opt.ts_recent_stamp = tcptw->tw_ts_recent_stamp;
		sock_hold(sktw);
		return 1;
	}

	return 0;
}

区别

作用

  • recycle: 加速了 timewait 的等时间从 2MSL(60s) 为 rto.
  • reuse: 在请求出去的时候, 利用端口.

范围

明显 recycle 的意义, 更大一些, 所有的 tw 都可以快速加收. 而 reuse 只在请求出动的时候有可能起作用.
对于服务器而言并不能解决 tw 大的问题.

依赖

  • recycle: 依赖于用户的 timestamp
  • reuse: 依赖于本地的 timestamp

可靠性

recycle 的依赖是有问题的, 用户的 timestamp 是不可靠的.
而 reuse 基本是可靠的. kernel 内对于 reuse 的态度也是”一般情况下没有什么问题”.
recycle 是不符合 tcp 协议的, 文档上都有所说明: “请在专业人士的指导下进行”.

close-wait

close-wait

当 close-wait 出现, 这一般意味着, 应用程序出现了问题.

为了解决这个问题, epoll 引入了 EPOLLRDHUP 事件, 用于即时通知应用层, 当收到 fin 的情况下.

但是一些情况下, 还是会出现 close-wait 一些逻辑上的问题.

当 close-wait 出现的情况下, 理论上这个情况会一直这样下去. 因为 tcp 是双工的,
一端不再向你发送数据了, 但是你不能不让我发送数据. 所以我可以一直占用着这个 tcp fd.

但是事实又有一些出入, 我观察到一些情况下, close-wait 还是会消失的.
但是程序的并没有调用 close. 细细思考这个问题: 在对端 fin 之后, 我还可以
向对方发送数据, 但是内核就不能知道对方可能完全不再接收数据了吗.
基于一些其它的方式比如 keepalive, 内核还是可以知道, 就算上层再发送数据,
这个时候, 数据也是会发送失败的. 内核可以先释放这个端口.

这个时候去看 lsof 就会发现如下的信息.

can't identify protocol

我理解, 由于内核已经完全闭关了连接, 现在只有应用层占用了一个 fd.
fd 下的连接相关的资源已经加收了.

当然这样, 最后虽然没有 close-wait, 但是还是有可能出现句柄不够用的情况.

这个过程中, 内核如何探知可以完全 shutdown 连接的, 这个还不清楚.

以上是 close-wiat 在线上的一些发现.

proc.net.sockstat

proc.net.sockstat

这里记录了一些 sock 相关的总体性的数据. 我比较关注的是其中的 mem:

sockets: used 121
TCP: inuse 18 orphan 0 tw 64 alloc 20 mem 6
UDP: inuse 0 mem 0
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

其中 mem 的单位很少有说清的地方, 我就自己看一一下 kernel 代码:

static int sockstat_seq_show(struct seq_file *seq, void *v)
{
        struct net *net = seq->private;
        unsigned int frag_mem;
        int orphans, sockets;

        local_bh_disable();
        orphans = percpu_counter_sum_positive(&tcp_orphan_count);
        sockets = proto_sockets_allocated_sum_positive(&tcp_prot);
        local_bh_enable();

        socket_seq_show(seq);
        seq_printf(seq, "TCP: inuse %d orphan %d tw %d alloc %d mem %ld\n",
                   sock_prot_inuse_get(net, &tcp_prot), orphans,
                   tcp_death_row.tw_count, sockets,
                   proto_memory_allocated(&tcp_prot));
        seq_printf(seq, "UDP: inuse %d mem %ld\n",
                   sock_prot_inuse_get(net, &udp_prot),
                   proto_memory_allocated(&udp_prot));
        seq_printf(seq, "UDPLITE: inuse %d\n",
                   sock_prot_inuse_get(net, &udplite_prot));
        seq_printf(seq, "RAW: inuse %d\n",
                   sock_prot_inuse_get(net, &raw_prot));
        frag_mem = ip_frag_mem(net);
        seq_printf(seq,  "FRAG: inuse %u memory %u\n", !!frag_mem, frag_mem);
        return 0;
}

static inline long
proto_memory_allocated(struct proto *prot)
{
        return atomic_long_read(prot->memory_allocated);
}

static inline long
sk_memory_allocated_add(struct sock *sk, int amt)
{
        return atomic_long_add_return(amt, sk->sk_prot->memory_allocated);
}

int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
        struct proto *prot = sk->sk_prot;
        long allocated = sk_memory_allocated_add(sk, amt);

        if (mem_cgroup_sockets_enabled && sk->sk_memcg &&
            !mem_cgroup_charge_skmem(sk->sk_memcg, amt))
                goto suppress_allocation;


       ....
}

int __sk_mem_schedule(struct sock *sk, int size, int kind)
{
        int ret, amt = sk_mem_pages(size);

        sk->sk_forward_alloc += amt << SK_MEM_QUANTUM_SHIFT;
        ret = __sk_mem_raise_allocated(sk, size, amt, kind);
        if (!ret)
                sk->sk_forward_alloc -= amt << SK_MEM_QUANTUM_SHIFT;
        return ret;
}

以上是相关性的函数. 在 __sk_mem_schedule 中有一个 size 转成 amt 的过程, 这个过程是基于 page
进行计算的. 后面对于memory_allocated 的增加都是直接增加的 amt 这个变量. 所以这里认为其就是以 page
为单位的.

malloc_trim

malloc_trim

malloc_trim

最近遇到了一个 glibc/ptmalloc 的内存空洞的问题. 遇到了一个 malloc_trim 的问题.
本来就内存空洞来说, malloc_trim 应该是没有什么作用的.

       The malloc_trim() function attempts to release free memory at the top
       of the heap (by calling sbrk(2) with a suitable argument).

       The pad argument specifies the amount of free space to leave
       untrimmed at the top of the heap.  If this argument is 0, only the
       minimum amount of memory is maintained at the top of the heap (i.e.,
       one page or less).  A nonzero argument can be used to maintain some
       trailing space at the top of the heap in order to allow future
       allocations to be made without having to extend the heap with
       sbrk(2).

但是看了一下相关的文章:

 http://www.cnblogs.com/lookof/archive/2013/03/26/2981768.html ,

 https://medium.com/@jackyu/memory-leak-malloc_trim-cccc33b0d9e1#.44b4b9zho .

都说 malloc_trim 好像可以释放空洞的空间. 我本来是不相信的. 但是我测试了一下.

使用

gdb --batch -p <pid> --ex "call malloc_trim(0)"

发现内存 RES 确实减小了. 这样的话有两个解释:

  • heap top 上本来就有很大的空间, 可以释放.
  • heap holes 确实被释放了.

第一点不可能. 我查看了所有源代码也都没有对于 M_TRIM_THRESHOLD 的修改.
那就是 holes 真的被释放了. 我进一步查看, 注意到虚拟内存的大小并没有发生变化.
我打印了, 操作 malloc_trim 前后的进程的 pmap,diff 了一下发现是一样的.
这直接否定了, 前面的两个猜测.

基于这个事实我猜测: 是不是直接进行了物理内存的映射的解除.

没有线索的情况下, 查看了 malloc_trim 的源代码:

static int
mtrim (mstate av, size_t pad)
{
  /* Don't touch corrupt arenas.  */
  if (arena_is_corrupt (av))
    return 0;

  /* Ensure initialization/consolidation */
  malloc_consolidate (av);

  const size_t ps = GLRO (dl_pagesize);
  int psindex = bin_index (ps);
  const size_t psm1 = ps - 1;

  int result = 0;
  for (int i = 1; i < NBINS; ++i)
    if (i == 1 || i >= psindex)
      {
        mbinptr bin = bin_at (av, i);

        for (mchunkptr p = last (bin); p != bin; p = p->bk)
          {
            INTERNAL_SIZE_T size = chunksize (p);

            if (size > psm1 + sizeof (struct malloc_chunk))
              {
                /* See whether the chunk contains at least one unused page.  */
                char *paligned_mem = (char *) (((uintptr_t) p
                                                + sizeof (struct malloc_chunk)
                                                + psm1) & ~psm1);

                assert ((char *) chunk2mem (p) + 4 * SIZE_SZ <= paligned_mem);
                assert ((char *) p + size > paligned_mem);

                /* This is the size we could potentially free.  */
                size -= paligned_mem - (char *) p;

                if (size > psm1)
                  {
#if MALLOC_DEBUG
                    /* When debugging we simulate destroying the memory
                       content.  */
                    memset (paligned_mem, 0x89, size & ~psm1);
#endif
                    __madvise (paligned_mem, size & ~psm1, MADV_DONTNEED);

                    result = 1;
                  }
              }
          }
      }

#ifndef MORECORE_CANNOT_TRIM
  return result | (av == &main_arena ? systrim (pad, av) : 0);

#else
  return result;
#endif
}

结合一些资料, 注意到了 __madvise(就是 madvise).

The madvise() system call advises the kernel about how to handle paging
input/output in the address range beginning at address addr and with
size length bytes. It allows an application to tell the kernel how it
expects to use some mapped or shared memory areas, so that the kernel
can choose appropriate read-ahead and caching techniques. This call does
not influence the semantics of the application (except in the case of
MADV_DONTNEED), but may influence its performance. The kernel is free to
ignore the advice.


MADV_DONTNEED
    Do not expect access in the near future. (For the time being, the
    application is finished with the given range, so the kernel can free
    resources associated with it.) Subsequent accesses of pages in this
    range will succeed, but will result either in reloading of the memory
    contents from the underlying mapped file (see mmap(2)) or
    zero-fill-on-demand pages for mappings without an underlying file.

我的猜测是正确的. 在 malloc_trim 的情况, 内存的空洞的物理映射被解除了.
这样就把物理内存返回给了系统, 但是实际上进程的虚拟内存并没有受到影响.

大概也正是因为虚拟内存空间没有收到的影响, 所以 malloc_trim
的 man 上并没有说明这个问题.

其它

在研究内存空洞的问题的过程中, 最大的一个问题就是如何把程序的内存泄漏与 ptmalloc 的
内存空洞问题进行区分. 下面的命令无疑是一个非常有效的方法.

gdb --batch -p <pid> --ex "call malloc_trim(0)"

参考

 http://man7.org/linux/man-pages/man3/malloc_trim.3.html 
 https://linux.die.net/man/2/madvise 

RES VS 共享内存

RES VS 共享内存

共享内存在计算 res 的时候是怎么计算的. 这个问题比较头痛. 我们在统计系统的内存占用的时候总是会有这样的
疑问.

#include <stdio.h>
#include <stddef.h>
#include <unistd.h>
#include <string.h>

#include <sys/mman.h>


int size = 1024 * 1024 * 10;

main()
{
    void *p;
    p = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON,-1,0);
    memset(p, 0, size);

    if (0 == fork())
    {
        memset(p + size/2, 0, size/2);
        sleep(3999999999);
    }

    sleep(3999999999);
}

使用这样的方式进行了一下测试, 发现 ps 出的父进程使用了 10M 左右的内存, 子进程使用了 5M 的内存.
这是实际上子进程的这 5M 的内存, 父进程也在使用. 所以 RES 统计的结果是, 进程实际使用的到的内存,
无论这个内存是不是同时也被其它的进程使用了.

同理, 我们在统计系统的所有进程使用的内存的时候, 共享内存都是重复计算的.

此外还有一个就是进程文件的, 特别是库的占用空间都是重复计算了的.

HEAPPROFILE 实践

HEAPPROFILE 实践

执行

LD_PRELOAD=/tmp/libtcmalloc.so.4 HEAPPROFILE=/tmp/p  HEAP_PROFILE_MMAP=1 HEAP_PROFILE_ALLOCATION_INTERVAL=209715200 nginx

生成 heap 文件

遇到的第一个问题就是无法生成 heap 文件.
如果设置正确会有输出:

Starting tracking the heap

并马上就会输出一个 heap 文件.
我当时总是在想是不是还没有达到 heap 的生成条件, 一直没有看到 heap 文件.

原因是我使用的 tcmalloc 库是从一个机器上复制到线上的, 我不知道那个库是谁生成的.
他好像是关闭了这个功能. 这个花了一些时间.

多进程

由于我要分析的是 nginx. 多个进程, 这样就要对于 heap 文件进行区分, 官方的文档里说是可以支持
子进程, 会在输出的文件名里加入 pid. 但是实际上没有. 有人提出这个问题.
 https://github.com/gperftools/gperftools/issues/601 我按他的方法修改了代码可以正常输出我想要用的格式.

内存大小不对

终于得到了我想用的结果, 但是内存的大小好像不正常. 这我早有猜测, 应该是没有统计 mmap 的内存.
增加参数. 官方的说明是 “If heap-profiling is on, also profile mmap, mremap, and sbrk”.

HEAP_PROFILE_MMAP=1

其它的参数

生成 heap 文件, 那么什么时候生成. 如何控制生成的周期.

HEAP_PROFILE_ALLOCATION_INTERVAL

这个参数实现控制的. 默认的是 1G. 也就是程序的内存上升了 1G 就 dump 一次.

还有一些相关的参数在:
 https://github.com/gperftools/gperftools/blob/7822b5b0b9fa7e016e1f6b46ea86f26f4691a457/src/heap-profiler.cc#L95 

pprof

pprof 是一个 perl 脚本用于 heap 结果. 可以生成多种格式. text, svg 之类的. 也可以使用 raw 生成一个数据文件,

net.ipv4.tcp_wmem not work

net.ipv4.tcp_wmem not work

经常发现 net.ipv4.tcp_wmem 没有作用. 这个参数的修改一般, 都看不出效果.
感觉很奇怪, 文档上写的很明白. 但是就是不启效果.

今天, 看代码的时候, 突然想到, accept 的 socket 的 sndbuf 是不是继承自 listen socket.
之前发现过这一特点. 新的 socket 有一些是继承自 listen socket 的.

实验

systemtap

probe kernel.function("tcp_sendmsg")
{
    if (execname() == "test")
        printf("%s sk_sndbuf: %d \n", execname(), $sk->sk_sndbuf);
}

server code

/**
 *   author       :   丁雪峰
 *   time         :   2016-08-31 09:37:57
 *   email        :   fengidri@yeah.net
 *   version      :   1.0.1
 *   description  :
 */
#include <stdio.h>
#include <stddef.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <malloc.h>
#include "sws.h"

int main()
{
    int  s = sws_net_server("127.0.0.1", 9999, 0, 1);
    if (s < 0)
    {
        printf("sws_net_server fail\n");
        return -1;
    }

    while (1)
    {
        int fd = accept(s, NULL, 0);
        if (fd < 0)
        {
            printf("accept err\n");
            continue;
        }
        sws_net_noblock(fd, 1);

        int size = 1024 * 1024 * 20;
        char *buf = malloc(size);


        int n = write(fd, buf, size);
        printf("write: %d\n", n);
        close(fd);
    }
}

compile:

gcc test.c -L ../sws -I ../sws -lsws -g -o test

sws 是我的一个简单的代码库.

操作

nc 127.0.0.1 9999 > /dev/null -v

反复操作上面的命令, stap 会打印出在 tcp_sendmsg 的下的 sk_sndbuf, server 的代码会打印出成功写入到 socket 的
数据的大小. 可以看到两者的大小是相对应.

但是这个时候, 如果调整了 net.ipv4.tcp_wmem, stap 的输出值并不会变化.

但是重启 test 程序, stap 的输出值就是新的调整的值了.

这就可以证明前面的猜测了.

kernel code

如下的代码是, accept api 对应的内核代码, 创建新的 socket 的时候, 会调用 sctp_copy_sock.
这个函数里会把 sk_sndbuf, sk_rcvbuf
之类的值都复制到新的 socket.

/* Create and initialize a new sk for the socket returned by accept(). */
static struct sock *sctp_v4_create_accept_sk(struct sock *sk,
					     struct sctp_association *asoc)
{
	struct sock *newsk = sk_alloc(sock_net(sk), PF_INET, GFP_KERNEL,
			sk->sk_prot);
	struct inet_sock *newinet;

	if (!newsk)
		goto out;

	sock_init_data(NULL, newsk);

	sctp_copy_sock(newsk, sk, asoc);
	sock_reset_flag(newsk, SOCK_ZAPPED);

	newinet = inet_sk(newsk);

	newinet->inet_daddr = asoc->peer.primary_addr.v4.sin_addr.s_addr;

	sk_refcnt_debug_inc(newsk);

	if (newsk->sk_prot->init(newsk)) {
		sk_common_release(newsk);
		newsk = NULL;
	}

out:
	return newsk;
}

void sctp_copy_sock(struct sock *newsk, struct sock *sk,
		    struct sctp_association *asoc)
{
	struct inet_sock *inet = inet_sk(sk);
	struct inet_sock *newinet;

	newsk->sk_type = sk->sk_type;
	newsk->sk_bound_dev_if = sk->sk_bound_dev_if;
	newsk->sk_flags = sk->sk_flags;
	newsk->sk_no_check_tx = sk->sk_no_check_tx;
	newsk->sk_no_check_rx = sk->sk_no_check_rx;
	newsk->sk_reuse = sk->sk_reuse;

	newsk->sk_shutdown = sk->sk_shutdown;
	newsk->sk_destruct = sctp_destruct_sock;
	newsk->sk_family = sk->sk_family;
	newsk->sk_protocol = IPPROTO_SCTP;
	newsk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
	newsk->sk_sndbuf = sk->sk_sndbuf;
	newsk->sk_rcvbuf = sk->sk_rcvbuf;
	newsk->sk_lingertime = sk->sk_lingertime;
	newsk->sk_rcvtimeo = sk->sk_rcvtimeo;
	newsk->sk_sndtimeo = sk->sk_sndtimeo;

	newinet = inet_sk(newsk);

	/* Initialize sk's sport, dport, rcv_saddr and daddr for
	 * getsockname() and getpeername()
	 */
	newinet->inet_sport = inet->inet_sport;
	newinet->inet_saddr = inet->inet_saddr;
	newinet->inet_rcv_saddr = inet->inet_rcv_saddr;
	newinet->inet_dport = htons(asoc->peer.port);
	newinet->pmtudisc = inet->pmtudisc;
	newinet->inet_id = asoc->next_tsn ^ jiffies;

	newinet->uc_ttl = inet->uc_ttl;
	newinet->mc_loop = 1;
	newinet->mc_ttl = 1;
	newinet->mc_index = 0;
	newinet->mc_list = NULL;
}