Linux吃掉我的内存
在Windows下资源管理器查看内存使用的情况,如果使用率达到80%以上,再运行大程序就能感觉到系统不流畅了,因为在内存紧缺的情况下使用交换分区,频繁地从磁盘上换入换出页会极大地影响系统的性能。而当我们使用free命令查看Linux系统内存使用情况时,会发现内存使用一直处于较高的水平,即使此时系统并没有运行多少软件。这正是Windows和Linux在内存管理上的区别,乍一看,Linux系统吃掉我们的内存(Linux ate my ram),但其实这也正是其内存管理的特点。
free命令介绍
下面为使用free命令查看我们实验室文件服务器内存得到的结果,-m选项表示使用MB为单位:
[root@archlab-server2 ~]# free -m total used free shared buffers cached Mem: 3920 1938 1982 0 497 1235 -/+ buffers/cache: 205 3714 Swap: 4095 0 4095
输出的第二行表示系统内存的使用情况:
Mem: total(总量)= 3920MB,
used(已使用)= 1938MB,
free(空闲)= 1982MB,
shared(共享内存)= 0MB,
buffers = 497MB,
cached = 1235MB
注:前面四项都比较好理解,buffer 和 cache找不到合适的词来翻译,它们的区别在于:
- A buffer is something that has yet to be "written" to disk.
- A cache is something that has been "read" from the disk and stored for later use.
即buffer用于存放要输出到磁盘的数据,而cache是从磁盘读出存放到内存中待今后使用的数据。它们的引入均是为了提供IO的性能。
输出的第三行表示在第二行的基础上-/+ buffers/cache得到的:
- buffers/cache used = Mem used – buffers – cached = 1938MB – 497MB – 1235MB = 205MB
+ buffers/cache free = Mem free + buffers + cached = 1982MB + 497MB + 1235MB = 3714MB
输出的第三行表示交换分区使用的情况:
Swap:total(总量)= 4095MB
used(使用)= 0MB
free(空闲)= 4095MB
由于系统当前内存还比较充足,并未使用到交换分区。
上面输出的结果比较难理解的可能是第三行,为什么要向用户展示这行数据呢?内存使用量减去系统buffer/cached的内存表示何意呢?系统空闲内存加上buffer/cached的内存又表示何意?
内存的分类
我们把内存分为三类,从用户和操作系统的角度对其使用情况有不同的称呼:
Memory that is |
You'd call it |
Linux calls it |
taken by applications |
Used |
Used |
available for applications, and used for something |
Free |
Used |
not used for anything |
Free |
Free |
上表中something代表的正是free命令中"buffers/cached"的内存,由于这块内存从操作系统的角度确实被使用,但如果用户要使用,这块内存是可以很快被回收被用户程序使用,因此从用户角度这块内存应划为空闲状态。
再次回到free命令输出的结果,第三行输出的结果应该就能理解了,这行的数字表示从用户角度看系统内存的使用情况。因此,如果你用top或者free命令查看系统的内存还剩多少,其实你应该将空闲内存加上buffer/cached的内存,那才是实际系统空闲的内存。
buffers/cached好处
Linux 内存管理做了很多精心的设计,除了对dentry进行缓存(用于VFS,加速文件路径名到inode的转换),还采取了两种主要Cache方式:Buffer Cache和Page Cache,目的就是为了提升磁盘IO的性能。从低速的块设备上读取数据会暂时保存在内存中,即使数据在当时已经不再需要了,但在应用程序下一次访问该数据时,它可以从内存中直接读取,从而绕开低速的块设备,从而提高系统的整体性能。而Linux会充分利用这些空闲的内存,设计思想是内存空闲还不如拿来多缓存一些数据,等下次程序再次访问这些数据速度就快了,而如果程序要使用内存而系统中内存又不足时,这时不是使用交换分区,而是快速回收部分缓存,将它们留给用户程序使用。
因此,可以看出,buffers/cached真是百益而无一害,真正的坏处可能让用户产生一种错觉——Linux耗内存!其实不然,Linux并没有吃掉你的内存,只要还未使用到交换分区,你的内存所剩无几时,你应该感到庆幸,因为Linux 缓存了大量的数据,也许下一次你就从中受益!
实验证明
下面通过实验来验证上面的结论:
我们先后读入一个大文件,比较两次读入的实践:
-
首先生成一个1G的大文件
[root@archlab-server2 ~]# dd if=/dev/zero of=bigfile bs=1M count=1000 1000+0 records in 1000+0 records out 1048576000 bytes (1.0 GB) copied, 15.8598 s, 66.1 MB/s [root@archlab-server2 ~]# du -h bigfile 1001M bigfile
-
清空缓存
[root@archlab-server2 ~]# echo 3 | tee /proc/sys/vm/drop_caches 3 [root@archlab-server2 ~]# free -m total used free shared buffers cached Mem: 3920 154 3766 0 0 33 -/+ buffers/cache: 120 3800 Swap: 4095 0 4095
-
读入这个文件,测试消耗的时间
[root@archlab-server2 ~]# time cat bigfile > /dev/null real 0m18.449s user 0m0.013s sys 0m0.617s [root@archlab-server2 ~]# free -m total used free shared buffers cached Mem: 3920 1159 2761 0 3 1035 -/+ buffers/cache: 120 3800 Swap: 4095 0 4095
-
再次读入该文件,测试消耗的时间
[root@archlab-server2 ~]# time cat bigfile > /dev/null real 0m0.310s user 0m0.005s sys 0m0.304s
从上面看出,第一次读这个1G的文件大约耗时18s,而第二次再次读的时候,只耗时0.3s,足足提升60倍!
参考资料:
https://groups.google.com/forum/#!topic/shlug/Dvc-ciKGt7s/discussion 实验室师兄以前提出"cp命令占用双倍内存问题",在SLUG上的讨论,正是这个疑惑一直在我心中
http://www.linuxatemyram.com/ 前两天在Hacker News看到"Linux ate my ram"这个网页,写的通俗易懂,也顺藤摸瓜解决心中很多疑惑
http://www.linuxatemyram.com/play.html 作者给出了其他的实验来分别验证磁盘cache对程序分配、交换分区、程序加载时间等的影响
http://www.cnblogs.com/coldplayerest/archive/2010/02/20/1669949.html 一篇对free命令介绍的博客
《Linux多线程服务端编程:使用muduo C++网络库》上市半年重印两次,总印数达到了9000册
《Linux多线程服务端编程:使用muduo C++网络库》这本书自今年一月上市以来,半年之内已经重印两次(加上首印,一共是三次印刷),总印数达到了9000册,这在技术书里已经算是相当不错的成绩。本书购买方式见配套网站 http://chenshuo.com/book 。
以下谈一谈这本书的写作背景与内容取舍的原因。
参加工作以来,我编写并维护了若干C++/Java多线程网络服务程序,这本书总结了我在开发维护这类服务程序方面的经验。工作中,我没有见过单线程的网络服务程序,没有见过C语言写的网络服务程序,也没有见过运行在Windows下的网络服务程序,因此本书不涉及这些内容。
在“Linux服务端开发”这一背景下,这本书主要讲三个方面的内容[1]:现代C++、多线程、网络编程,分别对应书的第3、1、2部分。这不是一本入门书,本书的读者应该在以上三方面已经具备相当的基础[2]:网络编程方面,能轻松读懂6.1节的两个Python程序;C++方面,对12.8节的代码不感到陌生;多线程方面,能明白第1章要解决什么问题。
第9章“分布式系统工程实践”详细介绍了这本书的应用背景,即开发公司内部的分布式服务系统,书中的很多决策(design decision)和技术取舍(trade-off)是在这一应用场景下做出的。以下是各章直接的交叉引用关系图(没有计算引用次数),其中第0章是前言,字母章节是附录。可见第9章是被引用最多的一章,某种意义上可以说第9章既是本书的先决条件,又是本书的终极目标。由于章节之间存在众多的交叉引用,去掉任何一章都会破坏内容的完整性。
这本书的书名原本打算叫“Linux C++ 多线程系统编程”。写完之后发现,与其他Unix/Linux系统编程方面的书不同,这本书有明确的应用场景,因此可以砍掉很多内容,突出重点。甚至可以说我主要讲别的书没有讲到的内容。这不是一本面面俱到的书,因此最终的书名也就不叫“系统编程”了。
同时,我认为很多教科书上介绍的一些做法是过时的(signal),一些是不推荐使用的(从外部终止线程、TCP OOB数据),一些是大多数情况下没必要使用的(内存池、lock-free 编程)。作为全面的教材和手册,把这些内容放进去可以理解。但是这本书定位是经验总结,我略去了教科书上那些基本用不到的知识点,以免喧宾夺主,也建议读者不要把精力花在那些次要问题上。
- 这本书没有花很大的篇幅去讲signal,而是在第4.10节说明多线程程序不要使用signal作为IPC。并且,在muduo-protorpc的示例中给出了Linux专有的signalfd(2)的用法,可以避免传统signal handler的常见陷阱,也更符合UNIX的“everything is a file”哲学。第4.4节说明不要从外部终止线程,因此也就不必去细究Pthreads cancellation point了。多线程程序最好不要fork()(第4.9节)。
- 这本书没介绍daemon进程,我认为daemon是过时的做法。因为daemon进程的父进程是init(1),配置文件在本机,不便于多机统一监控与管理(第9.8节)。另外,Java/Python/Go写的服务程序似乎也没有做成daemon的习惯,C++程序没有理由要特殊对待。补充一点,Linux的进程管理机制很落后(从UNIX继承而来),子进程退出的事件只能被父进程以SIGCHLD信号的方式收到(而且这个signal可能丢失),kill(pid) 也存在很多race condition(你怎么保证pid在kill之前的一瞬间还代表你想kill的那个进程,而不是一个新启动的进程?close(fd)就不会有这种 race condition。)。这些困难在用户态无法克服,只能修改内核,引入新的系统调用才能治本。例如 FreeBSD 9.0 引入了 pdfork()/pdkill() 等,将子进程变成文件描述符,这样就能用IO事件框架统一处理了,也符合UNIX的“everything is a file”哲学。但愿Linux内核也能尽快引入类似的系统调用,减轻程序员的负担。
- 这本书没有讲内存池,而是说明不是每个程序都要自己写内存池(§12.2.8)。这本书也没有把“避免内存碎片”挂在嘴边,而是论证为什么一般的程序不必在意它(§A.1.8);
- 这本书只关注Linux,不考虑移植性。它推荐使用Linux专有的gettid()系统调用作为线程标识(第4.3节),而不是用pthread_self()。
- 这本书不讲POSIX中五花八门的定时函数,而专讲用Linux特有的timerfd来实现高精度定时(§7.8.2),因为它能方便地融入IO事件处理框架。muduo直接使用C++标准库来管理定时器,而不是自己实现小顶堆(heap),这样可以简化实现(§8.2.1)。
- 这本书只讲mutex和condition variable作为最基础的线程同步手段(第2章),并且我认为一个C++多线程程序代码里不应该直接出现pthread_mutex_lock之类的基本Pthreads调用。本书进一步建议只使用非递归的mutex(§2.1.1),这与某些网上文章的推荐正好相反。这本书第2.3节甚至建议不要使用读写锁和信号量(semaphore),因为一是容易用错,二是不见得能提高性能。mutex和condition variable是完备的,能实现多种更易用的同步设施,例如CountDownLatch和BlockingQueue。§12.8.3的代码展示了用BlockingQueue和ThreadPool控制并发度的手法,做到了“No locks. No condition variables. No callbacks.”
- 这本书不讲lock-free编程,因为编写可靠的lock-free代码并分析验证其正确性的难度远大于编写普通的使用mutex和condition variable的多线程代码,后者已经有了相当成熟的理论和工具。我认为lock-free不是每个多线程程序员应该掌握的技术,它投入高而用处少,可以适当了解,但不值得每个人都去深究。只需要少数人用它实现封装好的数据结构,像我这样的普通人就可以受益。
- 这本书只讲BSD Sockets作为进程间通信的手段,并且只用TCP长连接(§3.4)。这样就砍掉了pipe、FIFO、POSIX message queue、shared memory、STREAMS、UNIX domain socket等等内容,因为它们都只限本机进程间通信,无法扩展到多机。
- 网络编程方面(第6、7章),这本书不讲Sockets API的基本用法,而且代码中也不会直接使用它们。我认为在程序中直接使用 Sockets API是初学者的做法,当写一个新网络服务程序,如果一开始考虑的是怎么组织accept、read、epoll_wait等调用,这种做法无异于用铅笔刀锯大树,事倍功半,也不利于将来的功能扩展和维护。稍微像样点的公司都会用成熟的网络库(不一定开源),把网络编程的复杂性封装进去,暴露出良好易用的接口,让开发人员使用更高层的building blocks(消息传递或RPC)从功能的角度去设计程序,避免一次次反复掉到TCP网络编程的坑里。多个服务程序共享相同的基础库和事件处理框架的益处是显而易见的,一方面把网络编程的复杂性集中到一起,避免每个团队都去踏一遍坑;另一方面,基础库的bug修复与性能优化能惠及用到它的全部服务程序;最后,程序结构上的相似性让编程经验更加通用,多个服务程序在功能、性能、正确性等方面具有共性,能举一反三触类旁通,降低将来开发维护的成本。应该避免每个程序都另起炉灶,单独设计其IO事件处理结构。
- 这本书只讲非阻塞IO结合IO复用(IO-Multiplexing)这一种并发风格(归纳为三个半事件),并介绍在多线程下的扩展(one loop per thread)。IO复用方面,本书只讲level-trigger,不讲edge-trigger。一方面目前没有up to date的测试表明ET更快,相反,我认为LT在读取数据时可以节约一次read()调用(§8.7.2);另一方面,LT模式更容易与其他第三方库结合(§7.15)。多线程程序管理并发socket fd有很多风格可供选择,例如epoll fd是多个线程共享一个(多对一)还是每个线程有自己的epoll fd(一对一),每个socket fd是只属于一个epoll fd(多对一)还是可以同时属于多个 epoll fd(多对多),每个socket fd是只能被固定的一个线程读写还是可以被多个线程读写(如果是后者,那么读写的时候是加锁还是使用ONESHOT)。以上不是每种都可行,本书也没有一一加以分析,而是建议使用one loop per thread这种适用性较强的风格,首先是正确性容易验证,其次是性能也能满足要求。
- 本书不讲IPv6,因为目前世界上最大的公司的服务机群也用不完一个私有A类地址(10.0.0.0/8)。本书不讲UDP,因为《Unix网络编程》已经讲得很好了。
- 这本书举的网络编程的例子不再是简单的echo服务,而是有格式(因此引入codec)、多连接之间会交换数据的网络程序,更接近业务场景,也借机讲解如何避免TCP网络编程的常见陷阱。并且在示例代码中给出了分布式单词计数、多机求中位数等稍微复杂一点的程序。
- 在C++方面,这本书没有介绍动态链接库热更新这种“高级”技术,而是说明,在分布式系统中,为了部署方便,应该从源码编译全部的库,与主程序链接为一个standalone的可执行文件,以减小对运行环境的依赖(第10章)。第11章还讨论了程序库与应用程序之间的接口设计。
“信息”按照香农的定义,是“减少不确定性”,这本书包含的信息正是减少选用编程设施(facilities)方面的不确定性,让读者集中精力攻克本质问题。这本书介绍的方法不一定对于每个应用场景都是最好的,但肯定是简便易行的,是时间成本、功能、性能的一种合理折中。
[1] 这本书前言的第一句话“本书主要讲述采用现代 C++ 在 x86-64 Linux 上编写多线程 TCP 网络服务程序的主流常规技术”,封面印着“示范在多核时代采用现代 C++ 编写多线程 TCP 网络服务器的正规做法”。
[2] 前言写到:读者应该已经大致读过《现代操作系统》、《UNIX 环境高级编程》、《UNIX 网络编程》、《C++ Primer》或与之内容相近的书籍,熟悉基本概念,并掌握 Pthreads 和 Sockets API 的常规用法。