diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b8bd026..0000000 --- a/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e06d208..0000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/README.md b/README.md deleted file mode 100644 index 1b1b213..0000000 --- a/README.md +++ /dev/null @@ -1,733 +0,0 @@ - -# 计算机相关技术资料整理 - ----------- - -这里收录比较实用的计算机相关技术书籍,可以在短期之内入门的简单实用教程、一些技术网站以及一些写的比较好的博文,欢迎Fork,你也可以通过Pull Request参与编辑。 - -* [程序员必读书籍](http://www.ezlippi.com/blog/2014/07/qualified-programmer-should-read-what-books.html) - -## 目录 - - -* [语言相关类](#语言相关类) - * [Android](#android) - * [AWK](#awk) - * [SED](#SED) - * [C/C++](#cc) - * [CSS/HTML](#css) - * [Dart](#dart) - * [Erlang](#erlang) - * [Fortran](#fortran) - * [Go](#go) - * [Groovy](#groovy) - * [Haskell](#haskell) - * [iOS](#ios) - * [Java](#java) - * [JavaScript](#javascript) - * [LaTeX](#latex) - * [LISP](#lisp) - * [Lua](#lua) - * [Perl](#perl) - * [PHP](#php) - * [Prolog](#prolog) - * [Python](#python) - * [R](#r) - * [Ruby](#ruby) - * [Scala](#scala) - * [Scheme](#scheme) - * [Shell](#shell) - * [Swift](#swift) - * [WebAssembly](#webassembly) - -* [语言无关类](#语言无关类) - * [操作系统](#操作系统) - * [版本控制](#版本控制) - * [分布式系统](#分布式系统) - * [编辑器](#编辑器) - * [NoSQL](#nosql) - * [MySQL](#mysql) - * [PostgreSQL](#postgresql) - * [项目相关](#项目相关) - * [设计模式](#设计模式) - * [Web](#web) - * [大数据](#大数据) - * [编程艺术](#编程艺术) - * [函数式编程](#函数式编程) - * [运维监控](#运维监控) - * [WEB服务器](#web服务器) - -## 语言无关类 - -### 操作系统 - -* [开源世界旅行手册](http://i.linuxtoy.org/docs/guide/index.html) -* [鸟哥的Linux私房菜](http://vbird.dic.ksu.edu.tw/) -* [Linux 系统高级编程](http://sourceforge.net/apps/trac/elpi/wiki/ALP) -* [Zephyr OS 中文文档](https://github.com/tidyjiang8/zephyr-doc)(v1.6.0) -* [The Linux Command Line](http://billie66.github.io/TLCL/index.html) (中英文版) -* [Linux 设备驱动](http://oss.org.cn/kernel-book/ldd3/index.html) (第三版) -* [深入分析Linux内核源码](http://www.kerneltravel.net/kernel-book/%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90Linux%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81.html) -* [UNIX TOOLBOX](http://cb.vu/unixtoolbox_zh_CN.xhtml) -* [Docker中文指南](https://github.com/widuu/chinese_docker) -* [Docker —— 从入门到实践](https://github.com/yeasy/docker_practice) -* [Docker入门实战](http://yuedu.baidu.com/ebook/d817967416fc700abb68fca1) -* [Docker Cheat Sheet](https://github.com/wsargent/docker-cheat-sheet/tree/master/zh-cn#docker-cheat-sheet) -* [FreeRADIUS新手入门](http://freeradius.akagi201.org) -* [Mac 开发配置手册](https://aaaaaashu.gitbooks.io/mac-dev-setup/content/) -* [FreeBSD 使用手册](https://www.freebsd.org/doc/zh_CN/books/handbook/index.html) -* [Linux 命令行(中文版)](http://billie66.github.io/TLCL/book/) -* [Linux 构建指南](http://works.jinbuguo.com/lfs/lfs62/index.html) -* [Linux工具快速教程](https://github.com/me115/linuxtools_rst) -* [Linux Documentation (中文版)](https://www.gitbook.com/book/tinylab/linux-doc/details) -* [嵌入式 Linux 知识库 (eLinux.org 中文版)](https://www.gitbook.com/book/tinylab/elinux/details) -* [理解Linux进程](https://github.com/tobegit3hub/understand_linux_process) -* [Linux From Scratch systemd 中文翻译](https://github.com/ryanzz/LFS-systemd-zh_CN) -* [55分钟学会正则表达式](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/30-minutes-to-learn-regex.md) -* [每个Linux用户都应该知道的命令行技巧](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/use-linux.md) -* [每个程序员都应该了解的内存知识](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/memory.md) -* [每个程序员都应该了解的CPU缓存知识](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/cpu-cache.md) -* [每个程序员都应该了解的虚拟内存知识](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/virtual-memory.md) -* [shell脚本教程](http://billie66.gitbooks.io/tlcl-cn/content/) -* [查找命令行的网站](http://www.commandlinefu.com/commands/matching/ls/bHM=/sort-by-votes) -* [正则表达式在线测试](http://www.rubular.com/) -* [科学上网](http://www.ezlippi.com/blog/2017/02/ss-proxy-guide.html) - ---------------------------------- - -### 分布式系统 -* [走向分布式](http://dcaoyuan.github.io/papers/pdfs/Scalability.pdf) - ----------------------------------- - -### 函数式编程 - -* [傻瓜函数编程](https://github.com/justinyhuang/Functional-Programming-For-The-Rest-of-Us-Cn) - ----------------------------------- - -### web服务器 - -* [Nginx开发从入门到精通](http://tengine.taobao.org/book/index.html) (淘宝团队出品) -* [Nginx教程从入门到精通](http://www.ttlsa.com/nginx/nginx-stu-pdf/)(PDF版本,运维生存时间出品) -* [OpenResty最佳实践](https://www.gitbook.com/book/moonbingbing/openresty-best-practices/details) -* [Apache 中文手册](http://works.jinbuguo.com/apache/menu22/index.html) -* [Elasticsearch权威指南](http://looly.gitbooks.io/elasticsearch-the-definitive-guide-cn/) -* [25 台服务器是怎样支撑 StackOverflow 的?](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/how-stackoverflow-works.md) -* [图片服务架构演进(孔凡勇)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/picture-server.md) -* [最佳日志实践(王健)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/logging.md) - --------------------------------- - -### 版本控制 - -* [Git教程](http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000) -* [git简易指南](http://rogerdudler.github.io/git-guide/index.zh.html) -* [猴子都能懂的GIT入门](http://backlogtool.com/git-guide/cn/) -* [Git 参考手册](http://gitref.justjavac.com) -* [Pro Git](http://git-scm.com/book/zh/v2) -* [Pro Git 中文版](https://www.gitbook.com/book/0532/progit/details) (整理在gitbook上) -* [Git Magic](http://www-cs-students.stanford.edu/~blynn/gitmagic/intl/zh_cn/) -* [GotGitHub](http://www.worldhello.net/gotgithub/index.html) -* [Git权威指南](http://www.worldhello.net/gotgit/) -* [Git Community Book 中文版](http://gitbook.liuhui998.com/index.html) -* [Mercurial 使用教程](https://www.mercurial-scm.org/wiki/ChineseTutorial) -* [HgInit (中文版)](http://bucunzai.net/hginit/) -* [沉浸式学 Git](http://igit.linuxtoy.org/) -* [Git-Cheat-Sheet](https://github.com/flyhigher139/Git-Cheat-Sheet) (感谢 @flyhigher139 翻译了中文版) -* [GitHub秘籍](http://snowdream86.gitbooks.io/github-cheat-sheet/content/zh/index.html) -* [Github帮助文档](https://github.com/waylau/github-help) -* [git-flow 备忘清单](http://danielkummer.github.io/git-flow-cheatsheet/index.zh_CN.html) -* [svn 手册](http://svnbook.red-bean.com/nightly/zh/index.html) -* [GitHub漫游指南](https://github.com/phodal/github-roam) - -### 编辑器 - -* [exvim--vim 改良成IDE项目](http://exvim.github.io/docs-zh/intro/) -* [笨方法学Vimscript 中译本](http://learnvimscriptthehardway.onefloweroneworld.com/) -* [Vim中文文档](https://github.com/vimcn/vimcdoc) -* [所需即所获:像 IDE 一样使用 vim](https://github.com/yangyangwithgnu/use_vim_as_ide) -* [在线MarkDown编辑](https://stackedit.io/#) -* [简明VIM练级攻略](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/vim.md) - -------------------------------- - -### MySQL - -* [MySQL中文手册](http://www.php100.com/manual/MySQL/) -* [十步完全理解SQL](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/sql.md) -* [MySQL索引背后的数据结构及算法原理](http://www.cnblogs.com/leoo2sk/archive/2011/07/10/mysql-index.html) -* [21分钟MySQL入门教程](http://www.cnblogs.com/mr-wid/archive/2013/05/09/3068229.html) - ------------------------------------ - -### NoSQL - -* [NoSQL数据库笔谈](http://old.sebug.net/paper/databases/nosql/Nosql.html) -* [Redis 设计与实现](http://redisbook.com/) -* [Redis 命令参考](http://redisdoc.com/) -* [带有详细注释的 Redis 3.0 代码](https://github.com/huangz1990/redis-3.0-annotated) -* [带有详细注释的 Redis 2.6 代码](https://github.com/huangz1990/annotated_redis_source) -* [The Little MongoDB Book](https://github.com/justinyhuang/the-little-mongodb-book-cn/blob/master/mongodb.md) -* [The Little Redis Book](https://github.com/JasonLai256/the-little-redis-book/blob/master/cn/redis.md) -* [Neo4j 简体中文手册 v1.8](http://docs.neo4j.org.cn/) -* [Neo4j .rb 中文資源](http://neo4j.tw/) -* [Disque 使用教程](http://disquebook.com) -* [Apache Spark 设计与实现](https://github.com/JerryLead/SparkInternals/tree/master/markdown) -* [8种Nosql数据库系统对比](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/nosql.md) - ---------------------------- - -### PostgreSQL - -* [PostgreSQL 8.2.3 中文文档](http://works.jinbuguo.com/postgresql/menu823/index.html) -* [PostgreSQL 9.3.1 中文文档](http://www.postgres.cn/docs/9.3/index.html) -* [PostgreSQL 9.5.3 中文文档](http://www.postgres.cn/docs/9.5/index.html) - --------------------------- - -### 运维监控 - -* [ELKstack 中文指南](http://kibana.logstash.es) -* [Mastering Elasticsearch(中文版)](http://udn.yyuap.com/doc/mastering-elasticsearch/) -* [ElasticSearch 权威指南](https://www.gitbook.com/book/fuxiaopang/learnelasticsearch/details) -* [Elasticsearch 权威指南(中文版)](http://es.xiaoleilu.com) -* [Logstash 最佳实践](https://github.com/chenryn/logstash-best-practice-cn) -* [Puppet 2.7 Cookbook 中文版](http://bbs.konotes.org/workdoc/puppet-27/) - --------------------------- - -### 项目相关 - -* [Gradle实战](http://lippiouyang.gitbooks.io/gradle-in-action-cn/content/) -* [持续集成(第二版)](http://article.yeeyan.org/view/2251/94882) (译言网) -* [让开发自动化系列专栏](http://www.ibm.com/developerworks/cn/java/j-ap/) -* [追求代码质量](http://www.ibm.com/developerworks/cn/java/j-cq/) -* [selenium 中文文档](https://github.com/fool2fish/selenium-doc) -* [Joel谈软件](http://local.joelonsoftware.com/wiki/Chinese_\(Simplified\)) -* [約耳談軟體(Joel on Software)](http://local.joelonsoftware.com/wiki/%E9%A6%96%E9%A0%81) -* [Gradle 2 用户指南](https://github.com/waylau/Gradle-2-User-Guide) -* [编码规范](https://github.com/ecomfe/spec) -* [开源软件架构](http://www.ituring.com.cn/book/1143) -* [GNU make 指南](http://docs.huihoo.com/gnu/linux/gmake.html) -* [GNU make 中文手册](http://www.yayu.org/book/gnu_make/) -* [The Twelve-Factor App](http://12factor.net/zh_cn/) - ----------------------------- - -### 设计模式 - -* [图说设计模式](https://github.com/me115/design_patterns) -* [史上最全设计模式导学目录](http://blog.csdn.net/lovelion/article/details/17517213) -* [design pattern 包教不包会](https://github.com/AlfredTheBest/Design-Pattern) -* [设计模式 Java 版](https://quanke.gitbooks.io/design-pattern-java/content/) - ------------------------------- - -### Web - -* [网络传输基础](http://coolshell.info/blog/2015/04/web-transmission-basis.html) -* [关于浏览器和网络的 20 项须知](http://www.20thingsilearned.com/zh-CN/home) -* [前端知识体系](http://knowledge.ecomfe.com/) -* [浏览器开发工具的秘密](http://jinlong.github.io/2013/08/29/devtoolsecrets/) -* [Chrome 开发者工具中文手册](https://github.com/CN-Chrome-DevTools/CN-Chrome-DevTools) -* [Chrome扩展开发文档](http://open.chrome.360.cn/extension_dev/overview.html) -* [Grunt中文文档](http://www.gruntjs.org/) -* [Yeoman中文文档](http://yeomanjs.org/) -* [移动Web前端知识库](https://github.com/AlloyTeam/Mars) -* [正则表达式30分钟入门教程](http://deerchao.net/tutorials/regex/regex.htm) -* [前端开发体系建设日记](https://github.com/fouber/blog/issues/2) -* [移动前端开发收藏夹](https://github.com/hoosin/mobile-web-favorites) -* [JSON风格指南](https://github.com/darcyliu/google-styleguide/blob/master/JSONStyleGuide.md) -* [HTTP 接口设计指北](https://github.com/bolasblack/http-api-guide) -* [前端资源分享(一)](https://github.com/hacke2/hacke2.github.io/issues/1) -* [前端资源分享(二)](https://github.com/hacke2/hacke2.github.io/issues/3) -* [前端代码规范 及 最佳实践](http://coderlmn.github.io/code-standards/) -* [前端开发者手册](https://www.gitbook.com/book/dwqs/frontenddevhandbook/details) -* [前端工程师手册](https://www.gitbook.com/book/leohxj/front-end-database/details) -* [w3school教程整理](https://github.com/wizardforcel/w3school) -* [Wireshark用户手册](http://man.lupaworld.com/content/network/wireshark/index.html) -* [一站式学习Wireshark](https://community.emc.com/thread/194901) -* [HTTP 下午茶](http://happypeter.github.io/tealeaf-http/) -* [HTTP/2.0 中文翻译](http://yuedu.baidu.com/ebook/478d1a62376baf1ffc4fad99?pn=1) -* [RFC 7540 - HTTP/2 中文翻译版](https://github.com/abbshr/rfc7540-translation-zh_cn) -* [http2讲解](https://www.gitbook.com/book/ye11ow/http2-explained/details) -* [3 Web Designs in 3 Weeks](https://www.gitbook.com/book/juntao/3-web-designs-in-3-weeks/details) -* [站点可靠性工程](https://github.com/hellorocky/Site-Reliability-Engineering) - ------------------------------------ - -### 大数据 - -* [大数据/数据挖掘/推荐系统/机器学习相关资源](https://github.com/Flowerowl/Big-Data-Resources) -* [面向程序员的数据挖掘指南](https://github.com/jizhang/guidetodatamining) -* [大型集群上的快速和通用数据处理架构](https://code.csdn.net/CODE_Translation/spark_matei_phd) -* [数据挖掘中经典的算法实现和详细的注释](https://github.com/linyiqun/DataMiningAlgorithm) -* [Spark 编程指南简体中文版](https://aiyanbo.gitbooks.io/spark-programming-guide-zh-cn/content/) - ------------------------------- - -## 编程艺术 - -* [程序员编程艺术](https://github.com/julycoding/The-Art-Of-Programming-by-July) -* [每个程序员都应该了解的内存知识(译)](http://www.oschina.net/translate/what-every-programmer-should-know-about-memory-part1?print)【第一部分】 -* [取悦的工序:如何理解游戏](http://read.douban.com/ebook/4972883/) (豆瓣阅读,免费书籍) -* [编程技巧总汇](http://xiaobeicn.gitbooks.io/programming-skills-summary/) - --------------------------- - -## 语言相关类 - -### AWK - -* [awk程序设计语言](http://awk.readthedocs.org/en/latest/) -* [awk教程](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/awk.md) - --------------------------- - -### SED - -* [sed教程](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/sed.md) -* [SED简明教程](http://www.ezlippi.com/blog/2017/02/sed-introduction.html) - ------------------------- - -### Java - -* [Apache Shiro 用户指南](https://github.com/waylau/apache-shiro-1.2.x-reference) -* [Jersey 2.x 用户指南](https://github.com/waylau/Jersey-2.x-User-Guide) -* [Spring Framework 4.x参考文档](https://github.com/waylau/spring-framework-4-reference) -* [Spring Boot参考指南](https://github.com/qibaoguang/Spring-Boot-Reference-Guide) (翻译中) -* [MyBatis中文文档](http://mybatis.github.io/mybatis-3/zh/index.html) -* [用jersey构建REST服务](https://github.com/waylau/RestDemo) -* [Activiti 5.x 用户指南](https://github.com/waylau/activiti-5.x-user-guide) -* [Google Java编程风格指南](http://www.hawstein.com/posts/google-java-style.html) -* [Netty 4.x 用户指南](https://github.com/waylau/netty-4-user-guide) -* [Netty 实战(精髓)](https://github.com/waylau/essential-netty-in-action) -* [REST 实战](https://github.com/waylau/rest-in-action) -* [Java 编码规范](https://github.com/waylau/java-code-conventions) -* [Apache MINA 2 用户指南](https://github.com/waylau/apache-mina-2.x-user-guide) -* [JVM必备指南](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/jvm.md) -* [Java入门教程](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/java-string.md) -* [javarevisited博客](http://javarevisited.blogspot.com/) -* [journaldev教程](http://www.journaldev.com/) - -------------------------- - -### Android - -* [开发工具下载](http://www.androiddevtools.cn/) -* [CodePath Android教程](http://guides.codepath.com/android/Home) -* [Android Design(中文版)](http://www.apkbus.com/design/index.html) -* Google Material Design 正體中文版 ([译本一](http://wcc723.gitbooks.io/google_design_translate/content/style-icons.html) [译本二](https://github.com/1sters/material_design_zh)) -* [Google Android官方培训课程中文版](http://hukai.me/android-training-course-in-chinese/index.html) -* [Android学习之路](http://stormzhang.github.io/android/2014/07/07/learn-android-from-rookie/) -* [Android构建工具](http://tools.android.com/tech-docs/new-build-system) -* [Android开发技术前线(android-tech-frontier)](https://github.com/bboyfeiyu/android-tech-frontier) -* [Android内存优化(上)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/android-memory-prof1.md) -* [Android内存优化(中)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/android-memory-prof2.md) -* [Android内存优化(全)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/android-memory-prof3.md) -* [查找代码的一个网站](http://www.codota.com/) -* [Android开源库汇总](https://android-arsenal.com/free) -* [查找示例代码的网站](http://www.codota.com/) -* [Android SDK使用教程](http://tutsplus.com/) -* [Android最佳实践](https://github.com/futurice/android-best-practices) -* [Android Material icons](http://google.github.io/material-design-icons/#icons-for-android) - ------------------------- - -### C/C++ - -* [C/C++ 中文参考手册](http://zh.cppreference.com/) (欢迎大家参与在线翻译和校对) -* [C 语言编程透视](https://www.gitbook.com/book/tinylab/cbook/details) -* [C++ 并发编程指南](https://github.com/forhappy/Cplusplus-Concurrency-In-Practice) -* [Linux C编程一站式学习](http://akaedu.github.io/book/) (宋劲杉, 北京亚嵌教育研究中心) -* [CGDB中文手册](https://github.com/leeyiw/cgdb-manual-in-chinese) -* [100个gdb小技巧](https://github.com/hellogcc/100-gdb-tips/blob/master/src/index.md) -* [100个gcc小技巧](https://github.com/hellogcc/100-gcc-tips/blob/master/src/index.md) -* [学习gdb调试技巧](https://github.com/hellogcc/100-gdb-tips) -* [ZMQ 指南](https://github.com/anjuke/zguide-cn) -* [How to Think Like a Computer Scientist](http://www.ituring.com.cn/book/1203) (中英文版) -* [跟我一起写Makefile(PDF)](http://scc.qibebt.cas.cn/docs/linux/base/%B8%FA%CE%D2%D2%BB%C6%F0%D0%B4Makefile-%B3%C2%F0%A9.pdf) -* [GNU make中文手册](http://www.yayu.org/book/gnu_make/) -* [GNU make 指南](http://docs.huihoo.com/gnu/linux/gmake.html) -* [Google C++ 风格指南](http://zh-google-styleguide.readthedocs.org/en/latest/google-cpp-styleguide/contents/) -* [C/C++ Primer](https://github.com/andycai/cprimer) (by @andycai) -* [简单易懂的C魔法](http://www.nowamagic.net/librarys/books/contents/c) -* [Cmake 实践](http://sewm.pku.edu.cn/src/paradise/reference/CMake%20Practice.pdf) (PDF版) -* [C++ FAQ LITE(中文版)](http://www.sunistudio.com/cppfaq/) -* [C++ Primer 5th Answers](https://github.com/Mooophy/Cpp-Primer) -* [C++ 并发编程(基于C++11)](https://www.gitbook.com/book/chenxiaowei/cpp_concurrency_in_action/details) -* [QT 教程](http://www.kuqin.com/qtdocument/tutorial.html) -* [C进阶指南(1)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/c1.md) -* [libuv中文教程](https://github.com/luohaha/Chinese-uvbook) -* [Boost 库中文教程](http://zh.highscore.de/cpp/boost/) -* [笨办法学C](https://github.com/wizardforcel/lcthw-zh) -* [C进阶指南(2)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/c2.md) -* [C进阶指南(3)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/c3.md) -* [C语言全局变量那些事儿](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/c-globle-variable.md) -* [如何实现一个malloc](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/malloc.md) -* [在线编程和调试的网站](http://ideone.com/) - ------------------------------------- - -### CSS - -* [学习CSS布局](http://zh.learnlayout.com/) -* [通用 CSS 笔记、建议与指导](https://github.com/chadluo/CSS-Guidelines/blob/master/README.md) -* [CSS参考手册](http://css.doyoe.com/) -* [Emmet 文档](http://yanxyz.github.io/emmet-docs/) -* [前端代码规范](http://alloyteam.github.io/code-guide/) (腾讯alloyteam团队) -* [HTML和CSS编码规范](http://codeguide.bootcss.com/) -* [Sass Guidelines 中文](http://sass-guidelin.es/zh/) -* [CSS3 Tutorial 《CSS3 教程》](https://github.com/waylau/css3-tutorial) -* [MDN HTML 中文文档](https://developer.mozilla.org/zh-CN/docs/Web/HTML) -* [MDN CSS 中文文档](https://developer.mozilla.org/zh-CN/docs/Web/CSS) - ----------------------------- - -### Go - -* [Go编程基础](https://github.com/Unknwon/go-fundamental-programming) -* [Go入门指南](https://github.com/Unknwon/the-way-to-go_ZH_CN) -* [学习Go语言](http://mikespook.com/learning-go/) ([PDF](http://xxiyy.qiniudn.com/%E5%AD%A6%E4%B9%A0%20Go%20%E8%AF%AD%E8%A8%80\(Golang\).pdf?download)) -* [Go Web 编程](https://github.com/astaxie/build-web-application-with-golang) (此书已经出版,希望开发者们去购买,支持作者的创作) -* [Go实战开发](https://github.com/astaxie/Go-in-Action) (当我收录此项目时,作者已经写完第三章,如果读完前面章节觉得有帮助,可以给作者[捐赠](https://me.alipay.com/astaxie),以鼓励作者的继续创作) -* [Network programming with Go 中文翻译版本](https://github.com/astaxie/NPWG_zh) -* [Effective Go](http://www.hellogcc.org/effective_go.html) -* [Go 语言标准库](https://github.com/polaris1119/The-Golang-Standard-Library-by-Example) -* [Golang标准库文档](http://godoc.ml/) -* [Revel 框架手册](http://gorevel.cn/docs/manual/index.html) -* [Java程序员的Golang入门指南](http://blog.csdn.net/dc_726/article/details/46565241) -* [Go命令教程](https://github.com/hyper-carrot/go_command_tutorial) -* [Go语言博客实践](https://github.com/achun/Go-Blog-In-Action) -* [Go 官方文档翻译](https://github.com/golang-china/golangdoc.translations) -* [深入解析Go](https://github.com/tiancaiamao/go-internals) -* [Go语言圣经(中文版)](https://bitbucket.org/golang-china/gopl-zh/wiki/Home) ([GitBook](https://www.gitbook.com/book/wizardforcel/gopl-zh/details)) -* [Go语言高级编程](https://github.com/chai2010/advanced-go-programming-book) - -------------------------------- - -### Groovy - -* [实战 Groovy 系列](http://www.ibm.com/developerworks/cn/java/j-pg/) - ------------------ - -### Haskell - -* [Real World Haskell 中文版](http://rwh.readthedocs.org/en/latest/) -* [Haskell趣学指南](http://fleurer-lee.com/lyah/) -* [Learn You a Haskell for Great Good!](http://learnyouahaskell.com/chapters)(质量不错的一个网书) - --------------------- - -### iOS - -* [iOS开发60分钟入门](https://github.com/qinjx/30min_guides/blob/master/ios.md) -* [iOS7人机界面指南](http://isux.tencent.com/ios-human-interface-guidelines-ui-design-basics-ios7.html) -* [Google Objective-C Style Guide 中文版](http://zh-google-styleguide.readthedocs.org/en/latest/google-objc-styleguide/) -* [iPhone 6 屏幕揭秘](http://wileam.com/iphone-6-screen-cn/) -* [Apple Watch开发初探](http://nilsun.github.io/apple-watch/) -* [马上着手开发 iOS 应用程序](https://developer.apple.com/library/ios/referencelibrary/GettingStarted/RoadMapiOSCh/index.html) -* [网易斯坦福大学公开课:iOS 7应用开发字幕文件](https://github.com/jkyin/Subtitle) - ------------------------ - - -### JavaScript - -* [Google JavaScript 代码风格指南](http://bq69.com/blog/articles/script/868/google-javascript-style-guide.html) -* [Google JSON 风格指南](https://github.com/darcyliu/google-styleguide/blob/master/JSONStyleGuide.md) -* [Airbnb JavaScript 规范](https://github.com/adamlu/javascript-style-guide) -* [JavaScript 标准参考教程(alpha)](http://javascript.ruanyifeng.com/) -* [Javascript编程指南](http://pij.robinqu.me/) ([源码](https://github.com/RobinQu/Programing-In-Javascript)) -* [javascript 的 12 个怪癖](https://github.com/justjavac/12-javascript-quirks) -* [JavaScript 秘密花园](http://bonsaiden.github.io/JavaScript-Garden/zh/) -* [JavaScript核心概念及实践](http://icodeit.org/jsccp/) (PDF) (此书已由人民邮电出版社出版发行,但作者依然免费提供PDF版本,希望开发者们去购买,支持作者) -* [《JavaScript 模式》](https://github.com/jayli/javascript-patterns) “JavaScript patterns”中译本 -* [命名函数表达式探秘](http://justjavac.com/named-function-expressions-demystified.html) (注:原文由[为之漫笔](http://www.cn-cuckoo.com)翻译,原始地址无法打开,所以此处地址为我博客上的备份) -* [学用 JavaScript 设计模式](http://www.oschina.net/translate/learning-javascript-design-patterns) (开源中国) -* [深入理解JavaScript系列](http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html) -* [ECMAScript 6 入门](http://es6.ruanyifeng.com/) (作者:阮一峰) -* [JavaScript Promise迷你书](http://liubin.github.io/promises-book/) -* [You-Dont-Know-JS](https://github.com/getify/You-Dont-Know-JS) (深入JavaScript语言核心机制的系列图书) -* [JavaScript 教程](http://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000) 廖雪峰 -* [MDN JavaScript 中文文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript) - -* jQuery - * [jQuery 解构](http://www.cn-cuckoo.com/deconstructed/jquery.html) - * [简单易懂的JQuery魔法](http://www.nowamagic.net/librarys/books/contents/jquery) - * [How to write jQuery plugin](http://i5ting.github.io/How-to-write-jQuery-plugin/build/jquery.plugin.html) -* Node.js - * [Node入门](http://www.nodebeginner.org/index-zh-cn.html) - * [七天学会NodeJS](http://nqdeng.github.io/7-days-nodejs/) - * [Nodejs Wiki Book](https://github.com/nodejs-tw/nodejs-wiki-book) (繁体中文) - * [express.js 中文文档](http://expressjs.jser.us/) - * [koa 中文文档](https://github.com/guo-yu/koa-guide) - * [一起学koa](http://base-n.github.io/koa-generator-examples/) - * [使用 Express + MongoDB 搭建多人博客](https://github.com/nswbmw/N-blog) - * [Express框架](http://javascript.ruanyifeng.com/nodejs/express.html) - * [Node.js 包教不包会](https://github.com/alsotang/node-lessons) - * [Learn You The Node.js For Much Win! (中文版)](https://www.npmjs.org/package/learnyounode-zh-cn) - * [Node debug 三法三例](http://i5ting.github.io/node-debug-tutorial/) - * [nodejs中文文档](https://www.gitbook.com/book/0532/nodejs/details) - * [orm2 中文文档](https://github.com/wizardforcel/orm2-doc-zh-cn) -* underscore.js - * [Underscore.js中文文档](http://learningcn.com/underscore/) -* backbone.js - * [backbone.js中文文档](http://www.css88.com/doc/backbone/) - * [backbone.js入门教程](http://www.the5fire.com/backbone-js-tutorials-pdf-download.html) (PDF) - * [Backbone.js入门教程第二版](https://github.com/the5fire/backbonejs-learning-note) - * [Developing Backbone.js Applications(中文版)](http://feliving.github.io/developing-backbone-applications) -* AngularJS - * [AngularJS最佳实践和风格指南](https://github.com/mgechev/angularjs-style-guide/blob/master/README-zh-cn.md) - * [AngularJS中译本](https://github.com/peiransun/angularjs-cn) - * [AngularJS入门教程](https://github.com/zensh/AngularjsTutorial_cn) - * [构建自己的AngularJS](https://github.com/xufei/Make-Your-Own-AngularJS/blob/master/01.md) - * [在Windows环境下用Yeoman构建AngularJS项目](http://www.waylau.com/build-angularjs-app-with-yeoman-in-windows/) -* Zepto.js - * [Zepto.js 中文文档](http://mweb.baidu.com/zeptoapi/) -* Sea.js - * [Hello Sea.js](http://island205.github.io/HelloSea.js/) -* React.js - * [React.js 中文文档](http://reactjs.cn/) - * [React webpack-cookbook](https://github.com/fakefish/react-webpack-cookbook) - * [React 入门教程](http://fraserxu.me/intro-to-react/) - * [React Native 中文文档(含最新Android内容)](http://wiki.jikexueyuan.com/project/react-native/) - * [Learn React & Webpack by building the Hacker News front page](https://github.com/theJian/build-a-hn-front-page) -* impress.js - * [impress.js的中文教程](https://github.com/kokdemo/impress.js-tutorial-in-Chinese) -* CoffeeScript - * [CoffeeScript Cookbook](http://island205.github.io/coffeescript-cookbook.github.com/) - * [The Little Book on CoffeeScript中文版](http://island205.github.io/tlboc/) - * [CoffeeScript 编码风格指南](https://github.com/geekplux/coffeescript-style-guide) -* TypeScipt - * [TypeScript Handbook](https://zhongsp.gitbooks.io/typescript-handbook/content/) -* ExtJS - * [Ext4.1.0 中文文档](http://extjs-doc-cn.github.io/ext4api/) -* Meteor - * [Discover Meteor](http://zh.discovermeteor.com/) - * [Meteor 中文文档](http://docs.meteorhub.org/#/basic/) - * [Angular-Meteor 中文教程](http://angular.meteorhub.org/) -* [Chrome扩展及应用开发](http://www.ituring.com.cn/minibook/950) - ------------------ - -### LaTeX - -* [一份其实很短的 LaTeX 入门文档](http://liam0205.me/2014/09/08/latex-introduction/) -* [一份不太简短的 LATEX 2ε 介绍](http://www.mohu.org/info/lshort-cn.pdf) (PDF版) - ------------------ - -### LISP -* Common Lisp - * [ANSI Common Lisp 中文翻譯版](http://acl.readthedocs.org/en/latest/) - * [On Lisp 中文翻译版本](http://www.ituring.com.cn/minibook/862) -* Scheme - * [Yet Another Scheme Tutorial Scheme入门教程](http://deathking.github.io/yast-cn/) - * [Scheme语言简明教程](http://songjinghe.github.io/TYS-zh-translation/) - * Racket - * [Racket book](https://github.com/tyrchen/racket-book) - ---------------------- - -### Lua - -* [Lua编程入门](https://github.com/andycai/luaprimer) -* [Lua 5.1 参考手册 中文翻译](http://www.codingnow.com/2000/download/lua_manual.html) -* [Lua 5.3 参考手册 中文翻译](http://cloudwu.github.io/lua53doc/) -* [Lua源码欣赏](http://www.codingnow.com/temp/readinglua.pdf) - ------------------ - -### Perl - -* [Modern Perl 中文版](https://github.com/horus/modern_perl_book) -* [Perl 程序员应该知道的事](http://perl.linuxtoy.org/) - ------------------ - -### PHP - -* [PHP 官方手册](http://php.net/manual/zh/) -* [PHP调试技术手册](http://www.laruence.com/2010/06/21/1608.html)(PDF) -* [XDebug 2中文手册(译)](http://www.blogkun.com/project.html) (CHM) -* [PHP之道](http://wulijun.github.io/php-the-right-way/) -* [PHP 最佳实践](https://github.com/justjavac/PHP-Best-Practices-zh_CN) -* [PHP 开发者实践](http://ryancao.gitbooks.io/php-developer-prepares/content/) -* [深入理解PHP内核](https://github.com/reeze/tipi) -* [PHP扩展开发及内核应用](http://www.walu.cc/phpbook/) -* [CodeIgniter 用户指南](http://codeigniter.org.cn/user_guide/index.html) -* [Laravel4 中文文档](http://www.golaravel.com/docs/) -* [Laravel 入门](https://github.com/huanghua581/laravel-getting-started) -* [Symfony2中文文档](http://symfony-docs-chs.readthedocs.org/en/latest/) (未译完) -* [Phalcon中文文档](http://phalcon.5iunix.net/)(翻译进行中) -* [YiiBook几本Yii框架的在线教程](http://yiibook.com//doc) -* [深入理解 Yii 2.0](http://www.digpage.com/) -* [Yii 框架中文文檔](http://www.yiichina.com/) -* [简单易懂的PHP魔法](http://www.nowamagic.net/librarys/books/contents/php) -* [swoole文档及入门教程](https://github.com/LinkedDestiny/swoole-doc) -* [Composer 中文网](http://www.phpcomposer.com) -* [Slim 中文文档](http://ww1.minimee.org/php/slim) -* [Lumen 中文文档](http://lumen.laravel-china.org/) -* [PHPUnit 中文文档](https://phpunit.de/manual/current/zh_cn/installation.html) - --------------------- - -### Prolog - -* [笨办法学Prolog](http://fengdidi.github.io/blog/2011/11/15/qian-yan/) - ------------------ - -### Python - -* [廖雪峰 Python 2.7 中文教程](http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000) -* [廖雪峰 Python 3 中文教程](http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000) -* [简明Python教程](http://www.kuqin.com/abyteofpython_cn/) -* [零基础学 Python 第一版](http://www.kancloud.cn/kancloud/python-basic) -* [零基础学 Python 第二版](http://www.kancloud.cn/kancloud/starter-learning-python) -* [可爱的 Python](http://lovelypython.readthedocs.org/en/latest/) -* [Python 2.7 官方教程中文版](http://www.pythondoc.com/pythontutorial27/index.html) -* [Python 3.3 官方教程中文版](http://www.pythondoc.com/pythontutorial3/index.html) -* [Python Cookbook 中文版](http://www.kancloud.cn/thinkphp/python-cookbook) -* [Python3 Cookbook 中文版](https://github.com/yidao620c/python3-cookbook) -* [深入 Python](http://www.kuqin.com/docs/diveintopythonzh-cn-5.4b/html/toc/) -* [深入 Python 3](http://old.sebug.net/paper/books/dive-into-python3/) -* [PEP8 Python代码风格规范](https://code.google.com/p/zhong-wiki/wiki/PEP8) -* [Google Python 风格指南 中文版](http://zh-google-styleguide.readthedocs.org/en/latest/google-python-styleguide/) -* [Python入门教程](http://liam0205.me/2013/11/02/Python-tutorial-zh_cn/) ([PDF](http://liam0205.me/attachment/Python/The_Python_Tutorial_zh-cn.pdf)) -* [Python的神奇方法指南](http://article.yeeyan.org/view/311527/287706) -* [笨办法学 Python](http://sebug.net/paper/books/LearnPythonTheHardWay/) ([PDF](http://liam0205.me/attachment/Python/PyHardWay/Learn_Python_The_Hard_Way_zh-cn.pdf)版下载) -* [The Django Book 中文版](http://djangobook.py3k.cn/2.0/) -* [web.py 0.3 新手指南](http://webpy.org/tutorial3.zh-cn) -* [Web.py Cookbook 简体中文版](http://webpy.org/cookbook/index.zh-cn) -* [Dive Into Python 中文版](http://woodpecker.org.cn/diveintopython/) -* [Bottle 文档中文版](https://associates.amazon.cn/gp/associates/network/main.html) (需翻墙) -* [Flask 文档中文版](http://docs.jinkan.org/docs/flask/) -* [Jinja2 文档中文版](http://docs.jinkan.org/docs/jinja2/) -* [Werkzeug 文档中文版](http://werkzeug-docs-cn.readthedocs.org/zh_CN/latest/) -* [Flask之旅](http://spacewander.github.io/explore-flask-zh) -* [Introduction to Tornado 中文翻译](http://demo.pythoner.com/itt2zh/index.html) -* [Python自然语言处理中文版](http://pan.baidu.com/s/1qW4pvnY) (感谢陈涛同学的翻译,也谢谢 [@shwley](https://github.com/shwley) 联系了作者) -* [Python 绘图库 matplotlib 官方指南中文翻译](http://liam0205.me/2014/09/11/matplotlib-tutorial-zh-cn/) -* [Scrapy 0.25 文档](http://scrapy-chs.readthedocs.org/zh_CN/latest/) -* [ThinkPython](https://github.com/carfly/thinkpython-cn) -* [Python快速教程](http://www.cnblogs.com/vamei/archive/2012/09/13/2682778.html) -* [Python 正则表达式操作指南](http://wiki.ubuntu.org.cn/Python正则表达式操作指南) -* [python初级教程:入门详解](http://www.crifan.com/files/doc/docbook/python_beginner_tutorial/release/html/python_beginner_tutorial.html) -* [Python Cookbook 第3版 中文版](http://python3-cookbook.readthedocs.org/zh_CN/latest/) -* [Twisted 与异步编程入门](http://likebeta.gitbooks.io/twisted-intro-cn/) -* [TextGrocery 中文 API](http://textgrocery.readthedocs.org/zh/latest/index.html) ( 基于svm算法的一个短文本分类 Python 库 ) -* [Requests: HTTP for Humans](http://requests-docs-cn.readthedocs.org/zh_CN/latest/) -* [Pillow 中文文档](http://pillow-cn.readthedocs.org/en/latest/#) -* [PyMOTW 中文版](http://pymotwcn.readthedocs.org/en/latest/index.html) -* [Python 官方文档中文版](http://data.digitser.net/zh-CN/python_index.html) -* [Fabric 中文文档](http://fabric-chs.readthedocs.org) -* [Beautiful Soup 4.2.0 中文文档](http://beautifulsoup.readthedocs.org/zh_CN/latest/) -* [用Python做科学计算](http://old.sebug.net/paper/books/scipydoc) -* [Sphinx 中文文档](http://www.pythondoc.com/sphinx/index.html) -* [精通 Python 设计模式](https://github.com/cundi/Mastering.Python.Design.Patterns) -* [python 安全编程教程](https://github.com/smartFlash/pySecurity) -* [程序设计思想与方法](https://www.gitbook.com/book/wizardforcel/sjtu-cs902-courseware/details) -* [知乎周刊·编程小白学Python](https://read.douban.com/ebook/16691849/) -* [Scipy 讲义](https://github.com/cloga/scipy-lecture-notes_cn) -* [Python 学习笔记 基础篇](http://www.kuqin.com/docs/pythonbasic.html) -* [Python 学习笔记 模块篇](http://www.kuqin.com/docs/pythonmodule.html) -* [Python 标准库 中文版](http://old.sebug.net/paper/books/python/%E3%80%8APython%E6%A0%87%E5%87%86%E5%BA%93%E3%80%8B%E4%B8%AD%E6%96%87%E7%89%88.pdf) -* [Python进阶](https://www.gitbook.com/book/eastlakeside/interpy-zh/details) -* [Python 核心编程 第二版](https://wizardforcel.gitbooks.io/core-python-2e/content/) CPyUG译 -* [Python最佳实践指南](http://pythonguidecn.readthedocs.io/zh/latest/) -* [Python 精要教程](https://www.gitbook.com/book/wizardforcel/python-essential-tutorial/details) -* [Python 量化交易教程](https://www.gitbook.com/book/wizardforcel/python-quant-uqer/details) -* Django - * [Django 1.5 文档中文版](http://django-chinese-docs.readthedocs.org/en/latest/) 正在翻译中 - * [Diango 1.7 文档中文版](http://django-1-7-doc.coding.io/) 正在翻译中,目前只翻译了目录 - * [Django 1.8.2 文档中文版](http://python.usyiyi.cn/django/index.html) - 正在翻译中 - * [Django 最佳实践](https://github.com/yangyubo/zh-django-best-practices) - * [Django搭建简易博客教程](https://www.gitbook.com/book/andrew-liu/django-blog/details) - * [The Django Book 中文版](http://djangobook.py3k.cn/2.0/) - * [Django 设计模式与最佳实践](https://github.com/cundi/Django-Design-Patterns-and-Best-Practices) - * [Django 网站开发 Cookbook](https://github.com/cundi/Web.Development.with.Django.Cookbook) - * [Django Girls 學習指南](https://www.gitbook.com/book/djangogirlstaipei/django-girls-taipei-tutorial/details) -* Flask - * [Flask 文档中文版](http://docs.jinkan.org/docs/flask/) - * [Jinja2 文档中文版](http://docs.jinkan.org/docs/jinja2/) - * [Werkzeug 文档中文版](http://werkzeug-docs-cn.readthedocs.org/zh_CN/latest/) - * [Flask之旅](http://spacewander.github.io/explore-flask-zh/) - * [Flask 扩展文档汇总](https://www.gitbook.com/book/wizardforcel/flask-extension-docs/details) - * [Flask 大型教程](http://www.pythondoc.com/flask-mega-tutorial/index.html) - * [SQLAlchemy 中文文档](https://github.com/sixu05202004/sqlalchemy-docs-cn) -* web.py - * [web.py 0.3 新手指南](http://webpy.org/tutorial3.zh-cn) - * [Web.py Cookbook 简体中文版](http://webpy.org/cookbook/index.zh-cn) -* Tornado - * [Introduction to Tornado 中文翻译](http://demo.pythoner.com/itt2zh/index.html) - * [Tornado源码解析](http://www.nowamagic.net/academy/detail/13321002) - * [Tornado 4.3 文档中文版](https://tornado-zh.readthedocs.org/zh/latest/) - -### R - -* [R语言忍者秘笈](https://github.com/yihui/r-ninja) - ------------------ - -### Ruby - -* [Ruby 风格指南](https://github.com/JuanitoFatas/ruby-style-guide/blob/master/README-zhCN.md) -* [Rails 风格指南](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) -* [笨方法學 Ruby](http://lrthw.github.io/) -* [Ruby on Rails 指南](http://guides.ruby-china.org/) -* [Ruby on Rails 實戰聖經](http://ihower.tw/rails4/index.html) -* [Ruby on Rails Tutorial 原书第 3 版](http://railstutorial-china.org/) (本书网页版免费提供,电子版以 PDF、EPub 和 Mobi 格式提供购买,仅售 9.9 美元) -* [Rails 实践](http://rails-practice.com/content/index.html) -* [Rails 5 开发进阶(Beta)](https://www.gitbook.com/book/kelby/rails-beginner-s-guide/details) -* [Rails 102](https://www.gitbook.com/book/rocodev/rails-102/details) -* [编写Ruby的C拓展](https://wusuopu.gitbooks.io/write-ruby-extension-with-c/content/) -* [Ruby 源码解读](https://ruby-china.org/topics/22386) -* [Ruby中的元编程](http://deathking.github.io/metaprogramming-in-ruby/) - ------------------ - -### Scala - -* [Scala课堂](http://twitter.github.io/scala_school/zh_cn/index.html) (Twitter的Scala中文教程) -* [Effective Scala](http://twitter.github.io/effectivescala/index-cn.html)(Twitter的Scala最佳实践的中文翻译) -* [Scala指南](http://zh.scala-tour.com/) -* [Scala-for-the-impatient-2nd](https://www.amazon.com/Scala-Impatient-2nd-Cay-Horstmann/dp/0134540565)(自行购买或pdf) -* [Scala|写点什么](http://hongjiang.info/scala/)(国人的一个很好的关于Scala的博客) - ------------------ - -### Scheme -* [Yet Another Scheme Tutorial Scheme入门教程](http://deathking.github.io/yast-cn/) -* [Scheme语言简明教程](http://songjinghe.github.io/TYS-zh-translation/) - ------------------ - -### Shell - -* [Shell脚本编程30分钟入门](https://github.com/qinjx/30min_guides/blob/master/shell.md) -* [Bash脚本15分钟进阶教程](http://blog.sae.sina.com.cn/archives/3606) -* [Linux工具快速教程](https://github.com/me115/linuxtools_rst) -* [shell十三问](https://github.com/wzb56/13_questions_of_shell) - ------------------ - -### Swift - -* [The Swift Programming Language 中文版](http://numbbbbb.github.io/the-swift-programming-language-in-chinese/) -* [Swift 语言指南](http://dev.swiftguide.cn) -* [Stanford 公开课,Developing iOS 8 Apps with Swift 字幕翻译文件](https://github.com/x140yu/Developing_iOS_8_Apps_With_Swift) - ------------------ - -### WebAssembly - -* [C/C++面向WebAssembly编程](https://github.com/3dgen/cppwasm-book) - ------------------ - diff --git a/fonts/OpenSans-Bold-webfont.eot b/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 0000000..e1c7674 Binary files /dev/null and b/fonts/OpenSans-Bold-webfont.eot differ diff --git a/fonts/OpenSans-Bold-webfont.svg b/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 0000000..364b368 --- /dev/null +++ b/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/fonts/OpenSans-Bold-webfont.ttf b/fonts/OpenSans-Bold-webfont.ttf new file mode 100644 index 0000000..2d94f06 Binary files /dev/null and b/fonts/OpenSans-Bold-webfont.ttf differ diff --git a/fonts/OpenSans-Bold-webfont.woff b/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 0000000..cd86852 Binary files /dev/null and b/fonts/OpenSans-Bold-webfont.woff differ diff --git a/fonts/OpenSans-BoldItalic-webfont.eot b/fonts/OpenSans-BoldItalic-webfont.eot new file mode 100644 index 0000000..f44ac9a Binary files /dev/null and b/fonts/OpenSans-BoldItalic-webfont.eot differ diff --git a/fonts/OpenSans-BoldItalic-webfont.svg b/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100644 index 0000000..8392240 --- /dev/null +++ b/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/fonts/OpenSans-BoldItalic-webfont.ttf b/fonts/OpenSans-BoldItalic-webfont.ttf new file mode 100644 index 0000000..f74e0e3 Binary files /dev/null and b/fonts/OpenSans-BoldItalic-webfont.ttf differ diff --git a/fonts/OpenSans-BoldItalic-webfont.woff b/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 0000000..f3248c1 Binary files /dev/null and b/fonts/OpenSans-BoldItalic-webfont.woff differ diff --git a/fonts/OpenSans-Italic-webfont.eot b/fonts/OpenSans-Italic-webfont.eot new file mode 100644 index 0000000..277c189 Binary files /dev/null and b/fonts/OpenSans-Italic-webfont.eot differ diff --git a/fonts/OpenSans-Italic-webfont.svg b/fonts/OpenSans-Italic-webfont.svg new file mode 100644 index 0000000..29c7497 --- /dev/null +++ b/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/fonts/OpenSans-Italic-webfont.ttf b/fonts/OpenSans-Italic-webfont.ttf new file mode 100644 index 0000000..63f187e Binary files /dev/null and b/fonts/OpenSans-Italic-webfont.ttf differ diff --git a/fonts/OpenSans-Italic-webfont.woff b/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 0000000..469a29b Binary files /dev/null and b/fonts/OpenSans-Italic-webfont.woff differ diff --git a/fonts/OpenSans-Light-webfont.eot b/fonts/OpenSans-Light-webfont.eot new file mode 100644 index 0000000..837daab Binary files /dev/null and b/fonts/OpenSans-Light-webfont.eot differ diff --git a/fonts/OpenSans-Light-webfont.svg b/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 0000000..bdb6726 --- /dev/null +++ b/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/fonts/OpenSans-Light-webfont.ttf b/fonts/OpenSans-Light-webfont.ttf new file mode 100644 index 0000000..b50ef9d Binary files /dev/null and b/fonts/OpenSans-Light-webfont.ttf differ diff --git a/fonts/OpenSans-Light-webfont.woff b/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 0000000..99514d1 Binary files /dev/null and b/fonts/OpenSans-Light-webfont.woff differ diff --git a/fonts/OpenSans-LightItalic-webfont.eot b/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 0000000..f0ebf2c Binary files /dev/null and b/fonts/OpenSans-LightItalic-webfont.eot differ diff --git a/fonts/OpenSans-LightItalic-webfont.svg b/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 0000000..60765da --- /dev/null +++ b/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/fonts/OpenSans-LightItalic-webfont.ttf b/fonts/OpenSans-LightItalic-webfont.ttf new file mode 100644 index 0000000..5898c8c Binary files /dev/null and b/fonts/OpenSans-LightItalic-webfont.ttf differ diff --git a/fonts/OpenSans-LightItalic-webfont.woff b/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 0000000..9c978dc Binary files /dev/null and b/fonts/OpenSans-LightItalic-webfont.woff differ diff --git a/fonts/OpenSans-Regular-webfont.eot b/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 0000000..dd6fd2c Binary files /dev/null and b/fonts/OpenSans-Regular-webfont.eot differ diff --git a/fonts/OpenSans-Regular-webfont.svg b/fonts/OpenSans-Regular-webfont.svg new file mode 100644 index 0000000..01038bb --- /dev/null +++ b/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/fonts/OpenSans-Regular-webfont.ttf b/fonts/OpenSans-Regular-webfont.ttf new file mode 100644 index 0000000..05951e7 Binary files /dev/null and b/fonts/OpenSans-Regular-webfont.ttf differ diff --git a/fonts/OpenSans-Regular-webfont.woff b/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 0000000..274664b Binary files /dev/null and b/fonts/OpenSans-Regular-webfont.woff differ diff --git a/fonts/OpenSans-Semibold-webfont.eot b/fonts/OpenSans-Semibold-webfont.eot new file mode 100644 index 0000000..289aade Binary files /dev/null and b/fonts/OpenSans-Semibold-webfont.eot differ diff --git a/fonts/OpenSans-Semibold-webfont.svg b/fonts/OpenSans-Semibold-webfont.svg new file mode 100644 index 0000000..cc2ca42 --- /dev/null +++ b/fonts/OpenSans-Semibold-webfont.svg @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/fonts/OpenSans-Semibold-webfont.ttf b/fonts/OpenSans-Semibold-webfont.ttf new file mode 100644 index 0000000..6f15073 Binary files /dev/null and b/fonts/OpenSans-Semibold-webfont.ttf differ diff --git a/fonts/OpenSans-Semibold-webfont.woff b/fonts/OpenSans-Semibold-webfont.woff new file mode 100644 index 0000000..4e47cb1 Binary files /dev/null and b/fonts/OpenSans-Semibold-webfont.woff differ diff --git a/fonts/OpenSans-SemiboldItalic-webfont.eot b/fonts/OpenSans-SemiboldItalic-webfont.eot new file mode 100644 index 0000000..50a8a6f Binary files /dev/null and b/fonts/OpenSans-SemiboldItalic-webfont.eot differ diff --git a/fonts/OpenSans-SemiboldItalic-webfont.svg b/fonts/OpenSans-SemiboldItalic-webfont.svg new file mode 100644 index 0000000..65b50e2 --- /dev/null +++ b/fonts/OpenSans-SemiboldItalic-webfont.svg @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/fonts/OpenSans-SemiboldItalic-webfont.ttf b/fonts/OpenSans-SemiboldItalic-webfont.ttf new file mode 100644 index 0000000..55ba312 Binary files /dev/null and b/fonts/OpenSans-SemiboldItalic-webfont.ttf differ diff --git a/fonts/OpenSans-SemiboldItalic-webfont.woff b/fonts/OpenSans-SemiboldItalic-webfont.woff new file mode 100644 index 0000000..0adc6df Binary files /dev/null and b/fonts/OpenSans-SemiboldItalic-webfont.woff differ diff --git a/images/bullet.png b/images/bullet.png new file mode 100644 index 0000000..0614eb6 Binary files /dev/null and b/images/bullet.png differ diff --git a/images/hr.gif b/images/hr.gif new file mode 100644 index 0000000..bdb4168 Binary files /dev/null and b/images/hr.gif differ diff --git a/images/nav-bg.gif b/images/nav-bg.gif new file mode 100644 index 0000000..4743965 Binary files /dev/null and b/images/nav-bg.gif differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..f6f903d --- /dev/null +++ b/index.html @@ -0,0 +1,78 @@ + + +
+ + +可以短期之内学会的计算机相关技能
+This automatic page generator is the easiest way to create beautiful pages for all of your projects. Author your page content here using GitHub Flavored Markdown, select a template crafted by a designer, and publish. After your page is generated, you can check out the new branch:
+ +$ cd your_repo_root/repo_name
+$ git fetch origin
+$ git checkout gh-pages
+
+
+If you're using the GitHub for Mac, simply sync your repository and you'll see the new branch.
+ +We've crafted some handsome templates for you to use. Go ahead and continue to layouts to browse through them. You can easily go back to edit your page before publishing. After publishing your page, you can revisit the page generator and switch to another theme. Your Page content will be preserved if it remained markdown format.
+ +If you prefer to not use the automatic generator, push a branch named gh-pages to your repository to create a page manually. In addition to supporting regular HTML content, GitHub Pages support Jekyll, a simple, blog aware static site generator written by our own Tom Preston-Werner. Jekyll makes it easy to create site-wide headers and footers without having to copy them across every page. It also offers intelligent blog support and other advanced templating features.
You can @mention a GitHub username to generate a link to their profile. The resulting <a> element will link to the contributor's GitHub Profile. For example: In 2007, Chris Wanstrath (@defunkt), PJ Hyett (@pjhyett), and Tom Preston-Werner (@mojombo) founded GitHub.
Having trouble with Pages? Check out the documentation at https://help.github.com/pages or contact support@github.com and we’ll help you sort it out.
+推荐几个正则表达式编辑器
-正则表达式是一种查找以及字符串替换操作。正则表达式在文本编辑器中广泛使用,比如正则表达式被用于:
-与文本编辑器相似,几乎所有的高级编程语言都支持正则表达式。在这样的语境下,“文本”也就是一个字符串,可以执行的操作都是类似的。一些编程语言(比如Perl,JavaScript)会检查正则表达式的语法。
-正则表达式是什么?
-正则表达式只是一个字符串。没有长度限制,但是,这样的正则表达式长度往往较短。如下所示是一些正则表达式的例子:
-I had a \S+ day today[A-Za-z0-9\-_]{3,16}\d\d\d\d-\d\d-\d\dv(\d+)(\.\d+)*TotalMessages="(.*?)"<[^<>]>在实现中,正则表达式还有其他的特点。本文将重点讨论正则表达式的核心语法,在几乎所有的正则表达式中都可以见到这些规则。
-特别提示:正则表达式与文件通配语法无关,比如 *.xml
-正则表达式中包含了一系列的字符,这些字符只能匹配它们本身。有一些被称为“元字符”的特殊字符,可以匹配一些特殊规则。
-如下所示的例子中,我用红色标出了元字符。
-I had a \S+ day today[A-Za-z0-9\-_]{3,16}\d\d\d\d-\d\d-\d\dv(\d+)(\.\d+)*TotalMessages="(.*?)"<[^<>]*> 大部分的字符,包括所有的字母和数字字符,是普通字符。也就意味着,它们只能匹配它们自己,如下所示的正则表达式:
-cat
-意味着,只能匹配一个字符串,以“c”开头,然后是字符“a”,紧跟着是字符“t”的字符串。
-到目前为止,正则表达式的功能类似于
-String.indexOf() 函数strpos()函数我们第一个要讲解的元字符是“.”。这个符号意味着可以匹配任意一个字符。如下所示的正则表达式:
-c.t
-意味着匹配“以c开头,之后是任意一个字符,紧跟着是字母t”的字符串。
-在一段文本中,这样的正则表达式可以用来找出cat, cot, czt这样的字符串,甚至可以找出c.t这样的组合,但是不能找到ct或者是coot这样的字符串。
使用反斜杠“\”可以忽略元字符,使得元字符的功能与普通字符一样。所以,正则表达式
-c\.t
-表示“找到字母c,然后是一个句号(“.”),紧跟着字母t”
-反斜杠本身也是一个元字符,这意味着反斜杠本身也可以通过相似的方法变回到普通字符的用途。因此,正则表达式
-c\\t
-表示匹配“以字符c开头,然后是一个反斜杠,紧跟着是字母t”的字符串。
-注意!在正则表达式的实现中,.是不能用于匹配换行符的。”换行符“的表示方法在不同实现中也不同。实际编程时,请参考相关文档。在本文中,我认为.是可以匹配任意字符的。实现环境通常会提供一个Flag标志位,来控制这一点。
-字符类是一组在方括号内的字符,表示可以匹配其中的任何一个字符。
-包含忽略字符的例子
-\[a\]
表示匹配字符串[a]\[\]\ab]表示匹配的字符为”["或者'']”或者”a”,或者”b”\[\]]表示匹配的字符为”\”或者 “[”或者"]“在字符类中,字符的重复和出现顺序并不重要。[dabaaabcc]与[abc]是相同的
-重要提示:字符类中和字符类外的规则有时不同,一些字符在字符类中是元字符,在字符类外是普通字符。一些字符正好相反。还有一些字符在字符类中和字符类外都是元字符,这要视情况而定!
-比如,.表示匹配任意一个字符,而[.]表示匹配一个全角句号。这不是一回事!
-在字符集中,你可以通过使用短横线来表示匹配字母或数字的范围。
-使用目前我们已经讲解的正则表达式相关知识,在字典中匹配找到含有最多连续元音的单词,同时找到含有最多连续辅音的单词。
-[aeiou][aeiou][aeiou][aeiou][aeiou][aeiou] 这样的正则表达式,可以匹配连续含有六个元音的单词,比如 euouae 和 euouaes。
同样的,恐怖的正则表达式[bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz] 可以找到连续含有十个辅音的单词sulphhydryls.
下文中,我们会讲解,怎样有效缩短这样的正则表达式长度。
-在字符类之外,短横线没有特殊含义。正则表达式a-z,表示匹配字符串“以a开头,然后是一个短横线,以z结尾”。
-范围和单独的字符可能在一个字符类中同时出现:
-使用已经介绍过的正则表达式知识,匹配YYYY-MM-DD格式的日期。
-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].
同样的,下文中,我们会介绍怎样有效减少这样的正则表达式长度。
-虽然你可以尝试在正则表达式中使用一些非字母或数字作为范围的最后一个符号,比如abc[!-/]def,但是这并不是在每种实现中都合法。即使这样的语法是合法的,这样的语义也是模糊的。最好不要这样使用。
-同时,你必须谨慎选择范围的边界值。即使[A-z]在你使用的实现中,是合法的,也可能会产生无法预料的运行结果。(注意,在z到a之间,是有字符存在的)
-注意:范围的字符值代表的是字符而已,并不能代表数值范围,比如[1-31]表示匹配一个数字,是1或者2或者3,而不是匹配一个数值在1到31之间的数。
-你可以在字符类的起始位放一个反义符。
-在字典中,找到一个不满足“在e之前有i,但是没有c”的例子。
-cie和[^c]ei都要可以找到很多这样的例子,比如ancient,science,viel,weigh
- -\d这个正则表达式与[0-9]作用相同,都是匹配任何一个数字。(要匹配\d,应该使用正则表达式\\d)
-\w与[0-9A-Za-z]相同,都表示匹配一个数字或字母字符
-\s意味着匹配一个空字符(空格,制表符,回车或者换行)
-另外
-这些是你必须掌握的字符。你可能已经注意到了,一个全角句号“.”也是一个字符类,可以匹配任意一个字符。
-很多正则表达式的实现中,提供了更多的字符类,或者是标志位在ASCII码的基础上,扩展现有的字符类。
-特别提示:统一字符集中包含除了0至9之外的更多数字字符,同样的,也包含更多的空字符和字母字符。实际使用正则表达式时,请仔细查看相关文档。
-简化正则表达式 [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].
\d\d\d\d-\d\d-\d\d.
在字符或字符集之后,你可以使用{ }大括号来表示重复
-简化下面的正则表达式
-z.......z\d\d\d\d-\d\d-\d\d[aeiou][aeiou][aeiou][aeiou][aeiou][aeiou][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz]z.{7}z\d{4}-\d{2}-\d{2}[aeiou]{6}[bcdfghjklmnpqrstvwxyz]{10}注意:重复字符是没有记忆性的,比如[abc]{2}表示先匹配”a或者b或者c”,再匹配”a或者b或者c”,与匹配”aa或者ab或者ac或者ba或者bb或者bc或者ca或者cb或者cc“一样。[abc]{2}并不能表示匹配”aa或者bb或者cc“
-重复次数是可以指定范围的
-注意这样的正则表达式会优先匹配最长字符串,比如输入 I had an aaaaawful day会匹配单词aaaaawful中的aaaaa,而不会匹配其中的aaa。
重复次数是可以有范围的,但是有时候这样的方法也不能找到最佳答案。如果你的输入文本是I had an aaawful daaaaay那么在第一次匹配时,只能找到aaawful,只有再次执行匹配时才能找到daaaaay中的aaaaa.
-重复次数的范围可以是开区间
-使用正则表达式找到双引号。要求输入字符串可能包含任意个字符。
-调整你的正则表达式使得在一对双引号中间不再包含其他的双引号。
- ".{0,}", 然后 "[^"]{0,}".
?与{0,1}相同,比如,colou?r表示匹配colour或者color
-*与{0,}相同。比如,.*表示匹配任意内容
-+与{1,}相同。比如,\w+表示匹配一个词。其中”一个词”表示由一个或一个以上的字符组成的字符串,比如_var或者AccountName1.
-这些是你必须知道的常用转义字符,除此之外还有:
-简化下列的正则表达式:
-".{0,}" and "[^"]{0,}"x?x?x?y*y*z+z+z+z+".*" and "[^"]*"x{0,3}y*z{4,}写出正则表达式,寻找由非字母字符分隔的两个单词。如果是三个呢?六个呢?
-\w+\W+\w+, \w+\W+\w+\W+\w+, \w+\W+\w+\W+\w+\W+\w+\W+\w+\W+\w+.
下文中,我们将简化这个正则表达式。
-正则表达式 “.*” 表示匹配双引号,之后是任意内容,之后再匹配一个双引号。注意,其中匹配任意内容也可以是双引号。通常情况下,这并不是很有用。通过在句尾加上一个问号,可以使得字符串重复不再匹配最长字符。
-你可以使用|来分隔可以匹配的不同选择:
-简化下列正则表达式:
-s|t|u|v|waa|ab|ba|bb[abc]|[^abc][^ab]|[^bc][ab][ab][ab]?[ab]?答案
-
[s-w][ab]{2}.[^b][ab]{2,4}使用正则表达式匹配1到31之间的整数,[1-31]不是正确答案!
-这样的正则表达式不唯一. [1-9]|[12][0-9]|3[01] 是其中之一。
你可以使用括号表示分组:
-在《时间机器中》找到一对括号中的内容,然后通过修改正则表达式,找到不含括号的内容。
-\(.*\). 然后是, \([^()]*\).
分组可以包括空字符串:
-你也可以在分组的基础上使用重复:
-简化正则表达式 \w+\W+\w+\W+\w+ 以及 \w+\W+\w+\W+\w+\W+\w+\W+\w+\W+\w+.
\w+(\W+\w+){2}, \w+(\W+\w+){5}.
-
在单词和非单词之间有单词分隔符。记住,一个单词\w是[0-9A-Za-z_],而非单词字符是\W(大写),表示[^0-9A-Za-z_].
-在文本的开头和结尾通常也有单词分隔符。
-在输入文本it’s a cat中,实际有八个单词分隔符。如果我们在cat之后在上一个空格,那就有九个单词分隔符。.
-单词分隔符本身并不是字符。它们的宽度为0。下列正则表达式的作用不同
-(\bcat)\b(\bcat\b)\b(cat)\b\b(cat\b)在词典中找到最长的单词。
-在尝试之后发现,\b.{45,}\b可以在字典中找到最长单词
-
-
一篇文本中可以有一行或多行,行与行之间由换行符分隔,比如:
-注意,所有的文本都是以一行结束的,而不是以换行符结束。但是,任意一行都可能为空,包括最后一行。
-行的起始位置,是在换行符和下一行首字符之间的空间。考虑到单词分隔符,文本的起始位置也可以当做是首行位置。
-最后一行是最后一行的尾字符和换行符之间的空间。考虑到单词分隔符,文本的结束也可以认为是行的结束。
-那么新的格式表示如下:
-基于上述概念:
-^.*& 表示匹配全文内容,因为行的开始符号也是一个字符,"."会匹配这个符号。找到单独的一行,可以使用 ^.*?$与字符分隔符一样,换行符也不是字符。它们宽度为0.如下所示的正则表达式作用不同:
-(^cat)$(^cat$)^(cat)$^(cat$)使用正则表达式在《时间机器》中找到最长的一行。
-使用正则表达式^.{73,}$可以匹配长度为73的一行
- -在很多的正则表达式实现中,将^和$作为文本的开始符号和结束符号。
-还有一些实现中,用\A和\z作为文本的开始和结束符号。
- -从这里开始,正则表达式真正体现出了它的强大。
-你已经知道了使用括号可以匹配一组符号。使用括号也可以捕获子串。假设正则表达式是一个小型计算机程序,那么捕获子串就是它输出的一部分。
-正则表达式(\w*)ility表示匹配以ility结尾的词。第一个被捕获的部分是由\w*控制的。比如,输入的文本内容中有单词accessibility,那么首先被捕获的部分是accessib。如果输入的文本中有单独的ility,则首先被捕获的是一个空字符串。
-你可能会有很多的捕获字符串,它们可能靠得很近。捕获组从左向右编号。也就是只需要对左括号计数。
-假设有这样的正则表达式:(\w+) had a ((\w+) \w+)
-输入的内容是:I had a nice day
-
在一些正则表达式的实现中,你可以从零开始编号,编号零表示匹配整句话:I had a nice day.
在其他的实现中,如果没有制定捕获组,那么捕获组1会自动地填入捕获组0的信息。
-是的,这也意味着会有很多的括号。有一些正则表达式的实现中,提供了“非捕获组”的语法,但是这样的语法并不是标准语法,因此我们不会介绍。
-从一个成功的匹配中返回的捕获组个数,与使用原来的正则表达式获得的捕获组个数相同。记住这一点,你可以解释一些奇怪的现象。.
-正则表达式((cat)|dog)表示匹配cat或者dog。这里有两个捕获组,如果输入文本是dog,那么捕获组1是dog,捕获组2为空。
-正则表达式a(\w)*表示匹配一个以a开头的单词。这里只有一个捕获组
-假如你使用了一个正则表达式去匹配字符串,你可以描述另外一个字符串来替换其中的匹配字符。用来替换的字符串称为替换表达式。它的功能类似于
-将《时间机器》中所有的元音字母替换为r。
-使用正则表达式[aeiou]以及[AEIOU],对应的替换字符串分别为r,R.
-但是,你可以在替换表达式中引用捕获组。这是在替换表达式中,你可以唯一操作的地方。这也是非常有效的,因为这样你就不用重构你找到的字符串。
-假设你正在尝试将美国风格的日期表示MM/DD/YY替换为ISO 8601日期表示YYYY-MM-DD
-20\3-\1-\2.2005-03-04.在替换表达式中,你可以多次使用捕获组
-在某些实现中,采用美元符号$代替\
-使用正则表达式和替换表达式,将23h59这样的时间戳转化为23:59.
-正则表达式finds the timestamps, 替换表达式\1:\2
在一个正则表达式中,你也可以引用捕获组。这称作:反向引用
-比如,[abc]{2}表示匹配aa或者ab或者ac或者ba或者bb或者bc或者ca或者cb或者cc.但是{[abc]}\1表示只匹配aa或者bb或者cc.
-在字典中,找到包含两次重复子串的最长单词,比如papa, coco
\b(.{6,})\1\b 匹配 chiquichiqui.
如果我们不在乎单词的完整性,我们可以忽略单词的分解,使用正则表达式 (.{7,})\1匹配countercountermeasure 以及 countercountermeasures.
特别提醒:
-在一些编程语言,比如Java中,对于包含正则表达式的字符串没有特殊标记。字符串有着自己的过滤规则,这是优先于正则表达式规则的,这是频繁使用反斜杠的原因。
-比如在Java中
-"[^"]*" 变为String re = “\”[^\"]*\”"\[\]] 变为String re = “[\\\\\\[\\]
]”;String re = "\\s"; 和String re = "[ \t\r\n]"; 是等价的. 注意它们实际执行调用时的层次不同。在其他的编程语言中,正则表达式是由特殊标明的,比如使用/。下面是JavaScript的例子:
-var regExp = /\d/;.var regExp = /[\\\[\]]/;var regExp = /\s/; 和 var regExp = /[ \t\r\n]/; 是等价的var regExp = /https?:\/\//;.我希望现在你能明白,我为什么让你特别注意反斜杠。
- -当你动态创建一个正则表达式的时候请特别小心。如果你使用的字符串不够完善的话,可能会有意想不到的匹配结果。这可能导致语法错误,更糟糕的是,你的正则表达式语法正确,但是结果无法预料。
-错误的Java代码:
-String sep = System.getProperty(“file.separator”); String[] directories = filePath.split(sep);
-Bug:String.split() 认为sep是一个正则表达式。但是,在Windows中,Sep是表示匹配一个反斜杠,也就是与正则表达式”\\”相同。这个正则表达式是正确的,但是会返回一个异常:PatternSyntaxException.
任何好的编程语言都会提供一种良好的机制来跳过字符串中所有的元字符。在Java中,你可以这样实现:
-String sep = System.getProperty(“file.separator”);
-String[] directories = filePath.split(Pattern.quote(sep));
- -将正则表达式字符串加入反复运行的程序中,是一种开销很大的操作。如果你可以在循环中避免使用正则表达式,你可以大大提高效率。
- -在一个网站上,我输入了我的卡号比如 1234 5678 8765 4321 网站拒绝接收。因为它使用了正则表达式\d{16}。
正则表达式应该考虑到用户输入的空格和短横线。
实际上,为什么不先过滤掉所有的非数字字符,然后再进行有效性验证呢?这样做,可以先使用\D以及空的替换表达式。
-在不先过滤掉所有的非数字字符的情况下,使用正则表达式验证卡号的正确性。
-\D*(\d\D*){16} is one of several variations which would accomplish this.
不要使用正则表达式来验证姓名。实际上,即使可以,也不要企图验证姓名。
-程序员对名字的错误看法:
-不要使用正则表达式验证邮箱地址的正确性。
-首先,这样的验证很难是精确的。电子邮件地址是可以用正则表达式验证的,但是表达式会非常的长并且复杂。
-短的正则表达式会导致错误。(你知道吗?电子邮箱地址中会有一些注释)
-第二,即使一个电子邮件地址可以成功匹配正则表达式,也不代表这个邮箱实际存在。邮箱的唯一验证方法,是发送验证邮件。
- -在严格的应用场景中,不要使用正则表达式来解析HTML或者XML。解析HTML或者XML:
-找到一个已经有的解析库来完成这个工作
-总结:
-a b c d 1 2 3 4 etc.. [abc] [a-z] \d \w \s
-. 代表任何字符\d 表示“数字”\w 表示”字母”, [0-9A-Za-z_]\s 表示 “空格, 制表符,回车或换行符”[^abc] \D \W \S{4} {3,16} {1,} ? * +
-? 表示 “零次或一次”* 表示 “大于零次”+ 表示 “一次或一次以上”(Septem|Octo|Novem|Decem)ber\b ^ $ \A \z\1 \2 \3 etc. (在匹配表达式和替换表达式中都可用). \ [ ] { } ? * + | ( ) ^ $[ ] \ - ^\正则表达式非常常用而且非常有用。每个人在编辑文本或是编写程序时都必须了解怎样使用正则表达式。
-In this post you will learn how to use a micro framework called Spark to build a RESTful backend. The RESTful backend is consumed by a single page web application using AngularJS and MongoDB for data storage. I’ll also show you how to run Java 8 on OpenShift.
-You will develop a todo application which allows users to create and list todo items. The application will do the following:
-


Spark is a Java based microframework for building web applications with minimum fuss. It is inspired by a framework written in Ruby called Sinatra. It has a minimalist core providing all the essential features required to build a web application quickly with little code.
-To build the sample application developed in this blog, you would need the following on your machine.
-The code for today’s demo application is available on github: todoapp-spark.
-Open a new command-line terminal and navigate to the location where you want to create the project. Execute the command shown below to generate the template application.
-$ mvn archetype:generate -DgroupId=com.todoapp -DartifactId=todoapp -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false-
Import the project into your IDE and replace the pom.xml with the one shown below.
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0%20http://maven.apache.org/maven-v4_0_0.xsd"> - <modelVersion>4.0.0</modelVersion> - <groupId>com.todoapp</groupId> - <artifactId>todoapp</artifactId> - <packaging>jar</packaging> - <version>1.0-SNAPSHOT</version> - <name>todoapp</name> - <url>http://maven.apache.org</url> - - <properties> - <maven.compiler.source>1.8</maven.compiler.source> - <maven.compiler.target>1.8</maven.compiler.target> - </properties> - - <dependencies> - - </dependencies> -</project>-
In the pom.xml shown above, we changed the following:
-Delete App.java and AppTest.java files as we don’t need them.
-The application uses the Spark framework and a MongoDB database. So, update the dependencies section of pom.xml with the one shown below.
-<dependencies> - <dependency> - <groupId>com.sparkjava</groupId> - <artifactId>spark-core</artifactId> - <version>2.0.0</version> - </dependency> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-simple</artifactId> - <version>1.7.5</version> - </dependency> - <dependency> - <groupId>org.mongodb</groupId> - <artifactId>mongo-java-driver</artifactId> - <version>2.11.3</version> - </dependency> - <dependency> - <groupId>com.google.code.gson</groupId> - <artifactId>gson</artifactId> - <version>2.2.4</version> - </dependency> -</dependencies>-
Spark uses SLF4J for logging, so we added slf4j-simple binding in the dependencies. This is required to view the log and error messages. Also, we added the gson library as its used to convert objects to and from JSON.
-Create a new class called Bootstrap and add the following code to it.
-package com.todoapp;
-
-import spark.Request;
-import spark.Response;
-import spark.Route;
-
-import static spark.Spark.*;
-
-public class Bootstrap {
-
- public static void main(String[] args) {
- get("/", new Route() {
- @Override
- public Object handle(Request request, Response response) {
- return "Hello World!!";
- }
- });
- }
-}
-This code:
-To see the application in action, run the main program using your IDE. The application will start the embedded Jetty server at http://0.0.0.0:4567. When you open this link in your web browser, you will see “Hello World!!”.
-Take advantage of Java 8 lambda expressions to make your code more concise and clean. Spark is a modern Java web framework that takes advantage of Java 8 features.
-package com.todoapp;
-
-import spark.Request;
-import spark.Response;
-import spark.Route;
-
-import static spark.Spark.*;
-
-public class Bootstrap {
-
- public static void main(String[] args) {
- get("/", (request, response) -> "Hello World");
- }
-}
-Our goal is an application that stores and manages ToDo items. Our simple ToDo class is shown below.
-package com.todoapp;
-
-import com.mongodb.BasicDBObject;
-import com.mongodb.DBObject;
-import org.bson.types.ObjectId;
-
-import java.util.Date;
-
-public class Todo {
-
- private String id;
- private String title;
- private boolean done;
- private Date createdOn = new Date();
-
- public Todo(BasicDBObject dbObject) {
- this.id = ((ObjectId) dbObject.get("_id")).toString();
- this.title = dbObject.getString("title");
- this.done = dbObject.getBoolean("done");
- this.createdOn = dbObject.getDate("createdOn");
- }
-
- public String getTitle() {
- return title;
- }
-
- public boolean isDone() {
- return done;
- }
-
- public Date getCreatedOn() {
- return createdOn;
- }
-}
-Our TodoService class implement methods that use CRUD operations on the Todo object. It basically Creates, Reads, and Updates Todo documents stored in MongoDB.
-package com.todoapp;
-
-import com.google.gson.Gson;
-import com.mongodb.*;
-import org.bson.types.ObjectId;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-
-public class TodoService {
-
- private final DB db;
- private final DBCollection collection;
-
- public TodoService(DB db) {
- this.db = db;
- this.collection = db.getCollection("todos");
- }
-
- public List<Todo> findAll() {
- List<Todo> todos = new ArrayList<>();
- DBCursor dbObjects = collection.find();
- while (dbObjects.hasNext()) {
- DBObject dbObject = dbObjects.next();
- todos.add(new Todo((BasicDBObject) dbObject));
- }
- return todos;
- }
-
- public void createNewTodo(String body) {
- Todo todo = new Gson().fromJson(body, Todo.class);
- collection.insert(new BasicDBObject("title", todo.getTitle()).append("done", todo.isDone()).append("createdOn", new Date()));
- }
-
- public Todo find(String id) {
- return new Todo((BasicDBObject) collection.findOne(new BasicDBObject("_id", new ObjectId(id))));
- }
-
- public Todo update(String todoId, String body) {
- Todo todo = new Gson().fromJson(body, Todo.class);
- collection.update(new BasicDBObject("_id", new ObjectId(todoId)), new BasicDBObject("$set", new BasicDBObject("done", todo.isDone())));
- return this.find(todoId);
- }
-}
-This code does the following:
-It is generally not a good idea to add all of the code to one class so we will move the application REST endpoints to another class. This new class is called TodoResource and exposes CRUD operations over Todo objects.
-package com.todoapp;
-
-import com.google.gson.Gson;
-import spark.Request;
-import spark.Response;
-import spark.Route;
-
-import java.util.HashMap;
-
-import static spark.Spark.get;
-import static spark.Spark.post;
-import static spark.Spark.put;
-
-public class TodoResource {
-
- private static final String API_CONTEXT = "/api/v1";
-
- private final TodoService todoService;
-
- public TodoResource(TodoService todoService) {
- this.todoService = todoService;
- setupEndpoints();
- }
-
- private void setupEndpoints() {
- post(API_CONTEXT + "/todos", "application/json", (request, response) -> {
- todoService.createNewTodo(request.body());
- response.status(201);
- return response;
- }, new JsonTransformer());
-
- get(API_CONTEXT + "/todos/:id", "application/json", (request, response)
-
- -> todoService.find(request.params(":id")), new JsonTransformer());
-
- get(API_CONTEXT + "/todos", "application/json", (request, response)
-
- -> todoService.findAll(), new JsonTransformer());
-
- put(API_CONTEXT + "/todos/:id", "application/json", (request, response)
-
- -> todoService.update(request.params(":id"), request.body()), new JsonTransformer());
- }
-
-
-}
-The code shown above exposes the TodoService CRUD methods as REST API’s.
-JsonTransformer in the code shown above is an implementation of Spark’s ResponseTransformer interface. It allows you to convert response objects to other formats like JSON.
-package com.todoapp;
-
-import com.google.gson.Gson;
-import spark.Response;
-import spark.ResponseTransformer;
-
-import java.util.HashMap;
-
-public class JsonTransformer implements ResponseTransformer {
-
- private Gson gson = new Gson();
-
- @Override
- public String render(Object model) {
- return gson.toJson(model);
- }
-
-}
-Now we will write the entry point for our application. The Bootstrap class shown below configures all of the components. When you run this class as a Java application, it starts the Jetty server and start listening to requests.
-package com.todoapp;
-
-import com.mongodb.*;
-
-import static spark.Spark.setIpAddress;
-import static spark.Spark.setPort;
-import static spark.SparkBase.staticFileLocation;
-
-public class Bootstrap {
- private static final String IP_ADDRESS = System.getenv("OPENSHIFT_DIY_IP") != null ? System.getenv("OPENSHIFT_DIY_IP") : "localhost";
- private static final int PORT = System.getenv("OPENSHIFT_DIY_PORT") != null ? Integer.parseInt(System.getenv("OPENSHIFT_DIY_PORT")) : 8080;
-
- public static void main(String[] args) throws Exception {
- setIpAddress(IP_ADDRESS);
- setPort(PORT);
- staticFileLocation("/public");
- new TodoResource(new TodoService(mongo()));
- }
-
- private static DB mongo() throws Exception {
- String host = System.getenv("OPENSHIFT_MONGODB_DB_HOST");
- if (host == null) {
- MongoClient mongoClient = new MongoClient("localhost");
- return mongoClient.getDB("todoapp");
- }
- int port = Integer.parseInt(System.getenv("OPENSHIFT_MONGODB_DB_PORT"));
- String dbname = System.getenv("OPENSHIFT_APP_NAME");
- String username = System.getenv("OPENSHIFT_MONGODB_DB_USERNAME");
- String password = System.getenv("OPENSHIFT_MONGODB_DB_PASSWORD");
- MongoClientOptions mongoClientOptions = MongoClientOptions.builder().build();
- MongoClient mongoClient = new MongoClient(new ServerAddress(host, port), mongoClientOptions);
- mongoClient.setWriteConcern(WriteConcern.SAFE);
- DB db = mongoClient.getDB(dbname);
- if (db.authenticate(username, password.toCharArray())) {
- return db;
- } else {
- throw new RuntimeException("Not able to authenticate with MongoDB");
- }
- }
-}
-Create a new directory with name public and place the javascript and css files in it. You can checkout the required directory structure from the Github repository. Download the latest copy of AngularJS and Bootstrap from their respective official websites, or you can copy the resources from this project github repository.
-Create a new file called index.html inside of the src/main/resources/public directory and place the following content into it.
-<!DOCTYPE html> -<html ng-app="todoapp"> -<head> - <title>Todo App</title> - <link rel="stylesheet" href="http://github.com/css/bootstrap.css"> - <link rel="stylesheet" href="http://github.com/css/main.css"> -</head> -<body> -<div class="navbar navbar-inverse"> - <div class="container-fluid"> - <div class="navbar-header"> - <button type="button" class="navbar-toggle"> - <span class="sr-only">Toggle navigation</span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </button> - <a class="navbar-brand" href="http://github.com/">TodoApp</a> - </div> - </div> -</div> - -<div class="container" ng-view=""> - - -</div> - -<script src="http://github.com/js/jquery.js"></script> -<script src="http://github.com/js/angular.js"></script> -<script src="http://github.com/js/angular-route.js"></script> -<script src="http://github.com/js/angular-cookies.js"></script> -<script src="http://github.com/js/angular-sanitize.js"></script> -<script src="http://github.com/js/angular-resource.js"></script> - -<script src="http://github.com/scripts/app.js"></script> -</body> -</html>-
In the html shown above:
-The app.js houses all of the application specific JavaScript. All of the application routes are defined inside it. In the code shown below, we have defined two routes and each has a corresponding Angular controller.
-/**
- * Created by shekhargulati on 10/06/14.
- */
-
-var app = angular.module('todoapp', [
- 'ngCookies',
- 'ngResource',
- 'ngSanitize',
- 'ngRoute'
-]);
-
-app.config(function ($routeProvider) {
- $routeProvider.when('/', {
- templateUrl: 'views/list.html',
- controller: 'ListCtrl'
- }).when('/create', {
- templateUrl: 'views/create.html',
- controller: 'CreateCtrl'
- }).otherwise({
- redirectTo: '/'
- })
-});
-
-app.controller('ListCtrl', function ($scope, $http) {
- $http.get('/api/v1/todos').success(function (data) {
- $scope.todos = data;
- }).error(function (data, status) {
- console.log('Error ' + data)
- })
-
- $scope.todoStatusChanged = function (todo) {
- console.log(todo);
- $http.put('/api/v1/todos/' + todo.id, todo).success(function (data) {
- console.log('status changed');
- }).error(function (data, status) {
- console.log('Error ' + data)
- })
- }
-});
-
-app.controller('CreateCtrl', function ($scope, $http, $location) {
- $scope.todo = {
- done: false
- };
-
- $scope.createTodo = function () {
- console.log($scope.todo);
- $http.post('/api/v1/todos', $scope.todo).success(function (data) {
- $location.path('/');
- }).error(function (data, status) {
- console.log('Error ' + data)
- })
- }
-});
-The code shown above does the following:
-You can find the respective views for different routes in the application’s Github repository.
-You can either run the application using your IDE or package this application as an executable jar and then run it from the command-line. Add the following plugin to your project pom.xml to create an executable JAR. This plugin provides the capability to package the artifact in an uber-jar, including its dependencies and to shade – i.e. rename – the packages of some of the dependencies.
-<build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-shade-plugin</artifactId> - <version>2.3</version> - <configuration> - <createDependencyReducedPom>true</createDependencyReducedPom> - <filters> - <filter> - <artifact>*:*</artifact> - <excludes> - <exclude>META-INF/*.SF</exclude> - <exclude>META-INF/*.DSA</exclude> - <exclude>META-INF/*.RSA</exclude> - </excludes> - </filter> - </filters> - </configuration> - <executions> - <execution> - <phase>package</phase> - <goals> - <goal>shade</goal> - </goals> - <configuration> - <transformers> - <transformer - implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> - <mainClass>com.todoapp.Bootstrap</mainClass> - </transformer> - </transformers> - </configuration> - </execution> - </executions> - </plugin> - </plugins> -</build>-
To create an executable jar, run the mvn clean install command. This creates a todoapp-1.0-SNAPSHOT.jar in the target directory.
-To run the application:
-$ java -jar target/todoapp-1.0-SNAPSHOT.jar-
This would print following lines in the terminal.
-== Spark has ignited ...
->> Listening on localhost:8080
-[Thread-2] INFO org.eclipse.jetty.server.Server - jetty-9.0.z-SNAPSHOT
-[Thread-2] INFO org.eclipse.jetty.server.ServerConnector - Started ServerConnector@5bbf3bfb{HTTP/1.1}{localhost:8080}
-This blog would not be complete if I didn’t show you how to run this application on OpenShift. Today OpenShift does not support JDK 8 but that doesn’t mean you can’t run Java 8 applications. You can use the DIY cartridge and install your own JDK version. The next command creates the todo application you created in the above mentioned steps. It installs JDK 8 on the DIY gear and configures other settings.
-$ rhc app create todoapp diy mongodb-2.4 --repo=todoapp-os --from-code=https://github.com/shekhargulati/spark-openshift-quickstart.git-
After this commands successfully finishes, you will see the todo app running at http://todoapp-{domain-name}.rhcloud.com. Please replace {domain-name} with your OpenShift account domain name.
--一般Java在内存分配时会涉及到以下区域:
--
--寄存器(Registers):速度最快的存储场所,因为寄存器位于处理器内部,我们在程序中无法控制
--栈(Stack):存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中
--堆(Heap):堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器(GC)来管理。
--静态域(static field): 静态存储区域就是指在固定的位置存放应用程序运行时一直存在的数据,Java在内存中专门划分了一个静态存储区域来管理一些特殊的数据变量如静态的数据变量
--常量池(constant pool):虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。
--非RAM存储:硬盘等永久存储空间
--
-
堆栈特点对比:
-由于篇幅原因,下面只简单的介绍一下堆栈的一些特性。
-
栈:当定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
-
堆:当堆中的new产生数组和对象超出其作用域后,它们不会被释放,只有在没有引用变量指向它们的时候才变成垃圾,不能再被使用。即使这样,所占内存也不会立即释放,而是等待被垃圾回收器收走。这也是Java比较占内存的原因。
-
-
栈:存取速度比堆要快,仅次于寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
-堆:堆是一个运行时数据区,可以动态地分配内存大小,因此存取速度较慢。也正因为这个特点,堆的生存期不必事先告诉编译器,而且Java的垃圾收集器会自动收走这些不再使用的数据。
-
-
栈:栈中的数据可以共享, 它是由编译器完成的,有利于节省空间。
-例如:需要定义两个变量int a = 3;int - b = 3;
-编译器先处理int - a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。这时,如果再让a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并让a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
-堆:例如上面栈中a的修改并不会影响到b, 而在堆中一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。
-
-
内存耗用名词解析:
-VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
-RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)
-PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
-USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
-一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS
-
-
-
OOM:
-
-
内存泄露可以引发很多的问题:
-1.程序卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC)
-2.莫名消失(当你的程序所占内存越大,它在后台的时候就越可能被干掉。反之内存占用越小,在后台存在的时间就越长)
-3.直接崩溃(OutOfMemoryError)
-
-
-
ANDROID内存面临的问题:
- -1.有限的堆内存,原始只有16M
-2.内存大小消耗等根据设备,操作系统等级,屏幕尺寸的不同而不同
-3.程序不能直接控制
-4.支持后台多任务处理(multitasking)
-5.运行在虚拟机之上
- -
-
-
5R:
-本文主要通过如下的5R方法来对ANDROID内存进行优化:
-1.Reckon(计算)
-首先需要知道你的app所消耗内存的情况,知己知彼才能百战不殆
-2.Reduce(减少)
-消耗更少的资源
--
3.Reuse(重用)
-当第一次使用完以后,尽量给其他的使用
- -5.Recycle(回收)
-返回资源
-4.Review(检查)
-
回顾检查你的程序,看看设计或代码有什么不合理的地方。
--
-
Reckon (计算):
-了解自己应用的内存使用情况是很有必要的。如果当内存使用过高的话就需要对其进行优化,因为更少的使用内存可以减少ANDROID系统终止我们的进程的几率,也可以提高多任务执行效率和体验效果。
-下面从系统内存(system ram)和堆内存(heap)两个方面介绍一些查看和计算内存使用情况的方法:
-
-
System Ram(系统内存):
-
观察和计算系统内存使用情况,可以使用Android提供给我们的两个工具procstats,meminfo。他们一个侧重于后台的内存使用,另一个是运行时的内存使用。
- -
-
Heap(堆内存):
-在程序中可以使用如下的方法去查询内存使用情况
-
-
ActivityManager#getMemoryClass()
-查询可用堆内存的限制
-3.0(HoneyComb)以上的版本可以通过largeHeap=“true”来申请更多的堆内存(不过这算作“作弊”)
-
-
android.os.Debug#getMemoryInfo(Debug.MemoryInfo memoryInfo)
-得到的MemoryInfo中可以查看如下Field的属性:
- --
android.os.Debug#getNativeHeapSize()
-返回的是当前进程navtive堆本身总的内存大小
-android.os.Debug#getNativeHeapAllocatedSize()
-返回的是当前进程navtive堆中已使用的内存大小
-android.os.Debug#getNativeHeapFreeSize()
-
返回的是当前进程navtive堆中已经剩余的内存大小
-
-
Memory Analysis Tool(MAT):
-通常内存泄露分析被认为是一件很有难度的工作,一般由团队中的资深人士进行。不过,今天我们要介绍的 MAT(Eclipse Memory Analyzer)被认为是一个“傻瓜式“的堆转储文件分析工具,你只需要轻轻点击一下鼠标就可以生成一个专业的分析报告。
-如下图:
-
-
关于详细的MAT使用我推荐下面这篇文章:使用 Eclipse Memory Analyzer 进行堆转储文件分析
-
-
-
写在最后:
-我准备将文章分为上、中、下三部分。现在已经全部完成:
-内存简介,Recoken(计算)请看:ANDROID内存优化(大汇总——上)
-Reduce(减少),Reuse(重用) 请看:ANDROID内存优化(大汇总——中)
-Recycle(回收), Review(检查) 请看:ANDROID内存优化(大汇总——全)
-
-
写这篇文章的目的就是想弄一个大汇总,将零散的内存知识点总结一下,如果有错误、不足或建议都希望告诉我。
-
-
参考文章:
-AnDevCon开发者大会演讲PPT:Putting Your App on a Memory Diet
-深入Java核心 Java内存分配原理精讲(http://developer.51cto.com/art/201009/225071.htm)
- -Android内存性能优化(内部资料总结)(http://www.2cto.com/kf/201405/303276.html)
-
-
-OOM:
-
-
-
-内存泄露可以引发很多的问题:
-1.程序卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC)
-2.莫名消失(当你的程序所占内存越大,它在后台的时候就越可能被干掉。反之内存占用越小,在后台存在的时间就越长)
-3.直接崩溃(OutOfMemoryError)
-
-
-
-ANDROID内存面临的问题:
--
-1.有限的堆内存,原始只有16M
-2.内存大小消耗等根据设备,操作系统等级,屏幕尺寸的不同而不同
-3.程序不能直接控制
-4.支持后台多任务处理(multitasking)
-5.运行在虚拟机之上
-5R:
-本文主要通过如下的5R方法来对ANDROID内存进行优化:
-
-
1.Reckon(计算)
-首先需要知道你的app所消耗内存的情况,知己知彼才能百战不殆
-2.Reduce(减少)
-消耗更少的资源
--
3.Reuse(重用)
-当第一次使用完以后,尽量给其他的使用
-5.Recycle(回收)
-回收资源
-4.Review(检查)
-
回顾检查你的程序,看看设计或代码有什么不合理的地方。
-Reckon:
-
-
关于内存简介,和Reckon(内存计算)的内容请看上一篇文章:ANDROID内存优化(大汇总——上)
-
-
-
Reduce :
-
-
Reduce的意思就是减少,直接减少内存的使用是最有效的优化方式。
-下面来看看有哪些方法可以减少内存使用:
-
-
-
图片显示:
-我们需要根据需求去加载图片的大小。
-例如在列表中仅用于预览时加载缩略图(thumbnails )。
-只有当用户点击具体条目想看详细信息的时候,这时另启动一个fragment/activity/对话框等等,去显示整个图片
- -
-
图片大小:
-直接使用ImageView显示bitmap会占用较多资源,特别是图片较大的时候,可能导致崩溃。
-使用BitmapFactory.Options设置inSampleSize, 这样做可以减少对系统资源的要求。
-属性值inSampleSize表示缩略图大小为原始图片大小的几分之一,即如果这个值为2,则取出的缩略图的宽和高都是原始图片的1/2,图片大小就为原始大小的1/4。
-
BitmapFactory.Options bitmapFactoryOptions = new BitmapFactory.Options(); - bitmapFactoryOptions.inJustDecodeBounds = true; - bitmapFactoryOptions.inSampleSize = 2; - // 这里一定要将其设置回false,因为之前我们将其设置成了true - // 设置inJustDecodeBounds为true后,decodeFile并不分配空间,即,BitmapFactory解码出来的Bitmap为Null,但可计算出原始图片的长度和宽度 - options.inJustDecodeBounds = false; - Bitmap bmp = BitmapFactory.decodeFile(sourceBitmap, options);- -
-
图片像素:
- publicstaticBitmapreadBitMap(Contextcontext, intresId) {
- BitmapFactory.Optionsopt = newBitmapFactory.Options();
- opt.inPreferredConfig = Bitmap.Config.RGB_565;
- opt.inPurgeable = true;
- opt.inInputShareable = true;
- //获取资源图片
- InputStreamis = context.getResources().openRawResource(resId);
- returnBitmapFactory.decodeStream(is, null, opt);
- }图片回收:
--使用Bitmap过后,就需要及时的调用Bitmap.recycle()方法来释放Bitmap占用的内存空间,而不要等Android系统来进行释放。
--下面是释放Bitmap的示例代码片段。
--
- // 先判断是否已经回收
- if(bitmap != null && !bitmap.isRecycled()){
- // 回收并且置为null
- bitmap.recycle();
- bitmap = null;
- }
- System.gc();捕获异常:
-
经过上面这些优化后还会存在报OOM的风险,所以下面需要一道最后的关卡——捕获OOM异常:
- - Bitmap bitmap = null;
- try {
- // 实例化Bitmap
- bitmap = BitmapFactory.decodeFile(path);
- } catch (OutOfMemoryError e) {
- // 捕获OutOfMemoryError,避免直接崩溃
- }
- if (bitmap == null) {
- // 如果实例化失败 返回默认的Bitmap对象
- return defaultBitmapMap;
- }
-修改对象引用类型:
-
-
引用类型:
-引用分为四种级别,这四种级别由高到低依次为:强引用>软引用>弱引用>虚引用。
-
强引用(strong reference)
-如:Object object=new Object(),object就是一个强引用了。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
-
软引用(SoftReference)
-只有内存不够时才回收,常用于缓存;当内存达到一个阀值,GC就会去回收它;
-
-弱引用(WeakReference)
--弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
-
-
-
-虚引用(PhantomReference)
--"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
-
-
软引用和弱引用的应用实例:
-
注意:对于SoftReference(软引用)或者WeakReference(弱引用)的Bitmap缓存方案,现在已经不推荐使用了。自Android2.3版本(API - Level 9)开始,垃圾回收器更着重于对软/弱引用的回收,所以下面的内容可以选择忽略。
--在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。
--下面以使用软引用为例来详细说明(弱引用的使用方式与软引用是类似的):
--假设我们的应用会用到大量的默认图片,而且这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软引用技术来避免这个问题发生。
-
-首先定义一个HashMap,保存软引用对象。
-
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();再来定义一个方法,保存Bitmap的软引用到HashMap。
-
public void addBitmapToCache(String path) {
- // 强引用的Bitmap对象
- Bitmap bitmap = BitmapFactory.decodeFile(path);
- // 软引用的Bitmap对象
- SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
- // 添加该对象到Map中使其缓存
- imageCache.put(path, softBitmap);
- }
-获取的时候,可以通过SoftReference的get()方法得到Bitmap对象。public Bitmap getBitmapByPath(String path) {
- // 从缓存中取软引用的Bitmap对象
- SoftReference<Bitmap> softBitmap = imageCache.get(path);
- // 判断是否存在软引用
- if (softBitmap == null) {
- return null;
- }
- // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空
- Bitmap bitmap = softBitmap.get();
- return bitmap;
- }使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。
-需要注意的是,在垃圾回收器对这个Java对象回收前,SoftReference类所提供的get方法会返回Java对象的强引用,一旦垃圾线程回收该Java对象之后,get方法将返回null。所以在获取软引用对象的代码中,一定要判断是否为null,以免出现NullPointerException异常导致应用崩溃。
-
-
-
-到底什么时候使用软引用,什么时候使用弱引用呢?
--个人认为,如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
--还有就是可以根据对象是否经常使用来判断。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
--另外,和弱引用功能类似的是WeakHashMap。WeakHashMap对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的回收,回收以后,其条目从映射中有效地移除。WeakHashMap使用ReferenceQueue实现的这种机制。
-
-
-
-其他小tips:
--对常量使用static final修饰符
--让我们来看看这两段在类前面的声明:
-
-static int intVal = 42;
-static String strVal = "Hello, world!";
-编译器会生成一个叫做clinit的初始化类的方法,当类第一次被使用的时候这个方法会被执行。方法会将42赋给intVal,然后把一个指向类中常量表 的引用赋给strVal。当以后要用到这些值的时候,会在成员变量表中查找到他们。 下面我们做些改进,使用“final”关键字:
-static final int intVal = 42;
-static final String strVal = "Hello, world!";
-现在,类不再需要clinit方法,因为在成员变量初始化的时候,会将常量直接保存到类文件中。用到intVal的代码被直接替换成42,而使用strVal的会指向一个字符串常量,而不是使用成员变量。
--将一个方法或类声明为final不会带来性能的提升,但是会帮助编译器优化代码。举例说,如果编译器知道一个getter方法不会被重载,那么编译器会对其采用内联调用。
--你也可以将本地变量声明为final,同样,这也不会带来性能的提升。使用“final”只能使本地变量看起来更清晰些(但是也有些时候这是必须的,比如在使用匿名内部类的时候)。
--
-静态方法代替虚拟方法
-如果不需要访问某对象的字段,将方法设置为静态,调用会加速15%到20%。这也是一种好的做法,因为你可以从方法声明中看出调用该方法不需要更新此对象的状态。
-
-
减少不必要的全局变量
-尽量避免static成员变量引用资源耗费过多的实例,比如Context
- -
-避免创建不必要的对象
-
最常见的例子就是当你要频繁操作一个字符串时,使用StringBuffer代替String。
-对于所有所有基本类型的组合:int数组比Integer数组好,这也概括了一个基本事实,两个平行的int数组比 (int,int)对象数组性能要好很多。
-总体来说,就是避免创建短命的临时对象。减少对象的创建就能减少垃圾收集,进而减少对用户体验的影响。
- --
--避免使用浮点数
-通常的经验是,在Android设备中,浮点数会比整型慢两倍。
-
-
-使用实体类比接口好
- -假设你有一个HashMap对象,你可以将它声明为HashMap或者Map:
-Map map1 = new HashMap(); -HashMap map2 = new HashMap();-
哪个更好呢?
-按照传统的观点Map会更好些,因为这样你可以改变他的具体实现类,只要这个类继承自Map接口。传统的观点对于传统的程序是正确的,但是它并不适合嵌入式系统。调用一个接口的引用会比调用实体类的引用多花费一倍的时间。如果HashMap完全适合你的程序,那么使用Map就没有什么价值。如果有些地方你不能确定,先避免使用Map,剩下的交给IDE提供的重构功能好了。(当然公共API是一个例外:一个好的API常常会牺牲一些性能)
-
-
-避免使用枚举
-枚举变量非常方便,但不幸的是它会牺牲执行的速度和并大幅增加文件体积。
-使用枚举变量可以让你的API更出色,并能提供编译时的检查。所以在通常的时候你毫无疑问应该为公共API选择枚举变量。但是当性能方面有所限制的时候,你就应该避免这种做法了。 -
-
-
-for循环
-
-访问成员变量比访问本地变量慢得多,如下面一段代码:
-
-
-for(int i =0; i < this.mCount; i++) {}
-永远不要在for的第二个条件中调用任何方法,如下面一段代码:
--
-for(int i =0; i < this.getCount(); i++) {}
-对上面两个例子最好改为:
--
-int count = this.mCount; / int count = this.getCount();
-for(int i =0; i < count; i++) {}在java1.5中引入的for-each语法。编译器会将对数组的引用和数组的长度保存到本地变量中,这对访问数组元素非常好。 但是编译器还会在每次循环中产生一个额外的对本地变量的存储操作(如下面例子中的变量a),这样会比普通循环多出4个字节,速度要稍微慢一些:
--
-for (Foo a : mArray) {
- sum += a.mSplat;
-}了解并使用类库
-选择Library中的代码而非自己重写,除了通常的那些原因外,考虑到系统空闲时会用汇编代码调用来替代library方法,这可能比JIT中生成的等价的最好的Java代码还要好。
-当你在处理字串的时候,不要吝惜使用String.indexOf(),String.lastIndexOf()等特殊实现的方法。这些方法都是使用C/C++实现的,比起Java循环快10到100倍。
-System.arraycopy方法在有JIT的Nexus One上,自行编码的循环快9倍。
-android.text.format包下的Formatter类,提供了IP地址转换、文件大小转换等方法;DateFormat类,提供了各种时间转换,都是非常高效的方法。
-TextUtils类,对于字符串处理Android为我们提供了一个简单实用的TextUtils类,如果处理比较简单的内容不用去思考正则表达式不妨试试这个在android.text.TextUtils的类
-高性能MemoryFile类,很多人抱怨Android处理底层I/O性能不是很理想,如果不想使用NDK则可以通过MemoryFile类实现高性能的文件读写操作。MemoryFile适用于哪些地方呢?对于I/O需要频繁操作的,主要是和外部存储相关的I/O操作,MemoryFile通过将 NAND或SD卡上的文件,分段映射到内存中进行修改处理,这样就用高速的RAM代替了ROM或SD卡,性能自然提高不少,对于Android手机而言同时还减少了电量消耗。该类实现的功能不是很多,直接从Object上继承,通过JNI的方式直接在C底层执行。
-
-
-
-Reuse:
--Bitmap缓存分为两种:
--一种是内存缓存,一种是硬盘缓存。
-
-
-
内存缓存(LruCache):
-以牺牲宝贵的应用内存为代价,内存缓存提供了快速的Bitmap访问方式。系统提供的LruCache类是非常适合用作缓存Bitmap任务的,它将最近被引用到的对象存储在一个强引用的LinkedHashMap中,并且在缓存超过了指定大小之后将最近不常使用的对象释放掉。
-注意:以前有一个非常流行的内存缓存实现是SoftReference(软引用)或者WeakReference(弱引用)的Bitmap缓存方案,然而现在已经不推荐使用了。自Android2.3版本(API
- Level 9)开始,垃圾回收器更着重于对软/弱引用的回收,这使得上述的方案相当无效。
-
-
硬盘缓存(DiskLruCache):
- --一个内存缓存对加速访问最近浏览过的Bitmap非常有帮助,但是你不能局限于内存中的可用图片。GridView这样有着更大的数据集的组件可以很轻易消耗掉内存缓存。你的应用有可能在执行其他任务(如打电话)的时候被打断,并且在后台的任务有可能被杀死或者缓存被释放。一旦用户重新聚焦(resume)到你的应用,你得再次处理每一张图片。
--在这种情况下,硬盘缓存可以用来存储Bitmap并在图片被内存缓存释放后减小图片加载的时间(次数)。当然,从硬盘加载图片比内存要慢,并且应该在后台线程进行,因为硬盘读取的时间是不可预知的。
--注意:如果访问图片的次数非常频繁,那么ContentProvider可能更适合用来存储缓存图片,例如Image - Gallery这样的应用程序。
-更多关于内存缓存和硬盘缓存的内容请看Google官方教程https://developer.android.com/develop/index.html-1. Android-Universal-Image-Loader 图片缓存
-
-目前使用最广泛的图片缓存,支持主流图片缓存的绝大多数特性。
-项目地址:https://github.com/nostra13/Android-Universal-Image-Loader
-
-
-2. picasso square开源的图片缓存
-项目地址:https://github.com/square/picasso
-特点:(1)可以自动检测adapter的重用并取消之前的下载
-(2)图片变换
-(3)可以加载本地资源
-(4)可以设置占位资源
-(5)支持debug模式
-
-3. ImageCache 图片缓存,包含内存和Sdcard缓存
-项目地址:https://github.com/Trinea/AndroidCommon
-特点:
-(1)支持预取新图片,支持等待队列
-(2)包含二级缓存,可自定义文件名保存规则
-(3)可选择多种缓存算法(FIFO、LIFO、LRU、MRU、LFU、MFU等13种)或自定义缓存算法
-(4)可方便的保存及初始化恢复数据
-(5)支持不同类型网络处理
-(6)可根据系统配置初始化缓存等
@Override
- public View getView(int position, View convertView, ViewGroup parent) {
- ViewHolder vHolder = null;
- //如果convertView对象为空则创建新对象,不为空则复用
- if (convertView == null) {
- convertView = inflater.inflate(..., null);
- // 创建 ViewHodler 对象
- vHolder = new ViewHolder();
- vHolder.img= (ImageView) convertView.findViewById(...);
- vHolder.tv= (TextView) convertView.findViewById(...);
- // 将ViewHodler保存到Tag中(Tag可以接收Object类型对象,所以任何东西都可以保存在其中)
- convertView.setTag(vHolder);
- } else {
- //当convertView不为空时,通过getTag()得到View
- vHolder = (ViewHolder) convertView.getTag();
- }
- // 给对象赋值,修改显示的值
- vHolder.img.setImageBitmap(...);
- vHolder.tv.setText(...);
- return convertView;
- }
- //将显示的View 包装成类
- static class ViewHolder {
- TextView tv;
- ImageView img;
- }
-对象池:
-对象池使用的基本思路是:将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象所造成的开销。 并非所有对象都适合拿来池化――因为维护对象池也要造成一定开销。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。但是对于生成时开销可观的对象,池化技术就是提高性能的有效策略了。
-
-
-线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
--比如:一个应用要和网络打交道,有很多步骤需要访问网络,为了不阻塞主线程,每个步骤都创建个线程,在线程中和网络交互,用线程池就变的简单,线程池是对线程的一种封装,让线程用起来更加简便,只需要创一个线程池,把这些步骤像任务一样放进线程池,在程序销毁时只要调用线程池的销毁函数即可。
-
-
-
-java提供了ExecutorService和Executors类,我们可以应用它去建立线程池。
--通常可以建立如下4种:
--
-/** 每次只执行一个任务的线程池 */ -ExecutorService singleTaskExecutor = Executors.newSingleThreadExecutor(); - -/** 每次执行限定个数个任务的线程池 */ -ExecutorService limitedTaskExecutor = Executors.newFixedThreadPool(3); - -/** 所有任务都一次性开始的线程池 */ -ExecutorService allTaskExecutor = Executors.newCachedThreadPool(); - -/** 创建一个可在指定时间里执行任务的线程池,亦可重复执行 */ -ExecutorService scheduledTaskExecutor = Executors.newScheduledThreadPool(3);- -
-
-
-要根据情况适度使用缓存,因为内存有限。
--能保存路径地址的就不要存放图片数据,不经常使用的尽量不要缓存,不用时就清空。
--写在最后:
--
-我准备将文章分为上、中、下三部分。现在已经全部完成:
-内存简介,Recoken(计算)请看:ANDROID内存优化(大汇总——上)
-Reduce(减少),Reuse(重用) 请看:ANDROID内存优化(大汇总——中)
-Recycle(回收), Review(检查) 请看:ANDROID内存优化(大汇总——全)
--
-
-
-
-写这篇文章的目的就是想弄一个大汇总,将零散的内存知识点总结一下,如果有错误、不足或建议都希望告诉我。
-
-
-
-
-
-参考文章:
-解析Android开发优化之:软引用与弱引用的应用(http://www.jb51.net/article/36627.htm)Android研究院之应用开发线程池的经典使用(http://www.xuanyusong.com/archives/2439)
-
-
-
-内存泄露可以引发很多的问题:
--1.程序卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC)
--2.莫名消失(当你的程序所占内存越大,它在后台的时候就越可能被干掉。反之内存占用越小,在后台存在的时间就越长)
--3.直接崩溃(OutOfMemoryError)
-
-
-
-ANDROID内存面临的问题:
--
--1.有限的堆内存,原始只有16M
--2.内存大小消耗等根据设备,操作系统等级,屏幕尺寸的不同而不同
--3.程序不能直接控制
--4.支持后台多任务处理(multitasking)
--5.运行在虚拟机之上
- --5R:
--本文主要通过如下的5R方法来对ANDROID内存进行优化:
-
-
-
-1.Reckon(计算)
--首先需要知道你的app所消耗内存的情况,知己知彼才能百战不殆
--2.Reduce(减少)
--消耗更少的资源
--
--3.Reuse(重用)
--当第一次使用完以后,尽量给其他的使用
--5.Recycle(回收)
--回收资源
-
-4.Review(检查)
-
-回顾检查你的程序,看看设计或代码有什么不合理的地方。
-
-
-
-
-
- -Recycle(回收),回收可以说是在内存使用中最重要的部分。因为内存空间有限,无论你如何优化,如何节省内存总有用完的时候。而回收的意义就在于去清理和释放那些已经闲置,废弃不再使用的内存资源和内存空间。
--因为在Java中有垃圾回收(GC)机制,所以我们平时都不会太关注它,下面就来简单的介绍一下回收机制:
-
-
-
-
-
-垃圾回收(GC):
-
-
-
-Java垃圾回收器:
-
-在C,C++或其他程序设计语言中,资源或内存都必须由程序员自行声明产生和回收,否则其中的资源将消耗,造成资源的浪费甚至崩溃。但手工回收内存往往是一项复杂而艰巨的工作。
-
-于是,Java技术提供了一个系统级的线程,即垃圾收集器线程(Garbage Collection Thread),来跟踪每一块分配出去的内存空间,当Java 虚拟机(Java Virtual Machine)处于空闲循环时,垃圾收集器线程会自动检查每一快分配出去的内存空间,然后自动回收每一快可以回收的无用的内存块。
-
-
-
-作用:
--1.清除不用的对象来释放内存:
--采用一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。
--2.消除堆内存空间的碎片:
-
-由于创建对象和垃圾收集器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。
-
-
-
-垃圾回收器优点:
--1.减轻编程的负担,提高效率:
--使程序员从手工回收内存空间的繁重工作中解脱了出来,因为在没有垃圾收集机制的时候,可能要花许多时间来解决一个难懂的存储器问题。在用Java语言编程的时候,靠垃圾收集机制可大大缩短时间。
--2.它保护程序的完整性:
-
-因此垃圾收集是Java语言安全性策略的一个重要部份。
-
-
-
-垃圾回收器缺点:
--1.占用资源时间:
-
-Java虚拟机必须追踪运行程序中有用的对象,
- 而且最终释放没用的对象。这一个过程需要花费处理器的时间。
-
-2.不可预知:
-
-垃圾收集器线程虽然是作为低优先级的线程运行,但在系统可用内存量过低的时候,它可能会突发地执行来挽救内存资源。当然其执行与否也是不可预知的。
-
-3.不确定性:
--
-
-不能保证一个无用的对象一定会被垃圾收集器收集,也不能保证垃圾收集器在一段Java语言代码中一定会执行。
-
-同样也没有办法预知在一组均符合垃圾收集器收集标准的对象中,哪一个会被首先收集。
--4.不可操作
--
-垃圾收集器不可以被强制执行,但程序员可以通过调用System. gc方法来建议执行垃圾收集器。 -
-finalize():
-
-每一个对象都有一个finalize方法,这个方法是从Object类继承来的。
--当垃圾回收确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
-
-
-
-Java 技术允许使用finalize方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。一旦垃圾回收器准备好释放对象占用的空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
-简单的说finalize方法是在垃圾收集器删除对象之前对这个对象调用的
-
-System.gc():
-我们可以调用System.gc方法,建议虚拟机进行垃圾回收工作(注意,是建议,但虚拟机会不会这样干,我们也无法预知!)
-
-
-
-下面来看一个例子来了解finalize()和System.gc()的使用:
--
-public class TestGC {
- public TestGC() {}
-
- //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
- protected void finalize() {
- System.out.println("我已经被垃圾回收器回收了...");
- }
-
- public static void main(String [] args) {
- TestGC gc = new TestGC();
- gc = null;
- // 建议虚拟机进行垃圾回收工作
- System.gc();
- }
-}如上面的例子所示,大家可以猜猜重写的finalize方法会不会执行?
-
--答案是:不一定!
-
-
-
-因为无论是设置gc的引用为null还是调用System.gc()方法都只是"建议"垃圾回收器进行垃圾回收,但是最终所有权还在垃圾回收器手中,它会不会进行回收我们无法预知!
-
-垃圾回收面试题:
-最后通过网上找到的3道面试题来结束垃圾回收的内容。
-
--面试题一:
--
-1.fobj = new Object ( ) ; -2.fobj. Method ( ) ; -3.fobj = new Object ( ) ; -4.fobj. Method ( ) ;- -问:这段代码中,第几行的fobj 符合垃圾收集器的收集标准?
1.Object sobj = new Object ( ) ; -2.Object sobj = null ; -3.Object sobj = new Object ( ) ; -4.sobj = new Object ( ) ;问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准?
1.Object aobj = new Object ( ) ; -2.Object bobj = new Object ( ) ; -3.Object cobj = new Object ( ) ; -4.aobj = bobj; -5.aobj = cobj; -6.cobj = null; -7.aobj = null;问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准?
-
-
-
-
-资源的回收:
--刚才讲了一堆理论的东西,下面来点实际能用上的,资源的回收:
-
-
-
-Thread(线程)回收:
--线程中涉及的任何东西GC都不能回收(Anything reachable by a thread cannot be GC'd ),所以线程很容易造成内存泄露。
--如下面代码所示:
--
-Thread t = new Thread() {
- public void run() {
- while (true) {
- try {
- Thread.sleep(1000);
- System.out.println("thread is running...");
- } catch (InterruptedException e) {
-
- }
- }
- }
-};
-t.start();
-t = null;
-System.gc();如上在线程t中每间隔一秒输出一段话,然后将线程设置为null并且调用System.gc方法。
-
--最后的结果是线程并不会被回收,它会一直的运行下去。
-
-
-
-
--因为运行中的线程是称之为垃圾回收根(GC Roots)对象的一种,不会被垃圾回收。当垃圾回收器判断一个对象是否可达,总是使用垃圾回收根对象作为参考点。
-
-
-
-Cursor(游标)回收:
-
-
-Cursor是Android查询数据后得到的一个管理数据集合的类,在使用结束以后。应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android明显是倾向于编程者手动的将Cursor
-所以我们使用Cursor的方式一般如下:
--
- Cursor cursor = null;
- try {
- cursor = mContext.getContentResolver().query(uri,null, null,null,null);
- if(cursor != null) {
- cursor.moveToFirst();
- //do something
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }有一种情况下,我们不能直接将Cursor关闭掉,这就是在CursorAdapter中应用的情况,但是注意,CursorAdapter在Acivity结束时并没有自动的将Cursor关闭掉,因此,你需要在onDestroy函数中,手动关闭。@Override
-protected void onDestroy() {
- if (mAdapter != null && mAdapter.getCurosr() != null) {
- mAdapter.getCursor().close();
- }
- super.onDestroy();
-} -
-
-Receiver(接收器)回收
-
-
-@Override
-protected void onDestroy() {
- this.unregisterReceiver(receiver);
- super.onDestroy();
-}
--Stream/File(流/文件)回收:
--主要针对各种流,文件资源等等如:
-
-InputStream/OutputStream,SQLiteOpenHelper,SQLiteDatabase,Cursor,文件,I/O,Bitmap图片等操作等都应该记得显示关闭。
-和之前介绍的Cursor道理类似,就不多说了。
-
-
-
-
-
-
-
-
-
-Review:
-
-
-
-Review(回顾,检查),大家都知道Code Review的重要性。而这里我说的Review和Code Review差不多,主要目的就是检查代码中存在的不合理和可以改进的地方,当然这个Review需要大家自己来做啦。
-
-
-
-
-
-Code Review(代码检查):
-
-Code Review主要检查代码中存在的一些不合理或可以改进优化的地方,大家可以参考之前写的Reduce,Reuse和Recycle都是侧重讲解这方面的。
-
-
-
-
-
-
-
-UI Review(视图检查):
--Android对于视图中控件的布局渲染等会消耗很多的资源和内存,所以这部分也是我们需要注意的。
-
-
-
-
-
-如上图大家可以看到,hierarchyviewer可以非常清楚的看到当前视图的层级结构,并且可以查看视图的执行效率(视图上的小圆点,绿色表示流畅,黄色和红色次之),所以我们可以很方便的查看哪些view可能会影响我们的性能从而去进一步优化它。
-
-
-
-hierarchyviewer还提供另外一种列表式的查看方式,可以查看详细的屏幕画面,具体到像素级别的问题都可以通过它发现。
-
-
-
-
--ViewStub标签
--此标签可以使UI在特殊情况下,直观效果类似于设置View的不可见性,但是其更大的意义在于被这个标签所包裹的Views在默认状态下不会占用任何内存空间。
--
-
-
-
-include标签
-
-可以通过这个标签直接加载外部的xml到当前结构中,是复用UI资源的常用标签。
-
-
-
-merge标签
-
-它在优化UI结构时起到很重要的作用。目的是通过删减多余或者额外的层级,从而优化整个Android Layout的结构。
-
-
-
-(注意:灵活运用以上3个标签可以有效减少视图层级,具体使用大家可以上网搜搜)
-
-
-
-
--布局用Java代码比写在XML中快
--一般情况下对于Android程序布局往往使用XML文件来编写,这样可以提高开发效率,但是考虑到代码的安全性以及执行效率,可以通过Java代码执行创建,虽然Android编译过的XML是二进制的,但是加载XML解析器的效率对于资源占用还是比较大的,Java处理效率比XML快得多,但是对于一个复杂界面的编写,可能需要一些套嵌考虑,如果你思维灵活的话,使用Java代码来布局你的Android应用程序是一个更好的方法。
--重用系统资源:
--
-1. 利用系统定义的id
-比如我们有一个定义ListView的xml文件,一般的,我们会写类似下面的代码片段。
--
-<ListView - android:id="@+id/mylist" - android:layout_width="fill_parent" - android:layout_height="fill_parent"/>- -
这里我们定义了一个ListView,定义它的id是"@+id/mylist"。实际上,如果没有特别的需求,就可以利用系统定义的id,类似下面的样子。
- -<ListView - android:id="@android:id/list" - android:layout_width="fill_parent" - android:layout_height="fill_parent"/>在xml文件中引用系统的id,只需要加上“@android:”前缀即可。如果是在Java代码中使用系统资源,和使用自己的资源基本上是一样的。不同的是,需要使用android.R类来使用系统的资源,而不是使用应用程序指定的R类。这里如果要获取ListView可以使用android.R.id.list来获取。 - -
2. 利用系统的图片资源
-
-这样做的好处,一个是美工不需要重复的做一份已有的图片了,可以节约不少工时;另一个是能保证我们的应用程序的风格与系统一致。
-
3. 利用系统的字符串资源
--如果使用系统的字符串,默认就已经支持多语言环境了。如上述代码,直接使用了@android:string/yes和@android:string/no,在简体中文环境下会显示“确定”和“取消”,在英文环境下会显示“OK”和“Cancel”。
-4. 利用系统的Style
-假设布局文件中有一个TextView,用来显示窗口的标题,使用中等大小字体。可以使用下面的代码片段来定义TextView的Style。
- -<TextView - android:id="@+id/title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="?android:attr/textAppearanceMedium" />其中android:textAppearance="?android:attr/textAppearanceMedium"就是使用系统的style。需要注意的是,使用系统的style,需要在想要使用的资源前面加“?android:”作为前缀,而不是“@android:”。
5. 利用系统的颜色定义
-
除了上述的各种系统资源以外,还可以使用系统定义好的颜色。在项目中最常用的,就是透明色的使用。
-
-
-android:background ="@android:color/transparent"-
-
除了上面介绍的以外还有很多其他Android系统本身自带的资源,它们在应用中都可以直接使用。具体的,可以进入android-sdk的相应文件夹中去查看。例如:可以进入$android-sdk$\platforms\android-8\data\res,里面的系统资源就一览无余了。
-开发者需要花一些时间去熟悉这些资源,特别是图片资源和各种Style资源,这样在开发过程中,能重用的尽量重用,而且有时候使用系统提供的效果可能会更好。
--
--其他小tips:
-1. 分辨率适配-ldpi,-mdpi, - -hdpi配置不同精度资源,系统会根据设备自适应,包括drawable, layout,style等不同资源。
--2.尽量使用dp(density independent pixel)开发,不用px(pixel)。
--3.多用wrap_content, match_parent
--4.永远不要使用AbsoluteLayout
--5.使用9patch(通过~/tools/draw9patch.bat启动应用程序),png格式
--6.将Acitivity中的Window的背景图设置为空。getWindow().setBackgroundDrawable(null);android的默认背景是不是为空。
--7.View中设置缓存属性.setDrawingCache为true。
-
-
-
-
--Desgin Review(设计检查):
--Desgin Review主要侧重检查一下程序的设计是否合理,包括框架的设计,界面的设计,逻辑的设计(其实这些东西开发之前就应该想好了)。
-
-
-
-框架设计:
--是否定义了自己的Activity和fragment等常用控件的基类去避免进行重复的工作
--是否有完善的异常处理机制,即使真的出现OOM也不会直接崩溃导致直接退出程序
-
-
-
-界面设计:
--
-1.在视图中加载你所需要的,而不是你所拥有。因为用户不可能同时看到所有东西。最典型的例子就是ListView中的滑动加载。
-2.如果数据特别大,此时应该暗示用户去点击加载,而不是直接加载。
-3.合理运用分屏,转屏等,它是个双刃剑,因为它即可以使程序更加美观功能更加完善,但也相应增加了资源开销。
-
-
逻辑设计:
-避免子类直接去控制父类中内容,可以使用监听等方式去解决
-
-
关于这三点由于我工作经验比较少,加上一时半会也想不出来多少,如果大家有建议希望可以留言,之后我给加进去。
- diff --git a/src/awk.md b/src/awk.md deleted file mode 100644 index 0fcdcb3..0000000 --- a/src/awk.md +++ /dev/null @@ -1,621 +0,0 @@ -Awk是什么
-Awk、sed与grep,俗称Linux下的三剑客,它们之前有很多相似点,但是同样也各有各的特色,相似的地方是它们都可以匹配文本,其中sed和awk还可以用于文本编辑,而grep则不具备这个功用。sed是一种非交互式且面向字符流的编辑器(a “non-interactive” stream-oriented editor),而awk则是一门模式匹配的编程语言,因为它的主要功能是用于匹配文本并处理,同时它有一些编程语言才有的语法,例如函数、分支循环语句、变量等等,当然比起我们常见的编程语言,Awk相对比较简单。
-使用Awk,我们可以做以下事情:
-● 将文本文件视为由字段和记录组成的文本数据库;
-● 在操作文本数据库的过程中能够使用变量;
-● 能够使用数学运算和字符串操作;
-● 能够使用常见的编程结构,例如条件分支与循环;
-● 能够格式化输出;
-● 能够自定义函数;
-● 能够在awk脚本中执行UNIX命令;
-● 能够处理UNIX命令的输出结果;
-装备以上功能,awk能够做得事情非常多。但千里之行,始于足下,我们首先从最基本的命令行语法开始,一步一步得走入awk的编程世界。
--
命令行语法
-同sed一样,awk的命令行语法也有两种形式:
-awk [-F ERE] [-v assignment] ... program [argument ...] -awk [-F ERE] -f progfile ... [-v assignment] ...[argument ...]-
这里的program类似sed中的script,因为我们一直强调awk是一门编程语言,所以将awk的脚本视为一段代码。而awk的脚本同样可以写到一个文件中,并通过-f参数指定,这一点和sed是一样的。program一般多个pattern和action序列组成,当读入的记录匹配pattern时,才会执行相应的action命令。这里有一点要注意,在第一种形式中,除去命令行选项外,program参数一定要位于第一个位置。
-Awk的输入被解析成多个记录(Record),默认情况下,记录的分隔符是\n,因此可以认为一行就是一个记录,记录的分隔符可以通过内置变量RS更改。当记录匹配某个pattern时,才会执行后续的action命令。
而每个记录由进一步地被分隔成多个字段(Field),默认情况下字段的分隔符是空白符,例如空格、制表符等等,也可以通过-F ERE选项或者内置变量FS更改。在awk中,可以通过$1,$2…来访问对应位置的字段,同时$0存放整个记录,这一点有点类似shell下的命令行位置参数。关于这些内容,我们会在下面详细介绍,这里你只要知道有这些东西就好。
标准的awk命令行参数主要由以下三个:
-● -F ERE:定义字段分隔符,该选项的值可以是扩展的正则表达式(ERE);
● -f progfile:指定awk脚本,可以同时指定多个脚本,它们会按照在命令行中出现的顺序连接在一起;
● -v assignment:定义awk变量,形式同awk中的变量赋值,即name=value,赋值发生在awk处理文本之前;
为了便于理解,这里举几个简单的例子。通过-F参数设置冒号:为分隔符,并打印各个字段:
-[kodango@devops ~]$ echo "1:2:3" | awk -F: '{print $1 " and " $2 " and " $3}'
-1 and 2 and 3
-在awk的脚本中访问通过-v选项设置的变量:
-[kodango@devops ~]$ echo | awk -v a=1 'BEGIN {print a}'
-1
-从上面可以看到,通过-v选项设置的变量在BEGIN的位置就可以访问了。BEGIN是一个特殊的pattern,它在awk处理输入之前就会执行,可以认为是一个初始化语句,与此对应的还有END。
好像还没介绍如何指定处理的文件,是不是最后的argument就是指定的文件?在看我这本书之前,我也是这样认为的,但是实际上arguemnt有两种形式,它们分别是输入文件(file)和变量赋值(assignment)。
-awk可以同时指定多个输入文件,如果输入文件的文件名为’-',表示从标准输入读取内容。
-变量赋值类似-v选项,它的形式为name=value。awk中的变量名同一般的编程语言无太多区别,但是不能同awk的保留关键字重名,可以查看awk的man手册查询哪些是保留关键字。而变量值只有两种形式:字符串和数值。变量赋值必须位于脚本参数的后面,与文件名参数无先后顺序的要求,但是位于不同位置的赋值它的执行时机是不同的。
-我们用实际的例子来解释这个区别,假设有两个文件:a和b,它们的内容分别如下所示:
-[kodango@devops awk_temp]$ cat a -file a -[kodango@devops awk_temp]$ cat b -file b-
为了说明赋值操作发生的时机,我们在BEGIN,正常处理,END三个地方都打印变量的值。
-第一种情况: 变量赋值位于所有文件名参数之前
-[kodango@devops awk_temp]$ awk 'BEGIN {print "BEGIN: " var} {print "PROCESS: " var} \
-END {print "END: " var }' var=1 a
-BEGIN:
-PROCESS: 1
-END: 1
-结果:赋值操作发生在正常处理之前,BEGIN动作之后。
第二种情况:变量赋值位于所有文件名之后:
-[kodango@devops awk_temp]$ awk 'BEGIN {print "BEGIN: " var} {print "PROCESS: " var} \
-END {print "END: " var }' a var=1
-BEGIN:
-PROCESS:
-END: 1
-结果:赋值操作发生在正常处理之后,END动作之前。
第三种情况:变量赋值位于文件名之间:
-[kodango@devops awk_temp]$ awk 'BEGIN {print "BEGIN: " var} {print "PROCESS: " var} \
-END {print "END: " var }' a var=1 b
-BEGIN:
-PROCESS:
-PROCESS: 1
-END: 1
-结果:赋值操作发生在处理前面的文件之后,并且位于处理后面的文件之前;
-总结如下:
-1. 如果变量赋值在第一个文件参数之前,在BEGIN动作之后执行,影响到正常处理和END动作;
2. 如果变量赋值在最后一个文件参数之后,在END动作之前执行,仅影响END动作;
3. 如果文件参数不存在,情况同1所述;
-4. 如果变量赋值位于多个文件参数之间,在变量赋值前面的文件被处理后执行,影响到后续文件的处理和END动作;
所以变量赋值一定要考虑清楚用途,否则比较容易出错,不过一般情况下也不会用到变量赋值。
-自然地大家会将变量赋值与-v assignment选项进行比较,赋值的形式是一致的,但是-v选项的执行时机比变量赋值要早:
-[kodango@devops awk_temp]$ echo 1 | awk -v var=a 'BEGIN {print "BEGIN: " var}'
-BEGIN: a
-可见,-v选项的赋值操作在BEGIN动作之前就执行了。
变量赋值一定要小心不要与保留关键字重名,否则会报错:
-[kodango@devops awk_temp]$ echo 1 | awk -v BEGIN=1 'BEGIN {print "BEGIN: " BEGIN}'
-awk: fatal: cannot use gawk builtin `BEGIN' as variable name
--
记录(Record)与字段(Field)
-对于数据库来说,一个数据库表是由多条记录组成的,每一行表示一条记录(Record)。每条记录由多列组成,每一列表示一个字段(Field)。Awk将一个文本文件视为一个文本数据库,因此它也有记录和字段的概念。默认情况下,记录的分隔符是回车,字段的分隔符是空白符,所以文本文件的每一行表示一个记录,而每一行中的内容被空白分隔成多个字段。利用字段和记录,awk就可以非常灵活地处理文件的内容。
-可以通过-F选项来修改默认的字段分隔符,例如/etc/passwd的每一行都是由冒号分隔成多个字段的,所以这里就需要将分隔符设置成冒号:
-[kodango@devops awk_temp]$ awk -F: '{print $1}' /etc/passwd | head -3
-root
-bin
-daemon
-这里通过$1引用第一人字段,类似地$2表示第二个字段,$3表示第三个字段…. $0则表示整个记录。内置变量NF记录着字段的个数,所以$NF表示最后一个字段:
-[kodango@devops awk_temp]$ awk -F: '{print $NF}' /etc/passwd | head -3
-/bin/bash
-/bin/false
-/bin/false
-当然,$(NF-1)表示倒数第二个。
-内置变量FS也可以用于更改字段分隔符,它记录着当前的字段分隔符:
-[kodango@devops awk_temp]$ awk -F: '{print FS}' /etc/passwd | head -1
-:
-[kodango@devops awk_temp]$ awk -v FS=: '{print $1}' /etc/passwd | head -1
-root
-记录的分隔符可以通过内置变量RS更改:
-[kodango@devops awk_temp]$ awk -v RS=: '{print $0}' /etc/passwd | head -1
-root
-如果将RS设置成空,行为有就一点怪异了,它会将连续不为空行的所有行(一个段落)当作一个记录,而且强制回车为字段分隔符:
-[kodango@devops awk_temp]$ cat awk_man.txt
-
-The awk utility shall execute programs written in the awk programming language,
-which is specialized for textual data manipulation. An awk program is a sequence
-of patterns and corresponding actions. When input is read that matches a
-pattern, the action associated with that pattern is carried out.
-
-Input shall be interpreted as a sequence of records. By default, a record is a line,
-less its terminating <newline>, but this can be changed by using the RS built-in
-variable. Each record of input shall be matched in turn against each pattern in the
-program. For each pattern matched, the associated action shall be executed.
-
-[kodango@devops awk_temp]$ awk 'BEGIN {RS="";FS=":"} {print "First line: " $1}' awk_man.txt
-First line: The awk utility shall execute programs written in the awk programming language,
-First line: Input shall be interpreted as a sequence of records. By default, a record is a line,
-这里,我们将变量赋值放到BEGIN动作中执行,因为BEGIN动作是在文件处理之前执行的,专门用于放初始化的语句。FS的赋值在这里是无效的,awk依然使用回车符来分隔字段。
-
脚本(Script)组成
-命令行中的program部分,可以称为awk代码,也可以称为awk脚本。一段awk脚本是由多个’pattern { action }‘序列组成的。action是一个或者多个语句,它在输入行匹配pattern的时候被执行。如果pattern为空,表明这个action会在每一行处理时都会被执行。下面的例子简单地打印文件的每一行,这里不带任何参数的print语句打印的是整个记录,类似’print $0‘:
[kodango@devops awk_temp]$ echo -e 'line1\nline2' | awk '{print}'
-line1
-line2
-除了,还可以在脚本中定义自定义的函数,函数定义格式如下所示:pattern { action }
function name(parameter list) { statements }
-函数的参数列表用逗号分隔,参数默认是局部变量,无法在函数之外访问,而在函数中定义的变量为全局变量,可以在函数之外访问,如:
-[kodango@devops awk_temp]$ echo line1 | awk '
-function t(a) {
- b=a;
- print a;
-}
-
-{
- print b;
- t("kodango.me");
- print b;
-}'
-
-kodango.me
-kodango.me
-Awk脚本中的语句使用空行或者分号分隔,使用分号可以放在同一行,不过有时候会影响可读性,尤其是分支或循环结构中,很容易出错。
-如果Awk中的一个语句太长,要分成多行,可以在行为使用反斜杠’\':
-[kodango@devops awk_temp]$ cat test.awk
-
-function t(a)
-{
- b=a
- print "This is a very long line, so use backslash to escape the newline \
-then we will print the variable a: a=" a
-}
-
-{ print b; t("kodango.me"); print b;}
-[kodango@devops awk_temp]$ echo 1 | awk -f test.awk
-
-This is a very long line, so use backslash to escape the newline then we will print the variable a: a=kodango.me
-kodango.me
-这里我们将脚本写到文件中,并通过-f参数来指定。但是,在一些特殊符号之后,是可以直接换行的,例如”, { && ||”。
--
模式(Pattern)
-模式是awk中比较重要的一部分,它有以下几种情况:
-● /regular expression/: 扩展的正则表达式(Extended Regular Expression), 关于ERE可以参考这篇文章;
● relational expression: 关系表达式,例如大于、小于、等于,关系表达式结果为true表示匹配;
● BEGIN: 特殊的模式,在第一个记录处理之前被执行,常用于初始化语句的执行;
● END: 特殊的模式,在最后一个记录处理之前被执行,常用于输出汇总信息;
● pattern, pattern:模式对,匹配两者之间的所有记录,类似sed的地址对;
例如查找匹配数字3的行:
-[kodango@devops awk_temp]$ seq 1 20 | awk '/3/ {print}'
-3
-13
-相反地,可以在在正则表达式之前加上’!'表示不匹配:
-[kodango@devops awk_temp]$ seq 1 5 | awk '!/3/ {print}'
-1
-2
-4
-5
-除了BEGIN和END这两个特殊的模式外,其余的模式都可以使用’&&’或者’||’运算符组合,前者表示逻辑与,后者表示逻辑或:
[kodango@devops awk_temp]$ seq 1 50 | awk '/3/ && /1/ {print}'
-13
-31
-前面的正则都是整行匹配,有时候仅仅需要匹配某个字符,这样我们可以用表达式$n ~ /ere/:
[kodango@devops ~]$ awk '$1 ~ /ko/ {print}' /etc/passwd
-kodango:x:1000:1000::/home/kodango:/bin/bash
-有时候我们只想显示特定和行,例如显示第一行:
-[kodango@devops ~]$ seq 1 5 | awk 'NR==1 {print}'
-1
--
正则表达式(Regular Expression)
-和sed篇一样,这里我不会详细介绍正则表达式。因为正则表达式的内容介绍起来太麻烦,还是推荐同学阅读现有的文章(如Linux/Unix工具与正则表达式的POSIX规范),里面对各个流派的正则表达式归纳地很清楚了。
--
表达式(Expressions)
-表达式可以由常量、变量、运算符和函数组成,常数和变量的值可以为字符串和数值。
-Awk中的变量有三种类型:用户定义的变量,内置变量和字段变量。其中,内置变量名都是大写的。变量并不非一定要被声明或者被初始化,未初始化的字符串变量的值为”",未初始化的数值变量的值为0。字段变量可以用$n来引用,n的取值范围为[0,NF]。n可以为一个变量,例如$NF代码最后一个字段,而$(NF-1)表示倒数第二个字段。
--
数组
-数组是一种特殊的变量,在awk中,比较特殊地是,数组的下标可以为数字或者字符串。数组的赋值很简单,下面将value赋值给数组下标为index的元素:
-array[index]=value-
可以用for..in..语法遍历数组元素,其中item是数组元素对应的下标:
-for (item in array)-
当然也可以在if分支判断中使用in操作符:
-if (item in array)-
一个完整的例子如下所示:
-[kodango@devops ~]$ echo "1 2 3" | awk '{
-for (i=0;i<NF;i++)
- a[i]=i;
-}
-
-END {
-print 3 in a
-for (i in a)
- printf "%s: %s\n", i, a[i];
-}'
-0
-0: 0
-1: 1
-2: 2
--
内置变量
-Awk在内部维护了许多内置变量,或者称为系统变量,例如之前提到的FS、RS等等。常见的内置变量如下表所示
| 变量名 | -描述 | -
|---|---|
| ARGC | -命令行参数的各个,即ARGV数组的长度 | -
| ARGV | -存放命令行参数 | -
| CONVFMT | -定义awk内部数值转换成字符串的格式,默认值为”%.6g” | -
| OFMT | -定义输出时数值转换成字符串的格式,默认值为”%.6g” | -
| ENVIRON | -存放系统环境变量的关联数组 | -
| FILENAME | -当前被处理的文件名 | -
| NR | -记录的总个数 | -
| FNR | -当前文件中的记录的总个数 | -
| FS | -字段分隔符,默认为空白 | -
| NF | -每个记录中字段的个数 | -
| RS | -记录的分隔符,默认为回车 | -
| OFS | -输出时字段的分隔符,默认为空白 | -
| ORS | -输出时记录的分隔符,默认为回车 | -
| RLENGTH | -被match函数匹配的子串长度 | -
| RSTART | -被match函数匹配的子串位于目标字符串的起始下标 | -
-
下面主要介绍几个比较难理解的内置变量:
-1. ARGV与ARGC
ARGV与ARGC的意思比较好理解,就像C语言main(int argc, char **argv)。ARGV数组的下标从0开始到ARGC-1,它存放的是命令行参数,并且排除命令行选项(例如-v/-f)以及program部分。因此事实上ARGV只是存储argument的部分,即文件名(file)以及命令行变量赋值两部分的内容。
通过下面的例子可以大概了解ARGC与ARGV的用法:
-[kodango@devops awk_temp]$ awk 'BEGIN {
-> for (i = 0; i < ARGC; i++)
-> print ARGV[i]
-> }' inventory-shipped BBS-list
-awk
-inventory-shipped
-BBS-list
-ARGV的用法不仅限于此,它是可以修改的,可以更改数组元素的值,可以增加数组元素或者删除数组元素。
a. 更改ARGV元素的值
假设我们有a, b两个文件,它们各有一行内容:file a和file b。现在利用ARGV,我们可以做到偷梁换柱:
-[kodango@devops awk_temp]$ awk 'BEGIN{ARGV[1]="b"} {print}' a
-file b
-这里要注意ARGV[1]="b"的引号不能缺少,否则ARGV[1]=b会将变量b的值赋值给ARGV[1]。
当awk处理完一个文件之后,它会从ARGV的下一个元素获取参数,如果是一个文件则继续处理,如果是一个变量赋值则执行赋值操作:
[kodango@devops awk_temp]$ awk 'BEGIN{ARGV[1]="var=1"} {print var}' a b
-1
-为什么这里只打印一次变量值呢?可以回头再看看上一篇中介绍变量赋值的内容。
-而当下一个元素为空时,则跳过不处理,这样可以避开处理某个文件:
-[kodango@devops awk_temp]$ awk 'BEGIN{ARGV[1]=""} {print}' a b
-file b
-上面的例子中a这个文件就被跳过了。
-而当下一个元素的值为”-”时,表明从标准输入读取内容:
-[kodango@devops awk_temp]$ awk 'BEGIN{ARGV[1]="-"} {print}' a b
-a
-a # --> 这里按下CTRL+D停止输入
-file b
-b. 删除ARGV元素
删除ARGV元素和将元素的值赋值为空的效果是一样的,它们都会跳转对某个参数的处理:
[kodango@devops awk_temp]$ awk 'BEGIN{delete ARGV[1]} {print}' a b
-file b
-删除数组元素可以用delete语句。
c. 增加ARGV元素
我第一次看到ARGV变量的时候就在想,能不能利用ARGV变量避免提供命令行参数,就像这样:
awk 'BEGIN{ARGV[1]="a";} {print}'
-但是事实上这样不行,awk会依然从标准输入中获取内容。下面的方法倒是可以,首先增加ARGC的值,再增加ARGV元素,我到现在也没搞懂这两者的区别:
[kodango@devops awk_temp]$ awk 'BEGIN{ARGC+=1;ARGV[1]="a"} {print}'
-file a
-2. CONVFMT与OFMT
Awk中允许数值到字符串相互转换,其中内置变量CONVFMT定义了awk内部数值到字符串转换的格式,它的默认值为”%.6g”:
[kodango@devops awk_temp]$ awk 'BEGIN {
- printf "CONVFMT=%s, num=%f, str=%s\n", CONVFMT, 12.11, 12.11
-}'
-CONVFMT=%.6g, num=12.110000, str=12.11
-通过更改CONVFMT,我们可以定义自己的转换格式:
[kodango@devops awk_temp]$ awk 'BEGIN {
- CONVFMT="%d";
- printf "CONVFMT=%s, num=%f, str=%s\n", CONVFMT, 12.11, 12.11
-}'
-CONVFMT=%d, num=12.110000, str=12
-与此对应地还有一个内置变量OFMT,它与CONVFMT的作用是类似的,只不过是影响输出的时候数字转换成字符串的格式:
[kodango@devops awk_temp]$ awk 'BEGIN { OFMT="%d";print 12.11 }'
-12
-3. ENVIRON
ENVIRON是一个存放系统环境变量的关联数组,它的下标是环境变量名称,值是相应环境变量的值。例如:
[kodango@devops awk_temp]$ awk 'BEGIN { print ENVIRON["USER"] }'
-kodango
-利用环境变量也可以将值传递给awk:
-[kodango@devops awk_temp]$ U=hello awk 'BEGIN { print ENVIRON["U"] }'
-hello
-可以利用for..in循环遍历ENVIRON数组:
[kodango@devops awk_temp]$ awk 'BEGIN {
-for (env in ENVIRON)
- printf "%s=%s\n", env, ENVIRON[env];
-}'
-4. RLENGTH与RSTART
RLENGTH与RSTART都是与match函数相关的,前者表示匹配的子串长度,后者表示匹配的子串位于目标字符串的起始下标。例如:
[kodango@devops ~]$ awk 'BEGIN {match("hello,world", /llo/); print RSTART,RLENGTH}'
-3 3
-关于match函数,我们会在以后介绍。
-
运算符
-表达式中必然少不了运算符,awk支持的运算符可以参见man手册中的“Expressions in awk”一小节内容:
-[kodango@devops awk_temp]$ man awk | grep "^ *Table: Expressions in" -A 42 | sed 's/^ *//' - Table: Expressions in Decreasing Precedence in awk - -Syntax Name Type of Result Associativity -( expr ) Grouping Type of expr N/A -$expr Field reference String N/A -++ lvalue Pre-increment Numeric N/A --- lvalue Pre-decrement Numeric N/A -lvalue ++ Post-increment Numeric N/A -lvalue -- Post-decrement Numeric N/A -expr ^ expr Exponentiation Numeric Right -! expr Logical not Numeric N/A - -+ expr Unary plus Numeric N/A -- expr Unary minus Numeric N/A -expr * expr Multiplication Numeric Left - -...以下省略...- -
语句(Statement)
-到目前为止,用得比较多的语句就是print,其它的还有printf、delete、break、continue、exit、next等等。这些语句与函数不同的是,它们不会使用带括号的参数,并且没有返回值。不过也有意外,比如printf就可以像函数一样的调用:
[kodango@devops awk_temp]$ echo 1 | awk '{printf("%s\n", "abc")}'
-abc
-break和continue语句,大家应该比较了解,分别用于跳出循环和跳到下一个循环。
delete用于删除数组中的某个元素,这个我们在上面介绍ARGV的时候也使用过。
exit的用法顾名思义,就是退出awk的处理,然后会执行END部分的内容:
[kodango@devops awk_temp]$ echo $'line1\nline2' | awk '{print;exit} END {print "exit.."}'
-line1
-exit..
-next语句类似sed的n命令,它会读取下一条记录,并重新回到脚本的最开始处执行:
[kodango@devops awk_temp]$ echo $'line1\nline2' | awk '{
-> print "Before next.."
-> print $0
-> next
-> print "After next.."
-> }'
-Before next..
-line1
-Before next..
-line2
-从上面可以看出next后面的print语句不会执行。
print与printf语句是使用最多的,它们将内容输出到标准输出。注意在print语句中,输出的变量之间带不带逗号是有区别的:
-[kodango@devops awk_temp]$ echo "1 2" | awk '{print $1, $2}'
-1 2
-[kodango@devops awk_temp]$ echo "1 2" | awk '{print $1 $2}'
-12
-print输出时,字段之间的分隔符可以由OFS重新定义:
-[kodango@devops awk_temp]$ echo "1 2" | awk '{OFS=";";print $1,$2}'
-1;2
-除此之外,print的输出还可以重定向到某个文件中或者某个命令:
-print items > output-file -print items >> output-file -print items | command-
假设有这一样一个文件,第一列是语句名称,第二列是对应的说明:
-[kodango@devops awk_temp]$ cat column.txt -statement|description -delete|delete item from an array -exit|exit from the awk process -next|read next input record and process-
现在我们要将两列的内容分别输出到statement.txt和description.txt两个文件中:
-[kodango@devops awk_temp]$ awk -F'|' '{
-> print $1 > "statement.txt";
-> print $2 > "description.txt"
-> }' column.txt
-[kodango@devops awk_temp]$ cat statement.txt
-statement
-delete
-exit
-next
-[kodango@devops awk_temp]$ cat description.txt
-description
-delete item from an array
-exit from the awk process
-read next input record and process
-下面是一个重定向到命令的例子,假设我们要对下面的文件进行排序:
-[kodango@devops awk_temp]$ cat num.list -1 -3 -2 -9 -5-
可以通过将print的内容重定向到”sort -n”命令:
-[kodango@devops awk_temp]$ awk '{print | "sort -n"}' num.list
-1
-2
-3
-5
-9
-printf命令的用法与print类似,也可以重定向到文件或者输出,只不过printf比print多了格式化字符串的功能。printf的语法也大多数语言包括bash的printf命令类似,这里就不多介绍了。
-awk的函数分成数学函数、字符串函数、I/O处理函数以及用户自定义的函数,其中用户自定义的函数我们在上一篇中也有简单的介绍,下面我们一一来介绍这几类函数。
--
数学函数
-awk中支持以下数学函数:
-● atan2(y,x):反正切函数;
● cos(x):余弦函数;
● sin(x):正弦函数;
● exp(x):以自然对数e为底指数函数;
● log(x):计算以e 为底的对数值;
● sqrt(x):绝对值函数;
● int(x):将数值转换成整数;
● rand():返回0到1的一个随机数值,不包含1;
● srand([expr]):设置随机种子,一般与rand函数配合使用,如果参数为空,默认使用当前时间为种子;
例如,我们使用rand()函数生成一个随机数值:
[kodango@devops awk_temp]$ awk 'BEGIN {print rand(),rand();}'
-0.237788 0.291066
-[kodango@devops awk_temp]$ awk 'BEGIN {print rand(),rand();}'
-0.237788 0.291066
-但是你会发现,每次awk执行都会生成同样的随机数,但是在一次执行过程中产生的随机数又是不同的。因为每次awk执行都使用了同样的种子,所以我们可以用srand()函数来设置种子:
[kodango@devops awk_temp]$ awk 'BEGIN {srand();print rand(),rand();}'
-0.171625 0.00692412
-[kodango@devops awk_temp]$ awk 'BEGIN {srand();print rand(),rand();}'
-0.43269 0.782984
-这样每次生成的随机数就不一样了。
-利用rand()函数我们也可以生成1到n的整数:
[kodango@devops awk_temp]$ awk '
-> function randint(n) { return int(n*rand()); }
-> BEGIN { srand(); print randint(10);
-> }'
-3
-字符串函数
-awk中包含大多数常见的字符串操作函数。
-1. sub(ere, repl[, in])
描述:简单地说,就是将in中匹配ere的部分替换成repl,返回值是替换的次数。如果in参数省略,默认使用$0。替换的动作会直接修改变量的值。
-下面是一个简单的替换的例子:
-[kodango@devops ~]$ echo "hello, world" | awk '{print sub(/ello/, "i"); print}'
-1
-hi, world
-在repl参数中&是一个元字符,它表示匹配的内容,例如:
-[kodango@devops ~]$ awk 'BEGIN {var="kodango"; sub(/kodango/, "hello, &", var); print var}'
-hello, kodango
-2. gsub(ere, repl[, in])
描述:同sub()函数功能类似,只不过是gsub()是全局替换,即替换所有匹配的内容。
3. index(s, t)
描述:返回字符串t在s中出现的位置,注意这里位置是从1开始计算的,如果没有找到则返回0。
-例如:
-[kodango@devops ~]$ awk 'BEGIN {print index("kodango", "o")}'
-2
-[kodango@devops ~]$ awk 'BEGIN {print index("kodango", "w")}'
-0
-4. length[([s])]
描述:返回字符串的长度,如果参数s没有指定,则默认使用$0作为参数。
-例如:
-[kodango@devops ~]$ awk 'BEGIN {print length('kodango');}'
-0
-[kodango@devops ~]$ echo "first line" | awk '{print length();}'
-10
-5. match(s, ere)
描述: 返回字符串s匹配ere的起始位置,如果不匹配则返回0。该函数会定义RSTART和RLENGTH两个内置变量。RSTART与返回值相同,RLENGTH记录匹配子串的长度,如果不匹配则为-1。
例如:
-[kodango@devops ~]$ awk 'BEGIN {
-print match("kodango", /dango/);
-printf "Matched at: %d, Matched substr length: %d\n", RSTART, RLENGTH;
-}'
-3
-Matched at: 3, Matched substr length: 5
-6. split(s, a[, fs])
描述:将字符串按照分隔符fs,分隔成多个部分,并存到数组a中。注意,存放的位置是从第1个数组元素开始的。如果fs为空,则默认使用FS分隔。函数返回值分隔的个数。
-例如:
-[kodango@devops ~]$ awk 'BEGIN {
-> split("1;2;3;4;5", arr, ";")
-> for (i in arr)
-> printf "arr[%d]=%d\n", i, arr[i];
-> }'
-arr[4]=4
-arr[5]=5
-arr[1]=1
-arr[2]=2
-arr[3]=3
-这里有一个奇怪的地方是for..in..输出的数组不是按顺序输出的,如果要按顺序输出可以用常规的for循环:
-[kodango@devops ~]$ awk 'BEGIN {
-> split("1;2;3;4;5", arr, ";")
-> for (i=0;^C
-[kodango@devops ~]$ awk 'BEGIN {
-> n=split("1;2;3;4;5", arr, ";")
-> for (i=1; i<=n; i++)
-> printf "arr[%d]=%d\n", i, arr[i];
-> }'
-arr[1]=1
-arr[2]=2
-arr[3]=3
-arr[4]=4
-arr[5]=5
-7. sprintf(fmt, expr, expr, ...)
描述:类似printf,只不过不会将格式化后的内容输出到标准输出,而是当作返回值返回。
-例如:
-[kodango@devops ~]$ awk 'BEGIN {
-> var=sprintf("%s=%s", "name", "value")
-> print var
-> }'
-name=value
-8. substr(s, m[, n])
描述:返回从位置m开始的,长度为n的子串,其中位置从1开始计算,如果未指定n或者n值大于剩余的字符个数,则子串一直到字符串末尾为止。
-例如:
-[kodango@devops ~]$ awk 'BEGIN { print substr("kodango", 2, 3); }'
-oda
-[kodango@devops ~]$ awk 'BEGIN { print substr("kodango", 2); }'
-odango
-9. tolower(s)
描述:将字符串转换成小写字符。
-例如:
-[kodango@devops ~]$ awk 'BEGIN {print tolower("KODANGO");}'
-kodango
-10. toupper(s)
描述:将字符串转换成大写字符。
-例如
-[kodango@devops ~]$ awk 'BEGIN {print tolower("kodango");}'
-KODANGO
--
I/O处理函数
-1. getline
getline的用法相对比较复杂,它有几种不同的形式。不过它的主要作用就是从输入中每次获取一行输入。
a. expression | getline [var]
这种形式将前面管道前命令输出的结果作为getline的输入,每次读取一行。如果后面跟有var,则将读取的内容保存到var变量中,否则会重新设置$0和NF。
例如,我们将上面的statement.txt文件的内容显示作为getline的输入:
[kodango@devops awk_temp]$ awk 'BEGIN { while("cat statement.txt" | getline var) print var}'
-statement
-delete
-exit
-next
-上面的例子中命令要用双引号,”cat statement.txt“,这一点同print/printf是一样的。
如果不加var,则直接写到$0中,注意NF值也会被更新:
[kodango@devops awk_temp]$ awk 'BEGIN { while("cat statement.txt" | getline) print $0,NF}'
-statement 1
-delete 1
-exit 1
-next 1
-b. getline [var]
第二种形式是直接使用getline,它会从处理的文件中读取输入。同样地,如果var没有,则会设置$0,并且这时候会更新NF, NR和FNR:
[kodango@devops awk_temp]$ awk '{
-> while (getline)
-> print NF, NR, FNR, $0;
-> }' statement.txt
-1 2 2 delete
-1 3 3 exit
-1 4 4 next
-c. getline [var] < expression
第三种形式从expression中重定向输入,与第一种方法类似,这里就不加赘述了。
-2. close
close函数可以用于关闭已经打开的文件或者管道,例如getline函数的第一种形式用到管道,我们可以用close函数把这个管道关闭,close函数的参数与管道的命令一致:
[kodango@devops awk_temp]$ awk 'BEGIN {
-while("cat statement.txt" | getline) {
- print $0;
- close("cat statement.txt");
-}}'
-statement
-statement
-statement
-statement
-statement
-但是每次读了一行后,关闭管道,然后重新打开又重新读取第一行就死循环了。所以要慎用,一般情况下也很少会用到close函数。
3. system
这个函数很简单,就是用于执行外部命令,例如:
-[kodango@devops awk_temp]$ awk 'BEGIN {system("uname -r");}'
-3.6.2-1-ARCH
--
结束语
-快速了解Awk系列的几篇文章相对比较粗糙,我是参考Awk的man手册以及《Sed & Awk》附录B总结而成的,但是应该可以让大家对awk有一个大致的了解,欢迎大家一起交流。
-- \ No newline at end of file diff --git a/src/c-globle-variable.md b/src/c-globle-variable.md deleted file mode 100644 index cb0df04..0000000 --- a/src/c-globle-variable.md +++ /dev/null @@ -1,330 +0,0 @@ -
作为一名程序员,如果说沉迷一门编程语言算作一种乐趣的话,那么与此同时反过来去黑一门编程语言就是这种乐趣的升华。今天我们就来黑一把C语言,好好展示一下这门经典语言令人抓狂的一面。
-我们知道,全局变量是C语言语法和语义中一个很重要的知识点,首先它的存在意义需要从三个不同角度去理解:对于程序员来说,它是一个记录内容的变量(variable);对于编译/链接器来说,它是一个需要解析的符号(symbol);对于计算机来说,它可能是具有地址的一块内存(memory)。其次是语法/语义:从作用域上看,带static关键字的全局变量范围只能限定在文件里,否则会外联到整个模块和项目中;从生存期来看,它是静态的,贯穿整个程序或模块运行期间(注意,正是跨单元访问和持续生存周期这两个特点使得全局变量往往成为一段受攻击代码的突破口,了解这一点十分重要);从空间分配上看,定义且初始化的全局变量在编译时在数据段(.data)分配空间,定义但未初始化的全局变量暂存(tentative definition)在.bss段,编译时自动清零,而仅仅是声明的全局变量只能算个符号,寄存在编译器的符号表内,不会分配空间,直到链接或者运行时再重定向到相应的地址上。
-我们将向您展现一下,非static限定全局变量在编译/链接以及程序运行时会发生哪些有趣的事情,顺便可以对C编译器/链接器的解析原理管中窥豹。以下示例对ANSI C和GNU C标准都有效,笔者的编译环境是Ubuntu下的GCC-4.4.3。
--
/* t.h */
-#ifndef _H_
-#define _H_
-int a;
-#endif
-
-/* foo.c */
-#include <stdio.h>
-#include "t.h"
-
-struct {
- char a;
- int b;
-} b = { 2, 4 };
-
-int main();
-
-void foo()
-{
- printf("foo:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
- \tsizeof(b)=%d\n\tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
- &a, &b, sizeof b, b.a, b.b, main);
-}
-
-/* main.c */
-#include <stdio.h>
-#include "t.h"
-
-int b;
-int c;
-
-int main()
-{
- foo();
- printf("main:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
- \t(&c)=0x%08x\n\tsize(b)=%d\n\tb=%d\n\tc=%d\n",
- &a, &b, &c, sizeof b, b, c);
- return 0;
-}
-
-Makefile如下:
--test: main.o foo.o - gcc -o test main.o foo.o - -main.o: main.c -foo.o: foo.c - -clean: - rm *.o test --
运行情况:
--foo: (&a)=0x0804a024 - (&b)=0x0804a014 - sizeof(b)=8 - b.a=2 - b.b=4 - main:0x080483e4 -main: (&a)=0x0804a024 - (&b)=0x0804a014 - (&c)=0x0804a028 - size(b)=4 - b=2 - c=0 --
这个项目里我们定义了四个全局变量,t.h头文件定义了一个整型a,main.c里定义了两个整型b和c并且未初始化,foo.c里定义了一个初始化了的结构体,还定义了一个main的函数指针变量。由于C语言每个源文件单独编译,所以t.h分别包含了两次,所以int a就被定义了两次。两个源文件里变量b和函数指针变量main被重复定义了,实际上可以看做代码段的地址。但编译器并未报错,只给出一条警告:
-/usr/bin/ld: Warning: size of symbol 'b' changed from 4 in main.o to 8 in foo.o-
运行程序发现,main.c打印中b大小是4个字节,而foo.c是8个字节,因为sizeof关键字是编译时决议,而源文件中对b类型定义不一样。但令人惊奇的是无论是在main.c还是foo.c中,a和b都是相同的地址,也就是说,a和b被定义了两次,b还是不同类型,但内存映像中只有一份拷贝。我们还看到,main.c中b的值居然就是foo.c中结构体第一个成员变量b.a的值,这证实了前面的推断——即便存在多次定义,内存中只有一份初始化的拷贝。另外在这里c是置身事外的一个独立变量。
-为何会这样呢?这涉及到C编译器对多重定义的全局符号的解析和链接。在编译阶段,编译器将全局符号信息隐含地编码在可重定位目标文件的符号表里。这里有个“强符号(strong)”和“弱符号(weak)”的概念——前者指的是定义并且初始化了的变量,比如foo.c里的结构体b,后者指的是未定义或者定义但未初始化的变量,比如main.c里的整型b和c,还有两个源文件都包含头文件里的a。当符号被多重定义时,GNU链接器(ld)使用以下规则决议:
-像上面这个例子中,全局变量a和b存在重复定义。如果我们将main.c中的b初始化赋值,那么就存在两个强符号而违反了规则一,编译器报错。如果满足规则二,则仅仅提出警告,实际运行时决议的是foo.c中的强符号。而变量a都是弱符号,所以只选择一个(按照目标文件链接时的顺序)。
-事实上,这种规则是C语言里的一个大坑,编译器对这种全局变量多重定义的“纵容”很可能会无端修改某个变量,导致程序不确定行为。如果你还没有意识到事态严重性,我再举个例子。
-/* foo.c */
-#include <stdio.h>;
-
-struct {
- int a;
- int b;
-} b = { 2, 4 };
-
-int main();
-
-void foo()
-{
- printf("foo:\t(&b)=0x%08x\n\tsizeof(b)=%d\n
- \tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
- &b, sizeof b, b.a, b.b, main);
-}
-
-/* main.c */
-#include <stdio.h>
-
-int b;
-int c;
-
-int main()
-{
- if (0 == fork()) {
- sleep(1);
- b = 1;
- printf("child:\tsleep(1)\n\t(&b):0x%08x\n
- \t(&c)=0x%08x\n\tsizeof(b)=%d\n\tset b=%d\n\tc=%d\n",
- &b, &c, sizeof b, b, c);
- foo();
- } else {
- foo();
- printf("parent:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
- \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n\twait child...\n",
- &b, &c, sizeof b, b, c);
- wait(-1);
- printf("parent:\tchild over\n\t(&b)=0x%08x\n
- \t(&c)=0x%08x\n\tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
- &b, &c, sizeof b, b, c);
- }
- return 0;
-}
-运行情况如下:
--foo: (&b)=0x0804a020 - sizeof(b)=8 - b.a=2 - b.b=4 - main:0x080484c8 -parent: (&b)=0x0804a020 - (&c)=0x0804a034 - sizeof(b)=4 - b=2 - c=0 - wait child... -child: sleep(1) - (&b):0x0804a020 - (&c)=0x0804a034 - sizeof(b)=4 - set b=1 - c=0 -foo: (&b)=0x0804a020 - sizeof(b)=8 - b.a=1 - b.b=4 - main:0x080484c8 -parent: child over - (&b)=0x0804a020 - (&c)=0x0804a034 - sizeof(b)=4 - b=2 - c=0 --
(说明一点,运行情况是直接输出到stdout的打印,笔者曾经将./test输出重定向到log中,结果发现打印的执行序列不一致,所以采用默认输出。)
-这是一个多进程环境,首先我们看到无论父进程还是子进程,main.c还是foo.c,全局变量b和c的地址仍然是一致的(当然只是个逻辑地址),而且对b的大小不同模块仍然有不同的决议。这里值得注意的是,我们在子进程中对变量b进行赋值动作,从此子进程本身包括foo()调用中,整型b以及结构体成员b.a的值都是1,而父进程中整型b和结构体成员b.a的值仍是2,但它们显示的逻辑地址仍是一致的。
-个人认为可以这样解释,fork创建新进程时,子进程获得了父进程上下文“镜像”(自然包括全局变量),虚拟地址相同但属于不同的进程空间,而且此时真正映射的物理地址中只有一份拷贝,所以b的值是相同的(都是2)。随后子进程对b改写,触发了操作系统的写时拷贝(copy on write)机制,这时物理内存中才产生真正的两份拷贝,分别映射到不同进程空间的虚拟地址上,但虚拟地址的值本身仍然不变,这对于应用程序来说是透明的,具有隐瞒性。
-还有一点值得注意,这个示例编译时没有出现第一个示例的警告,即对变量b的sizeof决议,笔者也不知道为什么,或许是GCC的一个bug?
-这个例子代码同上一个一致,只不过我们将foo.c做成一个静态链接库libfoo.a进行链接,这里只给出Makefile的改动。
--test: main.o foo.o - ar rcs libfoo.a foo.o - gcc -static -o test main.o libfoo.a - -main.o: main.c -foo.o: foo.c - -clean: - rm -f *.o test --
运行情况如下:
--foo: (&b)=0x080ca008 - sizeof(b)=8 - b.a=2 - b.b=4 - main:0x08048250 -parent: (&b)=0x080ca008 - (&c)=0x080cc084 - sizeof(b)=4 - b=2 - c=0 - wait child... -child: sleep(1) - (&b):0x080ca008 - (&c)=0x080cc084 - sizeof(b)=4 - set b=1 - c=0 -foo: (&b)=0x080ca008 - sizeof(b)=8 - b.a=1 - b.b=4 - main:0x08048250 -parent: child over - (&b)=0x080ca008 - (&c)=0x080cc084 - sizeof(b)=4 - b=2 - c=0 --
从这个例子看不出有啥差别,只不过使用静态链接后,全局变量加载的地址有所改变,b和c的地址之间似乎相隔更远了些。不过这次编译器倒是给出了变量b的sizeof决议警告。
-到此为止,有些人可能会对上面的例子嗤之以鼻,觉得这不过是列举了C语言的某些特性而已,算不上黑。有些人认为既然如此,对于一切全局变量要么用static限死,要么定义同时初始化,杜绝弱符号,以便在编译时报错检测出来。只要小心地使用,C语言还是很完美的嘛~对于抱这样想法的人,我只想说,请你在夜深人静的时候竖起耳朵仔细聆听,你很可能听到Dennis Richie在九泉之下邪恶的笑声——不,与其说是嘲笑,不如说是诅咒……
-/* foo.c */
-#include <stdio.h>
-
-const struct {
- int a;
- int b;
-} b = { 3, 3 };
-
-int main();
-
-void foo()
-{
- b.a = 4;
- b.b = 4;
- printf("foo:\t(&b)=0x%08x\n\tsizeof(b)=%d\n
- \tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
- &b, sizeof b, b.a, b.b, main);
-}
-
-/* t1.c */
-#include <stdio.h>
-
-int b = 1;
-int c = 1;
-
-int main()
-{
- int count = 5;
- while (count-- > 0) {
- t2();
- foo();
- printf("t1:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
- \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
- &b, &c, sizeof b, b, c);
- sleep(1);
- }
- return 0;
-}
-
-/* t2.c */
-#include <stdio.h>
-
-int b;
-int c;
-
-int t2()
-{
- printf("t2:\t(&b)=0x%08x\n\t(&c)=0x%08x\n
- \tsizeof(b)=%d\n\tb=%d\n\tc=%d\n",
- &b, &c, sizeof b, b, c);
- return 0;
-}
-Makefile脚本:
-export LD_LIBRARY_PATH:=. - -all: test - ./test - -test: t1.o t2.o - gcc -shared -fPIC -o libfoo.so foo.c - gcc -o test t1.o t2.o -L. -lfoo - -t1.o: t1.c -t2.o: t2.c - -.PHONY:clean -clean: - rm -f *.o *.so test* --
执行结果:
--./test -t2: (&b)=0x0804a01c - (&c)=0x0804a020 - sizeof(b)=4 - b=1 - c=1 -foo: (&b)=0x0804a01c - sizeof(b)=8 - b.a=4 - b.b=4 - main:0x08048564 -t1: (&b)=0x0804a01c - (&c)=0x0804a020 - sizeof(b)=4 - b=4 - c=4 -t2: (&b)=0x0804a01c - (&c)=0x0804a020 - sizeof(b)=4 - b=4 - c=4 -foo: (&b)=0x0804a01c - sizeof(b)=8 - b.a=4 - b.b=4 - main:0x08048564 -t1: (&b)=0x0804a01c - (&c)=0x0804a020 - sizeof(b)=4 - b=4 - c=4 - ...-
其实前面几个例子只是开胃小菜而已,真正的大坑终于出现了!而且这次编译器既没报错也没警告,但我们确实眼睁睁地看到作为main()中强符号的b被改写了,而且一旁的c也“躺枪”了。眼尖的读者发现,这次foo.c是作为动态链接库运行时加载的,当t1第一次调用t2时,libfoo.so还未加载,一旦调用了foo函数,b立马中弹,而且c的地址居然还相邻着b,这使得c一同中弹了。不过笔者有些无法解释这种行为的原因,有种说法是强符号的全局变量在数据段中是连续分布的(相应地弱符号暂存在.bss段或者符号表里),或许可以上报GNU的编译器开发小组。
-另外笔者尝试过将t1.c中的b和c定义前面加上const限定词,编译器仍然默认通过,但程序在main()中第一次调用foo()时触发了Segment fault异常导致奔溃,在foo.c里使用指针改写它也一样。推断这是GCC对const常量所在地址启用了类似操作系统写保护机制,但我无法确定早期版本的GCC是否会让这个const常量被改写而程序不会奔溃。
-至于volatile关键词之于全局变量,自测似乎没有影响。
-怎么样?看了最后一个例子是否有点“不明觉厉”呢?C语言在你心目中是否还是当初那个“纯洁”、“干净”、“行为一致”的姑娘呢?也许趁着你不注意的时候她会偷偷给你戴顶绿帽,这一切都是通过全局变量,特别在动态链接的环境下,就算全部定义成强符号仍然无法为编译器所察觉。而一些IT界“恐怖分子”也经常将恶意代码包装成全局变量注入到root权限下存在漏洞的操作序列中,就像著名的栈溢出攻击那样。某一天当你傻傻地看着一个程序出现未定义的行为却无法定位原因的时候,请不要忘记Richie大爷那来自九泉之下最深沉的“问候”~
-或许有些人会偷换概念,把这一切归咎于编译器和链接器身上,认为这同语言无关,但我要提醒你,正是编译/链接器的行为支撑了整个语言的语法和语义。你可以反过来思考一下为何C的胞弟C++推出“命名空间(namespace)”的概念,或者你可以使用其它高级语言,对于重定义的全局变量是否能通过编译这一关。
-所以请时刻谨记,C是一门很恐怖的语言!
-P.S.题外话写在最后。我无意挑起语言之争,只是就事论事地去“黑(hack)”一门语言而已,而且要黑就要黑得有理有力有层次,还要带点娱乐精神。其实黑一门语言并非什么尖端复杂的技术,个人觉得起码要做到两点:
-(全文完) \ No newline at end of file diff --git a/src/c1.md b/src/c1.md deleted file mode 100644 index 90fd337..0000000 --- a/src/c1.md +++ /dev/null @@ -1,342 +0,0 @@ -# C进阶指南(1) # -
C语言可用于系统编程、嵌入式系统中,同时也是其他应用程序可能的实现工具之一。 当你对计算机编程怀有强烈兴趣的时候,却对C语言不感冒,这种可能性不大。想全方位地理解C语言是一件极具挑战性的事。
-Peter Fačka 在2014年1月份写下了这篇长文,内容包括:类型提升、内存分配,数组转指针、显式内联、打桩(interpositioning)和矢量变换。原文挺长,伯乐在线分三篇发出,这是第一篇。
--
多数C程序员以为,整型间的基本操作都是安全的。事实上,整型间基本操作也容易出现问题,例如下面的代码:
-int main(int argc, char** argv) {
- long i = -1;
-
- if (i < sizeof(i)) {
- printf("OK\n");
- }
- else {
- printf("error\n");
- }
-
- return 0;
-}
-上述代码中,变量 i 被转换为无符号整型。这样一来,它的值不再是-1,而是 size_t 的最大值。变量i的类型之所以被转换,是因为 sizeof 操作符的返回类型是无符号的。具体参见C99/C11标准之常用算术转换一章:
-“If the operand that has unsigned integer type has rank greater or equal to the rank of the type of the other operand, then the operand with signed integer type is converted to the type of the operand with unsigned integer type.”
-若无符号整型类型的操作数的转换优先级不低于另一操作数,则有符号数转为无符号数的类型。
-C标准中,size_t 被定义为不低于16位的无符号整型。通常 size_t 完全对应于 long。这样一来,int 和 size_t 的大小至少相等,可基于上述准则,强转为无符号整型。
-(译者注:本人印象深刻的相关问题是“if(-1U > 0L)”在32、64位机器上的判断结果分别是什么,为什么;除long long外,long 类型在涉及兼容性的产品代码中应被禁用)
-这个故事给了我们一个关于整型大小可移植性的观念。C标准并未定义short、int、long、long long 的确切大小及其无符号形式。标准仅限定了它们的最小长度。以x86_64架构为例,long 在Linux环境中是64比特,但在64位Windows系统中是32比特。为了使代码更具移植性,常见的方法是使用C99的 stdint.h 文件中定义的、指定长度的特殊类型,包括 uint16_t、int32_t 等。此文件定义了三种整型类型:
-但不幸的是,仅依靠 stdint.h 并不能根除类型转换的困扰。C标准中“整型提升规则”中写道:
-若int的表达范围足以覆盖所有的基础类型,此值将被转换为int;否则将转为unsigned int。这就叫做整型提升。整型提升过程中,所有其他的类型保持不变。
-下述代码在32位平台中将返回65536,在16位平台上返回0:
-uint32_t sum()
-{
- uint16_t a = 65535;
- uint16_t b = 1;
- return a+b;
-}
-无论C语言实现中,是否把未修饰的char看做有符号的,整型提升都连同符号一起把值保留下来。
-如何实现char类型通常取决于硬件体系或操作系统,常由其平台的ABI(应用程序二进制接口)指定。如果你愿意自己尝试的话,char会被转为signed char,下述代码将打印出-128和-127,而不是128和129。x86架构中可用GCC的-funsigned-char参数切换到强制无符号提升。
-char c = 128;
-char d = 129;
-printf("%d,%d\n",c,d);
-使用malloc分配指定字节大小的、未初始化的内存对象。若入参值为0,其行为取决于操作系统实现,或者说,这是C和POSIX标准均未定义的行为。
-若请求的空间大小为0,则结果视具体实现而定:返回值可以是空指针或特殊指针。
-malloc(0) 通常返回有效的特殊指针。或者返回的值可成为 free 函数的参数,且函数不会错误退出。例如 free 函数对NULL指针不做任何操作。
-因此,若空间大小参数是某个表达式的结果的话,要确保测试过整型溢出的情况。
-size_t computed_size;
-
-if (elem_size && num > SIZE_MAX / elem_size) {
- errno = ENOMEM;
- err(1, "overflow");
-}
-
-computed_size = elem_size*num;
-一般说来,要分配一个元素大小相同的序列,可考虑使用 calloc 而非用表达式计算大小。同时 calloc 将把分配的内存初始化为0。像往常一样使用 free 释放分配的内存。
-realloc 将改变已分配内存对象的大小。此函数返回一个指针,指针可能指向新的内存起始位置,内存大小取决于入参中请求的空间大小,内容不变。若新的空间更大,额外的空间未被初始化。若 realloc 入参中,指向旧对象的指针为NULL,并且大小非0,此行为等价于 malloc。若新的大小为0,且提供的指针非空,此时 realloc 的行为依赖于操作系统。
-多数实现将尝试释放对象内存,返回NULL或与malloc(0)相同的返回值。例如在Windows中,此操作会释放内存并返回NULL。OpenBSD也会释放内存,但返回的指针指向的空间大小为0。
-realloc 失败时会返回NULL,也因此断开与旧的内存对象的关联。所以不但要检查空间大小参数是否存在整型溢出,还要正确处理 realloc 失败时的对象大小。
-#include <stdio.h>
-#include <stdint.h>
-#include <malloc.h>
-#include <errno.h>
-
-#define VECTOR_OK 0
-#define VECTOR_NULL_ERROR 1
-#define VECTOR_SIZE_ERROR 2
-#define VECTOR_ALLOC_ERROR 3
-
-struct vector {
- int *data;
- size_t size;
-};
-
-int create_vector(struct vector *vc, size_t num) {
-
- if (vc == NULL) {
- return VECTOR_NULL_ERROR;
- }
-
- vc->data = 0;
- vc->size = 0;
-
- /* check for integer and SIZE_MAX overflow */
- if (num == 0 || SIZE_MAX / num < sizeof(int)) {
- errno = ENOMEM;
- return VECTOR_SIZE_ERROR;
- }
-
- vc->data = calloc(num, sizeof(int));
-
- /* calloc faild */
- if (vc->data == NULL) {
- return VECTOR_ALLOC_ERROR;
- }
-
- vc->size = num * sizeof(int);
- return VECTOR_OK;
-}
-
-int grow_vector(struct vector *vc) {
-
- void *newptr = 0;
- size_t newsize;
-
- if (vc == NULL) {
- return VECTOR_NULL_ERROR;
- }
-
- /* check for integer and SIZE_MAX overflow */
- if (vc->size == 0 || SIZE_MAX / 2 < vc->size) {
- errno = ENOMEM;
- return VECTOR_SIZE_ERROR;
- }
-
- newsize = vc->size * 2;
-
- newptr = realloc(vc->data, newsize);
-
- /* realloc faild; vector stays intact size was not changed */
- if (newptr == NULL) {
- return VECTOR_ALLOC_ERROR;
- }
-
- /* upon success; update new address and size */
- vc->data = newptr;
- vc->size = newsize;
- return VECTOR_OK;
-}
-一般避免动态内存分配问题的方法无非是尽可能把代码写得谨慎、有防御性。本文列举了一些常见问题和少量避免这些问题的方法。
-调用 free 可能导致此问题,此时入参指针可能为NULL(依照《C++ Primer Plus》,free(0)不会出现问题。译者注)、未使用 malloc 类函数分配的指针,或已经调用过 free / realloc(realloc参数中大小填0,可释放内存。译者注)的指针。考虑下列几点可让代码更健壮:
-char *ptr = NULL;
-
-/* ... */
-
-void nullfree(void **pptr) {
- void *ptr = *pptr;
- assert(ptr != NULL)
- free(ptr);
- *pptr = NULL;
-}
-代码中的检查规则应只用于NULL或有效的指针。对于去除指针和分配的动态内存间联系的函数或代码块,可在开头检查空指针。
-(孔乙己式译者注:你能说出strcpy / strncpy / strlcpy的区别么,能的话这节就不必看)
-访问内存对象边界之外的地方并不一定导致程序崩溃。程序可能使用损坏了的数据继续运行,其行为可能很危险,也可能是故意而为之,利用此越界操作来改变程序的行为,以此获取其他受限的数据,甚至注入可执行代码。 老套地人工检查数组和动态分配内存的边界是避免此类问题的主要方法。内存对象边界的相关信息必须人工跟踪。数组的大小可由sizeof操作符指出,但数组被转换为指针后,函数调用sizeof仅返回指针大小(视机器位数而定,译者注),而非原来的数组大小。
-C11标准中边界检查接口Annex K定义了一些新的库函数集合,这些函数可用于替换标准库(如字符串和I/O操作)常见部分,它们更安全、更易于使用。例如[the slibc library][slibc]都是上述函数的开源实现,但接口不被广泛采用。基于BSD(或基于Mac OS X)的系统提供了strlcpy、strlcat 函数来完成更好的字符串操作。其他系统可通过libbsd库调用它们。
-许多操作系统提供了通过内存区域间接控制受保护内存的接口,以防止意外读/写操作,入Posxi mprotect。类似的间接访问的保护机制常用于所有的内存页。
-内存泄露,常由于程序中未释放不再使用的动态分配的内存导致。因此,真正理解所需要的分配的内存对象的范围大小是很有必要的。更重要的是,要明白何时调用 free。但当程序复杂度增加时,要确定 free 的调用时机将变得更加困难。早期设计决策时,规划内存很重要。
-以下是处理内存泄露的技能表:
-想让内存管理保持简单,一个方法是在启动时在堆中分配所有所需的内存。程序结束时,释放内存的重任就交给了操作系统。这种方法在许多场景中的效果令人满意,特别是当程序在一个批量操作中完成对输入的处理的情况。
-如果你需要有着变长大小的临时存储,并且其生命周期在变量内部时,可考虑VLA(Variable Length Array,变长数组)。但这有个限制:每个函数的空间不能超过数百字节。因为C99指出边长数组能自动存储,它们像其他自动变量一样受限于同一作用域。即便标准未明确规定,VLA的实现都是把内存数据放到栈中。VLA的最大长度为SIZE_MAX字节。考虑到目标平台的栈大小,我们必须更加谨慎小心,以保证程序不会面临栈溢出、下个内存段的数据损坏的尴尬局面。
-这个技术的想法是对某个内存对象的每次引用、去引用计数。赋值时,计数器会增加;去引用时,计数器减少。当引用计数变为0时,这意味着此内存对象不再被使用,可以释放。因为C不提供自动析构(事实上,GCC和Clang都支持cleanup语言扩展), 也不是重写赋值运算符,引用计数由调用retain/release的函数手动完成。更好的方式,是把它作为程序的可变部分,能通过这部分获取和释放一个内存对象的拥有权。但是,使用这种方法需要很多(编程)规范来防止忘记调用release(停止内存泄露)或不必要地调用释放函数(这将导致内存释放地过早)。若内存对象的生命期需要外部事件指出,或应用程序的数据结构隐含了某个内存对象的持有权的处理,无论何种情况,都容易导致问题。下述代码块含有简化了的内存管理引用计数。
-#include <stdlib.h>
-#include <stdint.h>
-
-#define MAX_REF_OBJ 100
-#define RC_ERROR -1
-
-struct mem_obj_t{
- void *ptr;
- uint16_t count;
-};
-
-static struct mem_obj_t references[MAX_REF_OBJ];
-static uint16_t reference_count = 0;
-
-/* create memory object and return handle */
-uint16_t create(size_t size){
-
- if (reference_count >= MAX_REF_OBJ)
- return RC_ERROR;
-
- if (size){
- void *ptr = calloc(1, size);
-
- if (ptr != NULL){
- references[reference_count].ptr = ptr;
- references[reference_count].count = 0;
- return reference_count++;
- }
- }
-
- return RC_ERROR;
-}
-
-/* get memory object and increment reference counter */
-void* retain(uint16_t handle){
-
- if(handle < reference_count && handle >= 0){
- references[handle].count++;
- return references[handle].ptr;
- } else {
- return NULL;
- }
-}
-
-/* decrement reference counter */
-void release(uint16_t handle){
- printf("release\n");
-
- if(handle < reference_count && handle >= 0){
- struct mem_obj_t *object = &references[handle];
-
- if (object->count <= 1){
- printf("released\n");
- free(object->ptr);
- reference_count--;
- } else {
- printf("decremented\n");
- object->count--;
- }
- }
-}
-如果你关心编译器的兼容性,可用 cleanup 属性在C中模拟自动析构。
-void cleanup_release(void** pmem) {
- int i;
- for(i = 0; i < reference_count; i++) {
- if(references[i].ptr == *pmem)
- release(i);
- }
-}
-
-void usage() {
- int16_t ref = create(64);
-
- void *mem = retain(ref);
- __attribute__((cleanup(cleanup_release), mem));
-
- /* ... */
-}
-上述方案的另一缺陷是提供对象地址让 cleanup_release 释放,而非引用计数值。这样一来,cleanup_release 必须在 references 数组中做开销大的查找操作。一种解决办法是,改变填充的接口为返回一个指向 struct mem_obj_t 的指针。另一种办法是使用下面的宏集合,这些宏能够创建保存引用计数值的变量并追加 clean 属性。
-/* helper macros */
-#define __COMB(X,Y) X##Y
-#define COMB(X,Y) __COMB(X,Y)
-#define __CLEANUP_RELEASE __attribute__((cleanup(cleanup_release)))
-
-#define retain_auto(REF) retain(REF); int16_t __CLEANUP_RELEASE COMB(__ref,__LINE__) = REF
-
-void cleanup_release(int16_t* phd) {
- release(*phd);
-}
-
-void usage() {
- int16_t ref = create(64);
-
- void *mem = retain_auto(ref);
- /* ... */
-}
-(译者注:##符号源自C99,用于连接两个变量的名称,一般用在宏里。如int a##b就会定义一个叫做ab的变量;__LINE__指代码行号,类似的还有__FUNCTION__或__func__和__FILE__,可用于打印调试信息;__attribute__符号来自gcc,主要用于指导编译器优化,也提供了一些如构造、析构、字节对齐等功能)
-若一个程序经过数阶段才能彻底执行,每阶段的开头都分配有内存池,需要分配内存时,就使用内存池的一部分。内存池的选择,要考虑分配的内存对象的生命周期,以及对象在程序中所属的阶段。每个阶段一旦结束,整个内存池就要立即释放。这种方法在记录型运行程序中特别有用,例如守护进程,它可能随着时间减少内存分段。下述代码是个内存池内存管理的仿真:
-#include <stdlib.h>
-#include <stdint.h>
-
-struct pool_t{
- void *ptr;
- size_t size;
- size_t used;
-};
-
-/* create memory pool*/
-struct pool_t* create_pool(size_t size) {
- struct pool_t* pool = calloc(1, sizeof(struct pool_t));
-
- if(pool == NULL)
- return NULL;
-
- if (size) {
- void *mem = calloc(1, size);
-
- if (mem != NULL) {
- pool->ptr = mem;
- pool->size = size;
- pool->used = 0;
- return pool;
- }
- }
- return NULL;
-}
-
-/* allocate memory from memory pool */
-void* pool_alloc(struct pool_t* pool, size_t size) {
-
- if(pool == NULL)
- return NULL;
-
- size_t avail_size = pool->size - pool->used;
-
- if (size && size <= avail_size){
- void *mem = pool->ptr + pool->used;
- pool->used += size;
- return mem;
- }
-
- return NULL;
-}
-
-/* release memory for whole pool */
-void delete_pool(struct pool_t* pool) {
- if (pool != NULL) {
- free(pool->ptr);
- free(pool);
- }
-}
-内存池的实现涉及非常艰难的任务。可能一些现有的库能很好地满足你的需求:
- -把数据存到正确的数据结构里,能解决很多内存管理问题。而数据结构的选择,大多取决于算法,这些算法访问数据、把数据保存到例如链表、哈希表或树中。按算法选择数据结构有额外的好处,例如能够遍历数据结构一次就能释放数据。因为标准库并未提供对数据结构的支持,这里列出几个支持数据结构的库:
-处理内存问题的另一种方式,就是利用自动垃圾收集器的优势,自此从自己清除内存中解放出来。于引用计数中内存不再需要时清除机制相反,垃圾收集器在发生指定事件是被调用,如内存分配错误,或分配后超过了确切的阀值。标记清除算法是实现垃圾收集器的一种方式。此算法先为每个引用到分配内存的对象遍历堆,标记这些仍然可用的内存对象,然后清除未标记的内存对象。
-可能C中最有名的类似垃圾收集器的实现是Boehm-Demers-Weiser conservative garbage collector 。使用垃圾收集器的瑕疵可能是性能问题,或向程序引入非确定性的延缓。另一问题是,这可能导致库函数使用 malloc,这些库函数申请的内存不受垃圾处理器监管,必须手动释放。
-虽然实时环境无法接受不可预料的卡顿,仍有许多环境从中获取的好处远超过不足。从性能的角度看,甚至有性能提升。一些项目使用含有Mono项目GNU Objective C运行环境或Irssi IRC客户端的Boehm垃圾收集器。
------------------------- - -- [C进阶指南(2)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/c2.md) -- [C进阶指南(3)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/c3.md) \ No newline at end of file diff --git a/src/c2.md b/src/c2.md deleted file mode 100644 index 88ad0fa..0000000 --- a/src/c2.md +++ /dev/null @@ -1,168 +0,0 @@ -- [C进阶指南(1)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/c1.md) -- [C进阶指南(3)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/c3.md) - ------------------------------ -尽管在某些上下文中数组和指针可相互替换,但在编译器看来二者完全不同,并且在运行时所表达的含义也不同。
-当我们说对象或表达式有类型的时候,我们通常想的是定位器值的类型,也叫做左值。当左值有完全non-const类型时,此类型不是数组类型(因为数组本质是内存的一部分,是个只读常量,译者注),我们称此左值为可修改左值,并且此变量是个值,当表达式放到赋值运算符左边的时候,它被赋值。若表达式在赋值运算符的右边,此变量不必被修改,变量成为了修改左值的的内容。若表达式有数组类型,则此表达式的值是个指向数组第一个元素的指针。
-上文描述了大多数场景下数组如何转为指针。在两种情形下,数组的值类型不被转换:当用在一元运算符 &(取地址)或 sizeof 时。参见C99/C11标准 6.3.2.1小节:
-(Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type “array of type” is converted to an expression with type “pointer to type” that points to the initial element of the array object and is not an lvalue.)
-除非它是sizeof或一元运算符&的操作数,再或者它是用于初始化数组的字符文本,否则有着“类型数组”类型的表达式被转换为“指向类型”类型的指针,此指针指向数组对象的首个元素且指针不是左值。
-由于数组没有可修改的左值,并且在绝大多数情况下,数组类型的表达式的值被转为指针,因此不可能用赋值运算符给数组变量赋值(即int a[10]; a = 1;是错的,译者注)。下面是一个小示例:
-short a[] = {1,2,3};
-short *pa;
-short (*px)[];
-
-void init(){
- pa = a;
- px = &a;
-
- printf("a:%p; pa:%p; px:%p\n", a, pa, px);
-
- printf("a[1]:%i; pa[1]:%i (*px)[1]:%i\n", a[1], pa[1], (*px)[1]);
-}
-(译者注:%i能识别输入的八进制和十六进制)
-a 是 int 型数组,pa 是指向 int 的指针,px 是个未完成的、指向数组的指针。a 赋值给 pa 前,它的值被转为一个指向数组开头的指针。右值表达式 &a 并非意味着指向 int,而是一个指针,指向 int 型数组因为当使用一元符号&时右值不被转换为指针。
-表达式 a[1] 中下标的使用等价于 *(a+1),且服从如同 pa[1] 的指针算术规则。但二者有一个重要区别。对于 a 是数组的情况,a 变量的实际内存地址用于获取指向第一个元素的指针。当对于 pa 是指针的情况,pa 的实际值并不用于定位。编译器必须注意到 a 和 pa见的类型区别,因此声明外部变量时,指明正确的类型很重要。
-int a[]; -int *pa;-
但在另外的编译单元使用下述声明是不正确的,将毁坏代码:
-extern int *a; -extern int pa[];-
某些类型数组变为指针的另一个场合在函数声明中。下述三个函数声明是等价的:
-void sum(int data[10]) {}
-
-void sum(int data[]) {}
-
-void sum(int *data) {}
-编译器应报告函数 sum 重定义相关错误,因为在编译器看来上述三个例子中的参数都是 int 型的。.
-多维数组是有点棘手的话题。首先,虽然用了“多维”这个词,C并不完全支持多维数组。数组的数组可能是更准确的描述。
-typedef int[4] vector;
-vector m[2] = {{1,2,3,4}, {4,5,6,7}};
-int n[2][4] = {{1,2,3,4}, {4,5,6,7}};
-变量 m 是长度为2的 vector 类型,vector 是长为4的 int 型数组。除了存储的内存位置不同外,数组 n 与 m 是相同的。从内存的角度讲,两个数组都如同括号内展示的内容那样,排布在连续的内存区域。访问到的和声明的完全一致。
-int *p = n[1]; -int y = p[2];-
通过使用下标符号 n[1],我们获取到了每个元素大小为4字节的整型数组。因为我们要定位数组的第二个元素, 其位置在多维数组中是数组开始偏移四倍的整型大小。我们知道,在这个表达式中整型数组被转为指向 int 的指针,然后存为 p。然后 p[2] 将访问之前表达式产生的数组中的第三个元素。上面代码中的 y 等价于下面代码中的 z:
-int z = *(*(n+1)+2);-
也等价于我们初学C时写的表达式:
-int x = n[1][2];-
当把上文中的二维数组作为参数传输时,第一“维”数组会转为指针,指向再次阵列的数组的第一个元素。因此不需要指明第一维。剩余的维度需要明确指出其长度。否则下标将不能正确工作。当我们能够随心所欲地使用下述表格中的任一形式来定义函数接受数组时,我们总是被强制显式地定义最里面的(即维度最低的)数组的维度。
-void sum(int data[2][4]) {}
-
-void sum(int data[][4]) {}
-
-void sum(int (*data)[4]) {}
-为绕过这一限制,可以转换数组为指针,然后计算所需元素的偏移。
-void list(int *arr, int max_i, int max_j){
- int i,j;
-
- for(i=0; i<max_i; i++){
-
- for(j=0; j<max_j; j++){
- int x = arr[max_i*i+j];
- printf("%i, ", x);
- }
-
- printf("\n");
- }
-}
-另一种方法是main函数用以传输参数列表的方式。main函数接收二级指针而非二维数组。这种方法的缺陷是,必须建立不同的数据,或者转换为二级指针的形式。不过,好在它运行我们像以前一样使用下标符号,因为我们现在有了每个子数组的首地址。
-int main(int argc, char **argv){
- int arr1[4] = {1,2,3,4};
- int arr2[4] = {5,6,7,8};
-
- int *arr[] = {arr1, arr2};
-
- list(arr, 2, 4);
-}
-
-void list(int **arr, int max_i, int max_j){
- int i,j;
-
- for(i=0; i<max_i; i++){
-
- for(j=0; j<max_j; j++){
- int x = arr[i][j];
- printf("%i, ", x);
- }
-
- printf("\n");
- }
-}
-用字符串类型的话,初始化部分变得相当简单,因为它允许直接初始化指向字符串的指针。
-const char *strings[] = {
- "one",
- "two",
- "three"
-};
-但这有个陷阱,字符串实例被转换成指针,用 sizeof 操作符时会返回指针大小,而不是整个字符串文本所占空间。另一个重要区别是,若直接用指针修改字符串内容,则此行为是未定义的。
-假设你能使用变长数组,那就有了第三种传多维数组给函数的方法。使用前面定义的变量来指定最里面数组的维度,变量 arr 变为一个指针,指向未完成的int数组。
-void list(int max_i, int max_j, int arr[][max_j]){
- /* ... */
- int x = arr[1][3];
-}
-此方法对更高维度的数组仍然有效,因为第一维总是被转换为指向数组的指针。类似的规则同样作用于函数指示器。若函数指示器不是 sizeof 或一元操作符 & 的参数,它的值是一个指向函数的指针。这就是我们传回调函数时不需要 & 操作符的原因。
-static void catch_int(int no) {
- /* ... */
-};
-
-int main(){
- signal(SIGINT, catch_int);
-
- /* ... */
-}
-
-打桩是一种用定制的函数替换链接库函数且不需重新编译的技术。甚至可用此技术替换系统调用(更确切地说,库函数包装系统调用)。可能的应用是沙盒、调试或性能优化库。为演示过程,此处给出一个简单库,以记录GNU/Linux中 malloc 调用次数。
-/* _GNU_SOURCE is needed for RTLD_NEXT, GCC will not define it by default */
-#define _GNU_SOURCE
-#include <stdio.h>
-#include <stdlib.h>
-#include <dlfcn.h>
-#include <stdint.h>
-#include <inttypes.h>
-
-static uint32_t malloc_count = 0;
-static uint64_t total = 0;
-
-void summary(){
- fprintf(stderr, "malloc called: %u times\n", count);
- fprintf(stderr, "total allocated memory: %" PRIu64 " bytes\n", total);
-}
-
-void *malloc(size_t size){
- static void* (*real_malloc)(size_t) = NULL;
- void *ptr = 0;
-
- if(real_malloc == NULL){
- real_malloc = dlsym(RTLD_NEXT, "malloc");
- atexit(summary);
- }
-
- count++;
- total += size;
-
- return real_malloc(size);
-}
-打桩要在链接libc.so之前加载此库,这样我们的 malloc 实现就会在二进制文件执行时被链接。可通过设置 LD_PRELOAD 环境变量为我们想让链接器优先链接的全路径。这也能确保其他动态链接库的调用最终使用我们的 malloc 实现。因为我们的目标只是记录调用次数,不是真正地实现内存分配,所以我们仍需要调用“真正”的 malloc 。通过传递 RTLD_NEXT 伪处理程序到 dlsym,我们获得了指向下一个已加载的链接库中 malloc 事件的指针。第一次 malloc 调用 libc 的 malloc,当程序终止时,会调用由 atexit 注册的获取和 summary 函数。看GNU/Linxu中打桩行为(真的184次调用!):
-$ gcc -shared -ldl -fPIC malloc_counter.c -o /tmp/libmcnt.so -$ export LD_PRELOAD="/tmp/libstr.so" -$ ps - PID TTY TIME CMD - 2758 pts/2 00:00:00 bash - 4371 pts/2 00:00:00 ps -malloc called: 184 times -total allocated memory: 302599 bytes-
默认情况下,所有的非静态函数可被导出,所有可能仅定义有着与其他动态链接库函数甚至模板文件相同特征标的函数,就可能在无意中插入其它名称空间。为防止意外打桩、污染导出的函数名称空间,有效的做法是把每个函数声明为静态的,此函数在目标文件之外不能被使用。
-在共享库中,另一种控制导出的共享目标的方式是用编译器扩展。GCC 4.x和Clang都支持 visibility 属性和 -fvisibility 编译命令来对每个目标文件设置全局规则。其中 default 意味着不修改可见性,hidden 对可见性的影响与 static 限定符相同。此符号不会被放入动态符号表,其他共享目标或可执行文件看不到此符号。
-#if __GNUC__ >= 4 || __clang__
- #define EXPORT_SYMBOL __attribute__ ((visibility ("default")))
- #define LOCAL_SYMBOL __attribute__ ((visibility ("hidden")))
-#else
- #define EXPORT_SYMBOL
- #define LOCAL_SYMBOL
-#endif
-全局可见性由编译器参数指定,可通过设置 visibility 属性被本地覆盖。实际上,全局策略设置为 hidden,则所有符号会被默认为本地的,只有修饰 __attribute__ ((visibility (“default”))) 才将被导出。
\ No newline at end of file diff --git a/src/c3.md b/src/c3.md deleted file mode 100644 index bf3ebd3..0000000 --- a/src/c3.md +++ /dev/null @@ -1,318 +0,0 @@ -- [C进阶指南(1)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/c1.md) -- [C进阶指南(2)](https://github.com/LippiOuYang/practical-computer-skills/blob/master/src/c2.md) - - ----------- - -(想让)函数代码被直接集成到调用函数中,而非产生独立的函数目标和单个调用,可显式地使用 inline 限定符来指示编译器这么做。根据 section 6.7.4 of C standard inline 限定符仅建议编译器使得”调用要尽可能快”,并且“此建议是否有效由具体实现定义”
-要用内联函数优点的最简单方法是把函数定义为 static ,然后将定义放入头文件。
-/* middle.h */
-static inline int middle(int a, int b){
- return (b-a)/2;
-}
-独立的函数对象仍然可能被导出,但在翻译单元的外部它是不可见的。这种头文件被包含在多个翻译单元中,编译器可能为每个单元发射函数的多份拷贝。因此,有可能两个变量指向相同的函数名,指针的值可能不相等。
-另一种方法是,既提供外部可连接的版本,也提供内联版本,两个版本功能相同,让编译器决定使用哪个。这实际上是内嵌限定符的定义:
--If all of the file scope declarations for a function in a translation unit include the inline function specifier without extern, then the definition in that translation unit is an inline definition. An inline definition does not provide an external definition for the function, and does not forbid an external definition in another translation unit. An inline definition provides an alternative to an external definition, which a translator may use to implement any call to the function in the same translation unit. It is unspecified whether a call to the function uses the inline definition or the external definition.
-在一个翻译单元中,若某个函数在所有的文件范围内都包含不带extern的内联函数限定符,则此翻译单元中此函数定义是内联定义。内联定义不为函数提供外部的定义,也不禁止其他翻译单元的外部定义。内联定义为外部定义提供一个可选项,在同一翻译单元内翻译器可用它实现对函数的任意调用。调用函数时,使用内联定义或外联定义是不确定的。
(译者注:即gcc中的 extern inline,优先使用内联版本,允许外部版本的存在)
-对于函数的两个版本,我们可以把下面的定义放在头文件中:
-/* middle.h */
-inline int middle(int a, int b){
- return (b-a)/2;
-}
-然后在具体的源文件中,用extern限定符发射翻译单元中外部可链接的版本:
-#include "middle.h" -extern int middle(int a, int b);-
GCC编译器的实现不同于上述译码方式。若函数由 inline 声明,GCC总是发射外部可链接的目标代码,并且程序中只存在一个这样的定义。若函数被声明为export inline的,GCC将永不为此函数发射外部可链接的目标代码。自GCC 4.3版本起,可使用-STD= c99的选项使能为内联定义使能C99规则。若C99的规则被启用,则定义GNUC_STDC_INLINE。之前描述的 static 使用方法不受GCC对内联函数解释的影响。如果你需要同时使用内联和外部可链接功能的函数,可考虑以下解决方案:
-/* global.h */ -#ifndef INLINE -# if __GNUC__ && !__GNUC_STDC_INLINE__ -# define INLINE extern inline -# else -# define INLINE inline -# endif -#endif-
头文件中有函数定义:
-/* middle.h */
-#include "global.h"
-INLINE int middle(int a, int b) {
- return (b-a)/2;
-}
-在某个具体实现的源文件中:
-#define INLINE -#include "middle.h-
若要对函数强制执行内联,GCC和Clang编译器都可用 always_inline 属性达成此目的。下面的例子中,独立的函数对象从未被发射。
-/* cdefs.h */
-# define __always_inline inline __attribute__((always_inline))
-
-/* middle.h */
-#include <cdefs.h>
-static __always_inline int middle(int a, int b) {
- return (b-a)/2;
-}
-一旦编译器内联失败,编译将因错误而终止。例如 Linux kernel 就使用这种方法。可在 cdefs.h 中上述代码中使用的 __always_inline 。
--
许多微处理器(特别是x86架构的)提供单指令多数据(SIMD)指令集来使能矢量操作。例如下面的代码:
-#include <stdint.h>
-#include <string.h>
-#define SIZE 8
-int16_t a[SIZE], b[SIZE];
-
-void addtwo(){
- int16_t i = 0;
-
- while (i < SIZE) {
- a[i] = b[i] + 2;
- i++;
- }
-}
-
-int main(){
- addtwo();
- return a[0];
-}
-addtwo 中的循环迭代 8 次,每次往数组 b 上加 2,数组 b 每个元素是 16 位的有符号整型。函数 addtwo 将被编译成下面的汇编代码:
-$ gcc -O2 auto.c -S -o auto_no.asm-
addtwo: -.LFB22: - .cfi_startproc - movl $0, %eax -.L2: - movzwl b(%rax), %edx - addl $2, %edx - movw %dx, a(%rax) - addq $2, %rax - cmpq $16, %rax - jne .L2 - rep - ret - .cfi_endproc-
起初,0 写入到 eax 寄存器。标签 L2 标着循环的开始。b 的首个元素由 movzwl 指令被装入的32位寄存器 edx 前16位。 edx寄存器的其余部分填 0。然后 addl 指令往 edx 寄存器中 a 的第一个元素的值加 2 并将结果存在 dx 寄存器中。累加结果从 dx(edx 寄存器的低16位)复制到 a 的第一个元素。最后,显然存放了步长为 2 (占2个字节 – 16位)的数组的 rax 寄存器与数组的总大小(以字节为单位)进行比较。如果 rax 不等于16,执行跳到 L2 ,否则会继续执行,函数返回。
-SSE2 指令集提供了能够一次性给 8 个 16 位整型做加法的指令 paddw。实际上,最现代化的编译器都能够自动使用如 paddw 之类的矢量指令优化代码。Clang 默认启用自动向量化。 GCC的编译器中可用 -ftree-vectorize 或 -O3 开关启用它。这样一来,向量指令优化后的 addtwo 函数汇编代码将会大有不同:
-$ gcc -O2 -msse -msse2 -ftree-vectorize -ftree-vectorizer-verbose=5 auto.c -S -o auto.asm-
addtwo: -.LFB22: - .cfi_startproc - movdqa .LC0(%rip), %xmm0 - paddw b(%rip), %xmm0 - movdqa %xmm0, a(%rip) - ret - .cfi_endproc - -;... - -.LC0: - .value 2 - .value 2 - .value 2 - .value 2 - .value 2 - .value 2 - .value 2 - .value 2-
最显着的区别在于循环处理消失了。首先,8 个 16 位值为 2 整数被标记为 LC0,由 movdqa 加载到 xmm0 寄存器。然后paddw 把 b 的每个 16 位的元素分别加到 xmm0 中的多个数值 2上。结果写回到 a,函数可以返回。指令 movqda 只能用在由16个字节对齐的内存对象上。这表明编译器能够对齐两个数组的内存地址以提高效率。
-数组的大小不必一定只是 8 个元素,但它必须以 16 字节对齐(需要的话,填充),因此也可以用 128 位向量。用内联函数也可能是一个好主意,特别是当数组作为参数传递的时候。因为数组被转换为指针,指针地址需要16字节对齐。如果函数是内联的,编译器也许能减少额外的对齐开销。
-#include <stdint.h>
-
-void __always_inline addtwo(int16_t* a, int16_t *b, int16_t size){
- int16_t i;
-
- for (i = 0; i < size; i++) {
- a[i] = b[i] + 2;
- }
-}
-
-int main(){
- const int16_t size = 1024;
- int16_t a[size], b[size];
-
- addtwo(a, b, size);
- return a[0];
-}
-循环迭代 1024 次,每次把两个长度为 16 比特的有符号整型相加。使用矢量操作的话,上例中的循环总数可减少到 128。但这也可能自动完成,在GCC环境中,可用 vector_size 定义矢量数据类型,用这些数据和属性显式指导编译器使用矢量扩展操作。此处列举出 emmintrin.h 定义的采用 SSE 指令集的多种矢量数据类型。
-/* SSE2 */ -typedef double __v2df __attribute__ ((__vector_size__ (16))); -typedef long long __v2di __attribute__ ((__vector_size__ (16))); -typedef int __v4si __attribute__ ((__vector_size__ (16))); -typedef short __v8hi __attribute__ ((__vector_size__ (16))); -typedef char __v16qi __attribute__ ((__vector_size__ (16)));-
这是用 __v8hi 类型优化之前的示例代码后的样子:
-#include <stdint.h>
-#include <string.h>
-#include <emmintrin.h>
-
-static void __always_inline _addtwo(__v8hi *a, __v8hi *b, const int16_t sz){
- __v8hi c = {2,2,2,2,2,2,2,2};
-
- int16_t i;
- for (i = 0; i < sz; i++) {
- a[i] = b[i] + c;
- }
-}
-
-static void __always_inline addtwo(int16_t *a, int16_t *b, const int16_t sz){
- _addtwo((__v8hi *) a, (__v8hi *) b, sz/8);
-}
-
-int main(){
- const int16_t size = 1024;
- int16_t a[size], b[size];
- /* ... */
-
- addtwo(a, b, size);
- return a[0];
-}
-关键是把数据转到合适的类型(此例中为 __v8hi),然后由此调整其他的代码。优化的效果主要看操作类型和处理数据量的大小,可能不同情况的结果差异很大。下表是上例中 addtwo 函数被循环调用 1 亿次的执行时间:
-| Compiler | -Time | -
|---|---|
| gcc 4.5.4 O2 | -1m 5.3s | -
| gcc 4.5.4 O2 auto vectorized | -12.7s | -
| gcc 4.5.4 O2 manual | -8.9s | -
| gcc 4.7.3 O2 auto vectorized | -25.s | -
| gcc 4.7.3 O2 manual | -8.9s | -
| clang 3.3 O3 auto vectorized | -8.1s | -
| clang 3.3 O3 manual | -9.5s | -
Clang 编译器自动矢量化得更快,可能是因为用以测试的外部循环被优化的更好。慢一点的 GCC 4.7.3在内存对齐(见下文)方面效率稍低。
-int32_t i;
-for(i=0; i < 100000000; i++){
- addtwo(a, b, size);
-}
-GCC 和 Clang 编译器也提供了内建函数,用来显式地调用汇编指令。
-确切的内建函数跟编译器联系很大。x86 平台下,GCC 和 Clang 编译器都提供了带有定义的头文件,通过 x86intrin.h 匹配 Intel 编译器的内建函数(即 GCC 和 Clang 用 Intel 提供的头文件,调用 Intel 的内建函数。译者注)。下表是含特殊指令集的头文件:
-使用内建函数后,前面的例子可以改为:
-#include <stdint.h>
-#include <string.h>
-#include <emmintrin.h>
-
-static void __always_inline addtwo(int16_t *a, int16_t *b, int16_t size){
-
- int16_t i;
- __m128i c = _mm_set1_epi16(2);
-
- for (i = 0; i < size; i+=8) {
- __m128i bb = _mm_loadu_si128(b+i); // movqdu b+i -> xmm0
- __m128i r = _mm_add_epi16(bb, c); // paddw c + xmm0 -> xmm0
- _mm_storeu_si128(a+i, r); // movqdu xmm0 -> a+i
- }
-}
-
-int main(){
- const int16_t size = 1024;
- int16_t a[size], b[size];
- /* ... */
-
- addtwo(a, b, size);
- return a[0];
-}
-当编译器产生次优的代码,或因代码中的 if 条件矢量类型不可能表达需要的操作时时,可能需要这种编写代码的方法。
-注意到上个例子用了与 movqdu 而非 movqda (上面的例子里仅用 SIMD 产生的汇编指令使用的是 movqda。译者注)同义的 _mm_loadu_si128。这因为不确定 a 或 b 是否已按 16 字节对齐。使用的指令是期望内存对象对齐的,但使用的内存对象是未对齐的,这样肯定会导致运行错误或数据毁坏。为了让内存对象对齐,可在定义时用 aligned 属性指导编译器对齐内存对象。某些情况下,可考虑把关键数据按 64 字节对齐,因为 x86 L1 缓存也是这个大小,这样能提高缓存使用率。
-#include <stdint.h>
-#include <string.h>
-#include <emmintrin.h>
-
-static void __always_inline addtwo(int16_t *a, int16_t *b, int16_t size){
-
- int16_t i;
- __m128i c = _mm_set1_epi16(2) __attribute__((aligned(16)));
-
- for (i = 0; i < size; i+=8) {
- __m128i bb = _mm_load_si128(b+i); // movqda b+i -> xmm0
- __m128i r = _mm_add_epi16(bb, c); // paddw c + xmm0 -> xmm0
- _mm_store_si128(a+i, r); // movqda xmm0 -> a+i
- }
-}
-
-int main(){
- const int16_t size = 1024;
- int16_t a[size], b[size] __attribute__((aligned(16)));
- /* ... */
-
- addtwo(a, b, size);
- return a[0];
-}
-考虑到程序运行速度,使用自动变量好过静态或全局变量,情况允许的话还应避免动态内存分配。当动态内存分配无法避免时,Posix 标准 和 Windows 分别提供了 posix_memalign 和 _aligned_malloc 函数返回对齐的内存。
-高效使用矢量扩展喊代码优化需要深入理解目标架构工作原理和能加速代码运行的汇编指令。这两个主题相关的信息源有 Agner`s CPU blog 和它的装订版 Optimization manuals。
--
本文最后一节讨论 C 编程语言里一些有趣的地方:
-array[i] == i[array];-
因为下标操作符等价于*(array + i),因此 array 和 i 是可交换的,二者等价。
-$ gcc -dM -E - < /dev/null | grep -e linux -e unix -#define unix 1 -#define linux 1-
默认情况下,GCC 把 linux 和 unix 都定义为 1,所以一旦把其中一个用作函数名,代码就会编不过。
-int x = 'FOO!'; -short y = 'BO';-
没错,字符表达式可扩展到任意整型大小。
-x = i+++k; -x = i++ +k;-
后缀自增符在加号之前被词法分析扫描到。
-(即示例中两句等价,不同于 x = i + (++k) 。译者注)
-x = i+++++k; //error -x = i++ ++ +k; //error - -y = i++ + ++k; //ok-
词法分析查找可被处理的最长的非空格字符序列(C标准6.4节)。第一行将被解析成第二行的样子,它们俩都会产生关于缺少左值的错误,缺失的左值本应该被第二个自增符处理。
-如果有两个选项让你选择,一个是速度非常快、但容量很小的内存,一个是速度还算快、但容量很多的内存,如果你的工作集比较大,超过了前一种情况,那么人们总是会选择第二个选项。原因在于辅存(一般为磁盘)的速度。由于工作集超过主存,那么必须用辅存来保存交换出去的那部分数据,而辅存的速度往往要比主存慢上好几个数量级。
-好在这问题也并不全然是非甲即乙的选择。在配置大量DRAM的同时,我们还可以配置少量SRAM。将地址空间的某个部分划给SRAM,剩下的部分划给DRAM。一般来说,SRAM可以当作扩展的寄存器来使用。
-上面的做法看起来似乎可以,但实际上并不可行。首先,将SRAM内存映射到进程的虚拟地址空间就是个非常复杂的工作,而且,在这种做法中,每个进程都需要管理这个SRAM区内存的分配。每个进程可能有大小完全不同的SRAM区,而组成程序的每个模块也需要索取属于自身的SRAM,更引入了额外的同步需求。简而言之,快速内存带来的好处完全被额外的管理开销给抵消了。 -因此,SRAM是作为CPU自动使用和管理的一个资源,而不是由OS或者用户管理的。在这种模式下,SRAM用来复制保存(或者叫缓存)主内存中有可能即将被CPU使用的数据。这意味着,在较短时间内,CPU很有可能重复运行某一段代码,或者重复使用某部分数据。从代码上看,这意味着CPU执行了一个循环,所以相同的代码一次又一次地执行(空间局部性的绝佳例子)。数据访问也相对局限在一个小的区间内。即使程序使用的物理内存不是相连的,在短期内程序仍然很有可能使用同样的数据(时间局部性)。这个在代码上表现为,程序在一个循环体内调用了入口一个位于另外的物理地址的函数。这个函数可能与当前指令的物理位置相距甚远,但是调用的时间差不大。在数据上表现为,程序使用的内存是有限的(相当于工作集的大小)。但是实际上由于RAM的随机访问特性,程序使用的物理内存并不是连续的。正是由于空间局部性和时间局部性的存在,我们才提炼出今天的CPU缓存概念。 -我们先用一个简单的计算来展示一下高速缓存的效率。假设,访问主存需要200个周期,而访问高速缓存需要15个周期。如果使用100个数据元素100次,那么在没有高速缓存的情况下,需要2000000个周期,而在有高速缓存、而且所有数据都已被缓存的情况下,只需要168500个周期。节约了91.5%的时间。
-用作高速缓存的SRAM容量比主存小得多。以我的经验来说,高速缓存的大小一般是主存的千分之一左右(目前一般是4GB主存、4MB缓存)。这一点本身并不是什么问题。只是,计算机一般都会有比较大的主存,因此工作集的大小总是会大于缓存。特别是那些运行多进程的系统,它的工作集大小是所有进程加上内核的总和。
-处理高速缓存大小的限制需要制定一套很好的策略来决定在给定的时间内什么数据应该被缓存。由于不是所有数据的工作集都是在完全相同的时间段内被使用的,我们可以用一些技术手段将需要用到的数据临时替换那些当前并未使用的缓存数据。这种预取将会减少部分访问主存的成本,因为它与程序的执行是异步的。所有的这些技术将会使高速缓存在使用的时候看起来比实际更大。我们将在3.3节讨论这些问题。 -我们将在第6章讨论如何让这些技术能很好地帮助程序员,让处理器更高效地工作。 -在深入介绍高速缓存的技术细节之前,有必要说明一下它在现代计算机系统中所处的位置。
----
- 图3.1: 最简单的高速缓存配置图 -
图3.1展示了最简单的高速缓存配置。早期的一些系统就是类似的架构。在这种架构中,CPU核心不再直连到主存。{在一些更早的系统中,高速缓存像CPU与主存一样连到系统总线上。那种做法更像是一种hack,而不是真正的解决方案。}数据的读取和存储都经过高速缓存。CPU核心与高速缓存之间是一条特殊的快速通道。在简化的表示法中,主存与高速缓存都连到系统总线上,这条总线同时还用于与其它组件通信。我们管这条总线叫“FSB”——就是现在称呼它的术语,参见第2.2节。在这一节里,我们将忽略北桥。
-在过去的几十年,经验表明使用了冯诺伊曼结构的 -计算机,将用于代码和数据的高速缓存分开是存在巨大优势的。自1993年以来,Intel -并且一直坚持使用独立的代码和数据高速缓存。由于所需的代码和数据的内存区域是几乎相互独立的,这就是为什么独立缓存工作得更完美的原因。近年来,独立缓存的另一个优势慢慢显现出来:常见处理器解码 -指令的步骤 -是缓慢的,尤其当管线为空的时候,往往会伴随着错误的预测或无法预测的分支的出现, -将高速缓存技术用于 -指令 -解码可以加快其执行速度。 -在高速缓存出现后不久,系统变得更加复杂。高速缓存与主存之间的速度差异进一步拉大,直到加入了另一级缓存。新加入的这一级缓存比第一级缓存更大,但是更慢。由于加大一级缓存的做法从经济上考虑是行不通的,所以有了二级缓存,甚至现在的有些系统拥有三级缓存,如图3.2所示。随着单个CPU中核数的增加,未来甚至可能会出现更多层级的缓存。
----
- 图3.2: 三级缓存的处理器 -
图3.2展示了三级缓存,并介绍了本文将使用的一些术语。L1d是一级数据缓存,L1i是一级指令缓存,等等。请注意,这只是示意图,真正的数据流并不需要流经上级缓存。CPU的设计者们在设计高速缓存的接口时拥有很大的自由。而程序员是看不到这些设计选项的。
-另外,我们有多核CPU,每个核心可以有多个“线程”。核心与线程的不同之处在于,核心拥有独立的硬件资源({早期的多核CPU甚至有独立的二级缓存。})。在不同时使用相同资源(比如,通往外界的连接)的情况下,核心可以完全独立地运行。而线程只是共享资源。Intel的线程只有独立的寄存器,而且还有限制——不是所有寄存器都独立,有些是共享的。综上,现代CPU的结构就像图3.3所示。
----
- 图3.3 多处理器、多核心、多线程 -
在上图中,有两个处理器,每个处理器有两个核心,每个核心有两个线程。线程们共享一级缓存。核心(以深灰色表示)有独立的一级缓存,同时共享二级缓存。处理器(淡灰色)之间不共享任何缓存。这些信息很重要,特别是在讨论多进程和多线程情况下缓存的影响时尤为重要。
-了解成本和节约使用缓存,我们必须结合在第二节中讲到的关于计算机体系结构和RAM技术,以及前一节讲到的缓存描述来探讨。
-默认情况下,CPU核心所有的数据的读或写都存储在缓存中。当然,也有内存区域不能被缓存的,但是这种情况只发生在操作系统的实现者对数据考虑的前提下;对程序实现者来说,这是不可见的。这也说明,程序设计者可以故意绕过某些缓存,不过这将是第六节中讨论的内容了。
-如果CPU需要访问某个字(word),先检索缓存。很显然,缓存不可能容纳主存所有内容(否则还需要主存干嘛)。系统用字的内存地址来对缓存条目进行标记。如果需要读写某个地址的字,那么根据标签来检索缓存即可。这里用到的地址可以是虚拟地址,也可以是物理地址,取决于缓存的具体实现。
-标签是需要额外空间的,用字作为缓存的粒度显然毫无效率。比如,在x86机器上,32位字的标签可能需要32位,甚至更长。另一方面,由于空间局部性的存在,与当前地址相邻的地址有很大可能会被一起访问。再回忆下2.2.1节——内存模块在传输位于同一行上的多份数据时,由于不需要发送新CAS信号,甚至不需要发送RAS信号,因此可以实现很高的效率。基于以上的原因,缓存条目并不存储单个字,而是存储若干连续字组成的“线”。在早期的缓存中,线长是32字节,现在一般是64字节。对于64位宽的内存总线,每条线需要8次传输。而DDR对于这种传输模式的支持更为高效。
-当处理器需要内存中的某块数据时,整条缓存线被装入L1d。缓存线的地址通过对内存地址进行掩码操作生成。对于64字节的缓存线,是将低6位置0。这些被丢弃的位作为线内偏移量。其它的位作为标签,并用于在缓存内定位。在实践中,我们将地址分为三个部分。32位地址的情况如下:
----
如果缓存线长度为2O,那么地址的低O位用作线内偏移量。上面的S位选择“缓存集”。后面我们会说明使用缓存集的原因。现在只需要明白一共有2S个缓存集就够了。剩下的32 - S - O = T位组成标签。它们用来区分别名相同的各条线{有相同S部分的缓存线被称为有相同的别名。}用于定位缓存集的S部分不需要存储,因为属于同一缓存集的所有线的S部分都是相同的。
-当某条指令修改内存时,仍然要先装入缓存线,因为任何指令都不可能同时修改整条线(只有一个例外——第6.1节中将会介绍的写合并(write-combine))。因此需要在写操作前先把缓存线装载进来。如果缓存线被写入,但还没有写回主存,那就是所谓的“脏了”。脏了的线一旦写回主存,脏标记即被清除。
-为了装入新数据,基本上总是要先在缓存中清理出位置。L1d将内容逐出L1d,推入L2(线长相同)。当然,L2也需要清理位置。于是L2将内容推入L3,最后L3将它推入主存。这种逐出操作一级比一级昂贵。这里所说的是现代AMD和VIA处理器所采用的独占型缓存(exclusive cache)。而Intel采用的是包容型缓存(inclusive cache),{并不完全正确,Intel有些缓存是独占型的,还有一些缓存具有独占型缓存的特点。}L1d的每条线同时存在于L2里。对这种缓存,逐出操作就很快了。如果有足够L2,对于相同内容存在不同地方造成内存浪费的缺点可以降到最低,而且在逐出时非常有利。而独占型缓存在装载新数据时只需要操作L1d,不需要碰L2,因此会比较快。
-处理器体系结构中定义的作为存储器的模型只要还没有改变,那就允许多CPU按照自己的方式来管理高速缓存。这表示,例如,设计优良的处理器,利用很少或根本没有内存总线活动,并主动写回主内存脏高速缓存行。这种高速缓存架构在如x86和x86-64各种各样的处理器间存在。制造商之间,即使在同一制造商生产的产品中,证明了的内存模型抽象的力量。
在对称多处理器(SMP)架构的系统中,CPU的高速缓存不能独立的工作。在任何时候,所有的处理器都应该拥有相同的内存内容。保证这样的统一的内存视图被称为“高速缓存一致性”。如果在其自己的高速缓存和主内存间,处理器设计简单,它将不会看到在其他处理器上的脏高速缓存行的内容。从一个处理器直接访问另一个处理器的高速缓存这种模型设计代价将是非常昂贵的,它是一个相当大的瓶颈。相反,当另一个处理器要读取或写入到高速缓存线上时,处理器会去检测。 -如果CPU检测到一个写访问,而且该CPU的cache中已经缓存了一个cache line的原始副本,那么这个cache line将被标记为无效的cache line。接下来在引用这个cache line之前,需要重新加载该cache line。需要注意的是读访问并不会导致cache line被标记为无效的。
-更精确的cache实现需要考虑到其他更多的可能性,比如第二个CPU在读或者写他的cache line时,发现该cache line在第一个CPU的cache中被标记为脏数据了,此时我们就需要做进一步的处理。在这种情况下,主存储器已经失效,第二个CPU需要读取第一个CPU的cache line。通过测试,我们知道在这种情况下第一个CPU会将自己的cache line数据自动发送给第二个CPU。这种操作是绕过主存储器的,但是有时候存储控制器是可以直接将第一个CPU中的cache line数据存储到主存储器中。对第一个CPU的cache的写访问会导致本地cache line的所有拷贝被标记为无效。
-随着时间的推移,一大批缓存一致性协议已经建立。其中,最重要的是MESI,我们将在第3.3.4节进行介绍。以上结论可以概括为几个简单的规则: -最后,我们至少应该关注高速缓存命中或未命中带来的消耗。下面是英特尔奔腾 M 的数据:
-| To Where | -Cycles | -
|---|---|
| Register | -<= 1 | -
| L1d | -~3 | -
| L2 | -~14 | -
| Main Memory | -~240 | -
这是在CPU周期中的实际访问时间。有趣的是,对于L2高速缓存的访问时间很大一部分(甚至是大部分)是由线路的延迟引起的。这是一个限制,增加高速缓存的大小变得更糟。只有当减小时(例如,从60纳米的Merom到45纳米Penryn处理器),可以提高这些数据。
-表格中的数字看起来很高,但是,幸运的是,整个成本不必须负担每次出现的缓存加载和缓存失效。某些部分的成本可以被隐藏。现在的处理器都使用不同长度的内部管道,在管道内指令被解码,并为准备执行。如果数据要传送到一个寄存器,那么部分的准备工作是从存储器(或高速缓存)加载数据。如果内存加载操作在管道中足够早的进行,它可以与其他操作并行发生,那么加载的全部发销可能会被隐藏。对L1D常常可能如此;某些有长管道的处理器的L2也可以。 -对于写操作,CPU并不需要等待数据被安全地放入内存。只要指令具有类似的效果,就没有什么东西可以阻止CPU走捷径了。它可以早早地执行下一条指令,甚至可以在影子寄存器(shadow register)的帮助下,更改这个写操作将要存储的数据。
----
- 图3.4: 随机写操作的访问时间 -
图3.4展示了缓存的效果。关于产生图中数据的程序,我们会在稍后讨论。这里大致说下,这个程序是连续随机地访问某块大小可配的内存区域。每个数据项的大小是固定的。数据项的多少取决于选择的工作集大小。Y轴表示处理每个元素平均需要多少个CPU周期,注意它是对数刻度。X轴也是同样,工作集的大小都以2的n次方表示。
-图中有三个比较明显的不同阶段。很正常,这个处理器有L1d和L2,没有L3。根据经验可以推测出,L1d有213字节,而L2有220字节。因为,如果整个工作集都可以放入L1d,那么只需不到10个周期就可以完成操作。如果工作集超过L1d,处理器不得不从L2获取数据,于是时间飘升到28个周期左右。如果工作集更大,超过了L2,那么时间进一步暴涨到480个周期以上。这时候,许多操作将不得不从主存中获取数据。更糟糕的是,如果修改了数据,还需要将这些脏了的缓存线写回内存。
-看了这个图,大家应该会有足够的动力去检查代码、改进缓存的利用方式了吧?这里的性能改善可不只是微不足道的几个百分点,而是几个数量级呀。在第6节中,我们将介绍一些编写高效代码的技巧。而下一节将进一步深入缓存的设计。虽然精彩,但并不是必修课,大家可以选择性地跳过。
-缓存的实现者们都要面对一个问题——主存中每一个单元都可能需被缓存。如果程序的工作集很大,就会有许多内存位置为了缓存而打架。前面我们曾经提过缓存与主存的容量比,1:1000也十分常见。
-我们可以让缓存的每条线能存放任何内存地址的数据。这就是所谓的全关联缓存(fully associative cache)。对于这种缓存,处理器为了访问某条线,将不得不检索所有线的标签。而标签则包含了整个地址,而不仅仅只是线内偏移量(也就意味着,图3.2中的S为0)。
-
高速缓存有类似这样的实现,但是,看看在今天使用的L2的数目,表明这是不切实际的。给定4MB的高速缓存和64B的高速缓存段,高速缓存将有65,536个项。为了达到足够的性能,缓存逻辑必须能够在短短的几个时钟周期内,从所有这些项中,挑一个匹配给定的标签。实现这一点的工作将是巨大的。
----
Figure 3.5: 全关联高速缓存原理图
-
对于每个高速缓存行,比较器是需要比较大标签(注意,S是零)。每个连接旁边的字母表示位的宽度。如果没有给出,它是一个单比特线。每个比较器都要比较两个T-位宽的值。然后,基于该结果,适当的高速缓存行的内容被选中,并使其可用。这需要合并多套O数据线,因为他们是缓存桶(译注:这里类似把O输出接入多选器,所以需要合并)。实现仅仅一个比较器,需要晶体管的数量就非常大,特别是因为它必须非常快。没有迭代比较器是可用的。节省比较器的数目的唯一途径是通过反复比较标签,以减少它们的数目。这是不适合的,出于同样的原因,迭代比较器不可用:它的时间太长。
-全关联高速缓存对 -小缓存是实用的(例如,在某些Intel处理器的TLB缓存是全关联的),但这些缓存都很小,非常小的。我们正在谈论的最多几十项。 ----
Figure 3.6: Direct-Mapped Cache Schematics
-
在图3.6中可以看出,这种直接映射的高速缓存,速度快,比较容易实现。它只是需要一个比较器,一个多路复用器(在这个图中有两个,标记和数据是分离的,但是对于设计这不是一个硬性要求),和一些逻辑来选择只是有效的高速缓存行的内容。由于速度的要求,比较器是复杂的,但是现在只需要一个,结果是可以花更多的精力,让其变得快速。这种方法的复杂性在于在多路复用器。一个简单的多路转换器中的晶体管的数量增速是O(log N)的,其中N是高速缓存段的数目。这是可以容忍的,但可能会很慢,在某种情况下,速度可提升,通过增加多路复用器晶体管数量,来并行化的一些工作和自身增速。晶体管的总数只是随着快速增长的高速缓存缓慢的增加,这使得这种解决方案非常有吸引力。但它有一个缺点:只有用于直接映射地址的相关的地址位均匀分布,程序才能很好工作。如果分布的不均匀,而且这是常态,一些缓存项频繁的使用,并因此多次被换出,而另一些则几乎不被使用或一直是空的。
----
Figure 3.7: 组关联高速缓存原理图
-
可以通过使高速缓存的组关联来解决此问题。组关联结合高速缓存的全关联和直接映射高速缓存特点,在很大程度上避免那些设计的弱点。图3.7显示了一个组关联高速缓存的设计。标签和数据存储分成不同的组并可以通过地址选择。这类似直接映射高速缓存。但是,小数目的值可以在同一个高速缓存组缓存,而不是一个缓存组只有一个元素,用于在高速缓存中的每个设定值是相同的一组值的缓存。所有组的成员的标签可以并行比较,这类似全关联缓存的功能。
-其结果是高速缓存,不容易被不幸或故意选择同属同一组编号的地址所击败,同时高速缓存的大小并不限于由比较器的数目,可以以并行的方式实现。如果高速缓存增长,只(在该图中)增加列的数目,而不增加行数。只有高速缓存之间的关联性增加,行数才会增加。今天,处理器的L2高速缓存或更高的高速缓存,使用的关联性高达16。 L1高速缓存通常使用8。
-| L2 Cache Size |
- Associativity | -|||||||
|---|---|---|---|---|---|---|---|---|
| Direct | -2 | -4 | -8 | -|||||
| CL=32 | -CL=64 | -CL=32 | -CL=64 | -CL=32 | -CL=64 | -CL=32 | -CL=64 | -|
| 512k | -27,794,595 | -20,422,527 | -25,222,611 | -18,303,581 | -24,096,510 | -17,356,121 | -23,666,929 | -17,029,334 | -
| 1M | -19,007,315 | -13,903,854 | -16,566,738 | -12,127,174 | -15,537,500 | -11,436,705 | -15,162,895 | -11,233,896 | -
| 2M | -12,230,962 | -8,801,403 | -9,081,881 | -6,491,011 | -7,878,601 | -5,675,181 | -7,391,389 | -5,382,064 | -
| 4M | -7,749,986 | -5,427,836 | -4,736,187 | -3,159,507 | -3,788,122 | -2,418,898 | -3,430,713 | -2,125,103 | -
| 8M | -4,731,904 | -3,209,693 | -2,690,498 | -1,602,957 | -2,207,655 | -1,228,190 | -2,111,075 | -1,155,847 | -
| 16M | -2,620,587 | -1,528,592 | -1,958,293 | -1,089,580 | -1,704,878 | -883,530 | -1,671,541 | -862,324 | -
Table 3.1: 高速缓存大小,关联行,段大小的影响
-给定我们4MB/64B高速缓存,8路组关联,相关的缓存留给我们的有8192组,只用标签的13位,就可以寻址缓集。要确定哪些(如果有的话)的缓存组设置中的条目包含寻址的高速缓存行,8个标签都要进行比较。在很短的时间内做出来是可行的。通过一个实验,我们可以看到,这是有意义的。
-
表3.1显示一个程序在改变缓存大小,缓存段大小和关联集大小,L2高速缓存的缓存失效数量(根据Linux内核相关的方面人的说法,GCC在这种情况下,是他们所有中最重要的标尺)。在7.2节中,我们将介绍工具来模拟此测试要求的高速缓存。
-万一这还不是很明显,所有这些值之间的关系是高速缓存的大小为:
-- cache line size × associativity × number of sets --
地址被映射到高速缓存使用
-- O = log - 2 cache line size --
- S = log - 2 number of sets -
在第3.2节中的图显示的方式。
----
Figure 3.8: 缓存段大小 vs 关联行 (CL=32)
-
图3.8表中的数据更易于理解。它显示一个固定的32个字节大小的高速缓存行的数据。对于一个给定的高速缓存大小,我们可以看出,关联性,的确可以帮助明显减少高速缓存未命中的数量。对于8MB的缓存,从直接映射到2路组相联,可以减少近44%的高速缓存未命中。组相联高速缓存和直接映射缓存相比,该处理器可以把更多的工作集保持在缓存中。
-在文献中,偶尔可以读到,引入关联性,和加倍高速缓存的大小具有相同的效果。在从4M缓存跃升到8MB缓存的极端的情况下,这是正确的。关联性再提高一倍那就肯定不正确啦。正如我们所看到的数据,后面的收益要小得多。我们不应该完全低估它的效果,虽然。在示例程序中的内存使用的峰值是5.6M。因此,具有8MB缓存不太可能有很多(两个以上)使用相同的高速缓存的组。从较小的缓存的关联性的巨大收益可以看出,较大工作集可以节省更多。
-在一般情况下,增加8以上的高速缓存之间的关联性似乎对只有一个单线程工作量影响不大。随着介绍一个使用共享L2的多核处理器,形势发生了变化。现在你基本上有两个程序命中相同的缓存, 实际上导致高速缓存减半(对于四核处理器是1/4)。因此,可以预期,随着核的数目的增加,共享高速缓存的相关性也应增长。一旦这种方法不再可行(16 路组关联性已经很难)处理器设计者不得不开始使用共享的三级高速缓存和更高级别的,而L2高速缓存只被核的一个子集共享。
-从图3.8中,我们还可以研究缓存大小对性能的影响。这一数据需要了解工作集的大小才能进行解读。很显然,与主存相同的缓存比小缓存能产生更好的结果,因此,缓存通常是越大越好。
-上文已经说过,示例中最大的工作集为5.6M。它并没有给出最佳缓存大小值,但我们可以估算出来。问题主要在于内存的使用并不连续,因此,即使是16M的缓存,在处理5.6M的工作集时也会出现冲突(参见2路集合关联式16MB缓存vs直接映射式缓存的优点)。不管怎样,我们可以有把握地说,在同样5.6M的负载下,缓存从16MB升到32MB基本已没有多少提高的余地。但是,工作集是会变的。如果工作集不断增大,缓存也需要随之增大。在购买计算机时,如果需要选择缓存大小,一定要先衡量工作集的大小。原因可以参见图3.10。
----
- 图3.9: 测试的内存分布情况 -
我们执行两项测试。第一项测试是按顺序地访问所有元素。测试程序循着指针n进行访问,而所有元素是链接在一起的,从而使它们的被访问顺序与在内存中排布的顺序一致,如图3.9的下半部分所示,末尾的元素有一个指向首元素的引用。而第二项测试(见图3.9的上半部分)则是按随机顺序访问所有元素。在上述两个测试中,所有元素都构成一个单向循环链表。
-3.3.2 Cache的性能测试
-用于测试程序的数据可以模拟一个任意大小的工作集:包括读、写访问,随机、连续访问。在图3.4中我们可以看到,程序为工作集创建了一个与其大小和元素类型相同的数组:
- struct l {
- struct l *n;
- long int pad[NPAD];
- };
-
-n字段将所有节点随机得或者顺序的加入到环形链表中,用指针从当前节点进入到下一个节点。pad字段用来存储数据,其可以是任意大小。在一些测试程序中,pad字段是可以修改的, 在其他程序中,pad字段只可以进行读操作。
-在性能测试中,我们谈到工作集大小的问题,工作集使用结构体l定义的元素表示的。2N 字节的工作集包含
-- 2 - N/sizeof(struct l) --
个元素. 显然sizeof(struct l) 的值取决于NPAD的大小。在32位系统上,NPAD=7意味着数组的每个元素的大小为32字节,在64位系统上,NPAD=7意味着数组的每个元素的大小为64字节。
-最简单的情况就是遍历链表中顺序存储的节点。无论是从前向后处理,还是从后向前,对于处理器来说没有什么区别。下面的测试中,我们需要得到处理链表中一个元素所需要的时间,以CPU时钟周期最为计时单元。图3.10显示了测试结构。除非有特殊说明, 所有的测试都是在Pentium 4 64-bit 平台上进行的,因此结构体l中NPAD=0,大小为8字节。
----
图 3.10: 顺序读访问, NPAD=0
-
---
图 3.11: 顺序读多个字节
-
一开始的两个测试数据收到了噪音的污染。由于它们的工作负荷太小,无法过滤掉系统内其它进程对它们的影响。我们可以认为它们都是4个周期以内的。这样一来,整个图可以划分为比较明显的三个部分:
-这样的结果很容易解释——是因为处理器有16KB的L1d和1MB的L2。而在这三个部分之间,并没有非常锐利的边缘,这是因为系统的其它部分也在使用缓存,我们的测试程序并不能独占缓存的使用。尤其是L2,它是统一式的缓存,处理器的指令也会使用它(注: Intel使用的是包容式缓存)。
-
在工作集超过L2的大小之后,预取的效果更明显了。前面我们说过,主存的访问需要耗时200个周期以上。但在预取的帮助下,实际耗时保持在9个周期左右。200 vs 9,效果非常不错。
-我们可以观察到预取的行为,至少可以间接地观察到。图3.11中有4条线,它们表示处理不同大小结构时的耗时情况。随着结构的变大,元素间的距离变大了。图中4条线对应的元素距离分别是0、56、120和248字节。
-图中最下面的这一条线来自前一个图,但在这里更像是一条直线。其它三条线的耗时情况比较差。图中这些线也有比较明显的三个阶段,同时,在小工作集的情况下也有比较大的错误(请再次忽略这些错误)。在只使用L1d的阶段,这些线条基本重合。因为这时候还不需要预取,只需要访问L1d就行。
-在L2阶段,三条新加的线基本重合,而且耗时比老的那条线高很多,大约在28个周期左右,差不多就是L2的访问时间。这表明,从L2到L1d的预取并没有生效。这是因为,对于最下面的线(NPAD=0),由于结构小,8次循环后才需要访问一条新缓存线,而上面三条线对应的结构比较大,拿相对最小的NPAD=7来说,光是一次循环就需要访问一条新线,更不用说更大的NPAD=15和31了。而预取逻辑是无法在每个周期装载新线的,因此每次循环都需要从L2读取,我们看到的就是从L2读取的时延。
-更有趣的是工作集超过L2容量后的阶段。快看,4条线远远地拉开了。元素的大小变成了主角,左右了性能。处理器应能识别每一步(stride)的大小,不去为NPAD=15和31获取那些实际并不需要的缓存线(参见6.3.1)。元素大小对预取的约束是根源于硬件预取的限制——它无法跨越页边界。如果允许预取器跨越页边界,而下一页不存在或无效,那么OS还得去寻找它。这意味着,程序需要遭遇一次并非由它自己产生的页错误,这是完全不能接受的。在NPAD=7或者更大的时候,由于每个元素都至少需要一条缓存线,预取器已经帮不上忙了,它没有足够的时间去从内存装载数据。 -另一个导致慢下来的原因是TLB缓存的未命中。TLB是存储虚实地址映射的缓存,参见第4节。为了保持快速,TLB只有很小的容量。如果有大量页被反复访问,超出了TLB缓存容量,就会导致反复地进行地址翻译,这会耗费大量时间。TLB查找的代价分摊到所有元素上,如果元素越大,那么元素的数量越少,每个元素承担的那一份就越多。 -为了观察TLB的性能,我们可以进行另两项测试。第一项:我们还是顺序存储列表中的元素,使NPAD=7,让每个元素占满整个cache line,第二项:我们将列表的每个元素存储在一个单独的页上,忽略每个页没有使用的部分以用来计算工作集的大小。(这样做可能不太一致,因为在前面的测试中,我计算了结构体中每个元素没有使用的部分,从而用来定义NPAD的大小,因此每个元素占满了整个页,这样以来工作集的大小将会有所不同。但是这不是这项测试的重点,预取的低效率多少使其有点不同)。结果表明,第一项测试中,每次列表的迭代都需要一个新的cache line,而且每64个元素就需要一个新的页。第二项测试中,每次迭代都会在一个新的页中加载一个新的cache line。
---结果见图3.12。该测试与图3.11是在同一台机器上进行的。基于可用RAM空间的有限性,测试设置容量空间大小为2的24次方字节,这就需要1GB的容量将对象放置在分页上。图3.12中下方的红色曲线正好对应了图3.11中NPAD等于7的曲线。我们看到不同的步长显示了高速缓存L1d和L2的大小。第二条曲线看上去完全不同,其最重要的特点是当工作容量到达2的13次方字节时开始大幅度增长。这就是TLB缓存溢出的时候。我们能计算出一个64字节大小的元素的TLB缓存有64个输入。成本不会受页面错误影响,因为程序锁定了存储器以防止内存被换出。 --
图 3.12: TLB 对顺序读的影响
-
可以看出,计算物理地址并把它存储在TLB中所花费的周期数量级是非常高的。图3.12的表格显示了一个极端的例子,但从中可以清楚的得到:TLB缓存效率降低的一个重要因素是大型NPAD值的减缓。由于物理地址必须在缓存行能被L2或主存读取之前计算出来,地址转换这个不利因素就增加了内存访问时间。这一点部分解释了为什么NPAD等于31时每个列表元素的总花费比理论上的RAM访问时间要高。
- 
图3.13 NPAD等于1时的顺序读和写
通过查看链表元素被修改时测试数据的运行情况,我们可以窥见一些更详细的预取实现细节。图3.13显示了三条曲线。所有情况下元素宽度都为16个字节。第一条曲线“Follow”是熟悉的链表走线在这里作为基线。第二条曲线,标记为“Inc”,仅仅在当前元素进入下一个前给其增加thepad[0]成员。第三条曲线,标记为"Addnext0", 取出下一个元素的thepad[0]链表元素并把它添加为当前链表元素的thepad[0]成员。
-在没运行时,大家可能会以为"Addnext0"更慢,因为它要做的事情更多——在没进到下个元素之前就需要装载它的值。但实际的运行结果令人惊讶——在某些小工作集下,"Addnext0"比"Inc"更快。这是为什么呢?原因在于,系统一般会对下一个元素进行强制性预取。当程序前进到下个元素时,这个元素其实早已被预取在L1d里。因此,只要工作集比L2小,"Addnext0"的性能基本就能与"Follow"测试媲美。
-但是,"Addnext0"比"Inc"更快离开L2,这是因为它需要从主存装载更多的数据。而在工作集达到2 21字节时,"Addnext0"的耗时达到了28个周期,是同期"Follow"14周期的两倍。这个两倍也很好解释。"Addnext0"和"Inc"涉及对内存的修改,因此L2的逐出操作不能简单地把数据一扔了事,而必须将它们写入内存。因此FSB的可用带宽变成了一半,传输等量数据的耗时也就变成了原来的两倍。
----
- 图3.14: 更大L2/L3缓存的优势 -
决定顺序式缓存处理性能的另一个重要因素是缓存容量。虽然这一点比较明显,但还是值得一说。图3.14展示了128字节长元素的测试结果(64位机,NPAD=15)。这次我们比较三台不同计算机的曲线,两台P4,一台Core 2。两台P4的区别是缓存容量不同,一台是32k的L1d和1M的L2,一台是16K的L1d、512k的L2和2M的L3。Core 2那台则是32k的L1d和4M的L2。
-图中最有趣的地方,并不是Core 2如何大胜两台P4,而是工作集开始增长到连末级缓存也放不下、需要主存热情参与之后的部分。
-| Set Size |
- Sequential | -Random | -||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| L2 Hit | -L2 Miss | -#Iter | -Ratio Miss/Hit | -L2 Accesses Per Iter | -L2 Hit | -L2 Miss | -#Iter | -Ratio Miss/Hit | -L2 Accesses Per Iter | -|
| 220 | -88,636 | -843 | -16,384 | -0.94% | -5.5 | -30,462 | -4721 | -1,024 | -13.42% | -34.4 | -
| 221 | -88,105 | -1,584 | -8,192 | -1.77% | -10.9 | -21,817 | -15,151 | -512 | -40.98% | -72.2 | -
| 222 | -88,106 | -1,600 | -4,096 | -1.78% | -21.9 | -22,258 | -22,285 | -256 | -50.03% | -174.0 | -
| 223 | -88,104 | -1,614 | -2,048 | -1.80% | -43.8 | -27,521 | -26,274 | -128 | -48.84% | -420.3 | -
| 224 | -88,114 | -1,655 | -1,024 | -1.84% | -87.7 | -33,166 | -29,115 | -64 | -46.75% | -973.1 | -
| 225 | -88,112 | -1,730 | -512 | -1.93% | -175.5 | -39,858 | -32,360 | -32 | -44.81% | -2,256.8 | -
| 226 | -88,112 | -1,906 | -256 | -2.12% | -351.6 | -48,539 | -38,151 | -16 | -44.01% | -5,418.1 | -
| 227 | -88,114 | -2,244 | -128 | -2.48% | -705.9 | -62,423 | -52,049 | -8 | -45.47% | -14,309.0 | -
| 228 | -88,120 | -2,939 | -64 | -3.23% | -1,422.8 | -81,906 | -87,167 | -4 | -51.56% | -42,268.3 | -
| 229 | -88,137 | -4,318 | -32 | -4.67% | -2,889.2 | -119,079 | -163,398 | -2 | -57.84% | -141,238.5 | -
表3.2: 顺序访问与随机访问时L2命中与未命中的情况,NPAD=0
-与我们预计的相似,最末级缓存越大,曲线停留在L2访问耗时区的时间越长。在220字节的工作集时,第二台P4(更老一些)比第一台P4要快上一倍,这要完全归功于更大的末级缓存。而Core 2拜它巨大的4M L2所赐,表现更为卓越。
-对于随机的工作负荷而言,可能没有这么惊人的效果,但是,如果我们能将工作负荷进行一些裁剪,让它匹配末级缓存的容量,就完全可以得到非常大的性能提升。也是由于这个原因,有时候我们需要多花一些钱,买一个拥有更大缓存的处理器。
-单线程随机访问模式的测量
-前面我们已经看到,处理器能够利用L1d到L2之间的预取消除访问主存、甚至是访问L2的时延。
----
- 图3.15: 顺序读取vs随机读取,NPAD=0 -
但是,如果换成随机访问或者不可预测的访问,情况就大不相同了。图3.15比较了顺序读取与随机读取的耗时情况。
-换成随机之后,处理器无法再有效地预取数据,只有少数情况下靠运气刚好碰到先后访问的两个元素挨在一起的情形。
-图3.15中有两个需要关注的地方。首先,在大的工作集下需要非常多的周期。这台机器访问主存的时间大约为200-300个周期,但图中的耗时甚至超过了450个周期。我们前面已经观察到过类似现象(对比图3.11)。这说明,处理器的自动预取在这里起到了反效果。
-其次,代表随机访问的曲线在各个阶段不像顺序访问那样保持平坦,而是不断攀升。为了解释这个问题,我们测量了程序在不同工作集下对L2的访问情况。结果如图3.16和表3.2。
-从图中可以看出,当工作集大小超过L2时,未命中率(L2未命中次数/L2访问次数)开始上升。整条曲线的走向与图3.15有些相似: 先急速爬升,随后缓缓下滑,最后再度爬升。它与耗时图有紧密的关联。L2未命中率会一直爬升到100%为止。只要工作集足够大(并且内存也足够大),就可以将缓存线位于L2内或处于装载过程中的可能性降到非常低。
-缓存未命中率的攀升已经可以解释一部分的开销。除此以外,还有一个因素。观察表3.2的L2/#Iter列,可以看到每个循环对L2的使用次数在增长。由于工作集每次为上一次的两倍,如果没有缓存的话,内存的访问次数也将是上一次的两倍。在按顺序访问时,由于缓存的帮助及完美的预见性,对L2使用的增长比较平缓,完全取决于工作集的增长速度。
----
- 图3.16: L2d未命中率 -
--而换成随机访问后,单位耗时的增长超过了工作集的增长,根源是TLB未命中率的上升。图3.17描绘的是NPAD=7时随机访问的耗时情况。这一次,我们修改了随机访问的方式。正常情况下是把整个列表作为一个块进行随机(以∞表示),而其它11条线则是在小一些的块里进行随机。例如,标签为'60'的线表示以60页(245760字节)为单位进行随机。先遍历完这个块里的所有元素,再访问另一个块。这样一来,可以保证任意时刻使用的TLB条目数都是有限的。 -NPAD=7对应于64字节,正好等于缓存线的长度。由于元素顺序随机,硬件预取不可能有任何效果,特别是在元素较多的情况下。这意味着,分块随机时的L2未命中率与整个列表随机时的未命中率没有本质的差别。随着块的增大,曲线逐渐逼近整个列表随机对应的曲线。这说明,在这个测试里,性能受到TLB命中率的影响很大,如果我们能提高TLB命中率,就能大幅度地提升性能(在后面的一个例子里,性能提升了38%之多)。 --
- 图3.17: 页意义上(Page-Wise)的随机化,NPAD=7 -
在我们开始研究多个线程或进程同时使用相同内存之前,先来看一下缓存实现的一些细节。我们要求缓存是一致的,而且这种一致性必须对用户级代码完全透明。而内核代码则有所不同,它有时候需要对缓存进行转储(flush)。
-这意味着,如果对缓存线进行了修改,那么在这个时间点之后,系统的结果应该是与没有缓存的情况下是相同的,即主存的对应位置也已经被修改的状态。这种要求可以通过两种方式或策略实现:
-写通比较简单。当修改缓存线时,处理器立即将它写入主存。这样可以保证主存与缓存的内容永远保持一致。当缓存线被替代时,只需要简单地将它丢弃即可。这种策略很简单,但是速度比较慢。如果某个程序反复修改一个本地变量,可能导致FSB上产生大量数据流,而不管这个变量是不是有人在用,或者是不是短期变量。
-写回比较复杂。当修改缓存线时,处理器不再马上将它写入主存,而是打上已弄脏(dirty)的标记。当以后某个时间点缓存线被丢弃时,这个已弄脏标记会通知处理器把数据写回到主存中,而不是简单地扔掉。
-写回有时候会有非常不错的性能,因此较好的系统大多采用这种方式。采用写回时,处理器们甚至可以利用FSB的空闲容量来存储缓存线。这样一来,当需要缓存空间时,处理器只需清除脏标记,丢弃缓存线即可。
-但写回也有一个很大的问题。当有多个处理器(或核心、超线程)访问同一块内存时,必须确保它们在任何时候看到的都是相同的内容。如果缓存线在其中一个处理器上弄脏了(修改了,但还没写回主存),而第二个处理器刚好要读取同一个内存地址,那么这个读操作不能去读主存,而需要读第一个处理器的缓存线。在下一节中,我们将研究如何实现这种需求。
-在此之前,还有其它两种缓存策略需要提一下:
-这两种策略用于真实内存不支持的特殊地址区,内核为地址区设置这些策略(x86处理器利用内存类型范围寄存器MTRR),余下的部分自动进行。MTRR还可用于写通和写回策略的选择。
-写入合并是一种有限的缓存优化策略,更多地用于显卡等设备之上的内存。由于设备的传输开销比本地内存要高的多,因此避免进行过多的传输显得尤为重要。如果仅仅因为修改了缓存线上的一个字,就传输整条线,而下个操作刚好是修改线上的下一个字,那么这次传输就过于浪费了。而这恰恰对于显卡来说是比较常见的情形——屏幕上水平邻接的像素往往在内存中也是靠在一起的。顾名思义,写入合并是在写出缓存线前,先将多个写入访问合并起来。在理想的情况下,缓存线被逐字逐字地修改,只有当写入最后一个字时,才将整条线写入内存,从而极大地加速内存的访问。
-最后来讲一下不可缓存的内存。一般指的是不被RAM支持的内存位置,它可以是硬编码的特殊地址,承担CPU以外的某些功能。对于商用硬件来说,比较常见的是映射到外部卡或设备的地址。在嵌入式主板上,有时也有类似的地址,用来开关LED。对这些地址进行缓存显然没有什么意义。比如上述的LED,一般是用来调试或报告状态,显然应该尽快点亮或关闭。而对于那些PCI卡上的内存,由于不需要CPU的干涉即可更改,也不该缓存。 -3.3.4 多处理器支持
-在上节中我们已经指出当多处理器开始发挥作用的时候所遇到的问题。甚至对于那些不共享的高速级别的缓存(至少在L1d级别)的多核处理器也有问题。
-直接提供从一个处理器到另一处理器的高速访问,这是完全不切实际的。从一开始,连接速度根本就不够快。实际的选择是,在其需要的情况下,转移到其他处理器。需要注意的是,这同样应用在相同处理器上无需共享的高速缓存。
-现在的问题是,当该高速缓存线转移的时候会发生什么?这个问题回答起来相当容易:当一个处理器需要在另一个处理器的高速缓存中读或者写的脏的高速缓存线的时候。但怎样处理器怎样确定在另一个处理器的缓存中的高速缓存线是脏的?假设它仅仅是因为一个高速缓存线被另一个处理器加载将是次优的(最好的)。通常情况下,大多数的内存访问是只读的访问和产生高速缓存线,并不脏。在高速缓存线上处理器频繁的操作(当然,否则为什么我们有这样的文件呢?),也就意味着每一次写访问后,都要广播关于高速缓存线的改变将变得不切实际。 -多年来,人们开发除了MESI缓存一致性协议(MESI=Modified, Exclusive, Shared, Invalid,变更的、独占的、共享的、无效的)。协议的名称来自协议中缓存线可以进入的四种状态:
-MESI协议开发了很多年,最初的版本比较简单,但是效率也比较差。现在的版本通过以上4个状态可以有效地实现写回式缓存,同时支持不同处理器对只读数据的并发访问。
-
图3.18: MESI协议的状态跃迁图
在协议中,通过处理器监听其它处理器的活动,不需太多努力即可实现状态变更。处理器将操作发布在外部引脚上,使外部可以了解到处理过程。目标的缓存线地址则可以在地址总线上看到。在下文讲述状态时,我们将介绍总线参与的时机。
-一开始,所有缓存线都是空的,缓存为无效(Invalid)状态。当有数据装进缓存供写入时,缓存变为变更(Modified)状态。如果有数据装进缓存供读取,那么新状态取决于其它处理器是否已经状态了同一条缓存线。如果是,那么新状态变成共享(Shared)状态,否则变成独占(Exclusive)状态。
-如果本地处理器对某条Modified缓存线进行读写,那么直接使用缓存内容,状态保持不变。如果另一个处理器希望读它,那么第一个处理器将内容发给第一个处理器,然后可以将缓存状态置为Shared。而发给第二个处理器的数据由内存控制器接收,并放入内存中。如果这一步没有发生,就不能将这条线置为Shared。如果第二个处理器希望的是写,那么第一个处理器将内容发给它后,将缓存置为Invalid。这就是臭名昭著的"请求所有权(Request For Ownership,RFO)"操作。在末级缓存执行RFO操作的代价比较高。如果是写通式缓存,还要加上将内容写入上一层缓存或主存的时间,进一步提升了代价。 -对于Shared缓存线,本地处理器的读取操作并不需要修改状态,而且可以直接从缓存满足。而本地处理器的写入操作则需要将状态置为Modified,而且需要将缓存线在其它处理器的所有拷贝置为Invalid。因此,这个写入操作需要通过RFO消息发通知其它处理器。如果第二个处理器请求读取,无事发生。因为主存已经包含了当前数据,而且状态已经为Shared。如果第二个处理器需要写入,则将缓存线置为Invalid。不需要总线操作。 -Exclusive状态与Shared状态很像,只有一个不同之处: 在Exclusive状态时,本地写入操作不需要在总线上声明,因为本地的缓存是系统中唯一的拷贝。这是一个巨大的优势,所以处理器会尽量将缓存线保留在Exclusive状态,而不是Shared状态。只有在信息不可用时,才退而求其次选择shared。放弃Exclusive不会引起任何功能缺失,但会导致性能下降,因为E→M要远远快于S→M。
-从以上的说明中应该已经可以看出,在多处理器环境下,哪一步的代价比较大了。填充缓存的代价当然还是很高,但我们还需要留意RFO消息。一旦涉及RFO,操作就快不起来了。
-RFO在两种情况下是必需的:
-多线程或多进程的程序总是需要同步,而这种同步依赖内存来实现。因此,有些RFO消息是合理的,但仍然需要尽量降低发送频率。除此以外,还有其它来源的RFO。在第6节中,我们将解释这些场景。缓存一致性协议的消息必须发给系统中所有处理器。只有当协议确定已经给过所有处理器响应机会之后,才能进行状态跃迁。也就是说,协议的速度取决于最长响应时间。{这也是现在能看到三插槽AMD Opteron系统的原因。这类系统只有三个超级链路(hyperlink),其中一个连接南桥,每个处理器之间都只有一跳的距离。}总线上可能会发生冲突,NUMA系统的延时很大,突发的流量会拖慢通信。这些都是让我们避免无谓流量的充足理由。
-此外,关于多处理器还有一个问题。虽然它的影响与具体机器密切相关,但根源是唯一的——FSB是共享的。在大多数情况下,所有处理器通过唯一的总线连接到内存控制器(参见图2.1)。如果一个处理器就能占满总线(十分常见),那么共享总线的两个或四个处理器显然只会得到更有限的带宽。
-即使每个处理器有自己连接内存控制器的总线,如图2.2,但还需要通往内存模块的总线。一般情况下,这种总线只有一条。退一步说,即使像图2.2那样不止一条,对同一个内存模块的并发访问也会限制它的带宽。
-对于每个处理器拥有本地内存的AMD模型来说,也是同样的问题。的确,所有处理器可以非常快速地同时访问它们自己的内存。但是,多线程呢?多进程呢?它们仍然需要通过访问同一块内存来进行同步。
-对同步来说,有限的带宽严重地制约着并发度。程序需要更加谨慎的设计,将不同处理器访问同一块内存的机会降到最低。以下的测试展示了这一点,还展示了与多线程代码相关的其它效果。
-多线程测量
-为了帮助大家理解问题的严重性,我们来看一些曲线图,主角也是前文的那个程序。只不过这一次,我们运行多个线程,并测量这些线程中最快那个的运行时间。也就是说,等它们全部运行完是需要更长时间的。我们用的机器有4个处理器,而测试是做多跑4个线程。所有处理器共享同一条通往内存控制器的总线,另外,通往内存模块的总线也只有一条。
----
- 图3.19: 顺序读操作,多线程 -
图3.19展示了顺序读访问时的性能,元素为128字节长(64位计算机,NPAD=15)。对于单线程的曲线,我们预计是与图3.11相似,只不过是换了一台机器,所以实际的数字会有些小差别。
-更重要的部分当然是多线程的环节。由于是只读,不会去修改内存,不会尝试同步。但即使不需要RFO,而且所有缓存线都可共享,性能仍然分别下降了18%(双线程)和34%(四线程)。由于不需要在处理器之间传输缓存,因此这里的性能下降完全由以下两个瓶颈之一或同时引起: 一是从处理器到内存控制器的共享总线,二是从内存控制器到内存模块的共享总线。当工作集超过L3后,三种情况下都要预取新元素,而即使是双线程,可用的带宽也无法满足线性扩展(无惩罚)。
-当加入修改之后,场面更加难看了。图3.20展示了顺序递增测试的结果。
----
- 图3.20: 顺序递增,多线程 -
图中Y轴采用的是对数刻度,不要被看起来很小的差值欺骗了。现在,双线程的性能惩罚仍然是18%,但四线程的惩罚飙升到了93%!原因在于,采用四线程时,预取的流量与写回的流量加在一起,占满了整个总线。
-我们用对数刻度来展示L1d范围的结果。可以发现,当超过一个线程后,L1d就无力了。单线程时,仅当工作集超过L1d时访问时间才会超过20个周期,而多线程时,即使在很小的工作集情况下,访问时间也达到了那个水平。
-这里并没有揭示问题的另一方面,主要是用这个程序很难进行测量。问题是这样的,我们的测试程序修改了内存,所以本应看到RFO的影响,但在结果中,我们并没有在L2阶段看到更大的开销。原因在于,要看到RFO的影响,程序必须使用大量内存,而且所有线程必须同时访问同一块内存。如果没有大量的同步,这是很难实现的,而如果加入同步,则会占满执行时间。
----
- 图3.21: 随机的Addnextlast,多线程 -
最后,在图3.21中,我们展示了随机访问的Addnextlast测试的结果。这里主要是为了让大家感受一下这些巨大到爆的数字。极端情况下,甚至用了1500个周期才处理完一个元素。如果加入更多线程,真是不可想象哪。我们把多线程的效能总结了一下:
----
- -
- - 表3.3: 多线程的效能 -- -#Threads -Seq Read -Seq Inc -Rand Add -- -2 -1.69 -1.69 -1.54 -- - -4 -2.98 -2.07 -1.65 -
这个表展示了图3.21中多线程运行大工作集时的效能。表中的数字表示测试程序在使用多线程处理大工作集时可能达到的最大加速因子。双线程和四线程的理论最大加速因子分别是2和4。从表中数据来看,双线程的结果还能接受,但四线程的结果表明,扩展到双线程以上是没有什么意义的,带来的收益可以忽略不计。只要我们把图3.21换个方式呈现,就可以很容易看清这一点。
-
图3.22: 通过并行化实现的加速因子
图3.22中的曲线展示了加速因子,即多线程相对于单线程所能获取的性能加成值。测量值的精确度有限,因此我们需要忽略比较小的那些数字。可以看到,在L2与L3范围内,多线程基本可以做到线性加速,双线程和四线程分别达到了2和4的加速因子。但是,一旦工作集的大小超出L3,曲线就崩塌了,双线程和四线程降到了基本相同的数值(参见表3.3中第4列)。也是部分由于这个原因,我们很少看到4CPU以上的主板共享同一个内存控制器。如果需要配置更多处理器,我们只能选择其它的实现方式(参见第5节)。
-可惜,上图中的数据并不是普遍情况。在某些情况下,即使工作集能够放入末级缓存,也无法实现线性加速。实际上,这反而是正常的,因为普通的线程都有一定的耦合关系,不会像我们的测试程序这样完全独立。而反过来说,即使是很大的工作集,即使是两个以上的线程,也是可以通过并行化受益的,但是需要程序员的聪明才智。我们会在第6节进行一些介绍。
-特例: 超线程
-由CPU实现的超线程(有时又叫对称多线程,SMT)是一种比较特殊的情况,每个线程并不能真正并发地运行。它们共享着除寄存器外的绝大多数处理资源。每个核心和CPU仍然是并行工作的,但核心上的线程则受到这个限制。理论上,每个核心可以有大量线程,不过到目前为止,Intel的CPU最多只有两个线程。CPU负责对各线程进行时分复用,但这种复用本身并没有多少厉害。它真正的优势在于,CPU可以在当前运行的超线程发生延迟时,调度另一个线程。这种延迟一般由内存访问引起。
-如果两个线程运行在一个超线程核心上,那么只有当两个线程合起来的运行时间少于单线程运行时间时,效率才会比较高。我们可以将通常先后发生的内存访问叠合在一起,以实现这个目标。有一个简单的计算公式,可以帮助我们计算如果需要某个加速因子,最少需要多少的缓存命中率。
-程序的执行时间可以通过一个只有一级缓存的简单模型来进行估算(参见[htimpact]):
-- T - exe - = N[(1-F - mem - )T - proc - + F - mem - (G - hit - T - cache - + (1-G - hit - )T - miss - )] --
各变量的含义如下:
---- -
-- -N -= -指令数 -- -Fmem -= -N中访问内存的比例 -- -Ghit -= -命中缓存的比例 -- -Tproc -= -每条指令所用的周期数 -- -Tcache -= -缓存命中所用的周期数 -- -Tmiss -= -缓冲未命中所用的周期数 -- - -Texe -= -程序的执行时间 -
为了让任何判读使用双线程,两个线程之中任一线程的执行时间最多为单线程指令的一半。两者都有一个唯一的变量缓存命中数。 如果我们要解决最小缓存命中率相等的问题需要使我们获得的线程的执行率不少于50%或更多,如图 3.23.
----
图 3.23: 最小缓存命中率-加速
-
X轴表示单线程指令的缓存命中率Ghit,Y轴表示多线程指令所需的缓存命中率。这个值永远不能高于单线程命中率,否则,单线程指令也会使用改良的指令。为了使单线程的命中率在低于55%的所有情况下优于使用多线程,cup要或多或少的足够空闲因为缓存丢失会运行另外一个超线程。
-绿色区域是我们的目标。如果线程的速度没有慢过50%,而每个线程的工作量只有原来的一半,那么它们合起来的耗时应该会少于单线程的耗时。对我们用的示例系统来说(使用超线程的P4机器),如果单线程代码的命中率为60%,那么多线程代码至少要达到10%才能获得收益。这个要求一般来说还是可以做到的。但是,如果单线程代码的命中率达到了95%,那么多线程代码要做到80%才行。这就很难了。而且,这里还涉及到超线程,在两个超线程的情况下,每个超线程只能分到一半的有效缓存。因为所有超线程是使用同一个缓存来装载数据的,如果两个超线程的工作集没有重叠,那么原始的95%也会被打对折——47%,远低于80%。 -因此,超线程只在某些情况下才比较有用。单线程代码的缓存命中率必须低到一定程度,从而使缓存容量变小时新的命中率仍能满足要求。只有在这种情况下,超线程才是有意义的。在实践中,采用超线程能否获得更快的结果,取决于处理器能否有效地将每个进程的等待时间与其它进程的执行时间重叠在一起。并行化也需要一定的开销,需要加到总的运行时间里,这个开销往往是不能忽略的。
-在6.3.4节中,我们会介绍一种技术,它将多个线程通过公用缓存紧密地耦合起来。这种技术适用于许多场合,前提是程序员们乐意花费时间和精力扩展自己的代码。
-如果两个超线程执行完全不同的代码(两个线程就像被当成两个处理器,分别执行不同进程),那么缓存容量就真的会降为一半,导致缓冲未命中率大为攀升,这一点应该是很清楚的。这样的调度机制是很有问题的,除非你的缓存足够大。所以,除非程序的工作集设计得比较合理,能够确实从超线程获益,否则还是建议在BIOS中把超线程功能关掉。{我们可能会因为另一个原因 -开启 -超线程,那就是调试,因为SMT在查找并行代码的问题方面真的非常好用。} -我们已经介绍了地址的组成,即标签、集合索引和偏移三个部分。那么,实际会用到什么样的地址呢?目前,处理器一般都向进程提供虚拟地址空间,意味着我们有两种不同的地址: 虚拟地址和物理地址。
-虚拟地址有个问题——并不唯一。随着时间的变化,虚拟地址可以变化,指向不同的物理地址。同一个地址在不同的进程里也可以表示不同的物理地址。那么,是不是用物理地址会比较好呢?
-问题是,处理器指令用的虚拟地址,而且需要在内存管理单元(MMU)的协助下将它们翻译成物理地址。这并不是一个很小的操作。在执行指令的管线(pipeline)中,物理地址只能在很后面的阶段才能得到。这意味着,缓存逻辑需要在很短的时间里判断地址是否已被缓存过。而如果可以使用虚拟地址,缓存查找操作就可以更早地发生,一旦命中,就可以马上使用内存的内容。结果就是,使用虚拟内存后,可以让管线把更多内存访问的开销隐藏起来。 -处理器的设计人员们现在使用虚拟地址来标记第一级缓存。这些缓存很小,很容易被清空。在进程页表树发生变更的情况下,至少是需要清空部分缓存的。如果处理器拥有指定变更地址范围的指令,那么可以避免缓存的完全刷新。由于一级缓存L1i及L1d的时延都很小(~3周期),基本上必须使用虚拟地址。
-对于更大的缓存,包括L2和L3等,则需要以物理地址作为标签。因为这些缓存的时延比较大,虚拟到物理地址的映射可以在允许的时间里完成,而且由于主存时延的存在,重新填充这些缓存会消耗比较长的时间,刷新的代价比较昂贵。
-一般来说,我们并不需要了解这些缓存处理地址的细节。我们不能更改它们,而那些可能影响性能的因素,要么是应该避免的,要么是有很高代价的。填满缓存是不好的行为,缓存线都落入同一个集合,也会让缓存早早地出问题。对于后一个问题,可以通过缓存虚拟地址来避免,但作为一个用户级程序,是不可能避免缓存物理地址的。我们唯一可以做的,是尽最大努力不要在同一个进程里用多个虚拟地址映射同一个物理地址。 -另一个细节对程序员们来说比较乏味,那就是缓存的替换策略。大多数缓存会优先逐出最近最少使用(Least Recently Used,LRU)的元素。这往往是一个效果比较好的策略。在关联性很大的情况下(随着以后核心数的增加,关联性势必会变得越来越大),维护LRU列表变得越来越昂贵,于是我们开始看到其它的一些策略。
-在缓存的替换策略方面,程序员可以做的事情不多。如果缓存使用物理地址作为标签,我们是无法找出虚拟地址与缓存集之间关联的。有可能会出现这样的情形: 所有逻辑页中的缓存线都映射到同一个缓存集,而其它大部分缓存却空闲着。即使有这种情况,也只能依靠OS进行合理安排,避免频繁出现。
-虚拟化的出现使得这一切变得更加复杂。现在不仅操作系统可以控制物理内存的分配。虚拟机监视器(VMM,也称为 hypervisor)也负责分配内存。
-对程序员来说,最好 a) 完全使用逻辑内存页面 b) 在有意义的情况下,使用尽可能大的页面大小来分散物理地址。更大的页面大小也有其他好处,不过这是另一个话题(见第4节)。
-其实,不光处理器使用的数据被缓存,它们执行的指令也是被缓存的。只不过,指令缓存的问题相对来说要少得多,因为:
-有一些准则是需要程序员们遵守的,但大都是关于如何使用工具的,我们会在第6节介绍它们。而在这里我们只介绍一下指令缓存的技术细节。
-随着CPU的核心频率大幅上升,缓存与核心的速度差越拉越大,CPU的处理开始管线化。也就是说,指令的执行分成若干阶段。首先,对指令进行解码,随后,准备参数,最后,执行它。这样的管线可以很长(例如,Intel的Netburst架构超过了20个阶段)。在管线很长的情况下,一旦发生延误(即指令流中断),需要很长时间才能恢复速度。管线延误发生在这样的情况下: 下一条指令未能正确预测,或者装载下一条指令耗时过长(例如,需要从内存读取时)。 -为了解决这个问题,CPU的设计人员们在分支预测上投入大量时间和芯片资产(chip real estate),以降低管线延误的出现频率。
-在CISC处理器上,指令的解码阶段也需要一些时间。x86及x86-64处理器尤为严重。近年来,这些处理器不再将指令的原始字节序列存入L1i,而是缓存解码后的版本。这样的L1i被叫做“追踪缓存(trace cache)”。追踪缓存可以在命中的情况下让处理器跳过管线最初的几个阶段,在管线发生延误时尤其有用。
-前面说过,L2以上的缓存是统一缓存,既保存代码,也保存数据。显然,这里保存的代码是原始字节序列,而不是解码后的形式。
-在提高性能方面,与指令缓存相关的只有很少的几条准则:
-这些准则一般会由编译器的代码生成阶段强制执行。至于程序员可以参与的部分,我们会在第6节介绍。
-在计算机的早期岁月里,内存十分昂贵。人们想尽千方百计,只为了尽量压缩程序容量,给数据多留一些空间。其中,有一种方法是修改程序自身,称为自修改代码(SMC)。现在,有时候我们还能看到它,一般是出于提高性能的目的,也有的是为了攻击安全漏洞。
-一般情况下,应该避免SMC。虽然一般情况下没有问题,但有时会由于执行错误而出现性能问题。显然,发生改变的代码是无法放入追踪缓存(追踪缓存放的是解码后的指令)的。即使没有使用追踪缓存(代码还没被执行或有段时间没执行),处理器也可能会遇到问题。如果某个进入管线的指令发生了变化,处理器只能扔掉目前的成果,重新开始。在某些情况下,甚至需要丢弃处理器的大部分状态。
-最后,由于处理器认为代码页是不可修改的(这是出于简单化的考虑,而且在99.9999999%情况下确实是正确的),L1i用到并不是MESI协议,而是一种简化后的SI协议。这样一来,如果万一检测到修改的情况,就需要作出大量悲观的假设。
-因此,对于SMC,强烈建议能不用就不用。现在内存已经不再是一种那么稀缺的资源了。最好是写多个函数,而不要根据需要把一个函数改来改去。也许有一天可以把SMC变成可选项,我们就能通过这种方式检测入侵代码。如果一定要用SMC,应该让写操作越过缓存,以免由于L1i需要L1d里的数据而产生问题。更多细节,请参见6.1节。
-在Linux上,判断程序是否包含SMC是很容易的。利用正常工具链(toolchain)构建的程序代码都是写保护(write-protected)的。程序员需要在链接时施展某些关键的魔术才能生成可写的代码页。现代的Intel x86和x86-64处理器都有统计SMC使用情况的专用计数器。通过这些计数器,我们可以很容易判断程序是否包含SMC,即使它被准许运行。
-我们已经看过内存访问没有命中缓存时,那陡然猛涨的高昂代价。但是有时候,这种情况又是无法避免的,因此我们需要对真正的代价有所认识,并学习如何缓解这种局面。
-
为了更好地理解处理器的能力,我们测量了各种理想环境下能够达到的带宽值。由于不同处理器的版本差别很大,所以这个测试比较有趣,也因为如此,这一节都快被测试数据灌满了。我们使用了x86和x86-64处理器的SSE指令来装载和存储数据,每次16字节。工作集则与其它测试一样,从1kB增加到512MB,测量的具体对象是每个周期所处理的字节数。
----
- 图3.24: P4的带宽 -
图3.24展示了一颗64位Intel Netburst处理器的性能图表。当工作集能够完全放入L1d时,处理器的每个周期可以读取完整的16字节数据,即每个周期执行一条装载指令(moveaps指令,每次移动16字节的数据)。测试程序并不对数据进行任何处理,只是测试读取指令本身。当工作集增大,无法再完全放入L1d时,性能开始急剧下降,跌至每周期6字节。在218工作集处出现的台阶是由于DTLB缓存耗尽,因此需要对每个新页施加额外处理。由于这里的读取是按顺序的,预取机制可以完美地工作,而FSB能以5.3字节/周期的速度传输内容。但预取的数据并不进入L1d。当然,真实世界的程序永远无法达到以上的数字,但我们可以将它们看作一系列实际上的极限值。
-更令人惊讶的是写操作和复制操作的性能。即使是在很小的工作集下,写操作也始终无法达到4字节/周期的速度。这意味着,Intel为Netburst处理器的L1d选择了写通(write-through)模式,所以写入性能受到L2速度的限制。同时,这也意味着,复制测试的性能不会比写入测试差太多(复制测试是将某块内存的数据拷贝到另一块不重叠的内存区),因为读操作很快,可以与写操作实现部分重叠。最值得关注的地方是,两个操作在工作集无法完全放入L2后出现了严重的性能滑坡,降到了0.5字节/周期!比读操作慢了10倍!显然,如果要提高程序性能,优化这两个操作更为重要。 -再来看图3.25,它来自同一颗处理器,只是运行双线程,每个线程分别运行在处理器的一个超线程上。
----
- 图3.25: P4开启两个超线程时的带宽表现 -
图3.25采用了与图3.24相同的刻度,以方便比较两者的差异。图3.25中的曲线抖动更多,是由于采用双线程的缘故。结果正如我们预期,由于超线程共享着几乎所有资源(仅除寄存器外),所以每个超线程只能得到一半的缓存和带宽。所以,即使每个线程都要花上许多时间等待内存,从而把执行时间让给另一个线程,也是无济于事——因为另一个线程也同样需要等待。这里恰恰展示了使用超线程时可能出现的最坏情况。
----
- 图3.26: Core 2的带宽表现 -
再来看Core 2处理器的情况。看看图3.26和图3.27,再对比下P4的图3.24和3.25,可以看出不小的差异。Core 2是一颗双核处理器,有着共享的L2,容量是P4 L2的4倍。但更大的L2只能解释写操作的性能下降出现较晚的现象。
-当然还有更大的不同。可以看到,读操作的性能在整个工作集范围内一直稳定在16字节/周期左右,在220处的下降同样是由于DTLB的耗尽引起。能够达到这么高的数字,不但表明处理器能够预取数据,并且按时完成传输,而且还意味着,预取的数据是被装入L1d的。
-写/复制操作的性能与P4相比,也有很大差异。处理器没有采用写通策略,写入的数据留在L1d中,只在必要时才逐出。这使得写操作的速度可以逼近16字节/周期。一旦工作集超过L1d,性能即飞速下降。由于Core 2读操作的性能非常好,所以两者的差值显得特别大。当工作集超过L2时,两者的差值甚至超过20倍!但这并不表示Core 2的性能不好,相反,Core 2永远都比Netburst强。
----
- 图3.27: Core 2运行双线程时的带宽表现 -
在图3.27中,启动双线程,各自运行在Core 2的一个核心上。它们访问相同的内存,但不需要完美同步。从结果上看,读操作的性能与单线程并无区别,只是多了一些多线程情况下常见的抖动。
-有趣的地方来了——当工作集小于L1d时,写操作与复制操作的性能很差,就好像数据需要从内存读取一样。两个线程彼此竞争着同一个内存位置,于是不得不频频发送RFO消息。问题的根源在于,虽然两个核心共享着L2,但无法以L2的速度处理RFO请求。而当工作集超过L1d后,性能出现了迅猛提升。这是因为,由于L1d容量不足,于是将被修改的条目刷新到共享的L2。由于L1d的未命中可以由L2满足,只有那些尚未刷新的数据才需要RFO,所以出现了这样的现象。这也是这些工作集情况下速度下降一半的原因。这种渐进式的行为也与我们期待的一致: 由于每个核心共享着同一条FSB,每个核心只能得到一半的FSB带宽,因此对于较大的工作集来说,每个线程的性能大致相当于单线程时的一半。 -由于同一个厂商的不同处理器之间都存在着巨大差异,我们没有理由不去研究一下其它厂商处理器的性能。图3.28展示了AMD家族10h Opteron处理器的性能。这颗处理器有64kB的L1d、512kB的L2和2MB的L3,其中L3缓存由所有核心所共享。
----
- 图3.28: AMD家族10h Opteron的带宽表现 -
大家首先应该会注意到,在L1d缓存足够的情况下,这个处理器每个周期能处理两条指令。读操作的性能超过了32字节/周期,写操作也达到了18.7字节/周期。但是,不久,读操作的曲线就急速下降,跌到2.3字节/周期,非常差。处理器在这个测试中并没有预取数据,或者说,没有有效地预取数据。
-另一方面,写操作的曲线随几级缓存的容量而流转。在L1d阶段达到最高性能,随后在L2阶段下降到6字节/周期,在L3阶段进一步下降到2.8字节/周期,最后,在工作集超过L3后,降到0.5字节/周期。它在L1d阶段超过了Core 2,在L2阶段基本相当(Core 2的L2更大一些),在L3及主存阶段比Core 2慢。
-复制的性能既无法超越读操作的性能,也无法超越写操作的性能。因此,它的曲线先是被读性能压制,随后又被写性能压制。
-图3.29显示的是Opteron处理器在多线程时的性能表现。
----
- 图3.29: AMD Fam 10h在双线程时的带宽表现 -
读操作的性能没有受到很大的影响。每个线程的L1d和L2表现与单线程下相仿,L3的预取也依然表现不佳。两个线程并没有过渡争抢L3。问题比较大的是写操作的性能。两个线程共享的所有数据都需要经过L3,而这种共享看起来却效率很差。即使是在L3足够容纳整个工作集的情况下,所需要的开销仍然远高于L3的访问时间。再来看图3.27,可以发现,在一定的工作集范围内,Core 2处理器能以共享的L2缓存的速度进行处理。而Opteron处理器只能在很小的一个范围内实现相似的性能,而且,它仅仅只能达到L3的速度,无法与Core 2的L2相比。
-3.5.2 关键字加载
-内存以比缓存线还小的块从主存储器向缓存传送。如今64位可一次性传送,缓存线的大小为64或128比特。这意味着每个缓存线需要8或16次传送。
-DRAM芯片可以以触发模式传送这些64位的块。这使得不需要内存控制器的进一步指令和可能伴随的延迟,就可以将缓存线充满。如果处理器预取了缓存,这有可能是最好的操作方式。
-
如果程序在访问数据或指令缓存时没有命中(这可能是强制性未命中或容量性未命中,前者是由于数据第一次被使用,后者是由于容量限制而将缓存线逐出),情况就不一样了。程序需要的并不总是缓存线中的第一个字,而数据块的到达是有先后顺序的,即使是在突发模式和双倍传输率下,也会有明显的时间差,一半在4个CPU周期以上。举例来说,如果程序需要缓存线中的第8个字,那么在首字抵达后它还需要额外等待30个周期以上。
-当然,这样的等待并不是必需的。事实上,内存控制器可以按不同顺序去请求缓存线中的字。当处理器告诉它,程序需要缓存中具体某个字,即「关键字(critical word)」时,内存控制器就会先请求这个字。一旦请求的字抵达,虽然缓存线的剩余部分还在传输中,缓存的状态还没有达成一致,但程序已经可以继续运行。这种技术叫做关键字优先及较早重启(Critical Word First & Early Restart)。
-现在的处理器都已经实现了这一技术,但有时无法运用。比如,预取操作的时候,并不知道哪个是关键字。如果在预取的中途请求某条缓存线,处理器只能等待,并不能更改请求的顺序。
----
- 图3.30: 关键字位于缓存线尾时的表现 -
在关键字优先技术生效的情况下,关键字的位置也会影响结果。图3.30展示了下一个测试的结果,图中表示的是关键字分别在线首和线尾时的性能对比情况。元素大小为64字节,等于缓存线的长度。图中的噪声比较多,但仍然可以看出,当工作集超过L2后,关键字处于线尾情况下的性能要比线首情况下低0.7%左右。而顺序访问时受到的影响更大一些。这与我们前面提到的预取下条线时可能遇到的问题是相符的。
-3.5.3 缓存设定
-缓存放置的位置与超线程,内核和处理器之间的关系,不在程序员的控制范围之内。但是程序员可以决定线程执行的位置,接着高速缓存与使用的CPU的关系将变得非常重要。
-这里我们将不会深入(探讨)什么时候选择什么样的内核以运行线程的细节。我们仅仅描述了在设置关联线程的时候,程序员需要考虑的系统结构的细节。
-超线程,通过定义,共享除去寄存器集以外的所有数据。包括 L1 缓存。这里没有什么可以多说的。多核处理器的独立核心带来了一些乐趣。每个核心都至少拥有自己的 L1 缓存。除此之外,下面列出了一些不同的特性:
-关于各种处理器模型的优点,已经在它们各自的宣传手册里写得够多了。在每个核心的工作集互不重叠的情况下,独立的L2拥有一定的优势,单线程的程序可以表现优良。考虑到目前实际环境中仍然存在大量类似的情况,这种方法的表现并不会太差。不过,不管怎样,我们总会遇到工作集重叠的情况。如果每个缓存都保存着某些通用运行库的常用部分,那么很显然是一种浪费。
-如果像Intel的双核处理器那样,共享除L1外的所有缓存,则会有一个很大的优点。如果两个核心的工作集重叠的部分较多,那么综合起来的可用缓存容量会变大,从而允许容纳更大的工作集而不导致性能的下降。如果两者的工作集并不重叠,那么则是由Intel的高级智能缓存管理(Advanced Smart Cache management)发挥功用,防止其中一个核心垄断整个缓存。
-即使每个核心只使用一半的缓存,也会有一些摩擦。缓存需要不断衡量每个核心的用量,在进行逐出操作时可能会作出一些比较差的决定。我们来看另一个测试程序的结果。
----
- 图3.31: 两个进程的带宽表现 -
这次,测试程序两个进程,第一个进程不断用SSE指令读/写2MB的内存数据块,选择2MB,是因为它正好是Core 2处理器L2缓存的一半,第二个进程则是读/写大小变化的内存区域,我们把这两个进程分别固定在处理器的两个核心上。图中显示的是每个周期读/写的字节数,共有4条曲线,分别表示不同的读写搭配情况。例如,标记为读/写(read/write)的曲线代表的是后台进程进行写操作(固定2MB工作集),而被测量进程进行读操作(工作集从小到大)。
-图中最有趣的是220到223之间的部分。如果两个核心的L2是完全独立的,那么所有4种情况下的性能下降均应发生在221到222之间,也就是L2缓存耗尽的时候。但从图上来看,实际情况并不是这样,特别是背景进程进行写操作时尤为明显。当工作集达到1MB(220)时,性能即出现恶化,两个进程并没有共享内存,因此并不会产生RFO消息。所以,完全是缓存逐出操作引起的问题。目前这种智能的缓存处理机制有一个问题,每个核心能实际用到的缓存更接近1MB,而不是理论上的2MB。如果未来的处理器仍然保留这种多核共享缓存模式的话,我们唯有希望厂商会把这个问题解决掉。 -推出拥有双L2缓存的4核处理器仅仅只是一种临时措施,是开发更高级缓存之前的替代方案。与独立插槽及双核处理器相比,这种设计并没有带来多少性能提升。两个核心是通过同一条总线(被外界看作FSB)进行通信,并没有什么特别快的数据交换通道。
-未来,针对多核处理器的缓存将会包含更多层次。AMD的10h家族是一个开始,至于会不会有更低级共享缓存的出现,还需要我们拭目以待。我们有必要引入更多级别的缓存,因为频繁使用的高速缓存不可能被许多核心共用,否则会对性能造成很大的影响。我们也需要更大的高关联性缓存,它们的数量、容量和关联性都应该随着共享核心数的增长而增长。巨大的L3和适度的L2应该是一种比较合理的选择。L3虽然速度较慢,但也较少使用。
-对于程序员来说,不同的缓存设计就意味着调度决策时的复杂性。为了达到最高的性能,我们必须掌握工作负载的情况,必须了解机器架构的细节。好在我们在判断机器架构时还是有一些支援力量的,我们会在后面的章节介绍这些接口。
-FSB在性能中扮演了核心角色。缓存数据的存取速度受制于内存通道的速度。我们做一个测试,在两台机器上分别跑同一个程序,这两台机器除了内存模块的速度有所差异,其它完全相同。图3.32展示了Addnext0测试(将下一个元素的pad[0]加到当前元素的pad[0]上)在这两台机器上的结果(NPAD=7,64位机器)。两台机器都采用Core 2处理器,一台使用667MHz的DDR2内存,另一台使用800MHz的DDR2内存(比前一台增长20%)。
----
- 图3.32: FSB速度的影响 -
图上的数字表明,当工作集大到对FSB造成压力的程度时,高速FSB确实会带来巨大的优势。在我们的测试中,性能的提升达到了18.5%,接近理论上的极限。而当工作集比较小,可以完全纳入缓存时,FSB的作用并不大。当然,这里我们只测试了一个程序的情况,在实际环境中,系统往往运行多个进程,工作集是很容易超过缓存容量的。
-如今,一些英特尔的处理器,支持前端总线(FSB)的速度高达1,333 MHz,这意味着速度有另外60%的提升。将来还会出现更高的速度。速度是很重要的,工作集会更大,快速的RAM和高FSB速度的内存肯定是值得投资的。我们必须小心使用它,因为即使处理器可以支持更高的前端总线速度,但是主板的北桥芯片可能不会。使用时,检查它的规范是至关重要的。 - diff --git a/src/how-stackoverflow-works.md b/src/how-stackoverflow-works.md deleted file mode 100644 index 9f96fed..0000000 --- a/src/how-stackoverflow-works.md +++ /dev/null @@ -1,308 +0,0 @@ -摘要:同时使用Linux和Windows平台产品,大量使用静态的方法和类,Stack Overflow是个重度性能控。同时,取代横向扩展,他们坚持着纵向扩展思路,因为“硬件永远比程序员便宜”。【编者按】StackOverflow是一个IT技术问答网站,用户可以在网站上提交和回答问题。当下的StackOverflow已拥有400万个用户,4000万个回答,月PV5.6亿,世界排行第54。然而值得关注的是,支撑他们网站的全部服务器只有25台,并且都保持着非常低的资源使用率,这是一场高有效性、负载均衡、缓存、数据库、搜索及高效代码上的较量。近日,High - Scalability创始人Todd Hoff根据Marco Cecconi的演讲视频“ - The architecture of StackOverflow”以及Nick Craver的博文“ - What it takes to run Stack Overflow”总结了StackOverflow的成功原因。
以下为译文 -
-意料之中,也是意料之外,Stack Overflow仍然重度使用着微软的产品。他们认为既然微软的基础设施可以满足需求,又足够便宜,那么没有什么理由去做根本上的改变。而在需要的地方,他们同样使用了Linux。究其根本,一切都是为了性能。
-另一个值得关注的地方是,Stack Overflow仍然使用着纵向扩展策略,没有使用云。他们使用了384GB的内存和2TB的SSD来支撑SQL - Servers,如果使用AWS的话,花费可想而知。没有使用云的另一个原因是Stack Overflow认为云会一定程度上的降低性能,同时也会给优化和排查系统问题增加难度。此外,他们的架构也并不需要横向扩展。峰值期间是横向扩展的杀手级应用场景,然而他们有着丰富的系统调整经验去应对。该公司仍然坚持着Jeff - Atwood的名言——硬件永远比程序员便宜。
-Marco Ceccon曾提到,在谈及系统时,有一件事情必须首先弄明白——需要解决问题的类型。首先,从简单方面着手,StackExchange究竟是用来做什么的——首先是一些主题,然后围绕这些主题建立社区,最后就形成了这个令人敬佩的问答网站。
-其次则是规模相关。StackExchange在飞速增长,需要处理大量的数据传输,那么这些都是如何完成的,特别是只使用了25台服务器,下面一起追根揭底:
--
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
1. 为什么使用MS产品的同时还使用Redis?什么好用用什么,不要做无必要的系统之争,比如C#在Windows机器上运行最好,我们使用IIS;Redis在*nix机器上可以得到充分发挥,我们使用*nix。
-2. Overkill即策略。平常的利用率并不能代表什么,当某些特定的事情发生时,比如备份、重建等完全可以将资源使用拉满。
-3. 坚固的SSD。所有数据库都建立在SSD之上,这样可以获得0延时。
-4. 了解你的读写负载。
-5. 高效的代码意味着更少的主机。只有新项目上线时才会因为特殊需求增加硬件,通常情况下是添加内存,但在此之外,高效的代码就意味着0硬件添加。所以经常只讨论两个问题:为存储增加新的SSD;为新项目增加硬件。
-6. 不要害怕定制化。SO在Tag上使用复杂查询,因此专门开发了所需的Tag Engine。
-7. 只做必须做的事情。之所以不需要测试是因为有一个活跃的社区支撑,比如,开发者不用担心出现“Square Wheel”效应,如果开发者可以制作一个更更轻量级的组件,那就替代吧。
-8. 注重硬件知识,比如IL。一些代码使用IL而不是C#。聚焦SQL查询计划。使用web server的内存转储究竟做了些什么。探索,比如为什么一个split会产生2GB的垃圾。
-9. 切勿官僚作风。总有一些新的工具是你需要的,比如,一个编辑器,新版本的Visual Studio,降低提升过程中的一切阻力。
-10. 垃圾回收驱动编程。SO在减少垃圾回收成本上做了很多努力,跳过类似TDD的实践,避免抽象层,使用静态方法。虽然极端,但是确实打造出非常高效的代码。
-11. 高效代码的价值远远超出你想象,它可以让硬件跑的更快,降低资源使用,切记让代码更容易被程序员理解。
\ No newline at end of file diff --git a/src/java-string.md b/src/java-string.md deleted file mode 100644 index 345f4f4..0000000 --- a/src/java-string.md +++ /dev/null @@ -1,333 +0,0 @@ -String in Java is very special class and most frequently used class as -well. There are lot many things to learn about String in Java -than any other class, and having a good knowledge of different String -functionalities makes you to use it properly. Given heavy use of Java String in almost -any kind of project, it become even more important to know subtle detail about -String. Though I have shared lot of String related article already here in -Javarevisited, this is an effort to bring some of String feature together. In -this tutorial we will see some important points about Java String, which is -worth remembering. You can also refer my earlier post 10 -advanced Java String questions to know more about String. Though I -tried to cover lot of things, there are definitely few things, which I might -have missed; please let me know if you have any question or doubt on java.lang.String -functionality and I will try to address them here.
As I Said earlier String is special class in Java and all String literal
-e.g. "abc" (anything
-which is inside double quotes are String literal in Java) are maintained in a
-separate String pool, special memory location inside Java memory, more
-precisely inside PermGen
-Space. Any time you create a new String object using String literal, JVM
-first checks String pool and if an object with similar content available, than
-it returns that and doesn't create a new object. JVM doesn't perform String
-pool check if you create object using new operator.Java虚拟机(JVM)是Java应用的运行环境,从一般意义上来讲,JVM是通过规范来定义的一个虚拟的计算机,被设计用来解释执行从Java源码编译而来的字节码。更通俗地说,JVM是指对这个规范的具体实现。这种实现基于严格的指令集和全面的内存模型。另外,JVM也通常被形容为对软件运行时环境的实现。通常JVM实现主要指的是HotSpot。
-JVM规范保证任何的实现都能够以同样的方式解释执行字节码。其实现可以多样化,包括进程、独立的Java操作系统或者直接执行字节码的处理器芯片。我们了解最多的JVM是作为软件实现,运行在流行的操作系统平台上(包括Windows、OS X、Linux和Solaris等)。
-JVM的结构允许对一个Java应用进行更细微的控制。这些应用运行在沙箱(Sandbox)环境中。确保在没有恰当的许可时,无法访问到本地文件系统、处理器和网络连接。远程执行时,代码还需要进行证书认证。
-除了解释执行Java字节码,大多数的JVM实现还包含一个JIT(just-in-time 即时)编译器,用于为常用的方法生成机器码。机器码使用的是CPU的本地语言,相比字节码有着更快的运行速度。
-虽然理解JVM不是开发或运行Java程序的必要条件,但是如果多了解一些JVM知识,那么就有机会避免很多性能上的问题。理解了JVM,实际上这些问题会变得简单明了。
-JVM规范定义了一系列子系统以及它们的外部行为。JVM主要有以下子系统:
-Class Loader 类加载器。 用于读入Java源代码并将类加载到数据区。Execution Engine 执行引擎。 执行来自数据区的指令。数据区使用的是底层操作系统分配给JVM的内存。
-
JVM在下面几种不同的层面使用不同的类加载器:
-当一个类加载器收到一个加载类的请求,首先它会检查缓存,确认该类是否已经被加载,然后把请求代理给它的父类。如果父类没能成功的加载类,那么子类就会自己去尝试加载该类。子类可检查父类加载器的缓存,但父类不能看到子类所加载的类。之所类加载体系会这样设计,是认为一个子类不应该重复加载已经被父类加载过的类。
-执行引擎一个接一个地执行被加载到数据区的字节码。为了保证字节码指令对于机器来说是可读的,执行引擎使用下面两个方法:
-尽管即时编译比解释执行要占用更多的时间,但是对于需要使用成千上万次的方法,只需要处理一次。相比每次都解释执行,以本地代码的方式运行会节约很多执行时间。
-JVM规范中并不规定一定要使用即时编译。即时编译也不是用于提高JVM性能的唯一的手段。规范仅仅规定了每条字节码对应的本地代码,至于执行引擎如何实现这一对应过程的,完全由JVM的具体实现来决定。
-Java内存模型建立在自动内存管理的概念之上。当一个对象不再被一个应用所引用,垃圾回收器就会回收它,从而释放相应的内存。这一点和其他很多需要自行释放内存的语言有很大不同。
-JVM从底层操作系统中分配内存,并将它们分为以下几个区域:
-一个有效的管理内存方法是把对空间划分为不同代,这样垃圾回收器就不用扫描整个堆区。大多数的对象的生命周期都很段短暂,那些生命周期较长的对象往往直到应用退出才需要被清除。
-当一个Java应用创建了一个对象,这个对象是被存储到“初生池”(eden pool)。一旦初生池存储满了,就会在新生代触发一次minor gc(小范围的垃圾回收)。首先,垃圾回收器会标记出那些“死对象”(不再被应用所引用的对象),同时延长所有保留对象的生命周期(这个生命周期长度是用数字来描述,代表了期所经历过的垃圾回收的次数)。然后,垃圾回收器会回收这些死对象,并把剩余的活着的对象移动到“幸存池”(survivor pool),从而清空初生池。
当一个对象存活达到一定的周期后,它就会被移动到堆中的老生代:“终身代”(tenured pool)。最后,当终身代被填满时,就会触发一次full gc或major gc(完全的垃圾回收),以清理终身代。
(译者注:一般我们把初生池和幸存池所在的区域合并成为新生代,把终身代所在的区域成为老生代。对应的,在新生代上产生的gc称为minor gc,在老生代上产生的gc称为full gc。希望这样大家在其他地方看到对应的术语时能更好理解)
-当垃圾回收(gc)执行的时候,所有应用线程都要被停止,系统产生一次暂停。minor gc非常频繁,所以被优化的能够快速的回收死对象,是新生代的内存的主要的回收方式。major gc运行起来就相对慢得多,因为要扫描非常多的活着的对象。垃圾回收器本身也有多种实现,有些垃圾回收器在一定情况下能更快的执行major gc。
-堆的大小是动态的,只有堆需要扩张的时候才会从内存中分配。当堆被填满时,JVM会重新给堆分配更多的内存,直到达到堆大小的上限,这种重新分配同样会导致应用的短暂停止。
-JVM是运行在一个独立的进程中的,但它可以并发执行多个线程,每个线程都运行自己的方法,这是Java必备的一个部分。以即时消息客户端这样一个应用为例,它至少运行两个线程。一个线程用于等待用户输入,另一个检查服务端是否有新的消息传输。再以服务端应用为例,有时一个请求可能要涉及多个线程并发执行,所以需要多线程来处理请求。
-在JVM的进程中,所有的线程共享内存和其他可用的资源。每一个JVM进程在进入点(main方法)处都要启动一个主线程,其他线程都从主线程启动,成为执行过程中的一个独立部分。线程可以再不同的处理器上并行执行,同样也可以共享一个处理器,线程调度器负责处理多个线程共享一个处理器的情况。
-很多应用(特别是服务端应用)会处理很多任务,需要并行运行。这些任务中有些是非常重要的,需要实时执行的。而另外一些是后台任务,可以在CPU空闲时执行。任务是在不同的线程中运行的。举例子来说,服务端可能有一些低优先级的线程,它们会根据一些数据来计算统计信息。同时也会启动一些高优先级的进程用于处理传入的数据,响应对这些统计信息的请求。这里可能有很多的源数据,很多来自客户端的数据请求,每个请求都会使服务端短暂的停止后台计算的线程以响应这个请求。所以,你必须监控在运行的线程数目并且保证有足够的CPU时间来执行必要的计算。
-(译者注:这一段在原文中是在性能优化的章节,译者认为这可能是作者的不小心,似乎放在线程的章节更合适。)
-JVM的性能取决于其配置是否与应用的功能相匹配。尽管垃圾回收器和内存回收进程是自动管理内存的,但是你必须掌管它们的频率。通常来说,你的应用可使用的内存越多,那么这些会导致应用暂停的内存管理进程需要起作用的就越少。
-如果垃圾回收发生的频率比你想的要多很多,那么可以在启动JVM的时候为其配置更大的最大堆大小值。堆被填满的时间越久,就越能降低垃圾回收发生的频率。最大堆大小值可以在启动JVM的时候,用-Xmx参数来设定。默认的最大堆大小是被设置为可用的操作系统内存的四分之一,或者最小1GB。
如果问题出在经常重新分配内存,那么你可以把初始化堆大小设置为和最大堆大小一样。这就意味着JVM永远不需要为堆重新分配内存。但这样做就会失去动态堆大小适配的优化,堆的大小从一开始就被固定下来。配置初始化对大小是在启动JVM,用-Xms来设定。默认初始化堆大小会被设定为操作系统可用的物理内存的六十四分之一,或者设置一个最小值。这个值是根据不同的平台来确定的。
如果你清楚是哪种垃圾回收(minor gc或major gc)导致了性能问题,可以在不改变整个堆大小的情况下设定新生代和老生代的大小比例。对于需要产生大量临时对象的应用,需要增大新生代的比例(当然,后果是减小了老生代的大小)。对于长生命周期对象较多的应用,则需增大老生代的比例(自然需要减少新生代的大小)。以下几种方法可以用来设定新生代和老生代的大小:
--XX:NewRatio参数来具体指定新生代和老生代的大小比例。比如,如果想让老生代的大小是新生代的五倍,则设置参数为-XX:NewRatio=5,默认这个参数设定为2(即老生代占用堆空间的三分之二,新生代占用三分之一)。-Xmn参数设定初始化和最大新生代大小,那么堆中的剩余大小即是老生代的大小。-XX:NewSize和-XX:MaxNewSize参数设定初始化和最大新生代大小,那么堆中的剩余大小即是老生代的大小。每一个线程都有一个栈,用于保存函数调用、返回地址等等,这些栈有着对应的内存分配。如果线程过多,就会导致OutOfMemory错误。即使你有足够的空间的堆来存放对象,你的应用也可能会因为创建一个新的线程而崩溃。这种情况下,需要考虑限制线程中的栈大小的最大值。线程栈大小可以在JVM启动的时候,通过-Xss参数来设置,默认这个值被设定为320KB至1024KB之间,这和平台相关。
当开发或运行一个Java应用的时候,对JVM的性能进行监控是很重要的。配置JVM不是一次配置就万事大吉的,特别是你要应对的是Java服务器应用的情况。你必须持续的检查堆内存和非堆内存的分配和使用情况,线程数的创建情况和内存中加载的类的数据情况等。这些都是核心参数。
-使用Anturis控制台,你可以为任何的硬件组件上运行的JVM配置监控(例如,在一台电脑上运行的一个Tomcat网页服务器)。
-JVM监控可以使用以下衡量标准:
-日志用来记录用户操作、系统运行状态等,是一个系统的重要组成部分。然而由于日志并非系统核心功能,通常情况下并不受团队的重视。在出现问题需要通过日志来定位时,才发现日志还存在很多问题。
-日志记录的好坏直接关系到系统出现问题时定位的速度,同时可以通过对日志的观察和分析,提前发现系统可能的风险,避免线上事故的发生。
-我们在开发和运维NOS(网易对象存储,Netease Object Storage)的过程中,对整个系统的日志进行了分析优化,积累出一些经验,归纳如下。
我们通常使用的日志库(如log4j等),将日志基本分为以下几类(从低到高):
-TRACE - The TRACE Level designates finer-grained informational events than the DEBUG
-DEBUG – The DEBUG Level designates fine-grained informational events that are most useful to debug an application.
-INFO - The INFO level designates informational messages that highlight the progress of the application at coarse-grained level.
-WARN - The WARN level designates potentially harmful situations.
-ERROR - The ERROR level designates error events that might still allow the application to continue running.
-FATAL - The FATAL level designates very severe error events that will presumably lead the application to abort.
尽管log4j官方文档对各个日志级别进行了简单定义。然而在实践中,究竟哪些操作需要记入日志,哪种错误应该记为WARN级别,而哪种错误又为ERROR级别,还需要进行进一步讨论。
-关于该问题,在StackOverflow上有一个讨论贴进行过讨论。
-此处对贴子中的一些观点,加上我们在平时运维过程中遇到的相关问题进行归纳:
-Rule 1:整个团队(包括运维人员)需要对日志级别有明确的规定,什么日志记入什么级别的日志,什么级别的错误出现要如何处理等
-由于DEBUG(或TRACE)级别的日志对于定位问题至关重要,因此该种日志记录是否完备且不冗余、格式是否规范等也需要花费大量精力来优化。此处有以下几个比较好的实践:
-Rule 2:需要定期对日志内容进行优化更新,目的就是通过日志快速准确的定位问题
-日志从功能来说,可分为诊断日志、统计日志、审计日志。
-诊断日志, 典型的有:
-统计日志:
-审计日志:
-将不同需求的日志记入到不同的日志文件中,可以方便相关问题(管理平台操作审计,用户操作计费等)的处理。针对每一种需求,需要对日志的格式,日志记录的内容等进行特别的记录。
-Rule 3:要明确不同日志的用途,对日志内容进行分类
-在很多应用中,用户都需要通过Fuse方式来挂载使用NOS。
-POSIX标准中文件系统接口不允许文件 /a 与目录 /a/ 同时存在,而NOS作为对象存储系统,/a 和 /a/ 是不同的对象,是能够同时存在的,一般地,NOS 中我们会规定 /a/ 是目录,/a 是文件,目录对象大小为0。
-POSIX标准对文件的getattr操作,无论是 /a 还是 /a/,对应的请求都是 /a。为了避免遗漏,需分别向 NOS 请求 HeadObject(“/a“)和 HeadObject(“/a/“)。如果命中/a,说明 /a 是一个文件,不用再请求 getattr(“/a/“)。
-因此当用户访问 */a/b/c.txt* 时,实际上向NOS发送了以下请求:
-# HeadObject(“/a”)
-# HeadObject(“/a/”)
-# HeadObject(“/a/b”)
-# HeadObject(“/a/b/”)
-# HeadObject(“/a/b/c.txt”)
对于上面的请求,实际上HeadObject(“/a”)和HeadObject(“/a/b”)都会返回NoSuchKey错误,而Fuse正是该错误来判断该文件不存在,而可能是个目录的。
-然而对于NOS来说,这将导致产生大量无意义的NoSuchKey日志(整个日志文件的80%都是该错误日志)。这些日志对于开发人员进行日志观察,运维人员定位问题,日志监控等都造成了困难。
-Rule 4: 绝不要打印没有用的日志,防止无用日志淹没重要信息
-解决办法:Fuse请求时,在Http头部加入 User-Agent 字段,当NOS发现请求是 Fuse发过来的且为HeadObject操作且为NoSuchKey错误时,则不打印错误日志。
-问题描述:
-NOS提供分块上传的接口,用户可以通过以下的调用序列,来实现一次分块上传的流程:
-之前在某个产品上线初期,由于其开发人员对NOS的熟悉程度不够等原因。出现过如下问题:客户端常常会收到NoSuchUpload的错误。该错误出现的原因是,用户在未调用InitMultiUpload之前,或者在调用了CompleteMultiUpload(AbortMultiUpload)之后再次调用UploadPart。
-然而当我们查日志,希望可以看到该UploadPart请求对哪个UploadID进行操作,该UploadID又对应哪些操作时,却发现我们的日志中没有记录UploadPart请求对应的UploadID。
-类似的问题还有很多,很多针对特定请求的日志缺失,导致很多问题无法定位。
-因此,需要进一步对日志中需要记录哪些内容进行规定,此处推荐的需要在日志中记录的内容有:
-而不推荐记录日志的内容有:
-Rule 5:日志信息要准确全面,能做到仅凭日志就可以定位问题
-解决办法:整理所有的请求处理流程,针对每一个操作(去重,分块上传……)打印特定的日志。
-测试代码(单元测试,接口测试……)的日志同样重要。特别是,当一个测试失败时,可以通过日志很快确定是测试代码有问题,还是系统出现了故障,如果做不到这一点,那就需要优化测试的日志了。
-测试日志应该包含以下内容:
-Rule 6:要以同样严格的要求对待测试程序的日志
-在线上出现问题的时候,需要尽快发现问题并解决,而同时,需要借此机会好好思考一下当前系统的日志是否合理。需要考虑以下问题:
-通过系统出现的问题来优化日志,应该是一项长期的实践,不断地从日志发现系统的问题,不断地从系统异常发现日志的问题。
-Rule 7:日志的优化是一件持续不断需要投入精力的事,需要不断从错误中学习
-如今NOS有8台机器,共40个tomcat对外提供服务。通常用户在请求出错的时候,我们都希望用户告诉我们请求的RequestID,以此我们可以确定请求是在哪台机器上进行处理的。
-NOS通过以下信息生成一个请求的RequestID:
-因此我们可以通过一个简单的程序从RequestID中得到该请求的处理时间和处理请求的服务器地址,更方便的去查看日志:
- ./decode.sh 4b2c009a0a7800000142789f42b8ca96
- Thu Nov 21 11:06:12 CST 2013
- 10.120.202.150
- 4b2c009a
-Rule 8:在RequestID中尽量编码更多的信息
-在NOS性能测试中,之前存在的一个问题是,由于在打印错误堆栈的地方,并没有打印请求的RequestID,因此当一个请求出现错误时,很难(日志量太大)将该请求的错误堆栈和具体的请求关联起来。
-另一个问题是,NOS后端有视频服务器集群和图片处理服务器集群。因此我们可能会有以下需求:当用户视频截图失败时,用户会告诉我们请求的RequestID,由于NOS并没有将该RequestID转发到后端的图片处理服务器,因此无法利用该信息去查看视频处理服务器上的日志,而需要通过用户请求的URL进行查找。同时,由于我们无法知道该请求是在哪个具体的视频处理的worker上进行,进一步导致查找日志的困难。
-还有一个潜在的问题是:如果NOS将所有的日志收集起来(tomcat,图片处理集群,视频处理集群……),我们无法做到通过requestID来查找一个请求的处理流程。
-Rule 9:将一个请求的整个处理流程和唯一的requestID关联起来
-问题描述:
-NOS的DEBUG日志非常详细的记录了请求处理相关信息,然而由于DEBUG日志量太大,因此通常线上只开INFO级别日志。然而INFO级别的日志却有可能导致部分问题无法定位。NOS线上一个请求可能随机地分发到4台机器进行处理,因此如果某一种错误在一段时间内多次出现,它也会在4台服务器上都出现。
-因此我们推荐的做法是,选择一台机器开启DEBUG级别的日志,方便定位问题。其实该做法背后的目的是,在线上任何问题的时候,都可以通过日志最快的找到问题的根源。
-Rule 10:让一台机器开启DEBUG日志
-随着NOS开始服务越来越多的产品,NOS每次版本升级之后,通过对日志的观察来确定服务是否正常变得至关重要。同时在上线新功能时,来发人员需要通过观察一些特定的日志,来确定新功能是否工作正常。
-举例来说:
-NOS在实现了桶表缓存的功能之后,首先上线一台服务器,并对该功能是否工作正常进行观察。通过将桶缓存的所有操作(如插入,查找,过期删除等)以及桶缓存的状态(如缓存桶数量)都记录在DEBUG级别的日志中。将新上线的机器的日志级别调为DEBUG,并对桶缓存的相关操作是否正确,缓存桶数量等信息进行观察,确认一切正常之后再上线其他机器。
-Rule 11:新上线服务器后一定要对日志进行观察,特别地,开发人员可以通过观察日志来确认新功能是否工作正常
-NOS在接收到一个请求的时候,会记录请求的接收时间(T1),在请求处理完成待发送的时候,会记录请求发送时间(T2),通常一个请求的日志都记为INFO级别,然而当出现请求处理时间(T2-T1)超过一定时间(如10s)时,会将该日志提升为WARN级别。通过该方法,可以预先发现系统可能存在的一些问题。
-同样的慢操作日志还可以用来记录系统一些外部依赖的处理时间,如NOS依赖外部认证服务器来进行认证。我们会记录每个请求的认证时间,如果认证时间超过某个值,也需要将该事件的日志级别进行提升,这样我们可以尽早发现认证服务器是不是需要扩容等问题。
-慢日志的时间阀值应该是可以动态调整的,这样在进行系统优化时,可以将该报警时间阀值逐渐调小,不断地对系统进行优化。
-Rule 12:通过日志级别的提升来发现潜在问题
-错误日志报警:
-NOS通过[运维平台|https://m.hz.netease.com/]设置了日志监控报警,周期性的(1分钟,5分钟)对服务器新产生的日志进行监控,如果发现错误数超过某个阀值,则进行报警。这类报警通常不一定是我们服务本身的问题,也有可能是用户使用NOS不当造成的。
-此处需要注意的问题是,日志报警相当于grep操作,如果日志量过大,或者匹配规则过多,可能对线上的服务产生影响。因此在设置好日志报警后,需要周期性的关注每次日志扫描的时间,评估日志监控是否对服务产生影响。
-Rule 13:对日志进行监控报警,比客户先发现系统问题
-关键字报警:
-NOS为每个用户分配了一定量的存储配额,当用户容量超限时,会限制用户的上传操作。通过在日志中记录关键字,如“Quota Warning”等,可以及时提醒用户进行扩容,避免用户服务中断。
-类似的关键字报警还有很多:如对InternalError的数量进行监控,对缓存的桶数量进行监控等等。
-Rule 14:通过日志中的关键字来确定系统的运行状态
-日志格式一定要统一,不能任由开发人员的喜好来。举例来说,对于NOS视频截图超时的ERROR日志,有以下几种方式打印:
-第一种:
-logger.error(“Gearman timeout exception for request ” + getRequestID() + ” value: ” + value, e);
第二种:
-logger.error(“RequestID: ” + getRequestID() + “, Error Message: Gearman timeout exception: ” + e);
第三种:
-logger.error(getErrorMessage(getRequestID(), getErrorMessage(), e));
第一种方式打印日志即是开发人员按照自己的喜好来的,这种方法带来的问题是:
-而第三种方式,通过一个函数来规范日志格式,所有开发人员便可以通过该接口实现统一的日志。
-Rule 15:日志格式要统一规范
-在性能测试中遇到的另一个问题是,当并发量很大时,可能会有一些请求处理失败(如0.5%),为了对这些错误进行分析,需要去查这些错误请求的日志。而由于这种情况下并发量很大,使得对错误日志的分析变得困难。
-这种情况下可以将所有的错误日志同时输出到一个单独的文件之中。
-Rule 16:将错误日志输出到一个单独的文件中进行分析
-日志文件不宜过大,过大的日志文件对于日志监控,问题定位等都会带来不便。因此需要进行日志文件的切分,日志文件的切分可以通过log4j等日志工具来配置,日志文件应该按天来分割,还是按照小时来分割,应该根据日志量来决定,原则就是方便开发或运维人员能快速查找日志。
-为了防止日志文件将整个磁盘空间占满,需要定期对日志文件进行删除。例如,在收到磁盘报警时,可以将两个月以前的日志文件删除。此处比较好的实践是:
-log4j关于日志切分的相关配置,可以参考这篇文章。
-Rule 17:要把日志的大小,如何切分,如何删除等作为规范建立起来
-此处对以上总结的所有经验进行汇总:
-[1] ”Optimal Logging” Anthony Vallone from Google http://googletesting.blogspot.jp/2013/06/optimal-logging.html
\ No newline at end of file diff --git a/src/malloc.md b/src/malloc.md deleted file mode 100644 index ec57169..0000000 --- a/src/malloc.md +++ /dev/null @@ -1,419 +0,0 @@ -任何一个用过或学过C的人对malloc都不会陌生。大家都知道malloc可以分配一段连续的内存空间,并且在不再使用时可以通过free释放掉。但是,许多程序员对malloc背后的事情并不熟悉,许多人甚至把malloc当做操作系统所提供的系统调用或C的关键字。实际上,malloc只是C的标准库中提供的一个普通函数,而且实现malloc的基本思想并不复杂,任何一个对C和操作系统有些许了解的程序员都可以很容易理解。
-这篇文章通过实现一个简单的malloc来描述malloc背后的机制。当然与现有C的标准库实现(例如glibc)相比,我们实现的malloc并不是特别高效,但是这个实现比目前真实的malloc实现要简单很多,因此易于理解。重要的是,这个实现和真实实现在基本原理上是一致的。
-这篇文章将首先介绍一些所需的基本知识,如操作系统对进程的内存管理以及相关的系统调用,然后逐步实现一个简单的malloc。为了简单起见,这篇文章将只考虑x86_64体系结构,操作系统为Linux。
- - -在实现malloc之前,先要相对正式地对malloc做一个定义。
-根据标准C库函数的定义,malloc具有如下原型:
-void* malloc(size_t size); --
这个函数要实现的功能是在系统中分配一段连续的可用的内存,具体有如下要求:
-对于malloc更多的说明可以在命令行中键入以下命令查看:
-man malloc --
在实现malloc之前,需要先解释一些Linux系统内存相关的知识。
-为了简单,现代操作系统在处理内存地址时,普遍采用虚拟内存地址技术。即在汇编程序(或机器语言)层面,当涉及内存地址时,都是使用虚拟内存地址。采用这种技术时,每个进程仿佛自己独享一片$2^N$字节的内存,其中$N$是机器位数。例如在64位CPU和64位操作系统下,每个进程的虚拟地址空间为$2^{64}$Byte。
-这种虚拟地址空间的作用主要是简化程序的编写及方便操作系统对进程间内存的隔离管理,真实中的进程不太可能(也用不到)如此大的内存空间,实际能用到的内存取决于物理内存大小。
-由于在机器语言层面都是采用虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运行的实际上下文将虚拟地址转换为物理内存地址,才能实现对真实内存数据的操作。这个转换一般由一个叫MMU(Memory Management Unit)的硬件完成。
-在现代操作系统中,不论是虚拟内存还是物理内存,都不是以字节为单位进行管理的,而是以页(Page)为单位。一个内存页是一段固定大小的连续内存地址的总称,具体到Linux中,典型的内存页大小为4096Byte(4K)。
-所以内存地址可以分为页号和页内偏移量。下面以64位机器,4G物理内存,4K页大小为例,虚拟内存地址和物理内存地址的组成如下:
-
上面是虚拟内存地址,下面是物理内存地址。由于页大小都是4K,所以页内偏移都是用低12位表示,而剩下的高地址表示页号。
-MMU映射单位并不是字节,而是页,这个映射通过查一个常驻内存的数据结构页表来实现。现在计算机具体的内存地址映射比较复杂,为了加快速度会引入一系列缓存和优化,例如TLB等机制。下面给出一个经过简化的内存地址翻译示意图,虽然经过了简化,但是基本原理与现代计算机真实的情况的一致的。
-
我们知道一般将内存看做磁盘的的缓存,有时MMU在工作时,会发现页表表明某个内存页不在物理内存中,此时会触发一个缺页异常(Page Fault),此时系统会到磁盘中相应的地方将磁盘页载入到内存中,然后重新执行由于缺页而失败的机器指令。关于这部分,因为可以看做对malloc实现是透明的,所以不再详细讲述,有兴趣的可以参考《深入理解计算机系统》相关章节。
-最后附上一张在维基百科找到的更加符合真实地址翻译的流程供大家参考,这张图加入了TLB和缺页异常的流程(图片来源页)。
-
明白了虚拟内存和物理内存的关系及相关的映射机制,下面看一下具体在一个进程内是如何排布内存的。
-以Linux 64位系统为例。理论上,64bit内存地址可用空间为0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,这是个相当庞大的空间,Linux实际上只用了其中一小部分(256T)。
-根据Linux内核相关文档描述,Linux64位操作系统仅使用低47位,高17位做扩展(只能是全0或全1)。所以,实际用到的地址为空间为0x0000000000000000 ~ 0x00007FFFFFFFFFFF和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF,其中前面为用户空间(User Space),后者为内核空间(Kernel Space)。图示如下:
-
对用户来说,主要关注的空间是User Space。将User Space放大后,可以看到里面主要分为如下几段:
-下面我们主要关注Heap区域的操作。对整个Linux内存排布有兴趣的同学可以参考其它资料。
-一般来说,malloc所申请的内存主要从Heap区域分配(本文不考虑通过mmap申请大块内存的情况)。
-由上文知道,进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。Linux对堆的管理示意如下:
-
Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。
-由上文知道,要增加一个进程实际的可用堆大小,就需要将break指针向高地址移动。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:
-int brk(void *addr); -void *sbrk(intptr_t increment); --
brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1。
-一个小技巧是,如果将increment设置为0,则可以获得当前break的地址。
-另外需要注意的是,由于Linux是按页进行内存映射的,所以如果break被设置为没有按页大小对齐,则系统实际上会在最后映射一个完整的页,从而实际已映射的内存空间比break指向的地方要大一些。但是使用break之后的地址是很危险的(尽管也许break之后确实有一小块可用内存地址)。
-系统对每一个进程所分配的资源不是无限的,包括可映射的内存空间,因此每个进程有一个rlimit表示当前进程可用的资源上限。这个限制可以通过getrlimit系统调用得到,下面代码获取当前进程虚拟内存空间的rlimit:
-int main() {
- struct rlimit *limit = (struct rlimit *)malloc(sizeof(struct rlimit));
- getrlimit(RLIMIT_AS, limit);
- printf("soft limit: %ld, hard limit: %ld\n", limit->rlim_cur, limit->rlim_max);
-}
-
-其中rlimit是一个结构体:
-struct rlimit {
- rlim_t rlim_cur; /* Soft limit */
- rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
-};
-
-每种资源有软限制和硬限制,并且可以通过setrlimit对rlimit进行有条件设置。其中硬限制作为软限制的上限,非特权进程只能设置软限制,且不能超过硬限制。
-在正式开始讨论malloc的实现前,我们可以利用上述知识实现一个简单但几乎没法用于真实的玩具malloc,权当对上面知识的复习:
-/* 一个玩具malloc */
-#include <sys/types.h>
-#include <unistd.h>
-void *malloc(size_t size)
-{
- void *p;
- p = sbrk(0);
- if (sbrk(size) == (void *)-1)
- return NULL;
- return p;
-}
-
-这个malloc每次都在当前break的基础上增加size所指定的字节数,并将之前break的地址返回。这个malloc由于对所分配的内存缺乏记录,不便于内存释放,所以无法用于真实场景。
-下面严肃点讨论malloc的实现方案。
-首先我们要确定所采用的数据结构。一个简单可行方案是将堆内存空间以块(Block)的形式组织起来,每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址。
-可以用如下结构体定义一个block:
-typedef struct s_block *t_block;
-struct s_block {
- size_t size; /* 数据区大小 */
- t_block next; /* 指向下个块的指针 */
- int free; /* 是否是空闲块 */
- int padding; /* 填充4字节,保证meta块长度为8的倍数 */
- char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
-};
-
-由于我们只考虑64位机器,为了方便,我们在结构体最后填充一个int,使得结构体本身的长度为8的倍数,以便内存对齐。示意图如下:
-
现在考虑如何在block链中查找合适的block。一般来说有两种查找算法:
-两种方法各有千秋,best fit具有较高的内存使用率(payload较高),而first fit具有更好的运行效率。这里我们采用first fit算法。
-/* First fit */
-t_block find_block(t_block *last, size_t size) {
- t_block b = first_block;
- while(b && !(b->free && b->size >= size)) {
- *last = b;
- b = b->next;
- }
- return b;
-}
-
-find_block从frist_block开始,查找第一个符合要求的block并返回block起始地址,如果找不到这返回NULL。这里在遍历时会更新一个叫last的指针,这个指针始终指向当前遍历的block。这是为了如果找不到合适的block而开辟新block使用的,具体会在接下来的一节用到。
-如果现有block都不能满足size的要求,则需要在链表最后开辟一个新的block。这里关键是如何只使用sbrk创建一个struct:
-#define BLOCK_SIZE 24 /* 由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 */
-
-t_block extend_heap(t_block last, size_t s) {
- t_block b;
- b = sbrk(0);
- if(sbrk(BLOCK_SIZE + s) == (void *)-1)
- return NULL;
- b->size = s;
- b->next = NULL;
- if(last)
- last->next = b;
- b->free = 0;
- return b;
-}
-
-First fit有一个比较致命的缺点,就是可能会让很小的size占据很大的一块block,此时,为了提高payload,应该在剩余数据区足够大的情况下,将其分裂为一个新的block,示意如下:
-
实现代码:
-void split_block(t_block b, size_t s) {
- t_block new;
- new = b->data + s;
- new->size = b->size - s - BLOCK_SIZE ;
- new->next = b->next;
- new->free = 1;
- b->size = s;
- b->next = new;
-}
-
-有了上面的代码,我们可以利用它们整合成一个简单但初步可用的malloc。注意首先我们要定义个block链表的头first_block,初始化为NULL;另外,我们需要剩余空间至少有BLOCK_SIZE + 8才执行分裂操作。
-由于我们希望malloc分配的数据区是按8字节对齐,所以在size不为8的倍数时,我们需要将size调整为大于size的最小的8的倍数:
-size_t align8(size_t s) {
- if(s & 0x7 == 0)
- return s;
- return ((s >> 3) + 1) << 3;
-}
-
-#define BLOCK_SIZE 24
-void *first_block=NULL;
-
-/* other functions... */
-
-void *malloc(size_t size) {
- t_block b, last;
- size_t s;
- /* 对齐地址 */
- s = align8(size);
- if(first_block) {
- /* 查找合适的block */
- last = first_block;
- b = find_block(&last, s);
- if(b) {
- /* 如果可以,则分裂 */
- if ((b->size - s) >= ( BLOCK_SIZE + 8))
- split_block(b, s);
- b->free = 0;
- } else {
- /* 没有合适的block,开辟一个新的 */
- b = extend_heap(last, s);
- if(!b)
- return NULL;
- }
- } else {
- b = extend_heap(NULL, s);
- if(!b)
- return NULL;
- first_block = b;
- }
- return b->data;
-}
-
-有了malloc,实现calloc只要两步:
-由于我们的数据区是按8字节对齐的,所以为了提高效率,我们可以每8字节一组置0,而不是一个一个字节设置。我们可以通过新建一个size_t指针,将内存区域强制看做size_t类型来实现。
-void *calloc(size_t number, size_t size) {
- size_t *new;
- size_t s8, i;
- new = malloc(number * size);
- if(new) {
- s8 = align8(number * size) >> 3;
- for(i = 0; i < s8; i++)
- new[i] = 0;
- }
- return new;
-}
-
-free的实现并不像看上去那么简单,这里我们要解决两个关键问题:
-首先我们要保证传入free的地址是有效的,这个有效包括两方面:
-第一个问题比较好解决,只要进行地址比较就可以了,关键是第二个问题。这里有两种解决方案:一是在结构体内埋一个magic number字段,free之前通过相对偏移检查特定位置的值是否为我们设置的magic number,另一种方法是在结构体内增加一个magic pointer,这个指针指向数据区的第一个字节(也就是在合法时free时传入的地址),我们在free前检查magic pointer是否指向参数所指地址。这里我们采用第二种方案:
-首先我们在结构体中增加magic pointer(同时要修改BLOCK_SIZE):
-typedef struct s_block *t_block;
-struct s_block {
- size_t size; /* 数据区大小 */
- t_block next; /* 指向下个块的指针 */
- int free; /* 是否是空闲块 */
- int padding; /* 填充4字节,保证meta块长度为8的倍数 */
- void *ptr; /* Magic pointer,指向data */
- char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
-};
-
-然后我们定义检查地址合法性的函数:
-t_block get_block(void *p) {
- char *tmp;
- tmp = p;
- return (p = tmp -= BLOCK_SIZE);
-}
-
-int valid_addr(void *p) {
- if(first_block) {
- if(p > first_block && p < sbrk(0)) {
- return p == (get_block(p))->ptr;
- }
- }
- return 0;
-}
-
-当多次malloc和free后,整个内存池可能会产生很多碎片block,这些block很小,经常无法使用,甚至出现许多碎片连在一起,虽然总体能满足某此malloc要求,但是由于分割成了多个小block而无法fit,这就是碎片问题。
-一个简单的解决方式时当free某个block时,如果发现它相邻的block也是free的,则将block和相邻block合并。为了满足这个实现,需要将s_block改为双向链表。修改后的block结构如下:
-typedef struct s_block *t_block;
-struct s_block {
- size_t size; /* 数据区大小 */
- t_block prev; /* 指向上个块的指针 */
- t_block next; /* 指向下个块的指针 */
- int free; /* 是否是空闲块 */
- int padding; /* 填充4字节,保证meta块长度为8的倍数 */
- void *ptr; /* Magic pointer,指向data */
- char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
-};
-
-合并方法如下:
-t_block fusion(t_block b) {
- if (b->next && b->next->free) {
- b->size += BLOCK_SIZE + b->next->size;
- b->next = b->next->next;
- if(b->next)
- b->next->prev = b;
- }
- return b;
-}
-
-有了上述方法,free的实现思路就比较清晰了:首先检查参数地址的合法性,如果不合法则不做任何事;否则,将此block的free标为1,并且在可以的情况下与后面的block进行合并。如果当前是最后一个block,则回退break指针释放进程内存,如果当前block是最后一个block,则回退break指针并设置first_block为NULL。实现如下:
-void free(void *p) {
- t_block b;
- if(valid_addr(p)) {
- b = get_block(p);
- b->free = 1;
- if(b->prev && b->prev->free)
- b = fusion(b->prev);
- if(b->next)
- fusion(b);
- else {
- if(b->prev)
- b->prev->prev = NULL;
- else
- first_block = NULL;
- brk(b);
- }
- }
-}
-
-为了实现realloc,我们首先要实现一个内存复制方法。如同calloc一样,为了效率,我们以8字节为单位进行复制:
-void copy_block(t_block src, t_block dst) {
- size_t *sdata, *ddata;
- size_t i;
- sdata = src->ptr;
- ddata = dst->ptr;
- for(i = 0; (i * 8) < src->size && (i * 8) < dst->size; i++)
- ddata[i] = sdata[i];
-}
-
-然后我们开始实现realloc。一个简单(但是低效)的方法是malloc一段内存,然后将数据复制过去。但是我们可以做的更高效,具体可以考虑以下几个方面:
-下面是realloc的实现:
-void *realloc(void *p, size_t size) {
- size_t s;
- t_block b, new;
- void *newp;
- if (!p)
- /* 根据标准库文档,当p传入NULL时,相当于调用malloc */
- return malloc(size);
- if(valid_addr(p)) {
- s = align8(size);
- b = get_block(p);
- if(b->size >= s) {
- if(b->size - s >= (BLOCK_SIZE + 8))
- split_block(b,s);
- } else {
- /* 看是否可进行合并 */
- if(b->next && b->next->free
- && (b->size + BLOCK_SIZE + b->next->size) >= s) {
- fusion(b);
- if(b->size - s >= (BLOCK_SIZE + 8))
- split_block(b, s);
- } else {
- /* 新malloc */
- newp = malloc (s);
- if (!newp)
- return NULL;
- new = get_block(newp);
- copy_block(b, new);
- free(p);
- return(newp);
- }
- }
- return (p);
- }
- return NULL;
-}
-
-以上是一个较为简陋,但是初步可用的malloc实现。还有很多遗留的可能优化点,例如:
-还有很多可能的优化,这里不一一赘述。下面附上一些参考文献,有兴趣的同学可以更深入研究。
-早期计算机比现在更为简单。系统的各种组件例如CPU,内存,大容量存储器和网口,由于被共同开发因而有非常均衡的表现。例如,内存和网口并不比CPU在提供数据的时候更(特别的)快。
-曾今计算机稳定的基本结构悄然改变,硬件开发人员开始致力于优化单个子系统。于是电脑一些组件的性能大大的落后因而成为了瓶颈。由于开销的原因,大容量存储器和内存子系统相对于其他组件来说改善得更为缓慢。
-大容量存储的性能问题往往靠软件来改善: 操作系统将常用(且最有可能被用)的数据放在主存中,因为后者的速度要快上几个数量级。或者将缓存加入存储设备中,这样就可以在不修改操作系统的前提下提升性能。{然而,为了在使用缓存时保证数据的完整性,仍然要作出一些修改。}这些内容不在本文的谈论范围之内,就不作赘述了。
-而解决内存的瓶颈更为困难,它与大容量存储不同,几乎每种方案都需要对硬件作出修改。目前,这些变更主要有以下这些方式:
-本文主要关心的是CPU缓存和内存控制器的设计。在讨论这些主题的过程中,我们还会研究DMA。不过,我们首先会从当今商用硬件的设计谈起。这有助于我们理解目前在使用内存子系统时可能遇到的问题和限制。我们还会详细介绍RAM的分类,说明为什么会存在这么多不同类型的内存。
-本文不会包括所有内容,也不会包括最终性质的内容。我们的讨论范围仅止于商用硬件,而且只限于其中的一小部分。另外,本文中的许多论题,我们只会点到为止,以达到本文目标为标准。对于这些论题,大家可以阅读其它文档,获得更详细的说明。
-当本文提到操作系统特定的细节和解决方案时,针对的都是Linux。无论何时都不会包含别的操作系统的任何信息,作者无意讨论其他操作系统的情况。如果读者认为他/她不得不使用别的操作系统,那么必须去要求供应商提供其操作系统类似于本文的文档。
-在开始之前最后的一点说明,本文包含大量出现的术语“经常”和别的类似的限定词。这里讨论的技术在现实中存在于很多不同的实现,所以本文只阐述使用得最广泛最主流的版本。在阐述中很少有地方能用到绝对的限定词。
-这个文档主要视为软件开发者而写的。本文不会涉及太多硬件细节,所以喜欢硬件的读者也许不会觉得有用。但是在我们讨论一些有用的细节之前,我们先要描述足够多的背景。
-在这个基础上,本文的第二部分将描述RAM(随机寄存器)。懂得这个部分的内容很好,但是此部分的内容并不是懂得其后内容必须部分。我们会在之后引用不少之前的部分,所以心急的读者可以跳过任何章节来读他们认为有用的部分。
-第三部分会谈到不少关于CPU缓存行为模式的内容。我们会列出一些图标,这样你们不至于觉得太枯燥。第三部分对于理解整个文章非常重要。第四部分将简短的描述虚拟内存是怎么被实现的。这也是你们需要理解全文其他部分的背景知识之一。
-第五部分会提到许多关于Non Uniform Memory Access (NUMA)系统。
-第六部分是本文的中心部分。在这个部分里面,我们将回顾其他许多部分中的信息,并且我们将给阅读本文的程序员许多在各种情况下的编程建议。如果你真的很心急,那么你可以直接阅读第六部分,并且我们建议你在必要的时候回到之前的章节回顾一下必要的背景知识。
-本文的第七部分将介绍一些能够帮助程序员更好的完成任务的工具。即便在彻底理解了某一项技术的情况下,距离彻底理解在非测试环境下的程序还是很遥远的。我们需要借助一些工具。
-第八部分,我们将展望一些在未来我们可能认为好用的科技。
-1.2 反馈问题
-作者会不定期更新本文档。这些更新既包括伴随技术进步而来的更新也包含更改错误。非常欢迎有志于反馈问题的读者发送电子邮件。
-1.3 致谢
-我首先需要感谢Johnray Fuller尤其是Jonathan Corbet,感谢他们将作者的英语转化成为更为规范的形式。Markus Armbruster提供大量本文中对于问题和缩写有价值的建议。
-1.4 关于本文
-本文题目对David Goldberg的经典文献《What Every Computer Scientist Should Know About Floating-Point Arithmetic》[goldberg]表示致敬。Goldberg的论文虽然不普及,但是对于任何有志于严格编程的人都会是一个先决条件。
-鉴于目前专业硬件正在逐渐淡出,理解商用硬件的现状变得十分重要。现如今,人们更多的采用水平扩展,也就是说,用大量小型、互联的商用计算机代替巨大、超快(但超贵)的系统。原因在于,快速而廉价的网络硬件已经崛起。那些大型的专用系统仍然有一席之地,但已被商用硬件后来居上。2007年,Red Hat认为,未来构成数据中心的“积木”将会是拥有最多4个插槽的计算机,每个插槽插入一个四核CPU,这些CPU都是超线程的。{超线程使单个处理器核心能同时处理两个以上的任务,只需加入一点点额外硬件}。也就是说,这些数据中心中的标准系统拥有最多64个虚拟处理器。当然可以支持更大的系统,但人们认为4插槽、4核CPU是最佳配置,绝大多数的优化都针对这样的配置。
-在不同商用计算机之间,也存在着巨大的差异。不过,我们关注在主要的差异上,可以涵盖到超过90%以上的硬件。需要注意的是,这些技术上的细节往往日新月异,变化极快,因此大家在阅读的时候也需要注意本文的写作时间。
-这么多年来,个人计算机和小型服务器被标准化到了一个芯片组上,它由两部分组成: 北桥和南桥,见图2.1。
----
![]()
图2.1 北桥和南桥组成的结构
-
CPU通过一条通用总线(前端总线,FSB)连接到北桥。北桥主要包括内存控制器和其它一些组件,内存控制器决定了RAM芯片的类型。不同的类型,包括DRAM、Rambus和SDRAM等等,要求不同的内存控制器。
-为了连通其它系统设备,北桥需要与南桥通信。南桥又叫I/O桥,通过多条不同总线与设备们通信。目前,比较重要的总线有PCI、PCI Express、SATA和USB总线,除此以外,南桥还支持PATA、IEEE 1394、串行口和并行口等。比较老的系统上有连接北桥的AGP槽。那是由于南北桥间缺乏高速连接而采取的措施。现在的PCI-E都是直接连到南桥的。
-这种结构有一些需要注意的地方:
-第二个瓶颈来自北桥与RAM间的总线。总线的具体情况与内存的类型有关。在早期的系统上,只有一条总线,因此不能实现并行访问。近期的RAM需要两条独立总线(或者说通道,DDR2就是这么叫的,见图2.8),可以实现带宽加倍。北桥将内存访问交错地分配到两个通道上。更新的内存技术(如FB-DRAM)甚至加入了更多的通道。
-由于带宽有限,我们需要以一种使延迟最小化的方式来对内存访问进行调度。我们将会看到,处理器的速度比内存要快得多,需要等待内存。如果有多个超线程核心或CPU同时访问内存,等待时间则会更长。对于DMA也是同样。
-除了并发以外,访问模式也会极大地影响内存子系统、特别是多通道内存子系统的性能。关于访问模式,可参见2.2节。
-在一些比较昂贵的系统上,北桥自己不含内存控制器,而是连接到外部的多个内存控制器上(在下例中,共有4个)。
---这种架构的好处在于,多条内存总线的存在,使得总带宽也随之增加了。而且也可以支持更多的内存。通过同时访问不同内存区,还可以降低延时。对于像图2.2中这种多处理器直连北桥的设计来说,尤其有效。而这种架构的局限在于北桥的内部带宽,非常巨大(来自Intel)。{出于完整性的考虑,还需要补充一下,这样的内存控制器布局还可以用于其它用途,比如说「内存RAID」,它可以与热插拔技术一起使用。} --
![]()
图2.2 拥有外部控制器的北桥
-
使用外部内存控制器并不是唯一的办法,另一个最近比较流行的方法是将控制器集成到CPU内部,将内存直连到每个CPU。这种架构的走红归功于基于AMD Opteron处理器的SMP系统。图2.3展示了这种架构。Intel则会从Nehalem处理器开始支持通用系统接口(CSI),基本上也是类似的思路——集成内存控制器,为每个处理器提供本地内存。
----
![]()
图2.3 集成的内存控制器
-
通过采用这样的架构,系统里有几个处理器,就可以有几个内存库(memory bank)。比如,在4 CPU的计算机上,不需要一个拥有巨大带宽的复杂北桥,就可以实现4倍的内存带宽。另外,将内存控制器集成到CPU内部还有其它一些优点,这里就不赘述了。
-同样也有缺点。首先,系统仍然要让所有内存能被所有处理器所访问,导致内存不再是统一的资源(NUMA即得名于此)。处理器能以正常的速度访问本地内存(连接到该处理器的内存)。但它访问其它处理器的内存时,却需要使用处理器之间的互联通道。比如说,CPU 1如果要访问CPU 2的内存,则需要使用它们之间的互联通道。如果它需要访问CPU 4的内存,那么需要跨越两条互联通道。
-使用互联通道是有代价的。在讨论访问远端内存的代价时,我们用「NUMA因子」这个词。在图2.3中,每个CPU有两个层级: 相邻的CPU,以及两个互联通道外的CPU。在更加复杂的系统中,层级也更多。甚至有些机器有不止一种连接,比如说IBM的x445和SGI的Altix系列。CPU被归入节点,节点内的内存访问时间是一致的,或者只有很小的NUMA因子。而在节点之间的连接代价很大,而且有巨大的NUMA因子。
-目前,已经有商用的NUMA计算机,而且它们在未来应该会扮演更加重要的角色。人们预计,从2008年底开始,每台SMP机器都会使用NUMA。每个在NUMA上运行的程序都应该认识到NUMA的代价。在第5节中,我们将讨论更多的架构,以及Linux内核为这些程序提供的一些技术。
-除了本节中所介绍的技术之外,还有其它一些影响RAM性能的因素。它们无法被软件所左右,所以没有放在这里。如果大家有兴趣,可以在第2.1节中看一下。介绍这些技术,仅仅是因为它们能让我们绘制的RAM技术全图更为完整,或者是可能在大家购买计算机时能够提供一些帮助。
-以下的两节主要介绍一些入门级的硬件知识,同时讨论内存控制器与DRAM芯片间的访问协议。这些知识解释了内存访问的原理,程序员可能会得到一些启发。不过,这部分并不是必读的,心急的读者可以直接跳到第2.2.5节。
-这些年来,出现了许多不同类型的RAM,各有差异,有些甚至有非常巨大的不同。那些很古老的类型已经乏人问津,我们就不仔细研究了。我们主要专注于几类现代RAM,剖开它们的表面,研究一下内核和应用开发人员们可以看到的一些细节。
-第一个有趣的细节是,为什么在同一台机器中有不同的RAM?或者说得更详细一点,为什么既有静态RAM(SRAM {SRAM还可以表示「同步内存」。}),又有动态RAM(DRAM)。功能相同,前者更快。那么,为什么不全部使用SRAM?答案是,代价。无论在生产还是在使用上,SRAM都比DRAM要贵得多。生产和使用,这两个代价因子都很重要,后者则是越来越重要。为了理解这一点,我们分别看一下SRAM和DRAM一个位的存储的实现过程。
-在本节的余下部分,我们将讨论RAM实现的底层细节。我们将尽量控制细节的层面,比如,在「逻辑的层面」讨论信号,而不是硬件设计师那种层面,因为那毫无必要。
----
![]()
图2.6 6-T静态RAM
-
图2.4展示了6晶体管SRAM的一个单元。核心是4个晶体管M1-M4,它们组成两个交叉耦合的反相器。它们有两个稳定的状态,分别代表0和1。只要保持Vdd有电,状态就是稳定的。
-当需要访问单元的状态时,升起字访问线WL。BL和BL上就可以读取状态。如果需要覆盖状态,先将BL和BL设置为期望的值,然后升起WL。由于外部的驱动强于内部的4个晶体管,所以旧状态会被覆盖。
-更多详情,可以参考[sramwiki]。为了下文的讨论,需要注意以下问题:
-一个单元需要6个晶体管。也有采用4个晶体管的SRAM,但有缺陷。
-维持状态需要恒定的电源。
-升起WL后立即可以读取状态。信号与其它晶体管控制的信号一样,是直角的(快速在两个状态间变化)。
-状态稳定,不需要刷新循环。
-SRAM也有其它形式,不那么费电,但比较慢。由于我们需要的是快速RAM,因此不在关注范围内。这些较慢的SRAM的主要优点在于接口简单,比动态RAM更容易使用。
-动态RAM比静态RAM要简单得多。图2.5展示了一种普通DRAM的结构。它只含有一个晶体管和一个电容器。显然,这种复杂性上的巨大差异意味着功能上的迥异。
----
![]()
图2.5 1-T动态RAM
-
动态RAM的状态是保持在电容器C中。晶体管M用来控制访问。如果要读取状态,升起访问线AL,这时,可能会有电流流到数据线DL上,也可能没有,取决于电容器是否有电。如果要写入状态,先设置DL,然后升起AL一段时间,直到电容器充电或放电完毕。
-动态RAM的设计有几个复杂的地方。由于读取状态时需要对电容器放电,所以这一过程不能无限重复,不得不在某个点上对它重新充电。
-更糟糕的是,为了容纳大量单元(现在一般在单个芯片上容纳10的9次方以上的RAM单元),电容器的容量必须很小(0.000000000000001法拉以下)。这样,完整充电后大约持有几万个电子。即使电容器的电阻很大(若干兆欧姆),仍然只需很短的时间就会耗光电荷,称为「泄漏」。
-这种泄露就是现在的大部分DRAM芯片每隔64ms就必须进行一次刷新的原因。在刷新期间,对于该芯片的访问是不可能的,这甚至会造成半数任务的延宕。(相关内容请察看【highperfdram】一章)
-这个问题的另一个后果就是无法直接读取芯片单元中的信息,而必须通过信号放大器将0和1两种信号间的电势差增大。
-最后一个问题在于电容器的冲放电是需要时间的,这就导致了信号放大器读取的信号并不是典型的矩形信号。所以当放大器输出信号的时候就需要一个小小的延宕,相关公式如下
----
这就意味着需要一些时间(时间长短取决于电容C和电阻R)来对电容进行冲放电。另一个负面作用是,信号放大器的输出电流不能立即就作为信号载体使用。图2.6显示了冲放电的曲线,x轴表示的是单位时间下的R*C
-
与静态RAM可以即刻读取数据不同的是,当要读取动态RAM的时候,必须花一点时间来等待电容的冲放电完全。这一点点的时间最终限制了DRAM的速度。
-当然了,这种读取方式也是有好处的。最大的好处在于缩小了规模。一个动态RAM的尺寸是小于静态RAM的。这种规模的减小不单单建立在动态RAM的简单结构之上,也是由于减少了静态RAM的各个单元独立的供电部分。以上也同时导致了动态RAM模具的简单化。
-综上所述,由于不可思议的成本差异,除了一些特殊的硬件(包括路由器什么的)之外,我们的硬件大多是使用DRAM的。这一点深深的影响了咱们这些程序员,后文将会对此进行讨论。在此之前,我们还是先了解下DRAM的更多细节。
-2.1.3 DRAM 访问
-一个程序选择了一个内存位置使用到了一个虚拟地址。处理器转换这个到物理地址最后将内存控制选择RAM芯片匹配了那个地址。在RAM芯片去选择单个内存单元,部分的物理地址以许多地址行的形式被传递。
-它单独地去处理来自于内存控制器的内存位置将完全不切实际:4G的RAM将需要 232 地址行。地址传递DRAM芯片的这种方式首先必须被路由器解析。一个路由器的N多地址行将有2N 输出行。这些输出行能被使用到选择内存单元。使用这个直接方法对于小容量芯片不再是个大问题
-但如果许多的单元生成这种方法不在适合。一个1G的芯片容量(我反感那些SI前缀,对于我一个giga-bit将总是230 而不是109字节)将需要30地址行和230 选项行。一个路由器的大小及许多的输入行以指数方式递增当速度不被牺牲时。一个30地址行路由器需要一大堆芯片的真实身份另外路由器也就复杂起来了。更重要的是,传递30脉冲在地址行同步要比仅仅传递15脉冲困难的多。较少列能精确布局相同长度或恰当的时机(现代DRAM类型像DDR3能自动调整时序但这个限制能让他什么都能忍受)
-
图2.7展示了一个很高级别的一个DRAM芯片,DRAM被组织在行和列里。他们能在一行中对奇但DRAM芯片需要一个大的路由器。通过阵列方法设计能被一个路由器和一个半的multiplexer获得{多路复用器(multiplexer)和路由器是一样的,这的multiplexer需要以路由器身份工作当写数据时候。那么从现在开始我们开始讨论其区别.}这在所有方面会是一个大的存储。例如地址linesa0和a1通过行地址选择路由器来选择整个行的芯片的地址列,当读的时候,所有的芯片目录能使其纵列选择路由器可用,依据地址linesa2和a3一个纵列的目录用于数据DRAM芯片的接口类型。这发生了许多次在许多DRAM芯片产生一个总记录数的字节匹配给一个宽范围的数据总线。
-对于写操作,内存单元的数据新值被放到了数据总线,当使用RAS和CAS方式选中内存单元时,数据是存放在内存单元内的。这是一个相当直观的设计,在现实中——很显然——会复杂得多,对于读,需要规范从发出信号到数据在数据总线上变得可读的时延。电容不会像前面章节里面描述的那样立刻自动放电,从内存单元发出的信号是如此这微弱以至于它需要被放大。对于写,必须规范从数据RAS和CAS操作完成后到数据成功的被写入内存单元的时延(当然,电容不会立刻自动充电和放电)。这些时间常量对于DRAM芯片的性能是至关重要的,我们将在下章讨论它。
-另一个关于伸缩性的问题是,用30根地址线连接到每一个RAM芯片是行不通的。芯片的针脚是非常珍贵的资源,以至数据必须能并行传输就并行传输(比如:64位为一组)。内存控制器必须有能力解析每一个RAM模块(RAM芯片集合)。如果因为性能的原因要求并发行访问多个RAM模块并且每个RAM模块需要自己独占的30或多个地址线,那么对于8个RAM模块,仅仅是解析地址,内存控制器就需要240+之多的针脚。
-在很长一段时间里,地址线被复用以解决DRAM芯片的这些次要的可扩展性问题。这意味着地址被转换成两部分。第一部分由地址位a0和a1选择行(如图2.7)。这个选择保持有效直到撤销。然后是第二部分,地址位a2和a3选择列。关键差别在于:只需要两根外部地址线。需要一些很少的线指明RAS和CAS信号有效,但是把地址线的数目减半所付出的代价更小。可是地址复用也带来自身的一些问题。我们将在2.2章中提到。 -2.1.4 总结
-如果这章节的内容有些难以应付,不用担心。纵观这章节的重点,有:
-在上文介绍DRAM的时候,我们已经看到DRAM芯片为了节约资源,对地址进行了复用。而且,访问DRAM单元是需要一些时间的,因为电容器的放电并不是瞬时的。此外,我们还看到,DRAM需要不停地刷新。在这一节里,我们将把这些因素拼合起来,看看它们是如何决定DRAM的访问过程。
-我们将主要关注在当前的科技上,不会再去讨论异步DRAM以及它的各种变体。如果对它感兴趣,可以去参考[highperfdram]及[arstechtwo]。我们也不会讨论Rambus DRAM(RDRAM),虽然它并不过时,但在系统内存领域应用不广。我们将主要介绍同步DRAM(SDRAM)及其后继者双倍速DRAM(DDR)。
-同步DRAM,顾名思义,是参照一个时间源工作的。由内存控制器提供一个时钟,时钟的频率决定了前端总线(FSB)的速度。FSB是内存控制器提供给DRAM芯片的接口。在我写作本文的时候,FSB已经达到800MHz、1066MHz,甚至1333MHz,并且下一代的1600MHz也已经宣布。但这并不表示时钟频率有这么高。实际上,目前的总线都是双倍或四倍传输的,每个周期传输2次或4次数据。报的越高,卖的越好,所以这些厂商们喜欢把四倍传输的200MHz总线宣传为“有效的”800MHz总线。
-以今天的SDRAM为例,每次数据传输包含64位,即8字节。所以FSB的传输速率应该是有效总线频率乘于8字节(对于4倍传输200MHz总线而言,传输速率为6.4GB/s)。听起来很高,但要知道这只是峰值速率,实际上无法达到的最高速率。我们将会看到,与RAM模块交流的协议有大量时间是处于非工作状态,不进行数据传输。我们必须对这些非工作时间有所了解,并尽量缩短它们,才能获得最佳的性能。
----
图2.8: SDRAM读访问的时序 -
图2.8展示了某个DRAM模块一些连接器上的活动,可分为三个阶段,图上以不同颜色表示。按惯例,时间为从左向右流逝。这里忽略了许多细节,我们只关注时钟频率、RAS与CAS信号、地址总线和数据总线。首先,内存控制器将行地址放在地址总线上,并降低RAS信号,读周期开始。所有信号都在时钟(CLK)的上升沿读取,因此,只要信号在读取的时间点上保持稳定,就算不是标准的方波也没有关系。设置行地址会促使RAM芯片锁住指定的行。
-CAS信号在tRCD(RAS到CAS时延)个时钟周期后发出。内存控制器将列地址放在地址总线上,降低CAS线。这里我们可以看到,地址的两个组成部分是怎么通过同一条总线传输的。
-至此,寻址结束,是时候传输数据了。但RAM芯片任然需要一些准备时间,这个时间称为CAS时延(CL)。在图2.8中CL为2。这个值可大可小,它取决于内存控制器、主板和DRAM模块的质量。CL还可能是半周期。假设CL为2.5,那么数据将在蓝色区域内的第一个下降沿准备就绪。
-既然数据的传输需要这么多的准备工作,仅仅传输一个字显然是太浪费了。因此,DRAM模块允许内存控制指定本次传输多少数据。可以是2、4或8个字。这样,就可以一次填满高速缓存的整条线,而不需要额外的RAS/CAS序列。另外,内存控制器还可以在不重置行选择的前提下发送新的CAS信号。这样,读取或写入连续的地址就可以变得非常快,因为不需要发送RAS信号,也不需要把行置为非激活状态(见下文)。是否要将行保持为“打开”状态是内存控制器判断的事情。让它一直保持打开的话,对真正的应用会有不好的影响(参见[highperfdram])。CAS信号的发送仅与RAM模块的命令速率(Command Rate)有关(常常记为Tx,其中x为1或2,高性能的DRAM模块一般为1,表示在每个周期都可以接收新命令)。
-在上图中,SDRAM的每个周期输出一个字的数据。这是第一代的SDRAM。而DDR可以在一个周期中输出两个字。这种做法可以减少传输时间,但无法降低时延。DDR2尽管看上去不同,但在本质上也是相同的做法。对于DDR2,不需要再深入介绍了,我们只需要知道DDR2更快、更便宜、更可靠、更节能(参见[ddrtwo])就足够了。
-图2.8并不完整,它只画出了访问DRAM的完整循环的一部分。在发送RAS信号之前,必须先把当前锁住的行置为非激活状态,并对新行进行预充电。在这里,我们主要讨论由于显式发送指令而触发以上行为的情况。协议本身作了一些改进,在某些情况下是可以省略这个步骤的,但预充电带来的时延还是会影响整个操作。
----
图2.9: SDRAM的预充电与激活 -
图2.9显示的是两次CAS信号的时序图。第一次的数据在CL周期后准备就绪。图中的例子里,是在SDRAM上,用两个周期传输了两个字的数据。如果换成DDR的话,则可以传输4个字。
-即使是在一个命令速率为1的DRAM模块上,也无法立即发出预充电命令,而要等数据传输完成。在上图中,即为两个周期。刚好与CL相同,但只是巧合而已。预充电信号并没有专用线,某些实现是用同时降低写使能(WE)线和RAS线的方式来触发。这一组合方式本身没有特殊的意义(参见[micronddr])。
-发出预充电信命令后,还需等待tRP(行预充电时间)个周期之后才能使行被选中。在图2.9中,这个时间(紫色部分)大部分与内存传输的时间(淡蓝色部分)重合。不错。但tRP大于传输时间,因此下一个RAS信号只能等待一个周期。
-如果我们补充完整上图中的时间线,最后会发现下一次数据传输发生在前一次的5个周期之后。这意味着,数据总线的7个周期中只有2个周期才是真正在用的。再用它乘于FSB速度,结果就是,800MHz总线的理论速率6.4GB/s降到了1.8GB/s。真是太糟了。第6节将介绍一些技术,可以帮助我们提高总线有效速率。程序员们也需要尽自己的努力。
-SDRAM还有一些定时值,我们并没有谈到。在图2.9中,预充电命令仅受制于数据传输时间。除此之外,SDRAM模块在RAS信号之后,需要经过一段时间,才能进行预充电(记为tRAS)。它的值很大,一般达到tRP的2到3倍。如果在某个RAS信号之后,只有一个CAS信号,而且数据只传输很少几个周期,那么就有问题了。假设在图2.9中,第一个CAS信号是直接跟在一个RAS信号后免的,而tRAS为8个周期。那么预充电命令还需要被推迟一个周期,因为tRCD、CL和tRP加起来才7个周期。
-DDR模块往往用w-z-y-z-T来表示。例如,2-3-2-8-T1,意思是:
-- w 2 CAS时延(CL) --
x 3 RAS-to-CAS时延(t - RCD) -
y 2 RAS预充电时间(t - RP) -
z 8 激活到预充电时间(t - RAS) -
T T1 命令速率 -
当然,除以上的参数外,还有许多其它参数影响命令的发送与处理。但以上5个参数已经足以确定模块的性能。
-在解读计算机性能参数时,这些信息可能会派上用场。而在购买计算机时,这些信息就更有用了,因为它们与FSB/SDRAM速度一起,都是决定计算机速度的关键因素。
-喜欢冒险的读者们还可以利用它们来调优系统。有些计算机的BIOS可以让你修改这些参数。SDRAM模块有一些可编程寄存器,可供设置参数。BIOS一般会挑选最佳值。如果RAM模块的质量足够好,我们可以在保持系统稳定的前提下将减小以上某个时延参数。互联网上有大量超频网站提供了相关的文档。不过,这是有风险的,需要大家自己承担,可别怪我没有事先提醒哟。
-谈到DRAM的访问时,重充电是常常被忽略的一个主题。在2.1.2中曾经介绍,DRAM必须保持刷新。……行在充电时是无法访问的。[highperfdram]的研究发现,“令人吃惊,DRAM刷新对性能有着巨大的影响”。
-根据JEDEC规范,DRAM单元必须保持每64ms刷新一次。对于8192行的DRAM,这意味着内存控制器平均每7.8125µs就需要发出一个刷新命令(在实际情况下,由于刷新命令可以纳入队列,因此这个时间间隔可以更大一些)。刷新命令的调度由内存控制器负责。DRAM模块会记录上一次刷新行的地址,然后在下次刷新请求时自动对这个地址进行递增。
-对于刷新及发出刷新命令的时间点,程序员无法施加影响。但我们在解读性能参数时有必要知道,它也是DRAM生命周期的一个部分。如果系统需要读取某个重要的字,而刚好它所在的行正在刷新,那么处理器将会被延迟很长一段时间。刷新的具体耗时取决于DRAM模块本身。
-我们有必要花一些时间来了解一下目前流行的内存,以及那些即将流行的内存。首先从SDR(单倍速)SDRAM开始,因为它们是DDR(双倍速)SDRAM的基础。SDR非常简单,内存单元和数据传输率是相等的。
----
图2.10: SDR SDRAM的操作 -
在图2.10中,DRAM单元阵列能以等同于内存总线的速率输出内容。假设DRAM单元阵列工作在100MHz上,那么总线的数据传输率可以达到100Mb/s。所有组件的频率f保持相同。由于提高频率会导致耗电量增加,所以提高吞吐量需要付出很高的的代价。如果是很大规模的内存阵列,代价会非常巨大。{功率 = 动态电容 x 电压2 x 频率}。而且,提高频率还需要在保持系统稳定的情况下提高电压,这更是一个问题。因此,就有了DDR SDRAM(现在叫DDR1),它可以在不提高频率的前提下提高吞吐量。
----
图2.11 DDR1 SDRAM的操作 -
我们从图2.11上可以看出DDR1与SDR的不同之处,也可以从DDR1的名字里猜到那么几分,DDR1的每个周期可以传输两倍的数据,它的上升沿和下降沿都传输数据。有时又被称为“双泵(double-pumped)”总线。为了在不提升频率的前提下实现双倍传输,DDR引入了一个缓冲区。缓冲区的每条数据线都持有两位。它要求内存单元阵列的数据总线包含两条线。实现的方式很简单,用同一个列地址同时访问两个DRAM单元。对单元阵列的修改也很小。
-SDR DRAM是以频率来命名的(例如,对应于100MHz的称为PC100)。为了让DDR1听上去更好听,营销人员们不得不想了一种新的命名方案。这种新方案中含有DDR模块可支持的传输速率(DDR拥有64位总线):
-- 100MHz x 64位 x 2 = 1600MB/s --
于是,100MHz频率的DDR模块就被称为PC1600。由于1600 > 100,营销方面的需求得到了满足,听起来非常棒,但实际上仅仅只是提升了两倍而已。{我接受两倍这个事实,但不喜欢类似的数字膨胀戏法。}
----
- 图2.12: DDR2 SDRAM的操作 -
为了更进一步,DDR2有了更多的创新。在图2.12中,最明显的变化是,总线的频率加倍了。频率的加倍意味着带宽的加倍。如果对单元阵列的频率加倍,显然是不经济的,因此DDR2要求I/O缓冲区在每个时钟周期读取4位。也就是说,DDR2的变化仅在于使I/O缓冲区运行在更高的速度上。这是可行的,而且耗电也不会显著增加。DDR2的命名与DDR1相仿,只是将因子2替换成4(四泵总线)。图2.13显示了目前常用的一些模块的名称。
---- -
-- -阵列频率 -总线频率 -数据率 -名称(速率) -名称 -
(FSB)- -133MHz -266MHz -4,256MB/s -PC2-4200 -DDR2-533 -- -166MHz -333MHz -5,312MB/s -PC2-5300 -DDR2-667 -- -200MHz -400MHz -6,400MB/s -PC2-6400 -DDR2-800 -- -250MHz -500MHz -8,000MB/s -PC2-8000 -DDR2-1000 -- - -266MHz -533MHz -8,512MB/s -PC2-8500 -DDR2-1066 -
- 图2.13: DDR2模块名 -
在命名方面还有一个拧巴的地方。FSB速度是用有效频率来标记的,即把上升、下降沿均传输数据的因素考虑进去,因此数字被撑大了。所以,拥有266MHz总线的133MHz模块有着533MHz的FSB“频率”。
-DDR3要求更多的改变(这里指真正的DDR3,而不是图形卡中假冒的GDDR3)。电压从1.8V下降到1.5V。由于耗电是与电压的平方成正比,因此可以节约30%的电力。加上管芯(die)的缩小和电气方面的其它进展,DDR3可以在保持相同频率的情况下,降低一半的电力消耗。或者,在保持相同耗电的情况下,达到更高的频率。又或者,在保持相同热量排放的情况下,实现容量的翻番。
-DDR3模块的单元阵列将运行在内部总线的四分之一速度上,DDR3的I/O缓冲区从DDR2的4位提升到8位。见图2.14。
----
- 图2.14: DDR3 SDRAM的操作 -
一开始,DDR3可能会有较高的CAS时延,因为DDR2的技术相比之下更为成熟。由于这个原因,DDR3可能只会用于DDR2无法达到的高频率下,而且带宽比时延更重要的场景。此前,已经有讨论指出,1.3V的DDR3可以达到与DDR2相同的CAS时延。无论如何,更高速度带来的价值都会超过时延增加带来的影响。
-DDR3可能会有一个问题,即在1600Mb/s或更高速率下,每个通道的模块数可能会限制为1。在早期版本中,这一要求是针对所有频率的。我们希望这个要求可以提高一些,否则系统容量将会受到严重的限制。
-图2.15显示了我们预计中各DDR3模块的名称。JEDEC目前同意了前四种。由于Intel的45nm处理器是1600Mb/s的FSB,1866Mb/s可以用于超频市场。随着DDR3的发展,可能会有更多类型加入。
---- -
- 图2.15: DDR3模块名 -- -阵列频率 -总线频率 -数据速率 -名称(速率) -名称 -
(FSB)- -100MHz -400MHz -6,400MB/s -PC3-6400 -DDR3-800 -- -133MHz -533MHz -8,512MB/s -PC3-8500 -DDR3-1066 -- -166MHz -667MHz -10,667MB/s -PC3-10667 -DDR3-1333 -- -200MHz -800MHz -12,800MB/s -PC3-12800 -DDR3-1600 -- - -233MHz -933MHz -14,933MB/s -PC3-14900 -DDR3-1866 -
所有的DDR内存都有一个问题:不断增加的频率使得建立并行数据总线变得十分困难。一个DDR2模块有240根引脚。所有到地址和数据引脚的连线必须被布置得差不多一样长。更大的问题是,如果多于一个DDR模块通过菊花链连接在同一个总线上,每个模块所接收到的信号随着模块的增加会变得越来越扭曲。DDR2规范允许每条总线(又称通道)连接最多两个模块,DDR3在高频率下只允许每个通道连接一个模块。每条总线多达240根引脚使得单个北桥无法以合理的方式驱动两个通道。替代方案是增加外部内存控制器(如图2.2),但这会提高成本。
-这意味着商品主板所搭载的DDR2或DDR3模块数将被限制在最多四条,这严重限制了系统的最大内存容量。即使是老旧的32位IA-32处理器也可以使用64GB内存。即使是家庭对内存的需求也在不断增长,所以,某些事必须开始做了。
-一种解法是,在处理器中加入内存控制器,我们在第2节中曾经介绍过。AMD的Opteron系列和Intel的CSI技术就是采用这种方法。只要我们能把处理器要求的内存连接到处理器上,这种解法就是有效的。如果不能,按照这种思路就会引入NUMA架构,当然同时也会引入它的缺点。而在有些情况下,我们需要其它解法。
-Intel针对大型服务器方面的解法(至少在未来几年),是被称为全缓冲DRAM(FB-DRAM)的技术。FB-DRAM采用与DDR2相同的器件,因此造价低廉。不同之处在于它们与内存控制器的连接方式。FB-DRAM没有用并行总线,而用了串行总线(Rambus DRAM had this back when, too, 而SATA是PATA的继任者,就像PCI Express是PCI/AGP的继承人一样)。串行总线可以达到更高的频率,串行化的负面影响,甚至可以增加带宽。使用串行总线后
-FB-DRAM只有69个脚。通过菊花链方式连接多个FB-DRAM也很简单。FB-DRAM规范允许每个通道连接最多8个模块。
-在对比下双通道北桥的连接性,采用FB-DRAM后,北桥可以驱动6个通道,而且脚数更少——6x69对比2x240。每个通道的布线也更为简单,有助于降低主板的成本。
-全双工的并行总线过于昂贵。而换成串行线后,这不再是一个问题,因此串行总线按全双工来设计的,这也意味着,在某些情况下,仅靠这一特性,总线的理论带宽已经翻了一倍。还不止于此。由于FB-DRAM控制器可同时连接6个通道,因此可以利用它来增加某些小内存系统的带宽。对于一个双通道、4模块的DDR2系统,我们可以用一个普通FB-DRAM控制器,用4通道来实现相同的容量。串行总线的实际带宽取决于在FB-DRAM模块中所使用的DDR2(或DDR3)芯片的类型。
-我们可以像这样总结这些优势:
-- DDR2 FB-DRAM --
-如果在单个通道上使用多个DIMM,会有一些问题。信号在每个DIMM上都会有延迟(尽管很小),也就是说,延迟是递增的。不过,如果在相同频率和相同容量上进行比较,FB-DRAM总是能快过DDR2及DDR3,因为FB-DRAM只需要在每个通道上使用一个DIMM即可。而如果说到大型内存系统,那么DDR更是没有商用组件的解决方案。 -- -
-- --
DDR2 -FB-DRAM -- -脚 -240 -69 -- -通道 -2 -6 -- -每通道DIMM数 -2 -8 -- -最大内存 -16GB -192GB -- - -吞吐量 -~10GB/s -~40GB/s -
通过本节,大家应该了解到访问DRAM的过程并不是一个快速的过程。至少与处理器的速度相比,或与处理器访问寄存器及缓存的速度相比,DRAM的访问不算快。大家还需要记住CPU和内存的频率是不同的。Intel Core 2处理器运行在2.933GHz,而1.066GHz FSB有11:1的时钟比率(注: 1.066GHz的总线为四泵总线)。那么,内存总线上延迟一个周期意味着处理器延迟11个周期。绝大多数机器使用的DRAM更慢,因此延迟更大。在后续的章节中,我们需要讨论延迟这个问题时,请把以上的数字记在心里。
-前文中读命令的时序图表明,DRAM模块可以支持高速数据传输。每个完整行可以被毫无延迟地传输。数据总线可以100%被占。对DDR而言,意味着每个周期传输2个64位字。对于DDR2-800模块和双通道而言,意味着12.8GB/s的速率。
-但是,除非是特殊设计,DRAM的访问并不总是串行的。访问不连续的内存区意味着需要预充电和RAS信号。于是,各种速度开始慢下来,DRAM模块急需帮助。预充电的时间越短,数据传输所受的惩罚越小。
-硬件和软件的预取(参见第6.3节)可以在时序中制造更多的重叠区,降低延迟。预取还可以转移内存操作的时间,从而减少争用。我们常常遇到的问题是,在这一轮中生成的数据需要被存储,而下一轮的数据需要被读出来。通过转移读取的时间,读和写就不需要同时发出了。
-除了CPU外,系统中还有其它一些组件也可以访问主存。高性能网卡或大规模存储控制器是无法承受通过CPU来传输数据的,它们一般直接对内存进行读写(直接内存访问,DMA)。在图2.1中可以看到,它们可以通过南桥和北桥直接访问内存。另外,其它总线,比如USB等也需要FSB带宽,即使它们并不使用DMA,但南桥仍要通过FSB连接到北桥。
-DMA当然有很大的优点,但也意味着FSB带宽会有更多的竞争。在有大量DMA流量的情况下,CPU在访问内存时必然会有更大的延迟。我们可以用一些硬件来解决这个问题。例如,通过图2.3中的架构,我们可以挑选不受DMA影响的节点,让它们的内存为我们的计算服务。还可以在每个节点上连接一个南桥,将FSB的负荷均匀地分担到每个节点上。除此以外,还有许多其它方法。我们将在第6节中介绍一些技术和编程接口,它们能够帮助我们通过软件的方式改善这个问题。
-最后,还需要提一下某些廉价系统,它们的图形系统没有专用的显存,而是采用主存的一部分作为显存。由于对显存的访问非常频繁(例如,对于1024x768、16bpp、60Hz的显示设置来说,需要95MB/s的数据速率),而主存并不像显卡上的显存,并没有两个端口,因此这种配置会对系统性能、尤其是时延造成一定的影响。如果大家对系统性能要求比较高,最好不要采用这种配置。这种系统带来的问题超过了本身的价值。人们在购买它们时已经做好了性能不佳的心理准备。
-继续阅读:
-导读:Kristóf Kovács 是一位软件架构师和咨询顾问,他最近发布了一片对比各种类型NoSQL数据库的文章。
-虽然SQL数据库是非常有用的工具,但经历了15年的一支独秀之后垄断即将被打破。这只是时间问题:被迫使用关系数据库,但最终发现不能适应需求的情况不胜枚举。
-但是NoSQL数据库之间的不同,远超过两 SQL数据库之间的差别。这意味着软件架构师更应该在项目开始时就选择好一个适合的 NoSQL数据库。针对这种情况,这里对 Cassandra、Mongodb、CouchDB、Redis、 Riak、Membase、Neo4j 和 HBase 进行了比较:
-(编注1:NoSQL:是一项全新的数据库革命性运动,NoSQL的拥护者们提倡运用非关系型的数据存储。现今的计算机体系结构在数据存储方面要求具 备庞大的水平扩 展性,而NoSQL致力于改变这一现状。目前Google的 BigTable 和Amazon 的Dynamo使用的就是NoSQL型数据库。 参见NoSQL词条。)
--
1. CouchDB
--
最佳应用场景:适用于数据变化较少,执行预定义查询,进行数据统计的应用程序。适用于需要提供数据版本支持的应用程序。
-例如: CRM、CMS系统。 master-master复制对于多站点部署是非常有用的。
-(编注2:master-master复制:是一种数据库同步方法,允许数据在一组计算机之间共享数据,并且可以通过小组中任意成员在组内进行数据更新。)
--
2. Redis
--
最佳应用场景:适用于数据变化快且数据库大小可遇见(适合内存容量)的应用程序。
-例如:股票价格、数据分析、实时数据搜集、实时通讯。
-(编注3:Master-slave复制:如果同一时刻只有一台服务器处理所有的复制请求,这被称为 Master-slave复制,通常应用在需要提供高可用性的服务器集群。)
--
3. MongoDB
--
最佳应用场景:适用于需要动态查询支持;需要使用索引而不是 map/reduce功能;需要对大数据库有性能要求;需要使用 CouchDB但因为数据改变太频繁而占满内存的应用程序。
-例如:你本打算采用 MySQL或 PostgreSQL,但因为它们本身自带的预定义栏让你望而却步。
--
4. Riak
--
最佳应用场景:适用于想使用类似 Cassandra(类似Dynamo)数据库但无法处理 bloat及复杂性的情况。适用于你打算做多站点复制,但又需要对单个站点的扩展性,可用性及出错处理有要求的情况。
-例如:销售数据搜集,工厂控制系统;对宕机时间有严格要求;可以作为易于更新的 web服务器使用。
-5. Membase
--
最佳应用场景:适用于需要低延迟数据访问,高并发支持以及高可用性的应用程序
-例如:低延迟数据访问比如以广告为目标的应用,高并发的 web 应用比如网络游戏(例如 Zynga)
--
6. Neo4j
--
最佳应用场景:适用于图形一类数据。这是 Neo4j与其他nosql数据库的最显著区别
-例如:社会关系,公共交通网络,地图及网络拓谱
--
7. Cassandra
--
最佳应用场景:当使用写操作多过读操作(记录日志)如果每个系统组建都必须用 Java编写(没有人因为选用 Apache的软件被解雇)
-例如:银行业,金融业(虽然对于金融交易不是必须的,但这些产业对数据库的要求会比它们更大)写比读更快,所以一个自然的特性就是实时数据分析
--
8. HBase
-(配合 ghshephard使用)
--
最佳应用场景:适用于偏好BigTable:)并且需要对大数据进行随机、实时访问的场合。
-例如: Facebook消息数据库(更多通用的用例即将出现)
-编注4:Thrift 是一种接口定义语言,为多种其他语言提供定义和创建服务,由Facebook开发并开源。
-当然,所有的系统都不只具有上面列出的这些特性。这里我仅仅根据自己的观点列出一些我认为的重要特性。与此同时,技术进步是飞速的,所以上述的内容肯定需要不断更新。我会尽我所能地更新这个列表。
-\ No newline at end of file diff --git a/src/picture-server.md b/src/picture-server.md deleted file mode 100644 index 4c0464b..0000000 --- a/src/picture-server.md +++ /dev/null @@ -1,147 +0,0 @@ -
现在几乎任何一个网站、Web App以及移动APP等应用都需要有图片展示的功能,对于图片功能从下至上都是很重要的。必须要具有前瞻性的规划好图片服务器,图片的上传和下载速度至关重要,当然这并不是说一上来就搞很NB的架构,至少具备一定扩展性和稳定性。虽然各种架构设计都有,在这里我只是谈谈我的一些个人想法。
--
-
对于图片服务器来说IO无疑是消耗资源最为严重的,对于web应用来说需要将图片服务器做一定的分离,否则很可能因为图片服务器的IO负载导致应用崩溃。因此尤其对于大型网站和应用来说,非常有必要将图片服务器和应用服务器分离,构建独立的图片服务器集群,构建独立的图片服务器其主要优势:
-1)分担Web服务器的I/O负载-将耗费资源的图片服务分离出来,提高服务器的性能和稳定性。
-2)能够专门对图片服务器进行优化-为图片服务设置有针对性的缓存方案,减少带宽网络成本,提高访问速度。
-3)提高网站的可扩展性-通过增加图片服务器,提高图片服务吞吐能力。
--
从传统互联网的web1.0,历经web2.0时代以及发展到现在的web3.0,随着图片存储规模的增加,图片服务器的架构也在逐渐发生变化,以下主要论述三个阶段的图片服务器架构演进。
--
初始阶段
- -在介绍初始阶段的早期的小型图片服务器架构之前,首先让我们了解一下NFS技术,NFS是Network File System的缩写,即网络文件系统。NFS是由Sun开发并发展起来的一项用于在不同机器,不同操作系统之间通过网络互相分享各自的文件。NFS server也可以看作是一个FILE SERVER,用于在UNIX类系统之间共享文件,可以轻松的挂载(mount)到一个目录上,操作起来就像本地文件一样的方便。
--
如果不想在每台图片服务器同步所有图片,那么NFS是最简单的文件共享方式。NFS是个分布式的客户机/服务器文件系统,NFS的实质在于用户间计算机的共享,用户可以联结到共享计算机并象访问本地硬盘一样访问共享计算机上的文件。具体实现思路是:
--
1)所有前端web服务器都通过nfs挂载3台图片服务器export出来的目录,以接收web服务器写入的图片。然后[图片1]服务器挂载另外两台图片服务器的export目录到本地给apache对外提供访问。
-2) 用户上传图片
-用户通过Internet访问页面提交上传请求post到web服务器,web服务器处理完图片后由web服务器拷贝到对应的mount本地目录。
-3)用户访问图片
-用户访问图片时,通过[图片1]这台图片服务器来读取相应mount目录里边的图片。
-
以上架构存在的问题:
-1)性能:现有结构过度依赖nfs,当图片服务器的nfs服务器有问题时,可能影响到前端web服务器。NFS的问题主要是锁的问题. 很容易造成死锁, 只有硬件重启才能解决。尤其当图片达到一定的量级后,nfs会有严重的性能问题。
-2)高可用:对外提供下载的图片服务器只有一台,容易出现单点故障。
-3) 扩展性:图片服务器之间的依赖过多,而且横向扩展余地不够。
-4) 存储:web服务器上传热点不可控,造成现有图片服务器空间占用不均衡。
-5) 安全性:nfs方式对于拥有web服务器的密码的人来说,可以随意修改nfs里边的内容,安全级别不高。
-
当然图片服务器的图片同步可以不采用NFS,也可以采用ftp或rsync,采用ftp这样的话每个图片服务器就都保存一份图片的副本,也起到了备份的作用。但是缺点是将图片ftp到服务器比较耗时,如果使用异步方式去同步图片的话又会有延时,不过一般的小图片文件也还好了。使用rsync同步,当数据文件达到一定的量级后,每次rsync扫描会耗时很久也会带来一定的延时性。
--
-
发展阶段
-- -
-
当网站达到一定的规模后,对图片服务器的性能和稳定性有一定的要求后,上述NFS图片服务架构面临着挑战,严重的依赖NFS,而且系统存在单点机器容易出现故障,需要对整体架构进行升级。于是出现了上图图片服务器架构,出现了分布式的图片存储。
--
其实现的具体思路如下:
-1)用户上传图片到web服务器后,web服务器处理完图片,然后再由前端web服务器把图片post到到[图片1]、[图片2]…[图片N]其中的一个,图片服务器接收到post过来的图片,然后把图片写入到本地磁盘并返回对应成功状态码。前端web服务器根据返回状态码决定对应操作,如果成功的话,处理生成各尺寸的缩略图、打水印,把图片服务器对应的ID和对应图片路径写入DB数据库。
-2) 上传控制
-我们需要调节上传时,只需要修改web服务器post到的目的图片服务器的ID,就可以控制上传到哪台图片存储服务器,对应的图片存储服务器只需要安装nginx同时提供一个python或者php服务接收并保存图片,如果不想不想开启python或者php服务,也可以编写一个nginx扩展模块。
3) 用户访问流程
-用户访问页面的时候,根据请求图片的URL到对应图片服务器去访问图片。
如: http://imgN.xxx.com/image1.jpg
--
此阶段的图片服务器架构,增加了负载均衡和分布式图片存储,能够在一定程度上解决并发访问量高和存储量大的问题。负载均衡在有一定财力的情况下可以考虑F5硬负载,当然也可以考虑使用开源的LVS软负载(同时还可开启缓存功能)。此时将极大提升访问的并发量,可以根据情况随时调配服务器。当然此时也存在一定的瑕疵,那就是可能在多台Squid上存在同一张图片,因为访问图片时可能第一次分到squid1,在LVS过期后第二次访问到squid2或者别的,当然相对并发问题的解决,此种少量的冗余完全在我们的允许范围之内。在该系统架构中二级缓存可以使用squid也可以考虑使用varnish或者traffic server,对于cache的开源软件选型要考率以下几点
--
1)性能:varnish本身的技术上优势要高于squid,它采用了“Visual Page Cache”技术,在内存的利用上,Varnish比Squid具有优势,它避免了Squid频繁在内存、磁盘中交换文件,性能要比Squid高。varnish是不能cache到本地硬盘上的。还有强大的通过Varnish管理端口,可以使用正则表达式快速、批量地清除部分缓存。nginx是用第三方模块ncache做的缓冲,其性能基本达到varnish,但在架构中nginx一般作为反向(静态文件现在用nginx的很多,并发能支持到2万+)。在静态架构中,如果前端直接面对的是cdn活着前端了4层负载的话,完全用nginx的cache就够了。
--
2)避免文件系统式的缓存,在文件数据量非常大的情况下,文件系统的性能很差,像squid,nginx的proxy_store,proxy_cache之类的方式缓存,当缓存的量级上来后,性能将不能满足要求。开源的traffic server直接用裸盘缓存,是一个不错的选择,国内大规模应用并公布出来的主要是淘宝,并不是因为它做的差,而是开源时间晚。Traffic Server 在 Yahoo 内部使用了超过 4 年,主要用于 CDN 服务,CDN 用于分发特定的HTTP 内容,通常是静态的内容如图片、JavaScript、CSS。当然使用leveldb之类的做缓存,我估计也能达到很好的效果。
--
3)稳定性:squid作为老牌劲旅缓存,其稳定性更可靠一些,从我身边一些使用者反馈来看varnish偶尔会出现crash的情况。Traffic Server在雅虎目前使用期间也没有出现已知的数据损坏情况,其稳定性相对也比较可靠,对于未来我其实更期待Traffic Server在国内能够拥有更多的用户。
-以上图片服务架构设计消除了早期的NFS依赖以及单点问题,时能够均衡图片服务器的空间,提高了图片服务器的安全性等问题,但是又带来一个问题是图片服务器的横向扩展冗余问题。只想在普通的硬盘上存储,首先还是要考虑一下物理硬盘的实际处理能力。是 7200 转的还是 15000 转的,实际表现差别就很大。至于文件系统选择xfs、ext3、ext4还是reiserFs,需要做一些性能方面的测试,从官方的一些测试数据来看,reiserFs更适合存储一些小图片文件。创建文件系统的时候 Inode 问题也要加以考虑,选择合适大小的 inode size ,因为Linux 为每个文件分配一个称为索引节点的号码inode,可以将inode简单理解成一个指针,它永远指向本文件的具体存储位置。一个文件系统允许的inode节点数是有限的,如果文件数量太多,即使每个文件都是0字节的空文件,系统最终也会因为节点空间耗尽而不能再创建文件,因此需要在空间和速度上做取舍,构造合理的文件目录索引。
--
-
云存储阶段
- --
2011年李彦宏在百度联盟峰会上就提到过互联网的读图时代已经到来,图片服务早已成为一个互联网应用中占比很大的部分,对图片的处理能力也相应地变成企业和开发者的一项基本技能,图片的下载和上传速度显得更加重要,要想处理好图片,需要面对的三个主要问题是:大流量、高并发、海量存储。
--
阿里云存储服务(OpenStorageService,简称OSS),是阿里云对外提供的海量,安全,低成本,高可靠的云存储服务。用户可以通过简单的 REST接口,在任何时间、任何地点上传和下载数据,也可以使用WEB页面对数据进行管理。同时,OSS提供Java、Python、PHP SDK,简化用户的编程。基于OSS,用户可以搭建出各种多媒体分享网站、网盘、个人企业数据备份等基于大规模数据的服务。在以下图片云存储主要以阿里云的云存储OSS为切入点介绍,上图为OSS云存储的简单架构示意图。
--
真正意义上的“云存储”,不是存储而是提供云服务,使用云存储服务的主要优势有以下几点:
-1)用户无需了解存储设备的类型、接口、存储介质等。
-2)无需关心数据的存储路径。
-3)无需对存储设备进行管理、维护。
-4)无需考虑数据备份和容灾
-5)简单接入云存储,尽情享受存储服务。
- - - --
-
1)KV Engine
-OSS中的Object源信息和数据文件都是存放在KV Engine上。在6.15的版本,V Engine将使用0.8.6版本,并使用为OSS提供的OSSFileClient。
--
2)Quota
-此模块记录了Bucket和用户的对应关系,和以分钟为单位的Bucket资源使用情况。Quota还将提供HTTP接口供Boss系统查询。
--
3)安全模块
-安全模块主要记录User对应的ID和Key,并提供OSS访问的用户验证功能。
- - --
-
1 )Access Key ID & Access Key Secret (API密钥)
-用户注册OSS时,系统会给用户分配一对Access Key ID & Access Key Secret,称为ID对,用于标识用户,为访问OSS做签名验证。
-
2) Service
-OSS提供给用户的虚拟存储空间,在这个虚拟空间中,每个用户可拥有一个到多个Bucket。
-
3) Bucket
-Bucket是OSS上的命名空间;Bucket名在整个OSS中具有全局唯一性,且不能修改;存储在OSS上的每个Object必须都包含在某个Bucket中。一个应用,例如图片分享网站,可以对应一个或多个Bucket。一个用户最多可创建10个Bucket,但每个Bucket中存放的Object的数量和大小总和没有限制,用户不需要考虑数据的可扩展性。
4) Object
-在OSS中,用户的每个文件都是一个Object,每个文件需小于5TB。Object包含key、data和user meta。其中,key是Object的名字;data是Object的数据;user meta是用户对该object的描述。
-其使用方式非常简单,如下为java sdk:
OSSClient ossClient = new OSSClient(accessKeyId,accessKeySecret);
-PutObjectResult result = ossClient.putObject(bucketname, bucketKey, inStream, new ObjectMetadata());
-执行以上代码即可将图片流上传至OSS服务器上。
-图片的访问方式也非常简单其url为:http://bucketname.oss.aliyuncs.com/bucketKey
--
-
分布式文件系统
-用分布式存储有几个好处,分布式能自动提供冗余,不需要我们去备份,担心数据安全,在文件数量特别大的情况下,备份是一件很痛苦的事情,rsync扫一次可能是就是好几个小时,还有一点就是分布式存储动态扩容方便。当然在国内的其他一些文件系统里,TFS(http://code.taobao.org/p/tfs/src/)和FASTDFS也有一些用户,但是TFS的优势更是针对一些小文件存储,主要是淘宝在用。另外FASTDFS在并发高于300写入的情况下出现性能问题,稳定性不够友好。OSS存储使用的是阿里云基于飞天5k平台自主研发的高可用,高可靠的分布式文件系统盘古。分布式文件系统盘古和Google的GFS类似,盘古的架构是Master-Slave主从架构,Master负责元数据管理,Sliave叫做Chunk Server,负责读写请求。其中Master是基于Paxos的多Master架构,一个Master死了之后,另外一个Master可以很快接过去,基本能够做到故障恢复在一分钟以内 。文件是按照分片存放,每个会分三个副本,放在不同的机架上,最后提供端到端的数据校验。
--
-
HAPROXY负载均衡
-基于haproxy的自动hash架构 ,这是一种新的缓存架构,由nginx作为最前端,代理到缓存机器。 nginx后面是缓存组,由nginx经过url hash后将请求分到缓存机器。
-这个架构方便纯squid缓存升级,可以在squid的机器上加装nginx。 nginx有缓存的功能,可以将一些访问量特大的链接直接缓存在nginx上,就不用经过多一次代理的请求,能够保证图片服务器的高可用、高性能。比如favicon.ico和网站的logo。 负载均衡负责OSS所有的请求的负载均衡,后台的http服务器故障会自动切换,从而保证了OSS的服务不间断。
-
-
CDN
-阿里云CDN服务是一个遍布全国的分布式缓存系统,能够将网站文件(如图片或JavaScript代码文件)缓存到全国多个城市机房中的服务器上,当一个用户访问你的网站时,会就近到靠近TA的城市的服务器上获取数据,这样最终用户访问你的服务速度会非常快。
-阿里云CDN服务在全国部署超过100个节点,能提供给用户优良的网络加速效果。当网站业务突然爆发增长时,无需手忙脚乱地扩容网络带宽,使用CDN服务即可轻松应对。和OSS服务一样,使用CDN,需要先在aliyun.com网站上开通CDN服务。开通后,需要在网站上的管理中心创建你的distribution(即分发频道),每个distribution由两个必须的部分组成:distribution ID和源站地址。
-使用阿里云OSS和CDN可以非常方便的针对每个bucket进行内容加速,因为每个bucket对应一个独立的二级域名,针对每个文件进行CDN删除,简单、经济地解决服务的存储和网络问题,毕竟大多数网站或应用的存储和网络带宽多半是被图片或视频消耗掉的。
-从整个业界来看,最近这样的面向个人用户的云存储如国外的DropBox和Box.net非常受欢迎,国内的云存储目前比较不错的主要有七牛云存储和又拍云存储。
--
-
上传下载分而治之
-图片服务器的图片下载比例远远高于上传比例,业务逻辑的处理也区别明显,上传服器对图片重命名,记录入库信息,下载服务器对图片添加水印、修改尺寸之类的动态处理。从高可用的角度,我们能容忍部分图片下载失败,但绝不能有图片上传失败,因为上传失败,意味着数据的丢失。上传与下载分开,能保证不会因下载的压力影响图片的上传,而且还有一点,下载入口和上传入口的负载均衡策略也有所不同。上传需要经过Quota Server记录用户和图片的关系等逻辑处理,下载的逻辑处理如果绕过了前端缓存处理,穿透后端业务逻辑处理,需要从OSS获取图片路径信息。近期阿里云会推出基于CDN就近上传的功能,自动选择离用户最近的CDN节点,使得数据的上传下载速度均得到最优化。相较传统IDC,访问速度提升数倍。
--
-
图片防盗链处理
-如果服务不允许防盗链,那么访问量会引起带宽、服务器压力等问题。比较通用的解决方案是在nginx或者squid反向代理软件上添加refer ACL判断,OSS也提供了基于refer的防盗链技术。当然OSS也提供了更为高级的URL签名防盗链,其其实现思路如下:
--
首先,确认自己的bucket权限是private,即这个bucket的所有请求必须在签名认证通过后才被认为是合法的。然后根据操作类型、要访问的bucket、要访问的object以及超时时间,动态地生成一个经过签名的URL。通过这个签名URL,你授权的用户就可以在该签名URL过期时间前执行相应的操作。
--
签名的Python代码如下:
-h=hmac.new(“OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV”, “GET\n\n\n1141889120\n/oss-example/oss-api.jpg”,sha);
-urllib.quote_plus (base64.encodestring(h.digest()).strip());
--
其中method可以是PUT、GET、HEAD、DELETE中的任意一种;最后一个参数“timeout”是超时的时间,单位是秒。一个通过上面Python方法,计算得到的签名URL为:
-http://oss-example.oss-cn-hangzhou.aliyuncs.com/oss-api.jpg?OSSAccessKeyId=44CF9590006BF252F707&Expires=1141889120&Signature=vjbyPxybdZaNmGa%2ByT272YEAiv4%3D
--
通过这种动态计算签名URL的方法,可以有效地保护放在OSS上的数据,防止被其他人盗链。
--
-
图片编辑处理API
-对于在线图片的编辑处理,GraphicsMagick(GraphicsMagick(http://www.graphicsmagick.org/))对于从事互联网的技术人员应该不会陌生。GraphicsMagick是从 ImageMagick 5.5.2 分支出来的,但是现在他变得更稳定和优秀,GM更小更容易安装、GM更有效率、GM的手册非常丰富GraphicsMagick的命令与ImageMagick基本是一样的。
--
GraphicsMagick 提供了包括裁、缩放、合成、打水印、图像转换、填充等非常丰富的接口API,其中的开发包SDK也非常丰富,包括了JAVA(im4java)、C、C++、Perl、PHP、Tcl、Ruby等的调用,支持超过88中图像格式,包括重要的DPX、GIF、JPEG、JPEG-2000、PNG、PDF、PNM和TIFF,GraphicsMagick可以再绝大多数的平台上使用,Linux、Mac、Windows都没有问题。但是独立开发这些图片处理服务,对服务器的IO要求相对要高一些,而且目前这些开源的图片处理编辑库,相对来说还不是很稳定,笔者在使用GraphicsMagick 的时候就遇到了tomcat 进程crash情况,需要手动重启tomcat服务。
--
阿里云目前已经对外开放图片处理API,包括了大多数常用处理解决方案:缩略图、打水印、文字水印、样式、管道等。开发者可以非常方便的使用如上图片处理方案,希望越来越多的开发者能够基于OSS开放出更多优秀的产品。
\ No newline at end of file diff --git a/src/sed.md b/src/sed.md deleted file mode 100644 index 1ea6a28..0000000 --- a/src/sed.md +++ /dev/null @@ -1,1175 +0,0 @@ -最近在阅读《sed & awk(第二版)》,这本书是sed和awk相关书籍中比较经典的一本。我在读书的时候有一个习惯,就是会作一些笔记,如果有条件我会放到博客中。写博客不仅是给别人看的,更是写给自己看的,同时因为写给别人看,所以必然会在一些细节的地方写得很清楚明了,可以加深自己对原书的理解,同时以后回头看的时候,我自己也能快速的回忆起来。
-另外一方面,我会选择英文原版来阅读而非中文翻译版,主要是出于英文版的内容更加准确、容易领会作者的本意这个方面的原因。毕竟翻译的内容一方面因为翻译的时候会丢失一些原版的意思,同时因为不同的人有不同的理解,在翻译中可能会夹杂着自己个人的理解。就好比这一系列的文章,许多内容都是出自原书,我只不过是翻译了些内容加了点注解而已,所心也只能称之为笔记。
-文中对一些术语的翻译只是按本人自己的喜好而定,请见谅。
-本系列包含两部分的内容:sed篇和awk篇。
-sed篇总共分成6章:
-awk篇暂时还未计划。
--
Sed是什么
-《sed and awk》一书中(1.2 A Stream Editor)是这样解释的:
--Sed is a “non-interactive” stream-oriented editor. It is stream-oriented because, like many UNIX
-programs, input flows through the program and is directed to standard output.
Sed本质上是一个编辑器,但是它是非交互式的,这点与VIM不同;同时它又是面向字符流的,输入的字符流经过Sed的处理后输出。这两个特性使得Sed成为命令行下面非常有用的一个处理工具。
-如果对sed的历史有兴趣,可以看书中2.1节”Awk, by Sed and Grep, out of Ed”,这里就不多介绍。
-基本概念
-sed命令的语法如下所示:
-sed [options] script filename-
同大多数Linux命令一样,sed也是从stdin中读取输入,并且将输出写到stdout,但是当filename被指定时,则会从指定的文件中获取输入,输出可以重定向到文件中,但是需要注意的是,该文件绝对不能与输入的文件相同。
-options是指sed的命令行参数,这一块并不是重点,参数也不多。
-script是指需要对输入执行的一个或者多个操作指令(instruction),sed会依次读取输入文件的每一行到缓存中并应用script中指定的操作指令,因此而带来的变化并不会影响最初的文件(注:如果使用sed时指定-i参数则会影响最初的文件)。如果操作指令很多,为了不影响可读性,可以将其写到文件中,并通过-f参数指定scriptfile:
-sed -f scriptfile filename-
这里有一个建议,在命令行中指定的操作指令最好用单引号引起来,这样可以避免shell对特殊字符的处理(如空格、$等,关于这一点可以参考简洁的Bash编程技巧续篇 – 9. 引号之间的区别)。这个建议同样适用grep/awk等命令,当然如果有时候确实不适合使用单引号时,记得对特殊字符转义。
-无论是将操作指令通过命令行指定,还是写入到文件中作为一个sed脚本,必须包含至少一个指令,否则用sed就没有意义了。一般会同时指定多个操作指令,这时候指令之间的顺序就显得非常重要。而你的脑海中必须有这么一个概念,即每个指令应用后,当前输入的行会变成什么样子。要做到这一点首先必须要了解sed的工作原理,要做到“知其然,且知其所以然”。
-每条操作指令由pattern和procedure两部分组成,pattern一般是用’/'分隔的正则表达式(在sed中也有可能是行号,具体参见Sed命令地址匹配问题总结),而procedure则是一连串编辑命令(action)。
-sed的处理流程,简化后是这样的:
-1.读入新的一行内容到缓存空间;
-2.从指定的操作指令中取出第一条指令,判断是否匹配pattern;
-3.如果不匹配,则忽略后续的编辑命令,回到第2步继续取出下一条指令;
-4.如果匹配,则针对缓存的行执行后续的编辑命令;完成后,回到第2步继续取出下一条指令;
-5.当所有指令都应用之后,输出缓存行的内容;回到第1步继续读入下一行内容;
-6.当所有行都处理完之后,结束;
- -简单例子
-概念如果脱离实际的例子就会显得非常枯燥无趣,这本书在这一点上处理得很好,都是配上实际的例子来讲解的。不过有点遗憾的是,书中的例子大多是与troff macro有关的,我们很难有条件来实际测试例子,在笔记中我会尽量使用更加浅显易懂的例子来说明。
-下面是摘自书中的一个例子,假设有个文件list:
-John Daggett, 341 King Road, Plymouth MA -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls PA -Eric Adams, 20 Post Road, Sudbury MA -Hubert Sims, 328A Brook Road, Roanoke VA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -Sal Carpenter, 73 6th Street, Boston MA-
这个例子中用到的知识点都会在后文中介绍,你可以先知道有这个东西就行。
-假如,现在打算将MA替换成Massachusetts,可以使用替换命令s:
-$ sed -e 's/MA/Massachusetts/' list -John Daggett, 341 King Road, Plymouth Massachusetts -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls PA -Eric Adams, 20 Post Road, Sudbury Massachusetts -Hubert Sims, 328A Brook Road, Roanoke VA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -Sal Carpenter, 73 6th Street, Boston Massachusetts-
这里面的-e选项是可选的,这个参数只是在命令行中同时指定多个操作指令时才需要用到,如:
-$ sed -e 's/ MA/, Massachusetts/' -e 's/ PA/, Pennsylvania/' list -John Daggett, 341 King Road, Plymouth, Massachusetts -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls, Pennsylvania -Eric Adams, 20 Post Road, Sudbury, Massachusetts -Hubert Sims, 328A Brook Road, Roanoke VA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -Sal Carpenter, 73 6th Street, Boston, Massachusetts-
即使在多个操作指令的情况下,-e参数也不是必需的,我一般不会加-e参数,比如上面的例子可以换成下面的写法:
-$ sed 's/ MA/, Massachusetts/;s/ PA/, Pennsylvania/' list-
操作指令之间可以用逗号分隔,这点和shell命令可以用逗号分隔是一样的。
-这里提前说一点使用s替换命令需要小心的地方,不要忘记结尾的斜杠,如果你遇到下面的错误说明你犯了这个错误:
-$ sed -e 's/ MA/, Massachusetts' list -sed: -e expression #1, char 21: unterminated `s' command-
假如这时,我只想输出替换命令影响的行,而不想输出文件的所有内容,需要怎么办?这个时候可以利用替换命令s的p选项,它会将替换后的内容打印到标准输出。但是仅仅这样是不够的,否则输出的结果并非我们所想要的:
-$ sed -e 's/ MA/, Massachusetts/p' list -John Daggett, 341 King Road, Plymouth, Massachusetts -John Daggett, 341 King Road, Plymouth, Massachusetts -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls PA -Eric Adams, 20 Post Road, Sudbury, Massachusetts -Eric Adams, 20 Post Road, Sudbury, Massachusetts -Hubert Sims, 328A Brook Road, Roanoke VA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -Sal Carpenter, 73 6th Street, Boston, Massachusetts -Sal Carpenter, 73 6th Street, Boston, Massachusetts-
从上面可以看出,文件的所有行都被打印到标准输出,并且被替换命令修改的行输出了两遍。造成这个问题的原因是,sed命令应用完所有指定之后默认会将当前行打印输出,所以就有了上面的结果。解决方法是在使用sed命令是指定-n参数,该参数会抑制sed默认的输出:
-$ sed -n 's/ MA/, Massachusetts/p' list -John Daggett, 341 King Road, Plymouth, Massachusetts -Eric Adams, 20 Post Road, Sudbury, Massachusetts -Sal Carpenter, 73 6th Street, Boston, Massachusetts-
正则表达式
-书中的第3章的内容都是介绍正则表达式,这是因为sed的很多地方都需要用到正则表达式,比如操作指令的pattern部分以及替换命令中用到的匹配部分。关于正则这部分可以看Linux/Unix工具与正则表达式的POSIX规范,文章中已经讲得非常清楚了,我自已在忘记一些内容的时候也是去查阅这篇文章。
--
-
Sed&awk笔记之sed篇:模式空间与地址匹配
-模式空间
-在上一篇Sed&awk笔记之sed篇:简单介绍中,我们曾经介绍过简单的sed处理流程,这里首先回顾下:
-1.读入新的一行内容到缓存空间;
-2.从指定的操作指令中取出第一条指令,判断是否匹配pattern;
-3.如果不匹配,则忽略后续的编辑命令,回到第2步继续取出下一条指令;
-4.如果匹配,则针对缓存的行执行后续的编辑命令;完成后,回到第2步继续取出下一条指令;
-5.当所有指令都应用之后,输出缓存行的内容;回到第1步继续读入下一行内容;
-6.当所有行都处理完之后,结束;
-由此可见,sed并非是将一个编辑命令分别应用到每一行,然后再取下一个编辑命令。恰恰相反,sed是以行的方式来处理的。另外一方面,每一行都是被读入到一块缓存空间,该空间名为模式空间(pattern space),这是一个很重要的概念,在后文中会多次被提及。因此sed操作的都是最初行的拷贝,同时后续的编辑命令都是应用到前面的命令编辑后输出的结果,所以编辑命令之间的顺序就显得格外重要。
-简单例子
-让我们来看一个非常简单的例子,将一段文本中的pig替换成cow,并且将cow替换成horse:
-$ sed 's/pig/cow/;s/cow/hores/' input-
初看起来好像没有问题,但是实际上是错误的,原因是第一个替换命令将所有的pig都替换成cow,紧接着的替换命令是基于前一个结果处理的,将所有的cow都替换成horse,造成的结果是全部的pig/cow都被替换成了horse,这样违背了我们的初衷。
-在这种情况下,只需要调换下两个编辑命令的顺序:
-$ sed 's/cow/hores/;s/pig/cow/' input-
模式空间的转换
-sed只会缓存一行的内容在模式空间,这样的好处是sed可以处理大文件而不会有任何问题,不像一些编辑器因为要一次性载入文件的一大块内容到缓存中而导致内存不足。下面用一个简单的例子来讲解模式空间的转换过程,如下图所示:
- -现在要把一段文本中的Unix System与UNIX System都要统一替换成The UNIX Operating System,因此我们用两句替换命令来完成这个目的:
-s/Unix /UNIX / -s/UNIX System/UNIX Operating System/-
对应上图,过程如下:
-1.首先一行内容The Unix System被读入模式空间;
-2.应用第一条替换命令将Unix替换成UNIX;
-3.现在模式空间的内容变成The UNIX System;
-4.应用第二条替换命令将UNIX System替换成UNIX Operating System;
-5.现在模式空间的内容变成The UNIX Operating System;
-6.所有编辑命令执行完毕,默认输出模式空间中的行;
-地址匹配
-地址匹配在sed中是非常重要的一块内容,因为它限制sed的编辑命令到底应用在哪些行上。默认情况下,sed是全局匹配的,即对所有输入行都应用指定的编辑命令,这是因为sed依次读入每一行,每一行都会成为当前行并被处理,所以s/CA/California/g会将所有输入行的CA替换成California。这一点跟vi/vim是不一样的,众所周知,vim的替换命令默认是替换当前行的内容,除非你指定%s才会作全局替换。
可以通过指定地址来限制sed的处理范围,例如只想将替换包含Sebastopol的行:
-Sebastopol/s/CA/California/-
/Sebastopol/是一个正则表达式匹配包含Sebastopol的行,因此像行“San Francisco, CA”则不会被替换。
-sed命令中可以包含0个、1个或者2个地址(地址对),地址可以为正则表达式(如/Sebastopol/),行号或者特殊的行符号(如$表示最后一行):
-● 如果没有指定地址,默认将编辑命令应用到所有行;
-●如果指定一个地址,只将编辑命令应用到匹配该地址的行;
-●如果指定一个地址对(addr1,addr2),则将编辑命令应用到地址对中的所有行(包括起始和结束);
-●如果地址后面有一个感叹号(!),则将编辑命令应用到不匹配该地址的所有行;
-为了方便理解上述内容,我们以删除命令(d)为例,默认不指定地址将会删除所有行:
-$ sed 'd' list-
指定地址则删除匹配的行,如删除第一行:
-$ sed '1d' list -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls PA -Eric Adams, 20 Post Road, Sudbury MA -Hubert Sims, 328A Brook Road, Roanoke VA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -Sal Carpenter, 73 6th Street, Boston MA-
或者删除最后一行,$符号在这里表示最后一行,这点要下正则表达式中的含义区别开来:
-$ sed '$d' list -John Daggett, 341 King Road, Plymouth MA -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls PA -Eric Adams, 20 Post Road, Sudbury MA -Hubert Sims, 328A Brook Road, Roanoke VA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA-
这里通过指定行号删除,行号是sed命令内部维护的一个计数变量,该变量只有一个,并且在多个文件输入的情况下也不会被重置。
-前面都是以行号来指定地址,也可以通过正则表达式来指定地址,如删除包含MA的行:
-$ sed '/MA/d' list -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls PA -Hubert Sims, 328A Brook Road, Roanoke VA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA-
通过指定地址对可以删除该范围内的所有行,例如删除第3行到最后一行:
-$ sed '2,$d' list -John Daggett, 341 King Road, Plymouth MA-
使用正则匹配,删除从包含Alice的行开始到包含Hubert的行结束的所有行:
-$ sed '/Alice/,/Hubert/d' list -John Daggett, 341 King Road, Plymouth MA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -Sal Carpenter, 73 6th Street, Boston MA-
当然,行号和地址对是可以混用的:
-$ sed '2,/Hubert/d' list -John Daggett, 341 King Road, Plymouth MA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -Sal Carpenter, 73 6th Street, Boston MA-
如果在地址后面指定感叹号(!),则会将命令应用到不匹配该地址的行:
-$ sed '2,/Hubert/!d' list -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls PA -Eric Adams, 20 Post Road, Sudbury MA -Hubert Sims, 328A Brook Road, Roanoke VA-
以上介绍的都是最基本的地址匹配形式,GNU Sed(也是我们用的sed版本)基于此添加了几个扩展的形式,具体可以看man手册,或者可以看我之前写的Sed命令地址匹配问题总结一文。
-上面说的内容都是对匹配的地址执行单个命令,如果要执行多个编辑命令要怎么办?sed中可以用{}来组合命令,就好比编程语言中的语句块,例如:
-$ sed -n '1,4{s/ MA/, Massachusetts/;s/ PA/, Pennsylvania/;p}' list
-John Daggett, 341 King Road, Plymouth, Massachusetts
-Alice Ford, 22 East Broadway, Richmond VA
-Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
-Terry Kalkas, 402 Lans Road, Beaver Falls, Pennsylvania
--
Sed&awk笔记之sed篇:基础命令
-在开始之前,首先回顾上一篇的重点内容:地址匹配。上一篇中介绍过,地址可以指定0个,1个或者2个。地址的形式可以为斜杠分隔的正则表达式(例如/test/),行号(例如3,5)或者特殊符号(例如$)。如果没有指定地址,说明sed应用的编辑命令是全局的;如果是1个地址,编辑命令只是应用到匹配的那一行;如果是一对地址,编辑命令则应用到该地址对匹配的行范围。关于地址匹配的内容具体可以看Sed命令地址匹配问题总结。
-书中说,对于sed编辑命令的语法有两种约定,分别是
-[address]command # 第一种 -[line-address]command # 第二种-
第一种[address]是指可以为任意地址包括地址对,第二种[line-address]是指只能为单个地址。文中指出,例如i(后方插入命令)、a(前方追加命令)以及=(打印行号命令)等都属于第二种命令,但是实际上我在ArchLinux上测试,指定地址对也没有报错。不过为了兼容性,建议按作者所说的做。
-以下是要介绍的全部基础命令:
-| 名称 | -命令 | -语法 | -说明 | -
|---|---|---|---|
| 替换 | -s | -[address]s/pattern/replacement/flags | -替换匹配的内容 | -
| 删除 | -d | -[address]d | -删除匹配的行 | -
| 插入 | -i | -[line-address]i\text | -在匹配行的前方插入文本 | -
| 追加 | -a | -[line-address]a\text | -在匹配行的后方插入文本 | -
| 行替换 | -c | -[address]c\text | -将匹配的行替换成文本text | -
| 打印行 | -p | -[address]p | -打印在模式空间中的行 | -
| 打印行号 | -= | -[address]= | -打印当前行行号 | -
| 打印行 | -l | -[address]l | -打印在模式空间中的行,同时显示控制字符 | -
| 转换字符 | -y | -[address]y/SET1/SET2/ | -将SET1中出现的字符替换成SET2中对应位置的字符 | -
| 读取下一行 | -n | -[address]n | -将下一行的内容读取到模式空间 | -
| 读文件 | -r | -[line-address]r file | -将指定的文件读取到匹配行之后 | -
| 写文件 | -w | -[address]w file | -将匹配地址的所有行输出到指定的文件中 | -
| 退出 | -q | -[line-address]q | -读取到匹配的行之后即退出 | -
替换命令的语法是:
-[address]s/pattern/replacement/flags-
其中[address]是指地址,pattern是替换命令的匹配表达式,replacement则是对应的替换内容,flags是指替换的标志位,它可以包含以下一个或者多个值:
-● n: 一个数字(取值范围1-512),表明仅替换前n个被pattern匹配的内容;
-● g: 表示全局替换,替换所有被pattern匹配的内容;
-● p: 仅当行被pattern匹配时,打印模式空间的内容;
-● w file:仅当行被pattern匹配时,将模式空间的内容输出到文件file中;
-如果flags为空,则默认替换第一次匹配:
-$ echo "column1 column2 column3 column4" | sed 's/ /;/' -column1;column2 column3 column4-
如果flags中包含g,则表示全局匹配:
-$ echo "column1 column2 column3 column4" | sed 's/ /;/g' -column1;column2;column3;column4-
如果flags中明确指定替换第n次的匹配,例如n=2:
-$ echo "column1 column2 column3 column4" | sed 's/ /;/2' -column1 column2;column3 column4-
当替换命令的pattern与地址部分是一样的时候,比如/regexp/s/regexp/replacement/可以省略替换命令中的pattern部分,这在单个编辑命令的情况下没多大用处,但是在组合命令的场景下还是能省不少功夫的,例如下面是摘自文中的一个段落:
The substitute command is applied to the lines matching the address. If no address is specified, it is applied to all lines that match the pattern, a regular expression. If a regular expression is supplied as an address, and no pattern is specified, the substitute command matches what is matched by the address. This can be useful when the substitute command is one of multiple commands applied at the same address. For an example, see the section “Checking Out Reference Pages” later in this chapter.
-现在要在substitute command后面增加(“s”),同时在被修改的行前面增加+号,以下是使用的sed命令:
-$ sed '/substitute command/{s//&("s")/;s/^/+ /}' paragraph.txt
-这里我们用到了组合命令,并且地址匹配的部分和第一个替换命令的匹配部分是一样的,所以后者我们省略了,在replacement部分用到了&这个元字符,它代表之前匹配的内容,这点我们在后面介绍。执行后的结果为:
-+ The substitute command("s") is applied to the lines matching the address. If no
-address is specified, it is applied to all lines that match the pattern, a
-regular expression. If a regular expression is supplied as an address, and no
-+ pattern is specified, the substitute command("s") matches what is matched by the
-+ address. This can be useful when the substitute command("s") is one of multiple
-commands applied at the same address. For an example, see the section "Checking
-Out Reference Pages" later in this chapter.
-替换命令的一个技巧是中间的分隔符是可以更改的(这个技巧我们在简洁的Bash编程技巧续篇 – 5. 你知道sed的这个特性吗?中也曾经介绍过),这个技巧在有些地方非常有用,比如路径替换,下面是采用默认的分隔符和使用感叹号作为分隔符的对比:
-$ find /usr/local -maxdepth 2 -type d | sed 's//usr/local/man//usr/share/man/' -$ find /usr/local -maxdepth 2 -type d | sed 's!/usr/local/man!/usr/share/man!'-
替换命令中还有一个很重要的部分——replacement(替换内容),即将匹配的部分替换成replacement。在replacemnt部分中也有几个特殊的元字符,它们分别是:
-● &: 被pattern匹配的内容;
-●\num: 被pattern匹配的第num个分组(正则表达式中的概念,\(..\)括起来的部分称为分组;
-●\: 转义符号,用来转义&,\, 回车等符号
-&元字符我们已经在上面的例子中介绍过,这里举个例子介绍下\num的用法。在Linux系统中,默认都开了几个tty可以让你切换(ctrl+alt+1, ctrl+alt+2 …),例如CentOS 6.0+的系统默认是6个:
-[root@localhost ~]# grep -B 1 ACTIVE_CONSOLES /etc/sysconfig/init -# What ttys should gettys be started on? -ACTIVE_CONSOLES=/dev/tty[1-6]-
可以通过修改上面的配置来减少开机启动的时候创建的tty个数,比如我们只想要2个:
-[root@localhost ~]# sed -r -i 's!(ACTIVE_CONSOLES=/dev/tty[1-)6]!12]!' /etc/sysconfig/init -ACTIVE_CONSOLES=/dev/tty[1-2]-
其中-i参数表示直接修改原文件,-r参数是指使用扩展的正则表达式(ERE),扩展的正则表达式中分组的括号不需要用反斜杠转义”(ACTIVE_CONSOLES=/dev/tty
\[1-)"???[???????????????????????????????\1?????????????1????????????????
-????: d
-?????????
-[address]d-
?????????????????1,3d???1?3???????????????????????????????????????????????????????????????
$ sed '2,${d;=}' list
-John Daggett, 341 King Road, Plymouth MA
-??????????2???????????????(=)???????????????
-???/???/?????: i/a/c
-?????????????
-# Append ?? -[line-address]a\ -text -# Insert ?? -line-address]i\ -text -# Change ??? -[address]c\ -text-
?????????????c)??????????????????????????ArchLinux?????????????????????sed???GNU sed version 4.2.1?
-?????????????????text?????????????????????text????????????????????text???text?????????????????????????????????????????????????-n????????????????
-???????????????????????????????list?
-$ cat list -John Daggett, 341 King Road, Plymouth MA -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls PA -Eric Adams, 20 Post Road, Sudbury MA -Hubert Sims, 328A Brook Road, Roanoke VA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -Sal Carpenter, 73 6th Street, Boston MA-
????????2?????'------'?
-$ sed '2a --------------------------------------- -' list -John Daggett, 341 King Road, Plymouth MA -Alice Ford, 22 East Broadway, Richmond VA --------------------------------------- -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls PA -Eric Adams, 20 Post Road, Sudbury MA -Hubert Sims, 328A Brook Road, Roanoke VA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -Sal Carpenter, 73 6th Street, Boston MA-
??????3??????
-$ sed '3i --------------------------------------- -' list -John Daggett, 341 King Road, Plymouth MA -Alice Ford, 22 East Broadway, Richmond VA --------------------------------------- -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -Terry Kalkas, 402 Lans Road, Beaver Falls PA -Eric Adams, 20 Post Road, Sudbury MA -Hubert Sims, 328A Brook Road, Roanoke VA -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -Sal Carpenter, 73 6th Street, Boston MA-
???????????????????????????????????????????
-$ sed -n '2a --------------------------------------- -' list ----------------------------------------
??-n???????????????????????????????????????????
-?????????2??????????????'----'?
-$ sed '2,$c --------------------------------------- -' list -John Daggett, 341 King Road, Plymouth MA ----------------------------------------
????: p/l/=
-?????????????p????????(l?=)?p?????????????????????????
-??????????
-[address]p -[address]= -[address]l-
p??????????????????list???????
-$ sed -n '1p' list -John Daggett, 341 King Road, Plymouth MA-
l????p??????????????????vim?list????????
-$ echo "column1 column2 column3^M" | sed -n 'l' -column1tcolumn2tcolumn3r$-
=?????????????
-$ sed '=' list -1 -John Daggett, 341 King Road, Plymouth MA -2 -Alice Ford, 22 East Broadway, Richmond VA -3 -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK -4 -Terry Kalkas, 402 Lans Road, Beaver Falls PA -5 -Eric Adams, 20 Post Road, Sudbury MA -6 -Hubert Sims, 328A Brook Road, Roanoke VA -7 -Amy Wilde, 334 Bayshore Pkwy, Mountain View CA -8 -Sal Carpenter, 73 6th Street, Boston MA-
????: y
-?????????
-[address]y/SET1/SET2/-
?????????????SET1?????????SET2???????????1,3y/abc/xyz/??1?3?????a???x?b???y?c???z????????????????????tr???????????y???????????????????????
$ echo "hello, world" | sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/' -HELLO, WORLD-
??tr?????????
-$ echo "hello, world" | tr a-z A-Z � � � � � � � � � � � � � � � � � � � � � � � � � � -HELLO, WORLD-
??????: n
-???????????
-[address]n-
n????????????????????????????????????????????????????????????n?????d??????sed??????
-????????????n??????????????
-$ cat text -.H1 "On Egypt" - -Napoleon, pointing to the Pyramids, said to his troops: -"Soldiers, forty centuries have their eyes upon you."-
????.H1????????
-$ sed '/.H1/{n;/^$/d}' text
-.H1 "On Egypt"
-Napoleon, pointing to the Pyramids, said to his troops:
-"Soldiers, forty centuries have their eyes upon you."
-??????: r/w
-???????????
-[line-address]r file -[address]w file-
??????????????????????????????????(a)?????????????????????????text?
-$ cat text -For service, contact any of the following companies: -[Company-list] -Thank you.-
??????????????????company.list?
-$ cat company.list -Allied -Mayflower -United-
??????company.list??????[Company-list]???
-$ sed '/^[Company-list]/r company.list' text -For service, contact any of the following companies: -[Company-list] -Allied -Mayflower -United -Thank you.-
?????????????????[Company-list]??????????[Company-list]????
-$ sed '/^[Company-list]/r company.list;/^[Company-list]/d;' text -For service, contact any of the following companies: -[Company-list] -Thank you.-
????????????[Company-list]???company.list????????????????
-??????????????????r?????????????????company.list;/^\]
我们用-e选项将两个命令分开就正常了:
-$ sed -e '/^[Company-list]/r company.list' -e '/^[Company-list]/d;' text -For service, contact any of the following companies: -Allied -Mayflower -United -Thank you.-
写命令将匹配地址的所有行输出到指定的文件中。假设有一个文件内容如下,前半部分是人名,后半部分是区域名称:
-$ cat text -Adams, Henrietta Northeast -Banks, Freda South -Dennis, Jim Midwest -Garvey, Bill Northeast -Jeffries, Jane West -Madison, Sylvia Midwest -Sommes, Tom South-
现在我们要将不同区域的人名字写到不同的文件中:
-$ sed '/Northeast$/w region.northeast -> /South$/w region.south -> /Midwest$/w region.midwest -> /West$/w region.west' text -Adams, Henrietta Northeast -Banks, Freda South -Dennis, Jim Midwest -Garvey, Bill Northeast -Jeffries, Jane West -Madison, Sylvia Midwest -Sommes, Tom South-
查看输出的文件:
-$ cat region. -region.midwest region.northeast region.south region.west -$ cat region.midwest -Dennis, Jim Midwest -Madison, Sylvia Midwest-
我们也可以试试将所有的w命令放到一起用分号分隔,发现会出下面的错误,它把w空格后面的部分当作文件名,这样就验证了上面的猜测:
-$ sed '/Northeast$/w region.northeast;/south$/w region.south;' text -sed: couldn't open file region.northeast;/south$/w region.south;: No such file or directory-
退出命令: q
-退出命令的语法是
-[line-address]q-
当sed读取到匹配的行之后即退出,不会再读入新的行,并且将当前模式空间的内容输出到屏幕。例如打印前3行内容:
-$ sed '3q' list -John Daggett, 341 King Road, Plymouth MA -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK-
打印前3行也可以用p命令:
-$ sed -n '3p' list -John Daggett, 341 King Road, Plymouth MA -Alice Ford, 22 East Broadway, Richmond VA -Orville Thomas, 11345 Oak Bridge Road, Tulsa OK-
但是对于大文件来说,前者比后者效率更高,因为前者读取到第N行之后就退出了。后者虽然打印了前N行,但是后续的行还是要继续读入,只不会不作处理。
-到此为止,sed基础命令的部分就介绍完了。
-下面是我整理的一份思维导图(附.xmind文件下载地址):
- -Sed&awk笔记之sed篇:高级命令(一)
-在上一篇中介绍的基础命令都是面向行的,一般情况下,这种处理并没有什么问题,但是当匹配的内容是错开在两行时就会有问题,最明显的例子就是某些英文单词会被分成两行。
-幸运地是,sed允许将多行内容读取到模式空间,这样你就可以匹配跨越多行的内容。本篇笔记主要介绍这些命令,它们能够创建多行模式空间并且处理之。其中,N/D/P这三个多行命令分别对应于小写的n/d/p命令,后者我们在上一篇已经介绍。它们的功能是类似的,区别在于命令影响的内容不同。例如D命令与d命令同样是删除模式空间的内容,只不过d命令会删除模式空间中所有的内容,而D命令仅会删除模式空间中的第一行。
-读下一行:N
-N命令将下一行的内容读取到当前模式空间,但是下n命令不一样的地方是N命令并没有直接输出当前模式空间中的行,而是把下一行追加到当前模式空间,两行之间用回车符\n连接,如下图所示:
- -模式空间包含多行之后,正则表达式的^/$符号的意思就变了,^是匹配模式空间的最开始而非行首,$是匹配模式空间的最后位置而非行尾。
-书中给的第一例子,是替换以下文本中的"Owner and Operator Guide"为"Installation Guide",正如我们在本篇开头说的Owner and Operator Guide跨越在两行:
-Consult Section 3.1 in the Owner and Operator -Guide for a description of the tape drives -available on your system.-
我们用N命令试下手:
-$ sed '/Operator$/{N;s/Owner and OperatornGuide/Installation Guide/}' text
-Consult Section 3.1 in the Installation Guide for a description of the tape drives
-available on your system.
-不过这个例子有两个局限:
-● 我们知道Owner and Operator Guide分割的位置;
-● 执行替换命令后,前后两行拼接在一起,导致这行过长;
-第二点,可以这样解决:
-$ sed '/Operator$/{N;s/Owner and OperatornGuide /Installation Guiden/}' text
-Consult Section 3.1 in the Installation Guide
-for a description of the tape drives
-available on your system.
-现在去掉第1个前提,我们引入更加复杂的测试文本,这段文本中Owner and Operator Guide有位于一行的,也有跨越多行的情况:
-$ cat text -Consult Section 3.1 in the Owner and Operator -Guide for a description of the tape drives -available on your system. - -Look in the Owner and Operator Guide shipped with your system. - -Two manuals are provided including the Owner and -Operator Guide and the User Guide. - -The Owner and Operator Guide is shipped with your system.-
相应地修改下之前执行的命令,如下所示:
-$ sed 's/Owner and Operator Guide/Installation Guide/
-/Owner/{
-N
-s/ *n/ /
-s/Owner and Operator Guide */Installation Guide
-/
-}' text
-Consult Section 3.1 in the Installation Guide
-for a description of the tape drives
-available on your system.
-
-Look in the Installation Guide shipped with your system.
-
-Two manuals are provided including the Installation Guide
-and the User Guide.
-
-The Installation Guide is shipped with your system.
-这里我们首先将在单行出现的Owner and Operator Guide替换为Installation Guide,然后再寻找匹配Owner的行,匹配后读取下一行的内容到模式空间,并且将中间的换行符替换成空格,最后再替换Owner and Operator Guide。
-解释起来很简单,但是中间还是有些门道的。比如你可能觉得这里最前面的s/Owner and Operator Guide/Installation Guide/命令是多余的,假设你删除这一句:
-$ sed '/Owner/{
-> N
-> s/ *n/ /
-> s/Owner and Operator Guide */Installation Guide
-> /
-> }' text
-Consult Section 3.1 in the Installation Guide
-for a description of the tape drives
-available on your system.
-
-Look in the Installation Guide
-shipped with your system.
-Two manuals are provided including the Installation Guide
-and the User Guide.
-
-The Owner and Operator Guide is shipped with your system.
-最明显的问题是最后一行没有被替换,原因是当最后一行的被读入到模式空间后,匹配Owner,执行N命令读入下一行,但是因为当前已经是最后一行,所以N读取替换,告诉sed可以退出了,sed也不会继续执行接下来的替换命令(注:书中说最后一行不会被打印输出,我这边测试是会输出的)。所以这里需要在使用N的时候加一个判断,即当前行为最后一行时,不读取下一行的内容:$!N,这一点在很多场合都是有用的,更改后重新执行:
-$ sed '/Owner/{
-> $!N
-> s/ *n/ /
-> s/Owner and Operator Guide */Installation Guide
-> /
-> }' text
-Consult Section 3.1 in the Installation Guide
-for a description of the tape drives
-available on your system.
-
-Look in the Installation Guide
-shipped with your system.
-Two manuals are provided including the Installation Guide
-and the User Guide.
-
-The Installation Guide
-is shipped with your system.
-上面只是为了用例子说明N的用法,可能解决方案未必是最好的。
-删除行:D
-该命令删除模式空间中第一行的内容,而它对应的小d命令删除模式空间的所有内容。D不会导致读入新行,相反它会回到最初的编辑命令,重要应用在模式空间剩余的内容上。
-后面半句开始比较难以理解,用书中的一个例子来解释下。现在我们有一个文本文件,内容如下所示,行之间有空行:
-$ cat text -This line is followed by 1 blank line. - -This line is followed by 2 blank lines. - -This line is followed by 3 blank lines. - -This line is followed by 4 blank lines. - -This is the end.-
现在我们要删除多余的空行,将多个空行缩减成一行。假如我们使用d命令来删除,很简单的逻辑:
-$ sed '/^$/{N;/^n$/d}' text
-This line is followed by 1 blank line.
-
-This line is followed by 2 blank lines.
-This line is followed by 3 blank lines.
-
-This line is followed by 4 blank lines.
-This is the end.
-我们会发现一个奇怪的结果,奇数个数的相连空行已经被合并成一行,但是偶数个数的却全部被删除了。造成这样的原因需要重新翻译下上面的命令,当匹配一个空行是,将下一行也读取到模式空间,然后若下一行也是空行,则模式空间中的内容应该是\n,因此匹配^\n$,从而执行d命令会将模式空间中的内容清空,结果就是相连的两个空行都被删除。这样就可以理解为什么相连奇数个空行的情况下是正常的,而偶数个数就有问题了。
-这种情况下,我们就应该用D命令来处理,这样做就得到预期的结果了:
-$ sed '/^$/{N;/^n$/D}' text
-This line is followed by 1 blank line.
-
-This line is followed by 2 blank lines.
-
-This line is followed by 3 blank lines.
-
-This line is followed by 4 blank lines.
-
-This is the end.
-D命令只会删除模式空间的第一行,而且删除后会重新在模式空间的内容上执行编辑命令,类似形成一个循环,前提是相连的都是空行。当匹配一个空行时,N读取下一行内容,此时匹配^\n$导致模式空间中的第一行被删除。现在模式空间中的内容是空的,重新执行编辑命令,此时匹配/^$/。继续读取下一行,当下一行依然为空行时,重复之前的动作,否则输出当前模式空间的内容。造成的结果是连续多个空行,只有最后一个空行是保留输出的,其余的都被删除了。这样的结果才是我们最初希望得到的。
-打印行:P
-P命令与p命令一样是打印模式空间的内容,不同的是前者仅打印模式空间的第一行内容,而后者是打印所有的内容。因为编辑命令全部执行完之后,sed默认会输出模式空间的内容,所以一般情况下,p和P命令都是与-n选项一起使用的。但是有一种情况是例外的,即编辑命令的执行流程被改变的情况,例如N,D等。很多情况下,P命令都是用在N命令之后,D命令之前的。这三个命令合起来,可以形成一人输入输出的循环,并且每次只打印一行:读入一行后,N继续读下一行,P命令打印第一行,D命令删除第一行,执行流程回到最开始重复该过程。
-$ echo -e "line1nline2nline3" | sed '$!N;P;D'-
不过多行命令用起来要格外小心,你要用自己的大脑去演算一遍执行的过程,要不然很容易出错,比如:
-$ echo -e "line1nline2nline3" | sed -n 'N;1P'-
你可能期望打印第一行的内容,事实上并没有输出。原因是当N继续读入第二行后,当前行号已经是2了,我们在第二篇笔记中曾经说过,行号只是sed在内部维护的一个计数变量而已,每当读入新的一行,行号就加一:
-$ echo -e "line1nline2nline3" | sed -n '$!N;=' -2 -3-
我们依然用替换Unix System为Unix Operating System作为例子,介绍N/P/D三个命令是如何配合使用的。
-示例文本如下所示,为了举例方便,这段文本仅有三行内容,刚好可以演示一个循环的处理过程:
-$ cat text -The UNIX -System and UNIX -...-
执行的命令:
-$ sed '/UNIX$/{
-> N
-> s/nSystem/ Operating &/
-> P
-> D
-> }' text
-The UNIX Operating
-System and UNIX
-...
-
-以上三个命令的用法与之前介绍的基础命令是截然不同的,有些同学可能都没有接触过。在下一篇中,我会介绍更多高级的命令,同时为引入一个新的概念:保持空间(Hold Space)。
--
-
Sed&awk笔记之sed篇:高级命令(二)
-上一篇中介绍了N/D/P三个命令,它们可以形成多行的模式空间,在一点程度上弥补了单行模式空间的不足,我们用sed编辑文本的能力又进了一步。模式空间是sed内部维护的一个缓存空间,它存放着读入的一行或者多行内容。但是模式空间的一个限制是无法保存模式空间中被处理的行,因此sed又引入了另外一个缓存空间——模式空间(Hold Space)。
-保持空间
-保持空间用于保存模式空间的内容,模式空间的内容可以复制到保持空间,同样地保持空间的内容可以复制回模式空间。sed提供了几组命令用来完成复制的工作,其它命令无法匹配也不能修改模式空间的内容。
-操作保持空间的命令如下所示:
-| 名称 | -命令 | -说明 | -
|---|---|---|
| 保存(Hold) | -h/H | -将模式空间的内容复制或者追加到保持空间 | -
| 取回(Get) | -g/G | -将保持空间的内容复制或者追加到模式空间 | -
| 交换(Exchange) | -x | -交换模式空间和保持空间的内容 | -
这几组命令提供了保存、取回以及交换三个动作,交换命令比较容易理解,保存命令和取回命令都有大写和小写两种形式,这两种形式的区别是小写的是将会覆盖目的空间的内容,而大写的是将内容追加到目的空间,追加的内容和原有的内容是以\n分隔。
-基本使用
-我们随便试试这几个命令,假设有如下测试文本:
-$ cat text -1 -2 -11 -22 -111 -222-
1. 首先,仅使用h/H或者g/G命令:
-使用h命令:
-$ sed 'h' text -1 -2 -11 -22 -111 -222-
使用G命令:
-$ sed 'G' text -1 - -2 - -11 - -22 - -111 - -222-
前者返回的结果正常,因为复制到保持空间的内容并没有取回;后者每一行的后面都多了一个空行,原因是每行都会从保持空间取回一行,追加(大写的G)到模式空间的内容之后,以\n分隔。
-2. 使用x命令交换空间
-$ sed 'x' text - -1 -2 -11 -22 -111-
命令执行后,发现前面多了一个空行并且最后一行不见了。我在前面一直强调sed命令用好,要有用大脑回顾命令执行过程的能力:
-* 当读入第一行的时候,模式空间中的内容是第一行的内容,而保持空间是空的,这个时候交换两个空间,导致模式空间为空,保持空间为第一行的内容,因此输出为空行;
-* 当读入下一行之后,模式空间为第2行的内容,保持空间为第一行的内容,交换后输出第1行的内容;
-* 依次读入每一行,输出上一行的内容;
-* 直到最后一行被读入到模式空间,交换后输出倒数第二行的内容,而最后一行的内容并没有输出,此时命令执行结束。
-深入使用
-上面的例子简单地介绍了保持空间命令的基本使用方法,这些命令单个使用可能效果不大,但是组合起来的效果是非常好的。
-1.第一个例子: 使用逗号拼接行
-$ sed 'H;$!d;${x;s/^n//;s/n/,/g}' text
-1,11,2,11,22,111,222
-上面的命令执行过程是这样的,使用H将每一行都追加到保持空间,这里利用d命令打断常规的命令执行流程,让sed继续读入新的一行,直接到将最后一行都放到保持空间。这个时候使用x命令将保持空间的内容交换到模式空间,模式空间的内容现在是这样的:\n1\n11\n2\n11\n22\n111\n222。替换的步骤分成两个,首先去掉首个回车符,然后把剩余的回车符替换成逗号。
其实上面的过程可以包装成一个常用的函数:
-$ function join_lines()
-> {
-> sed 'H;$!d;${x;s/^n//;s/n/,/g}' $1
-> }
-$ join_lines text
-1,11,2,11,22,111,222
-进一步,我们可以让分隔符可以通过参数设置,另外一方面删除文件名的参数,而改成常见的过滤器命令形式(即通过管道传递输入):
-$ function join_lines()
-> {
-> local delim=${1:-,}
-> sed 'H;$!d;${x;s/^n//;s/n/'$delim'/g}'
-> }
-$ cat text | join_lines ';'
-1;11;2;11;22;111;222
-但是如果我们要用&作为符号,就会出现问题:
-$ cat text | join_lines '&' -1 -11 -2 -11 -22 -111 -222-
上面并没有&作为分隔符,这是因为&是元字符,表示匹配的部分,这里刚好是回车符\n。因此我们需要对分隔符进行转义:
-$ function join_lines()
-> {
-> local delim=${1:-,}
-> sed 'H;$!d;${x;s/^n//;s/n/'$delim'/g}'
-> }
-$ cat text | join_lines '&'
-1&11&2&11&22&111&222
-2.第二个例子:将语句中的特定单词转换成大写
-现在有这样的文本,有许多类似这样的find the Match statement语句,其中Match是语句的名称,但是这个单词的大小写不统一,有些地方是小写的,有些地方是首字符大写,现在我们要做的是把这个单词统一转换成大写。
-容易联想到的是Sed&awk笔记之sed篇:基础命令中介绍的y命令,利用y命令确实可以做到小写转换成大写,转换的命令是这样的:
-y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/-
但是y命令是会把模式空间的所有内容都转换,这样不能满足我们的需求。但是我们可以利用保持空间保存当前行,然后处理模式空间中的内容:
-/the .* statement/{
-h
-s/.*the (.*) statement.*/1/
-y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
-G
-s/(.*)n(.*the ).*( statement.*)/213/
-}
-老规矩一条一条过上面的命令,为了方便说明,每个命令解释后我都会给出当前模式空间和保持空间的内容。
-首先找到需要处理的行(假设当前行为find the Match statement)。
-Pattern space: find the Match statement-
将当前行保存到保持空间:
-Pattern space: find the Match statement -Hold space: find the Match statement-
然后利用替换命令获取需要处理的单词:
-Pattern space: Match -Hold space: find the Match statement-
然后通过转换命令将其转换成大写:
-Pattern space: MATCH -Hold space: find the Match statement-
现在再利用G命令将保持空间的内容追加到模式空间最后:
-Pattern space: MATCH\nfind the Match statement -Hold space: find the Match statement-
最后再次利用替换命令处理下:
-Pattern space: find the MATCH statement -Hold space: find the Match statement-
流程控制
-一般情况下,sed是将编辑命令从上到下依次应用到读入的行上,但是像d/n/D/N命令都能够在一定程度上改变默认的执行流程,甚至利用N/D/P三个命令可以形成一个强大的循环处理流程。除此之外,其实sed还提供了分支命令(b)和测试(test)两个命令来控制流程,这两个命令可以跳转到指定的标签(label)位置继续执行命令。标签是以冒号开头的标记,如下例中的:top标签:
-:top -command1 -command2 -/pattern/b top -command3-
当执行到/pattern/b top时,如果匹配pattern,则跳转到:top标签所在的位置,继续执行下一个命令command1。
如果没有指定标签,则将控制转移到脚本的结尾处。也许这只是一个默认的行为,但是有时候如果用得好也是非常有用的,例如:
-/pattern/b -command 1 -command 2 -command 3-
当执行到/pattern/b时,如果匹配pattern,则跳转到最后。这种情况下匹配pattern的行可以避开执行后续的命令,被排除在外。
下一个例子中,我们利用分支命令的跳转效果达到类似if语句的效果:
-command1 -/pattern/b end -command2 -:end -command3-
当执行到/pattern/b end时,如果匹配pattern,则跳转到:end标签所在的位置,跳过command2而不执行。
进一步地,利用两个分支命令可以达到if..else的分支效果:
-command1 -/pattern/b dothree -command2 -b -:dothree -command3-
这个例子中,当执行到/pattern/b dothree时,若匹配pattern则中转到:dothree标签,此时执行command3;若不匹配,则执行command2,并且跳转到最后。
上面的例子都是用到了分支命令,分支命令的跳转是无条件的。而与之相对的是测试命令,测试命令的跳转是有条件的,当且仅当当前行发生成功的替换时才跳转。
-为了说明测试命令的用法,我们用它来实现前文定义过的join_lines函数:
-$ sed ':a;$!N;s/n/,/;ta' text -1,11,2,11,22,111,222-
在最前面我们添加了一个标签:a,然后在再最后面利用测试命令跳转到该标签。可能,你会觉得这里也可以用分支命令,但是事实上分支命令会导致死循环,因为在它里他没有结束的条件。
-但是测试命令就不同了,这一点直到最后才体现出来。当最后一行被N命令读入之后,回车替换成逗号,此时ta继续跳转到最开头,因为所有行都已经被读入,所以$!不匹配,同时模式空间中的回车已经全部被替换成逗号,所以替换也不会发生。之前我们说过,当且仅当当前行发生成功的替换时测试命令才跳转。所以此时跳转不会发生,退出sed命令。
-我们可以利用sedsed这个工具来验证上面的过程,sedsed可以用来调试sed脚本。
-$ ./sedsed -d -e ':a;$!N;s/n/,/;ta' text -PATT:1$ -HOLD:$ -COMM::a -COMM:$ !N -PATT:1n11$ -HOLD:$ -COMM:s/n/,/ -PATT:1,11$ -HOLD:$ -COMM:t a -COMM:$ !N -PATT:1,11n2$ -HOLD:$ -... -... -COMM:$ !N -PATT:1,11,2,11,22,111,222n1111$ -HOLD:$ -COMM:s/n/,/ -PATT:1,11,2,11,22,111,222,1111$ -HOLD:$ -COMM:t a -COMM:$ !N -PATT:1,11,2,11,22,111,222,1111$ -HOLD:$ -COMM:s/n/,/ -PATT:1,11,2,11,22,111,222,1111$ -HOLD:$ -COMM:t a -PATT:1,11,2,11,22,111,222,1111$ -HOLD:$ -1,11,2,11,22,111,222,1111-
看第27行替换命令发生的时候,此时模式空间的内容为PATT:1,11,2,11,22,111,222,1111$,因此替换失败,ta命令不会发生跳转,脚本执行退出。
而如果在这里把测试命令换成分支命令,整个执行过程就会陷入死循环了:
-COMM:b a -COMM:$ !N -PATT:1,11,2,11,22,111,222,1111$ -HOLD:$ -COMM:s/n/,/ -PATT:1,11,2,11,22,111,222,1111$ -HOLD:$ -COMM:b a -COMM:$ !N -PATT:1,11,2,11,22,111,222,1111$-
高级命令总结
-到此为止,所有高级命令的用法就已经介绍完了。最后一段内容由于时间的关系,写得比较仓促。高级命令的用法比起基础命令相对复杂一点,而且容易出错,需要十分小心,如果不确定可以用上面介绍的sedsed工具来调式下,而且便于加深各种命令行为的理解。
--
-
Sed&awk笔记之sed篇:实战
-相信大家肯定用过grep这个命令,它可以找出匹配某个正则表达式的行,例如查看包含"the word"的行:
-$ grep "the word" filename-
但是grep是针对单行作匹配的,所以如果一个短句跨越了两行就无法匹配。这就给我们出了一个题目,如何用sed模仿grep的行为,并且支持跨行短句的匹配呢?
-当单词仅出现在一行时很简单,用普通的正则就可以匹配,这里我们用到分支命令,当匹配时则跳转到最后:
-/the word/b-
当单词跨越两行,容易想到用N命令将下一行读取到模式空间再做处理。例如我们同样要查找短句"the word",现在的文本是这样的:
-$ cat text -we want to find the phrase the -word, but it appears across two lines.-
当用N玲读入下一行时,模式空间的内容如下所示:
-Pattern space: we want to find the phrase the\nword, but it appears across two lines.-
因此,需要将回车符号删除,然后再作匹配。
-$!N -s/ *\n/ / -/the word/b-
可是这里会有一个问题,如果短句恰好在读入的第二行的话,虽然匹配,但是会打印出模式空间中的所有内容,这样不符合grep的行为,只显示包含短句的那一行。所以这里要再加一个处理,将模式空间的第一行内容删除,再在第二行中匹配。但是在此之前,首先要保存模式空间的内容,否则可没有后悔药可吃。
-h命令可以将模式空间的内容保存到保持空间,然后利用替换命令将模式空间的第一行内容清除,然后再作匹配处理:
-$!N -h -s/.*\n// -/the word/b-
如果不匹配,则短句可能确实是跨越两行,这时候我们首先用g命令将之前保存的内容从保持空间取回到模式空间,替换掉回车后再进行匹配:
-g -s/ *\n/ / -/the word/b-
这里如果匹配则直接跳转到最后,并且输出的内容是替换后的内容。
-但是我们要输出的是匹配的原文,而原文现在在保持空间还有一份拷贝,因此当匹配时,需要将原文从保持空间取回:
-g
-s/ *\n/ /
-/the word/{
-g
-b
-}
-同样地,我们要考虑不匹配的情况,不匹配的时候也要将会原文从保持空间取回,并且将模式空间的第一行删除,继续处理下一行:
-g
-s/ *\n/ /
-/the word/{
-g
-b
-}
-g
-D
-将所有的sed脚本合在一起,假设我们将以下内容保存到phrase.sed文件中:
-/the word/b
-$!N
-h
-s/.*\n//
-/the word/b
-g
-s/ *\n//
-/the word/{
-g
-b
-}
-g
-D
-接下来,我们用一段文本来测试下以上的脚本是否正确:
-$ cat text -We will use phrase.sed to print any line which contains -the word. Or if the word appears across two lines, like -below: - -It will print this line, because the -word appears across two lines. - -You can run sed -f phrase.sed text to test this.-
执行命令如下所示:
-$ sed -f phrase.sed text -the word. Or if the word appears across two lines, like -It will print this line, because the -word appears across two lines.-
上面的命令中的"the word"其实可以是一个变量,这样我们就可以将这个功能写成一个脚本或者函数,用在更多地方:
-$ cat phrase.sh
-#! /bin/sh
-# phrase -- search for words across lines
-# $1 = search string; remaining args = filenames
-
-search=$1
-
-for file in ${@:2}; do
- sed "/$search/b
- $!N
- h
- s/.*n//
- /$search/b
- g
- s/ *n/ /
- /$search/{
- g
- b
- }
- g
- D" $file
-done
-
-$ chmod +x phrase.sh
-$ ./phrase.sh 'the word' text
-the word. Or if the word appears across two lines, like
-It will print this line, because the
-word appears across two lines.
-这只是一个开头,或者你也可以在此基础上扩展更多的功能。sed的命令从单个看来并没是很复杂,但是要用得好就有点难度了,所以需要多多实践,多多思考,这一点跟正则表达式的学习是一样的。如果觉得没有现成的学习环境,sed1line是一个不错的开始。
--
- - \ No newline at end of file diff --git a/src/sql.md b/src/sql.md deleted file mode 100644 index aa96a17..0000000 --- a/src/sql.md +++ /dev/null @@ -1,288 +0,0 @@ -
很多程序员视 SQL 为洪水猛兽。SQL 是一种为数不多的声明性语言,它的运行方式完全不同于我们所熟知的命令行语言、面向对象的程序语言、甚至是函数语言(尽管有些人认为 SQL 语言也是一种函数式语言)。
-我们每天都在写 SQL 并且应用在开源软件 jOOQ 中。于是我想把 SQL 之美介绍给那些仍然对它头疼不已的朋友,所以本文是为了以下读者而特地编写的:
-1、 在工作中会用到 SQL 但是对它并不完全了解的人。
-2、 能够熟练使用 SQL 但是并不了解其语法逻辑的人。
-3、 想要教别人 SQL 的人。
-
本文着重介绍 SELECT 句式,其他的 DML (Data Manipulation Language 数据操纵语言命令)将会在别的文章中进行介绍。
-首先要把这个概念记在脑中:“声明”。 SQL 语言是为计算机声明了一个你想从原始数据中获得什么样的结果的一个范例,而不是告诉计算机如何能够得到结果。这是不是很棒?
-(译者注:简单地说,SQL 语言声明的是结果集的属性,计算机会根据 SQL 所声明的内容来从数据库中挑选出符合声明的数据,而不是像传统编程思维去指示计算机如何操作。)
-SELECT first_name, last_name FROM employees WHERE salary > 100000-
上面的例子很容易理解,我们不关心这些雇员记录从哪里来,我们所需要的只是那些高薪者的数据(译者注: salary>100000 )。
-我们从哪儿学习到这些?
-如果 SQL 语言这么简单,那么是什么让人们“闻 SQL 色变”?主要的原因是:我们潜意识中的是按照命令式编程的思维方式思考问题的。就好像这样:“电脑,先执行这一步,再执行那一步,但是在那之前先检查一下是否满足条件 A 和条件 B ”。例如,用变量传参、使用循环语句、迭代、调用函数等等,都是这种命令式编程的思维惯式。
-SQL 语句有一个让大部分人都感到困惑的特性,就是:SQL 语句的执行顺序跟其语句的语法顺序并不一致。SQL 语句的语法顺序是:
-为了方便理解,上面并没有把所有的 SQL 语法结构都列出来,但是已经足以说明 SQL 语句的语法顺序和其执行顺序完全不一样,就以上述语句为例,其执行顺序为:
-关于 SQL 语句的执行顺序,有三个值得我们注意的地方:
-1、 FROM 才是 SQL 语句执行的第一步,并非 SELECT 。数据库在执行 SQL 语句的第一步是将数据从硬盘加载到数据缓冲区中,以便对这些数据进行操作。(译者注:原文为“The first thing that happens is loading data from the disk into memory, in order to operate on such data.”,但是并非如此,以 Oracle 等常用数据库为例,数据是从硬盘中抽取到数据缓冲区中进行操作。)
-2、 SELECT 是在大部分语句执行了之后才执行的,严格的说是在 FROM 和 GROUP BY 之后执行的。理解这一点是非常重要的,这就是你不能在 WHERE 中使用在 SELECT 中设定别名的字段作为判断条件的原因。
-SELECT A.x + A.y AS z -FROM A -WHERE z = 10 -- z 在此处不可用,因为SELECT是最后执行的语句!-
如果你想重用别名z,你有两个选择。要么就重新写一遍 z 所代表的表达式:
-SELECT A.x + A.y AS z -FROM A -WHERE (A.x + A.y) = 10-
…或者求助于衍生表、通用数据表达式或者视图,以避免别名重用。请看下文中的例子。
-3、 无论在语法上还是在执行顺序上, UNION 总是排在在 ORDER BY 之前。很多人认为每个 UNION 段都能使用 ORDER BY 排序,但是根据 SQL 语言标准和各个数据库 SQL 的执行差异来看,这并不是真的。尽管某些数据库允许 SQL 语句对子查询(subqueries)或者派生表(derived tables)进行排序,但是这并不说明这个排序在 UNION 操作过后仍保持排序后的顺序。
-注意:并非所有的数据库对 SQL 语句使用相同的解析方式。如 MySQL、PostgreSQL和 SQLite 中就不会按照上面第二点中所说的方式执行。
-我们学到了什么?
-既然并不是所有的数据库都按照上述方式执行 SQL 预计,那我们的收获是什么?我们的收获是永远要记得: SQL 语句的语法顺序和其执行顺序并不一致,这样我们就能避免一般性的错误。如果你能记住 SQL 语句语法顺序和执行顺序的差异,你就能很容易的理解一些很常见的 SQL 问题。
-当然,如果一种语言被设计成语法顺序直接反应其语句的执行顺序,那么这种语言对程序员是十分友好的,这种编程语言层面的设计理念已经被微软应用到了 LINQ 语言中。
-由于 SQL 语句语法顺序和执行顺序的不同,很多同学会认为SELECT 中的字段信息是 SQL 语句的核心。其实真正的核心在于对表的引用。
-根据 SQL 标准,FROM 语句被定义为:
-<from clause> ::= FROM <table reference> [ { <comma> <table reference> }... ]
-FROM 语句的“输出”是一张联合表,来自于所有引用的表在某一维度上的联合。我们们慢慢来分析:
-FROM a, b-
上面这句 FROM 语句的输出是一张联合表,联合了表 a 和表 b 。如果 a 表有三个字段, b 表有 5 个字段,那么这个“输出表”就有 8 ( =5+3)个字段。
-这个联合表里的数据是 a*b,即 a 和 b 的笛卡尔积。换句话说,也就是 a 表中的每一条数据都要跟 b 表中的每一条数据配对。如果 a 表有3 条数据, b 表有 5 条数据,那么联合表就会有 15 ( =5*3)条数据。
-FROM 输出的结果被 WHERE 语句筛选后要经过 GROUP BY 语句处理,从而形成新的输出结果。我们后面还会再讨论这方面问题。
-如果我们从集合论(关系代数)的角度来看,一张数据库的表就是一组数据元的关系,而每个 SQL 语句会改变一种或数种关系,从而产生出新的数据元的关系(即产生新的表)。
-我们学到了什么?
-思考问题的时候从表的角度来思考问题提,这样很容易理解数据如何在 SQL 语句的“流水线”上进行了什么样的变动。
-灵活引用表能使 SQL 语句变得更强大。一个简单的例子就是 JOIN 的使用。严格的说 JOIN 语句并非是 SELECT 中的一部分,而是一种特殊的表引用语句。 SQL 语言标准中表的连接定义如下:
-<table reference> ::= - <table name> - | <derived table> - | <joined table>-
就拿之前的例子来说:
-FROM a, b-
a 可能输如下表的连接:
-a1 JOIN a2 ON a1.id = a2.id-
将它放到之前的例子中就变成了:
-FROM a1 JOIN a2 ON a1.id = a2.id, b-
尽管将一个连接表用逗号跟另一张表联合在一起并不是常用作法,但是你的确可以这么做。结果就是,最终输出的表就有了 a1+a2+b 个字段了。
-(译者注:原文这里用词为 degree ,译为维度。如果把一张表视图化,我们可以想象每一张表都是由横纵两个维度组成的,横向维度即我们所说的字段或者列,英文为columns;纵向维度即代表了每条数据,英文为 record ,根据上下文,作者这里所指的应该是字段数。)
-在 SQL 语句中派生表的应用甚至比表连接更加强大,下面我们就要讲到表连接。
-我们学到了什么?
-思考问题时,要从表引用的角度出发,这样就很容易理解数据是怎样被 SQL 语句处理的,并且能够帮助你理解那些复杂的表引用是做什么的。
-更重要的是,要理解 JOIN 是构建连接表的关键词,并不是 SELECT 语句的一部分。有一些数据库允许在 INSERT 、 UPDATE 、 DELETE 中使用 JOIN 。
-我们先看看刚刚这句话:
-FROM a, b-
高级 SQL 程序员也许学会给你忠告:尽量不要使用逗号来代替 JOIN 进行表的连接,这样会提高你的 SQL 语句的可读性,并且可以避免一些错误。
-利用逗号来简化 SQL 语句有时候会造成思维上的混乱,想一下下面的语句:
-FROM a, b, c, d, e, f, g, h -WHERE a.a1 = b.bx -AND a.a2 = c.c1 -AND d.d1 = b.bc --- etc...-
我们不难看出使用 JOIN 语句的好处在于:
-我们学到了什么?
-记着要尽量使用 JOIN 进行表的连接,永远不要在 FROM 后面使用逗号连接表。
-SQL 语句中,表连接的方式从根本上分为五种:
-EQUI JOIN
-这是一种最普通的 JOIN 操作,它包含两种连接方式:
-用例子最容易说明其中区别:
--- This table reference contains authors and their books.
--- There is one record for each book and its author.
--- authors without books are NOT included
-author JOIN book ON author.id = book.author_id
-
--- This table reference contains authors and their books
--- There is one record for each book and its author.
--- ... OR there is an "empty" record for authors without books
--- ("empty" meaning that all book columns are NULL)
-author LEFT OUTER JOIN book ON author.id = book.author_id
-SEMI JOIN
-这种连接关系在 SQL 中有两种表现方式:使用 IN,或者使用 EXISTS。“ SEMI ”在拉丁文中是“半”的意思。这种连接方式是只连接目标表的一部分。这是什么意思呢?再想一下上面关于作者和书名的连接。我们想象一下这样的情况:我们不需要作者 / 书名这样的组合,只是需要那些在书名表中的书的作者信息。那我们就能这么写:
--- Using IN -FROM author -WHERE author.id IN (SELECT book.author_id FROM book) - --- Using EXISTS -FROM author -WHERE EXISTS (SELECT 1 FROM book WHERE book.author_id = author.id)-
尽管没有严格的规定说明你何时应该使用 IN ,何时应该使用 EXISTS ,但是这些事情你还是应该知道的:
-因为使用 INNER JOIN 也能得到书名表中书所对应的作者信息,所以很多初学者机会认为可以通过 DISTINCT 进行去重,然后将 SEMI JOIN 语句写成这样:
--- Find only those authors who also have books -SELECT DISTINCT first_name, last_name -FROM author -JOIN book ON author.id = book.author_id-
这是一种很糟糕的写法,原因如下:
-更多的关于滥用 DISTINCT 的危害可以参考这篇博文
-(http://blog.jooq.org/2013/07/30/10-common-mistakes-java-developers-make-when-writing-sql/)。
-ANTI JOIN
-这种连接的关系跟 SEMI JOIN 刚好相反。在 IN 或者 EXISTS 前加一个 NOT 关键字就能使用这种连接。举个例子来说,我们列出书名表里没有书的作者:
--- Using IN -FROM author -WHERE author.id NOT IN (SELECT book.author_id FROM book) - --- Using EXISTS -FROM author -WHERE NOT EXISTS (SELECT 1 FROM book WHERE book.author_id = author.id)-
关于性能、可读性、表达性等特性也完全可以参考 SEMI JOIN。
-这篇博文介绍了在使用 NOT IN 时遇到 NULL 应该怎么办,因为有一点背离本篇主题,就不详细介绍,有兴趣的同学可以读一下
-(http://blog.jooq.org/2012/01/27/sql-incompatibilities-not-in-and-null-values/)。
-CROSS JOIN
-这个连接过程就是两个连接的表的乘积:即将第一张表的每一条数据分别对应第二张表的每条数据。我们之前见过,这就是逗号在 FROM 语句中的用法。在实际的应用中,很少有地方能用到 CROSS JOIN,但是一旦用上了,你就可以用这样的 SQL语句表达:
--- Combine every author with every book -author CROSS JOIN book-
DIVISION
-DIVISION 的确是一个怪胎。简而言之,如果 JOIN 是一个乘法运算,那么 DIVISION 就是 JOIN 的逆过程。DIVISION 的关系很难用 SQL 表达出来,介于这是一个新手指南,解释 DIVISION 已经超出了我们的目的。但是有兴趣的同学还是可以来看看这三篇文章
-(http://blog.jooq.org/2012/03/30/advanced-sql-relational-division-in-jooq/)
-(http://en.wikipedia.org/wiki/Relational_algebra#Division)
-(https://www.simple-talk.com/sql/t-sql-programming/divided-we-stand-the-sql-of-relational-division/)。
-我们学到了什么?
-学到了很多!让我们在脑海中再回想一下。 SQL 是对表的引用, JOIN 则是一种引用表的复杂方式。但是 SQL 语言的表达方式和实际我们所需要的逻辑关系之间是有区别的,并非所有的逻辑关系都能找到对应的 JOIN 操作,所以这就要我们在平时多积累和学习关系逻辑,这样你就能在以后编写 SQL 语句中选择适当的 JOIN 操作了。
-在这之前,我们学习到过 SQL 是一种声明性的语言,并且 SQL 语句中不能包含变量。但是你能写出类似于变量的语句,这些就叫做派生表:
-说白了,所谓的派生表就是在括号之中的子查询:
--- A derived table -FROM (SELECT * FROM author)-
需要注意的是有些时候我们可以给派生表定义一个相关名(即我们所说的别名)。
--- A derived table with an alias -FROM (SELECT * FROM author) a-
派生表可以有效的避免由于 SQL 逻辑而产生的问题。举例来说:如果你想重用一个用 SELECT 和 WHERE 语句查询出的结果,这样写就可以(以 Oracle 为例):
--- Get authors' first and last names, and their age in days -SELECT first_name, last_name, age -FROM ( - SELECT first_name, last_name, current_date - date_of_birth age - FROM author -) --- If the age is greater than 10000 days -WHERE age > 10000-
需要我们注意的是:在有些数据库,以及 SQL : 1990 标准中,派生表被归为下一级——通用表语句( common table experssion)。这就允许你在一个 SELECT 语句中对派生表多次重用。上面的例子就(几乎)等价于下面的语句:
-WITH a AS ( - SELECT first_name, last_name, current_date - date_of_birth age - FROM author -) -SELECT * -FROM a -WHERE age > 10000-
当然了,你也可以给“ a ”创建一个单独的视图,这样你就可以在更广泛的范围内重用这个派生表了。更多信息可以阅读下面的文章(http://en.wikipedia.org/wiki/View_%28SQL%29)。
-我们学到了什么?
-我们反复强调,大体上来说 SQL 语句就是对表的引用,而并非对字段的引用。要好好利用这一点,不要害怕使用派生表或者其他更复杂的语句。
-让我们再回想一下之前的 FROM 语句:
-FROM a, b-
现在,我们将 GROUP BY 应用到上面的语句中:
-GROUP BY A.x, A.y, B.z-
上面语句的结果就是产生出了一个包含三个字段的新的表的引用。我们来仔细理解一下这句话:当你应用 GROUP BY 的时候, SELECT 后没有使用聚合函数的列,都要出现在 GROUP BY 后面。(译者注:原文大意为“当你是用 GROUP BY 的时候,你能够对其进行下一级逻辑操作的列会减少,包括在 SELECT 中的列”)。
-SELECT A.x, A.y, SUM(A.z) -FROM A -GROUP BY A.x, A.y-
我们学到了什么?
-GROUP BY,再次强调一次,是在表的引用上进行了操作,将其转换为一种新的引用方式。
-我个人比较喜欢“映射”这个词,尤其是把它用在关系代数上。(译者注:原文用词为 projection ,该词有两层含义,第一种含义是预测、规划、设计,第二种意思是投射、映射,经过反复推敲,我觉得这里用映射能够更直观的表达出 SELECT 的作用)。一旦你建立起来了表的引用,经过修改、变形,你能够一步一步的将其映射到另一个模型中。 SELECT 语句就像一个“投影仪”,我们可以将其理解成一个将源表中的数据按照一定的逻辑转换成目标表数据的函数。
-通过 SELECT语句,你能对每一个字段进行操作,通过复杂的表达式生成所需要的数据。
-SELECT 语句有很多特殊的规则,至少你应该熟悉以下几条:
-一些更复杂的规则多到足够写出另一篇文章了。比如:为何你不能在一个没有 GROUP BY 的 SELECT 语句中同时使用普通函数和聚合函数?(上面的第 4 条)
-原因如下:
-糊涂了?是的,我也是。我们再回过头来看点浅显的东西吧。
-我们学到了什么?
-SELECT 语句可能是 SQL 语句中最难的部分了,尽管他看上去很简单。其他语句的作用其实就是对表的不同形式的引用。而 SELECT 语句则把这些引用整合在了一起,通过逻辑规则将源表映射到目标表,而且这个过程是可逆的,我们可以清楚的知道目标表的数据是怎么来的。
-想要学习好 SQL 语言,就要在使用 SELECT 语句之前弄懂其他的语句,虽然 SELECT 是语法结构中的第一个关键词,但它应该是我们最后一个掌握的。
-在学习完复杂的 SELECT 豫剧之后,我们再来看点简单的东西:
-集合运算( set operation):
-集合运算主要操作在于集合上,事实上指的就是对表的一种操作。从概念上来说,他们很好理解:
-排序运算( ordering operation):
-排序运算跟逻辑关系无关。这是一个 SQL 特有的功能。排序运算不仅在 SQL 语句的最后,而且在 SQL 语句运行的过程中也是最后执行的。使用 ORDER BY 和 OFFSET…FETCH 是保证数据能够按照顺序排列的最有效的方式。其他所有的排序方式都有一定随机性,尽管它们得到的排序结果是可重现的。
-OFFSET…SET是一个没有统一确定语法的语句,不同的数据库有不同的表达方式,如 MySQL 和 PostgreSQL 的 LIMIT…OFFSET、SQL Server 和 Sybase 的 TOP…START AT 等。具体关于 OFFSET..FETCH 的不同语法可以参考这篇文章
-(http://www.jooq.org/doc/3.1/manual/sql-building/sql-statements/select-statement/limit-clause/)。
-让我们在工作中尽情的使用 SQL!
-正如其他语言一样,想要学好 SQL 语言就要大量的练习。上面的 10 个简单的步骤能够帮助你对你每天所写的 SQL 语句有更好的理解。另一方面来讲,从平时常见的错误中也能积累到很多经验。下面的两篇文章就是介绍一些 JAVA 和其他开发者所犯的一些常见的 SQL 错误:
-为了获取文中提到的一个命令的更多信息,先试下“man <命令名称>”,在一些情况下,为了让这条命令可以正常执行,你必须安装相应的包,可以用aptitude 或者 yum。如果失败了,求助Google。
-find . -name \*.py | xargs grep some_function
-cat hosts | xargs -l{} ssh root@{} hostname
-#在当前目录下做一些事情 -(cd /一些/另外的/目录;执行别的操作) -#继续在原来的目录下执行-
if var==foo.pdf, then echo ${var%.pdf}.txt #会打印"foo.txt"。
-TCPKeepAlive=yes -ServerAliveInterval=15 -ServerAliveCountMax=6 -StrictHostKeyChecking=no -Compression=yes -ForwardAgent=yes-
cat a b | sort | uniq > c # c is a union b -cat a b | sort | uniq -d > c # c is a intersect b -cat a b b | sort | uniq -u > c # c is set difference a - b-
perl -pi.bak -e 's/old-string/new-string/g' my-files-*.txt-
uconv -f utf-8 -t utf-8 -x '::Any-Lower; ::Any-NFD; [:Nonspacing Mark:] >; ::Any-NFC; ' < input.txt > output.txt-
Vim的学习曲线相当的大,所以,如果你一开始看到的是一大堆VIM的命令分类,你一定会对这个编辑器失去兴趣的。下面的文章翻译自《Learn Vim Progressively》,我觉得这是给新手最好的VIM的升级教程了,没有列举所有的命令,只是列举了那些最有用的命令。非常不错。
- -——————————正文开始——————————
-你想以最快的速度学习人类史上最好的文本编辑器VIM吗?你先得懂得如何在VIM幸存下来,然后一点一点地学习各种戏法。
-Vim the Six Billion Dollar editor
-Better, Stronger, Faster.
-学习 vim 并且其会成为你最后一个使用的文本编辑器。没有比这个更好的文本编辑器了,非常地难学,但是却不可思议地好用。
-我建议下面这四个步骤:
-1. 存活
-2. 感觉良好
-3. 觉得更好,更强,更快
-4.使用VIM的超能力
-当你走完这篇文章,你会成为一个vim的 superstar。
-在开始学习以前,我需要给你一些警告:
-● 学习vim在开始时是痛苦的。
-● 需要时间
-● 需要不断地练习,就像你学习一个乐器一样。
-● 不要期望你能在3天内把vim练得比别的编辑器更有效率。
-● 事实上,你需要2周时间的苦练,而不是3天。
--
第一级 – 存活
-1. 安装 vim
-2. 启动 vim
-3. 什么也别干!请先阅读
-当你安装好一个编辑器后,你一定会想在其中输入点什么东西,然后看看这个编辑器是什么样子。但vim不是这样的,请按照下面的命令操作:
-● 启 动Vim后,vim在 Normal 模式下。
-● 让我们进入 Insert 模式,请按下键 i 。(陈皓注:你会看到vim左下角有一个–insert–字样,表示,你可以以插入的方式输入了)
-● 此时,你可以输入文本了,就像你用“记事本”一样。
-● 如果你想返回 Normal 模式,请按 ESC 键。
现在,你知道如何在 Insert 和 Normal 模式下切换了。下面是一些命令,可以让你在 Normal 模式下幸存下来:
-● i → Insert 模式,按 ESC 回到 Normal 模式.
● x → 删当前光标所在的一个字符。
● :wq → 存盘 + 退出 (:w 存盘, :q 退出) (陈皓注::w 后可以跟文件名)
● dd → 删除当前行,并把删除的行存到剪贴板里
● p → 粘贴剪贴板
推荐:
-● hjkl (强例推荐使用其移动光标,但不必需) →你也可以使用光标键 (←↓↑→). 注: j 就像下箭头。
● :help <command> → 显示相关命令的帮助。你也可以就输入 :help 而不跟命令。(陈皓注:退出帮助需要输入:q)
你能在vim幸存下来只需要上述的那5个命令,你就可以编辑文本了,你一定要把这些命令练成一种下意识的状态。于是你就可以开始进阶到第二级了。
-当是,在你进入第二级时,需要再说一下 Normal 模式。在一般的编辑器下,当你需要copy一段文字的时候,你需要使用 Ctrl 键,比如:Ctrl-C。也就是说,Ctrl键就好像功能键一样,当你按下了功能键Ctrl后,C就不在是C了,而且就是一个命令或是一个快键键了,在VIM的Normal模式下,所有的键就是功能键了。这个你需要知道。
标记:
-● 下面的文字中,如果是 Ctrl-λ我会写成 <C-λ>.
● 以 : 开始的命令你需要输入 <enter>回车,例如 — 如果我写成 :q 也就是说你要输入 :q<enter>.
第二级 – 感觉良好
-上面的那些命令只能让你存活下来,现在是时候学习一些更多的命令了,下面是我的建议:(陈皓注:所有的命令都需要在Normal模式下使用,如果你不知道现在在什么样的模式,你就狂按几次ESC键)
-1. 各种插入模式
-● a → 在光标后插入
● o → 在当前行后插入一个新行
● O → 在当前行前插入一个新行
● cw → 替换从光标所在位置后到一个单词结尾的字符
2. 简单的移动光标
-● 0 → 数字零,到行头
● ^ → 到本行第一个不是blank字符的位置(所谓blank字符就是空格,tab,换行,回车等)
● $ → 到本行行尾
● g_ → 到本行最后一个不是blank字符的位置。
● /pattern → 搜索 pattern 的字符串(陈皓注:如果搜索出多个匹配,可按n键到下一个)
3. 拷贝/粘贴 (陈皓注:p/P都可以,p是表示在当前位置之后,P表示在当前位置之前)
-● P → 粘贴
● yy → 拷贝当前行当行于 ddP
4. Undo/Redo
● u → undo
● <C-r> → redo
5. 打开/保存/退出/改变文件(Buffer)
-● :e <path/to/file> → 打开一个文件
● :w → 存盘
● :saveas <path/to/file> → 另存为 <path/to/file>
● :x, ZZ 或 :wq → 保存并退出 (:x 表示仅在需要时保存,ZZ不需要输入冒号并回车)
● :q! → 退出不保存 :qa! 强行退出所有的正在编辑的文件,就算别的文件有更改。
● :bn 和 :bp → 你可以同时打开很多文件,使用这两个命令来切换下一个或上一个文件。(陈皓注:我喜欢使用:n到下一个文件)
花点时间熟悉一下上面的命令,一旦你掌握他们了,你就几乎可以干其它编辑器都能干的事了。但是到现在为止,你还是觉得使用vim还是有点笨拙,不过没关系,你可以进阶到第三级了。
- -第三级 – 更好,更强,更快
-先恭喜你!你干的很不错。我们可以开始一些更为有趣的事了。在第三级,我们只谈那些和vi可以兼容的命令。
-更好
-下面,让我们看一下vim是怎么重复自己的:
-1. . → (小数点) 可以重复上一次的命令
2. N<command> → 重复某个命令N次
-下面是一个示例,找开一个文件你可以试试下面的命令:
-● 2dd → 删除2行
● 3p → 粘贴文本3次
● 100idesu [ESC] → 会写下 “desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu “
● . → 重复上一个命令—— 100 “desu “.
● 3. → 重复 3 次 “desu” (注意:不是 300,你看,VIM多聪明啊).
更强
-你要让你的光标移动更有效率,你一定要了解下面的这些命令,千万别跳过。
-1. NG → 到第 N 行 (陈皓注:注意命令中的G是大写的,另我一般使用 : N 到第N行,如 :137 到第137行)
2. gg → 到第一行。(陈皓注:相当于1G,或 :1)
3. G → 到最后一行。
4. 按单词移动:
-● w → 到下一个单词的开头。
● e → 到下一个单词的结尾。
> 如果你认为单词是由默认方式,那么就用小写的e和w。默认上来说,一个单词由字母,数字和下划线组成(陈皓注:程序变量)
-> 如果你认为单词是由blank字符分隔符,那么你需要使用大写的E和W。(陈皓注:程序语句)
- -下面,让我来说说最强的光标移动:
-● % : 匹配括号移动,包括 (, {, [. (陈皓注:你需要把光标先移到括号上)
● * 和 #: 匹配光标当前所在的单词,移动光标到下一个(或上一个)匹配单词(*是下一个,#是上一个)
相信我,上面这三个命令对程序员来说是相当强大的。
-更快
-你一定要记住光标的移动,因为很多命令都可以和这些移动光标的命令连动。很多命令都可以如下来干:
-<start position><command><end position>
例如 0y$ 命令意味着:
● 0 → 先到行头
● y → 从这里开始拷贝
● $ → 拷贝到本行最后一个字符
你可可以输入 ye,从当前位置拷贝到本单词的最后一个字符。
你也可以输入 y2/foo 来拷贝2个 “foo” 之间的字符串。
还有很多时间并不一定你就一定要按y才会拷贝,下面的命令也会被拷贝:
-● d (删除 )
● v (可视化的选择)
● gU (变大写)
● gu (变小写)
● 等等
-第四级 – Vim 超能力
-你只需要掌握前面的命令,你就可以很舒服的使用VIM了。但是,现在,我们向你介绍的是VIM杀手级的功能。下面这些功能是我只用vim的原因。
-在当前行上移动光标: 0 ^ $ f F t T , ;
● 0 → 到行头
● ^ → 到本行的第一个非blank字符
● $ → 到行尾
● g_ → 到本行最后一个不是blank字符的位置。
● fa → 到下一个为a的字符处,你也可以fs到下一个为s的字符。
● t, → 到逗号前的第一个字符。逗号可以变成其它字符。
● 3fa → 在当前行查找第三个出现的a。
● F 和 T → 和 f 和 t 一样,只不过是相反方向。
还有一个很有用的命令是 dt" → 删除所有的内容,直到遇到双引号—— "。
区域选择 <action>a<object> 或 <action>i<object>
在visual 模式下,这些命令很强大,其命令格式为
-<action>a<object> 和 <action>i<object>
● action可以是任何的命令,如 d (删除), y (拷贝), v (可以视模式选择)。
● object 可能是: w 一个单词, W 一个以空格为分隔的单词, s 一个句字, p 一个段落。也可以是一个特别的字符:"、 '、 )、 }、 ]。
假设你有一个字符串 (map (+) ("foo")).而光标键在第一个 o 的位置。
● vi" → 会选择 foo.
● va" → 会选择 "foo".
● vi) → 会选择 "foo".
● va) → 会选择("foo").
● v2i) → 会选择 map (+) ("foo")
● v2a) → 会选择 (map (+) ("foo"))
块操作: <C-v>
块操作,典型的操作: 0 <C-v> <C-d> I-- [ESC]
● ^ → 到行头
● <C-v> → 开始块操作
● <C-d> → 向下移动 (你也可以使用hjkl来移动光标,或是使用%,或是别的)
● I-- [ESC] → I是插入,插入“--”,按ESC键来为每一行生效。
在Windows下的vim,你需要使用 <C-q> 而不是 <C-v> ,<C-v> 是拷贝剪贴板。
自动提示: <C-n> 和 <C-p>
在 Insert 模式下,你可以输入一个词的开头,然后按 <C-p>或是<C-n>,自动补齐功能就出现了……
宏录制: qa 操作序列 q, @a, @@
● qa 把你的操作记录在寄存器 a。
● 于是 @a 会replay被录制的宏。
● @@ 是一个快捷键用来replay最新录制的宏。
示例
-在一个只有一行且这一行只有“1”的文本中,键入如下命令:
-● qaYp<C-a>q→
● qa 开始录制
● Yp 复制行.
● <C-a> 增加1.
● q 停止录制.
● @a → 在1下面写下 2
● @@ → 在2 正面写下3
● 现在做 100@@ 会创建新的100行,并把数据增加到 103.
可视化选择: v,V,<C-v>
前面,我们看到了 <C-v>的示例 (在Windows下应该是<C-q>),我们可以使用 v 和 V。一但被选好了,你可以做下面的事:
● J → 把所有的行连接起来(变成一行)
● < 或 > → 左右缩进
● = → 自动给缩进 (陈皓注:这个功能相当强大,我太喜欢了)
在所有被选择的行后加上点东西:
-● <C-v>
-● 选中相关的行 (可使用 j 或 <C-d> 或是 /pattern 或是 % 等……)
● $ 到行最后
● A, 输入字符串,按 ESC。
:split 和 vsplit.下面是主要的命令,你可以使用VIM的帮助 :help split. 你可以参考本站以前的一篇文章VIM分屏。
● :split → 创建分屏 (:vsplit创建垂直分屏)
● <C-w><dir> : dir就是方向,可以是 hjkl 或是 ←↓↑→ 中的一个,其用来切换分屏。
● <C-w>_ (或 <C-w>|) : 最大化尺寸 (<C-w>| 垂直分屏)
● <C-w>+ (或 <C-w>-) : 增加尺寸
结束语
-● 上面是作者最常用的90%的命令。
-● 我建议你每天都学1到2个新的命令。
-● 在两到三周后,你会感到vim的强大的。
-● 有时候,学习VIM就像是在死背一些东西。
-● 幸运的是,vim有很多很不错的工具和优秀的文档。
-● 运行vimtutor直到你熟悉了那些基本命令。
-● 其在线帮助文档中你应该要仔细阅读的是 :help usr_02.txt.
● 你会学习到诸如 !, 目录,寄存器,插件等很多其它的功能。
学习vim就像学弹钢琴一样,一旦学会,受益无穷。
-——————————正文结束——————————
-对于vi/vim只是点评一点:这是一个你不需要使用鼠标,不需使用小键盘,只需要使用大键盘就可以完成很多复杂功能文本编辑的编辑器。不然,Visual Studio也不就会有vim的插件了。
\ No newline at end of file diff --git a/src/virtual-memory.md b/src/virtual-memory.md deleted file mode 100644 index f3c3dc3..0000000 --- a/src/virtual-memory.md +++ /dev/null @@ -1,89 +0,0 @@ -处理器的虚拟内存子系统为每个进程实现了虚拟地址空间。这让每个进程认为它在系统中是独立的。虚拟内存的优点列表别的地方描述的非常详细,所以这里就不重复了。本节集中在虚拟内存的实际的实现细节,和相关的成本。
-虚拟地址空间是由CPU的内存管理单元(MMU)实现的。OS必须填充页表数据结构,但大多数CPU自己做了剩下的工作。这事实上是一个相当复杂的机制;最好的理解它的方法是引入数据结构来描述虚拟地址空间。
-由MMU进行地址翻译的输入地址是虚拟地址。通常对它的值很少有限制 — 假设还有一点的话。 虚拟地址在32位系统中是32位的数值,在64位系统中是64位的数值。在一些系统,例如x86和x86-64,使用的地址实际上包含了另一个层次的间接寻址:这些结构使用分段,这些分段只是简单的给每个逻辑地址加上位移。我们可以忽略这一部分的地址产生,它不重要,不是程序员非常关心的内存处理性能方面的东西。{x86的分段限制是与性能相关的,但那是另一回事了}
-有趣的地方在于由虚拟地址到物理地址的转换。MMU可以在逐页的基础上重新映射地址。就像地址缓存排列的时候,虚拟地址被分割为不同的部分。这些部分被用来做多个表的索引,而这些表是被用来创建最终物理地址用的。最简单的模型是只有一级表。
----
Figure 4.1: 1-Level Address Translation
-
图 4.1 显示了虚拟地址的不同部分是如何使用的。高字节部分是用来选择一个页目录的条目;那个目录中的每个地址可以被OS分别设置。页目录条目决定了物理内存页的地址;页面中可以有不止一个条目指向同样的物理地址。完整的内存物理地址是由页目录获得的页地址和虚拟地址低字节部分合并起来决定的。页目录条目还包含一些附加的页面信息,如访问权限。
-页目录的数据结构存储在内存中。OS必须分配连续的物理内存,并将这个地址范围的基地址存入一个特殊的寄存器。然后虚拟地址的适当的位被用来作为页目录的索引,这个页目录事实上是目录条目的列表。
-作为一个具体的例子,这是 x86机器4MB分页设计。虚拟地址的位移部分是22位大小,足以定位一个4M页内的每一个字节。虚拟地址中剩下的10位指定页目录中1024个条目的一个。每个条目包括一个10位的4M页内的基地址,它与位移结合起来形成了一个完整的32位地址。
-4MB的页不是规范,它们会浪费很多内存,因为OS需要执行的许多操作需要内存页的队列。对于4kB的页(32位机器的规范,甚至通常是64位机器的规范),虚拟地址的位移部分只有12位大小。这留下了20位作为页目录的指针。具有220个条目的表是不实际的。即使每个条目只要4比特,这个表也要4MB大小。由于每个进程可能具有其唯一的页目录,因为这些页目录许多系统中物理内存被绑定起来。
-解决办法是用多级页表。然后这些就能表示一个稀疏的大的页目录,目录中一些实际不用的区域不需要分配内存。因此这种表示更紧凑,使它可能为内存中的很多进程使用页表而并不太影响性能。.
-今天最复杂的页表结构由四级构成。图4.2显示了这样一个实现的原理图。
----
Figure 4.2: 4-Level Address Translation
-
在这个例子中,虚拟地址被至少分为五个部分。其中四个部分是不同的目录的索引。被引用的第4级目录使用CPU中一个特殊目的的寄存器。第4级到第2级目录的内容是对次低一级目录的引用。如果一个目录条目标识为空,显然就是不需要指向任何低一级的目录。这样页表树就能稀疏和紧凑。正如图4.1,第1级目录的条目是一部分物理地址,加上像访问权限的辅助数据。
-为了决定相对于虚拟地址的物理地址,处理器先决定最高级目录的地址。这个地址一般保存在一个寄存器。然后CPU取出虚拟地址中相对于这个目录的索引部分,并用那个索引选择合适的条目。这个条目是下一级目录的地址,它由虚拟地址的下一部分索引。处理器继续直到它到达第1级目录,那里那个目录条目的值就是物理地址的高字节部分。物理地址在加上虚拟地址中的页面位移之后就完整了。这个过程称为页面树遍历。一些处理器(像x86和x86-64)在硬件中执行这个操作,其他的需要OS的协助。 -系统中运行的每个进程可能需要自己的页表树。有部分共享树的可能,但是这相当例外。因此如果页表树需要的内存尽可能小的话将对性能与可扩展性有利。理想的情况是将使用的内存紧靠着放在虚拟地址空间;但实际使用的物理地址不影响。一个小程序可能只需要第2,3,4级的一个目录和少许第1级目录就能应付过去。在一个采用4kB页面和每个目录512条目的x86-64机器上,这允许用4级目录对2MB定位(每一级一个)。1GB连续的内存可以被第2到第4级的一个目录和第1级的512个目录定位。
-但是,假设所有内存可以被连续分配是太简单了。由于复杂的原因,大多数情况下,一个进程的栈与堆的区域是被分配在地址空间中非常相反的两端。这样使得任一个区域可以根据需要尽可能的增长。这意味着最有可能需要两个第2级目录和相应的更多的低一级的目录。
-但即使这也不常常匹配现在的实际。由于安全的原因,一个可运行的(代码,数据,堆,栈,动态共享对象,aka共享库)不同的部分被映射到随机的地址[未选中的]。随机化延伸到不同部分的相对位置;那意味着一个进程使用的不同的内存范围,遍布于虚拟地址空间。通过对随机的地址位数采用一些限定,范围可以被限制,但在大多数情况下,这当然不会让一个进程只用一到两个第2和第3级目录运行。
-如果性能真的远比安全重要,随机化可以被关闭。OS然后通常是在虚拟内存中至少连续的装载所有的动态共享对象(DSO)。
-页表的所有数据结构都保存在主存中;在那里OS建造和更新这些表。当一个进程创建或者一个页表变化,CPU将被通知。页表被用来解决每个虚拟地址到物理地址的转换,用上面描述的页表遍历方式。更多有关于此:至少每一级有一个目录被用于处理虚拟地址的过程。这需要至多四次内存访问(对一个运行中的进程的单次访问来说),这很慢。有可能像普通数据一样处理这些目录表条目,并将他们缓存在L1d,L2等等,但这仍然非常慢。
-
所以,替代于只是缓存目录表条目,物理页地址的完整的计算结果被缓存了。因为同样的原因,代码和数据缓存也工作起来,这样的地址计算结果的缓存是高效的。由于虚拟地址的页面位移部分在物理页地址的计算中不起任何作用,只有虚拟地址的剩余部分被用作缓存的标签。根据页面大小这意味着成百上千的指令或数据对象共享同一个标签,因此也共享同一个物理地址前缀。
-保存计算数值的缓存叫做旁路转换缓存(TLB)。因为它必须非常的快,通常这是一个小的缓存。现代CPU像其它缓存一样,提供了多级TLB缓存;越高级的缓存越大越慢。小号的L1级TLB通常被用来做全相联映像缓存,采用LRU回收策略。最近这种缓存大小变大了,而且在处理器中变得集相联。其结果之一就是,当一个新的条目必须被添加的时候,可能不是最久的条目被回收于替换了。
-正如上面提到的,用来访问TLB的标签是虚拟地址的一个部分。如果标签在缓存中有匹配,最终的物理地址将被计算出来,通过将来自虚拟地址的页面位移地址加到缓存值的方式。这是一个非常快的过程;也必须这样,因为每条使用绝对地址的指令都需要物理地址,还有在一些情况下,因为使用物理地址作为关键字的L2查找。如果TLB查询未命中,处理器就必须执行一次页表遍历;这可能代价非常大。
-通过软件或硬件预取代码或数据,会在地址位于另一页面时,暗中预取TLB的条目。硬件预取不可能允许这样,因为硬件会初始化非法的页面表遍历。因此程序员不能依赖硬件预取机制来预取TLB条目。它必须使用预取指令明确的完成。就像数据和指令缓存,TLB可以表现为多个等级。正如数据缓存,TLB通常表现为两种形式:指令TLB(ITLB)和数据TLB(DTLB)。高级的TLB像L2TLB通常是统一的,就像其他的缓存情形一样。
-4.3.1 使用TLB的注意事项
-TLB是以处理器为核心的全局资源。所有运行于处理器的线程与进程使用同一个TLB。由于虚拟到物理地址的转换依赖于安装的是哪一种页表树,如果页表变化了,CPU不能盲目的重复使用缓存的条目。每个进程有一个不同的页表树(不算在同一个进程中的线程),内核与内存管理器VMM(管理程序)也一样,如果存在的话。也有可能一个进程的地址空间布局发生变化。有两种解决这个问题的办法:
-第一种情况,只要执行一个上下文切换TLB就被刷新。因为大多数OS中,从一个线程/进程到另一个的切换需要执行一些核心代码,TLB刷新被限制进入或离开核心地址空间。在虚拟化的系统中,当内核必须调用内存管理器VMM和返回的时候,这也会发生。如果内核和/或内存管理器没有使用虚拟地址,或者当进程或内核调用系统/内存管理器时,能重复使用同一个虚拟地址,TLB必须被刷新。当离开内核或内存管理器时,处理器继续执行一个不同的进程或内核。
-刷新TLB高效但昂贵。例如,当执行一个系统调用,触及的内核代码可能仅限于几千条指令,或许少许新页面(或一个大的页面,像某些结构的Linux的就是这样)。这个工作将替换触及页面的所有TLB条目。对Intel带128ITLB和256DTLB条目的Core2架构,完全的刷新意味着多于100和200条目(分别的)将被不必要的刷新。当系统调用返回同一个进程,所有那些被刷新的TLB条目可能被再次用到,但它们没有了。内核或内存管理器常用的代码也一样。每条进入内核的条目上,TLB必须擦去再装,即使内核与内存管理器的页表通常不会改变。因此理论上说,TLB条目可以被保持一个很长时间。这也解释了为什么现在处理器中的TLB缓存都不大:程序很有可能不会执行时间长到装满所有这些条目。 -当然事实逃脱不了CPU的结构。对缓存刷新优化的一个可能的方法是单独的使TLB条目失效。例如,如果内核代码与数据落于一个特定的地址范围,只有落入这个地址范围的页面必须被清除出TLB。这只需要比较标签,因此不是很昂贵。在部分地址空间改变的场合,例如对去除内存页的一次调用,这个方法也是有用的,
-更好的解决方法是为TLB访问扩展标签。如果除了虚拟地址的一部分之外,一个唯一的对应每个页表树的标识(如一个进程的地址空间)被添加,TLB将根本不需要完全刷新。内核,内存管理程序,和独立的进程都可以有唯一的标识。这种场景唯一的问题在于,TLB标签可以获得的位数异常有限,但是地址空间的位数却不是。这意味着一些标识的再利用是有必要的。这种情况发生时TLB必须部分刷新(如果可能的话)。所有带有再利用标识的条目必须被刷新,但是希望这是一个非常小的集合。
-当多个进程运行在系统中时,这种扩展的TLB标签具有一般优势。如果每个可运行进程对内存的使用(因此TLB条目的使用)做限制,进程最近使用的TLB条目,当其再次列入计划时,有很大机会仍然在TLB。但还有两个额外的优势:
-4.3.2 影响TLB性能
-有一些因素会影响TLB性能。第一个是页面的大小。显然页面越大,装进去的指令或数据对象就越多。所以较大的页面大小减少了所需的地址转换总次数,即需要更少的TLB缓存条目。大多数架构允许使用多个不同的页面尺寸;一些尺寸可以并存使用。例如,x86/x86-64处理器有一个普通的4kB的页面尺寸,但它们也可以分别用4MB和2MB页面。IA-64 和 PowerPC允许如64kB的尺寸作为基本的页面尺寸。
-然而,大页面尺寸的使用也随之带来了一些问题。用作大页面的内存范围必须是在物理内存中连续的。如果物理内存管理的单元大小升至虚拟内存页面的大小,浪费的内存数量将会增长。各种内存操作(如加载可执行文件)需要页面边界对齐。这意味着平均每次映射浪费了物理内存中页面大小的一半。这种浪费很容易累加;因此它给物理内存分配的合理单元大小划定了一个上限。
-在x86-64结构中增加单元大小到2MB来适应大页面当然是不实际的。这是一个太大的尺寸。但这转而意味着每个大页面必须由许多小一些的页面组成。这些小页面必须在物理内存中连续。以4kB单元页面大小分配2MB连续的物理内存具有挑战性。它需要找到有512个连续页面的空闲区域。在系统运行一段时间并且物理内存开始碎片化以后,这可能极为困难(或者不可能)
-因此在Linux中有必要在系统启动的时候,用特别的Huge TLBfs文件系统,预分配这些大页面。一个固定数目的物理页面被保留,以单独用作大的虚拟页面。这使可能不会经常用到的资源捆绑留下来。它也是一个有限的池;增大它一般意味着要重启系统。尽管如此,大页面是进入某些局面的方法,在这些局面中性能具有保险性,资源丰富,而且麻烦的安装不会成为大的妨碍。数据库服务器就是一个例子。
-增大最小的虚拟页面大小(正如选择大页面的相反面)也有它的问题。内存映射操作(例如加载应用)必须确认这些页面大小。不可能有更小的映射。对大多数架构来说,一个可执行程序的各个部分位置有一个固定的关系。如果页面大小增加到超过了可执行程序或DSO(Dynamic Shared Object)创建时考虑的大小,加载操作将无法执行。脑海里记得这个限制很重要。图4.3显示了一个ELF二进制的对齐需求是如何决定的。它编码在ELF程序头部。
-$ eu-readelf -l /bin/ls -Program Headers: - Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align -... - LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x0132ac 0x0132ac R E 0x200000 - LOAD 0x0132b0 0x00000000006132b0 0x00000000006132b0 0x001a71 0x001a71 RW 0x200000 -... --
Figure 4.3: ELF 程序头表明了对齐需求
-在这个例子中,一个x86-64二进制,它的值为0x200000 = 2,097,152 = 2MB,符合处理器支持的最大页面尺寸。
-使用较大内存尺寸有第二个影响:页表树的级数减少了。由于虚拟地址相对于页面位移的部分增加了,需要用来在页目录中使用的位,就没有剩下许多了。这意味着当一个TLB未命中时,需要做的工作数量减少了。
-超出使用大页面大小,它有可能减少移动数据时需要同时使用的TLB条目数目,减少到数页。这与一些上面我们谈论的缓存使用的优化机制类似。只有现在对齐需求是巨大的。考虑到TLB条目数目如此小,这可能是一个重要的优化。
-OS映像的虚拟化将变得越来越流行;这意味着另一个层次的内存处理被加入了想象。进程(基本的隔间)或者OS容器的虚拟化,因为只涉及一个OS而没有落入此分类。类似Xen或KVM的技术使OS映像能够独立运行 — 有或者没有处理器的协助。这些情形下,有一个单独的软件直接控制物理内存的访问。
----
图 4.4: Xen 虚拟化模型
-
对Xen来说(见图4.4),Xen VMM(Xen内存管理程序)就是那个软件。但是,VMM没有自己实现许多硬件的控制,不像其他早先的系统(包括Xen VMM的第一个版本)的VMM,内存以外的硬件和处理器由享有特权的Dom0域控制。现在,这基本上与没有特权的DomU内核一样,就内存处理方面而言,它们没有什么不同。这里重要的是,VMM自己分发物理内存给Dom0和DomU内核,然后就像他们是直接运行在一个处理器上一样,实现通常的内存处理
-为了实现完成虚拟化所需的各个域之间的分隔,Dom0和DomU内核中的内存处理不具有无限制的物理内存访问权限。VMM不是通过分发独立的物理页并让客户OS处理地址的方式来分发内存;这不能提供对错误或欺诈客户域的防范。替代的,VMM为每一个客户域创建它自己的页表树,并且用这些数据结构分发内存。好处是对页表树管理信息的访问能得到控制。如果代码没有合适的特权,它不能做任何事。 -在虚拟化的Xen支持中,这种访问控制已被开发,不管使用的是参数的或硬件的(又名全)虚拟化。客户域以意图上与参数的和硬件的虚拟化极为相似的方法,给每个进程创建它们的页表树。每当客户OS修改了VMM调用的页表,VMM就会用客户域中更新的信息去更新自己的影子页表。这些是实际由硬件使用的页表。显然这个过程非常昂贵:每次对页表树的修改都需要VMM的一次调用。而没有虚拟化时内存映射的改变也不便宜,它们现在变得甚至更昂贵。 -考虑到从客户OS的变化到VMM以及返回,其本身已经相当昂贵,额外的代价可能真的很大。这就是为什么处理器开始具有避免创建影子页表的额外功能。这样很好不仅是因为速度的问题,而且它减少了VMM消耗的内存。Intel有扩展页表(EPTs),AMD称之为嵌套页表(NPTs)。基本上两种技术都具有客户OS的页表,来产生虚拟的物理地址。然后通过每个域一个EPT/NPT树的方式,这些地址会被进一步转换为真实的物理地址。这使得可以用几乎非虚拟化情境的速度进行内存处理,因为大多数用来内存处理的VMM条目被移走了。它也减少了VMM使用的内存,因为现在一个域(相对于进程)只有一个页表树需要维护。 -额外的地址转换步骤的结果也存储于TLB。那意味着TLB不存储虚拟物理地址,而替代以完整的查询结果。已经解释过AMD的帕西菲卡扩展为了避免TLB刷新而给每个条目引入ASID。ASID的位数在最初版本的处理器扩展中是一位;这正好足够区分VMM和客户OS。Intel有服务同一个目的的虚拟处理器ID(VPIDs),它们只有更多位。但对每个客户域VPID是固定的,因此它不能标记单独的进程,也不能避免TLB在那个级别刷新。 -对虚拟OS,每个地址空间的修改需要的工作量是一个问题。但是还有另一个内在的基于VMM虚拟化的问题:没有什么办法处理两层的内存。但内存处理很难(特别是考虑到像NUMA一样的复杂性,见第5部分)。Xen方法使用一个单独的VMM,这使最佳的(或最好的)处理变得困难,因为所有内存管理实现的复杂性,包括像发现内存范围之类“琐碎的”事情,必须被复制于VMM。OS有完全成熟的与最佳的实现;人们确实想避免复制它们。
---这就是为什么对VMM/Dom0模型的分析是这么有吸引力的一个选择。图4.5显示了KVM的Linux内核扩展如何尝试解决这个问题的。并没有直接运行在硬件之上且管理所有客户的单独的VMM,替代的,一个普通的Linux内核接管了这个功能。这意味着Linux内核中完整且复杂的内存管理功能,被用来管理系统的内存。客户域运行于普通的用户级进程,创建者称其为“客户模式”。虚拟化的功能,参数的或全虚拟化的,被另一个用户级进程KVM VMM控制。这也就是另一个进程用特别的内核实现的KVM设备,去恰巧控制一个客户域。 --
图 4.5: KVM 虚拟化模型
-
这个模型相较Xen独立的VMM模型好处在于,即使客户OS使用时,仍然有两个内存处理程序在工作,只需要在Linux内核里有一个实现。不需要像Xen VMM那样从另一段代码复制同样的功能。这带来更少的工作,更少的bug,或许还有更少的两个内存处理程序接触产生的摩擦,因为一个Linux客户的内存处理程序与运行于裸硬件之上的Linux内核的外部内存处理程序,做出了相同的假设。
-总的来说,程序员必须清醒认识到,采用虚拟化时,内存操作的代价比没有虚拟化要高很多。任何减少这个工作的优化,将在虚拟化环境付出更多。随着时间的过去,处理器的设计者将通过像EPT和NPT技术越来越减少这个差距,但它永远都不会完全消失。
- diff --git a/stylesheets/ie.css b/stylesheets/ie.css new file mode 100644 index 0000000..43882f2 --- /dev/null +++ b/stylesheets/ie.css @@ -0,0 +1,3 @@ +nav { + display: none; +} diff --git a/stylesheets/normalize.css b/stylesheets/normalize.css new file mode 100644 index 0000000..bc2ba93 --- /dev/null +++ b/stylesheets/normalize.css @@ -0,0 +1,459 @@ +/* normalize.css 2012-02-07T12:37 UTC - http://github.com/necolas/normalize.css */ +/* ============================================================================= + HTML5 display definitions + ========================================================================== */ +/* + * Corrects block display not defined in IE6/7/8/9 & FF3 + */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} + +/* + * Corrects inline-block display not defined in IE6/7/8/9 & FF3 + */ +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/* + * Prevents modern browsers from displaying 'audio' without controls + */ +audio:not([controls]) { + display: none; +} + +/* + * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 + * Known issue: no IE6 support + */ +[hidden] { + display: none; +} + +/* ============================================================================= + Base + ========================================================================== */ +/* + * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units + * http://clagnut.com/blog/348/#c790 + * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom + * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ + */ +html { + font-size: 100%; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -ms-text-size-adjust: 100%; + /* 2 */ +} + +/* + * Addresses font-family inconsistency between 'textarea' and other form elements. + */ +html, +button, +input, +select, +textarea { + font-family: sans-serif; +} + +/* + * Addresses margins handled incorrectly in IE6/7 + */ +body { + margin: 0; +} + +/* ============================================================================= + Links + ========================================================================== */ +/* + * Addresses outline displayed oddly in Chrome + */ +a:focus { + outline: thin dotted; +} + +/* + * Improves readability when focused and also mouse hovered in all browsers + * people.opera.com/patrickl/experiments/keyboard/test + */ +a:hover, +a:active { + outline: 0; +} + +/* ============================================================================= + Typography + ========================================================================== */ +/* + * Addresses font sizes and margins set differently in IE6/7 + * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 + */ +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.75em; + margin: 2.33em 0; +} + +/* + * Addresses styling not present in IE7/8/9, S5, Chrome + */ +abbr[title] { + border-bottom: 1px dotted; +} + +/* + * Addresses style set to 'bolder' in FF3+, S4/5, Chrome +*/ +b, +strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/* + * Addresses styling not present in S5, Chrome + */ +dfn { + font-style: italic; +} + +/* + * Addresses styling not present in IE6/7/8/9 + */ +mark { + background: #ff0; + color: #000; +} + +/* + * Addresses margins set differently in IE6/7 + */ +p, +pre { + margin: 1em 0; +} + +/* + * Corrects font family set oddly in IE6, S4/5, Chrome + * en.wikipedia.org/wiki/User:Davidgothberg/Test59 + */ +pre, +code, +kbd, +samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; +} + +/* + * 1. Addresses CSS quotes not supported in IE6/7 + * 2. Addresses quote property not supported in S4 + */ +/* 1 */ +q { + quotes: none; +} + +/* 2 */ +q:before, +q:after { + content: ''; + content: none; +} + +small { + font-size: 75%; +} + +/* + * Prevents sub and sup affecting line-height in all browsers + * gist.github.com/413930 + */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ============================================================================= + Lists + ========================================================================== */ +/* + * Addresses margins set differently in IE6/7 + */ +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/* + * Addresses paddings set differently in IE6/7 + */ +menu, +ol, +ul { + padding: 0 0 0 40px; +} + +/* + * Corrects list images handled incorrectly in IE7 + */ +nav ul, +nav ol { + list-style: none; + list-style-image: none; +} + +/* ============================================================================= + Embedded content + ========================================================================== */ +/* + * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 + * 2. Improves image quality when scaled in IE7 + * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ + */ +img { + border: 0; + /* 1 */ + -ms-interpolation-mode: bicubic; + /* 2 */ +} + +/* + * Corrects overflow displayed oddly in IE9 + */ +svg:not(:root) { + overflow: hidden; +} + +/* ============================================================================= + Figures + ========================================================================== */ +/* + * Addresses margin not present in IE6/7/8/9, S5, O11 + */ +figure { + margin: 0; +} + +/* ============================================================================= + Forms + ========================================================================== */ +/* + * Corrects margin displayed oddly in IE6/7 + */ +form { + margin: 0; +} + +/* + * Define consistent border, margin, and padding + */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/* + * 1. Corrects color not being inherited in IE6/7/8/9 + * 2. Corrects text not wrapping in FF3 + * 3. Corrects alignment displayed oddly in IE6/7 + */ +legend { + border: 0; + /* 1 */ + padding: 0; + white-space: normal; + /* 2 */ + *margin-left: -7px; + /* 3 */ +} + +/* + * 1. Corrects font size not being inherited in all browsers + * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome + * 3. Improves appearance and consistency in all browsers + */ +button, +input, +select, +textarea { + font-size: 100%; + /* 1 */ + margin: 0; + /* 2 */ + vertical-align: baseline; + /* 3 */ + *vertical-align: middle; + /* 3 */ +} + +/* + * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet + */ +button, +input { + line-height: normal; + /* 1 */ +} + +/* + * 1. Improves usability and consistency of cursor style between image-type 'input' and others + * 2. Corrects inability to style clickable 'input' types in iOS + * 3. Removes inner spacing in IE7 without affecting normal text inputs + * Known issue: inner spacing remains in IE6 + */ +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + /* 1 */ + -webkit-appearance: button; + /* 2 */ + *overflow: visible; + /* 3 */ +} + +/* + * Re-set default cursor for disabled elements + */ +button[disabled], +input[disabled] { + cursor: default; +} + +/* + * 1. Addresses box sizing set to content-box in IE8/9 + * 2. Removes excess padding in IE8/9 + * 3. Removes excess padding in IE7 + Known issue: excess padding remains in IE6 + */ +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ + *height: 13px; + /* 3 */ + *width: 13px; + /* 3 */ +} + +/* + * 1. Addresses appearance set to searchfield in S5, Chrome + * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) + */ +input[type="search"] { + -webkit-appearance: textfield; + /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + /* 2 */ + box-sizing: content-box; +} + +/* + * Removes inner padding and search cancel button in S5, Chrome on OS X + */ +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +/* + * Removes inner padding and border in FF3+ + * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ + */ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/* + * 1. Removes default vertical scrollbar in IE6/7/8/9 + * 2. Improves readability and alignment in all browsers + */ +textarea { + overflow: auto; + /* 1 */ + vertical-align: top; + /* 2 */ +} + +/* ============================================================================= + Tables + ========================================================================== */ +/* + * Remove most spacing between table cells + */ +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/stylesheets/pygment_trac.css b/stylesheets/pygment_trac.css new file mode 100644 index 0000000..c79bef4 --- /dev/null +++ b/stylesheets/pygment_trac.css @@ -0,0 +1,70 @@ +.highlight .hll { background-color: #404040 } +.highlight { color: #d0d0d0 } +.highlight .c { color: #999999; font-style: italic } /* Comment */ +.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ +.highlight .g { color: #d0d0d0 } /* Generic */ +.highlight .k { color: #6ab825; font-weight: normal } /* Keyword */ +.highlight .l { color: #d0d0d0 } /* Literal */ +.highlight .n { color: #d0d0d0 } /* Name */ +.highlight .o { color: #d0d0d0 } /* Operator */ +.highlight .x { color: #d0d0d0 } /* Other */ +.highlight .p { color: #d0d0d0 } /* Punctuation */ +.highlight .cm { color: #999999; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #cd2828; font-weight: normal } /* Comment.Preproc */ +.highlight .c1 { color: #999999; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #e50808; font-weight: normal; background-color: #520000 } /* Comment.Special */ +.highlight .gd { color: #d22323 } /* Generic.Deleted */ +.highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #d22323 } /* Generic.Error */ +.highlight .gh { color: #ffffff; font-weight: normal } /* Generic.Heading */ +.highlight .gi { color: #589819 } /* Generic.Inserted */ +.highlight .go { color: #cccccc } /* Generic.Output */ +.highlight .gp { color: #aaaaaa } /* Generic.Prompt */ +.highlight .gs { color: #d0d0d0; font-weight: normal } /* Generic.Strong */ +.highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */ +.highlight .gt { color: #d22323 } /* Generic.Traceback */ +.highlight .kc { color: #6ab825; font-weight: normal } /* Keyword.Constant */ +.highlight .kd { color: #6ab825; font-weight: normal } /* Keyword.Declaration */ +.highlight .kn { color: #6ab825; font-weight: normal } /* Keyword.Namespace */ +.highlight .kp { color: #6ab825 } /* Keyword.Pseudo */ +.highlight .kr { color: #6ab825; font-weight: normal } /* Keyword.Reserved */ +.highlight .kt { color: #6ab825; font-weight: normal } /* Keyword.Type */ +.highlight .ld { color: #d0d0d0 } /* Literal.Date */ +.highlight .m { color: #3677a9 } /* Literal.Number */ +.highlight .s { color: #9dd5f1 } /* Literal.String */ +.highlight .na { color: #bbbbbb } /* Name.Attribute */ +.highlight .nb { color: #24909d } /* Name.Builtin */ +.highlight .nc { color: #447fcf; text-decoration: underline } /* Name.Class */ +.highlight .no { color: #40ffff } /* Name.Constant */ +.highlight .nd { color: #ffa500 } /* Name.Decorator */ +.highlight .ni { color: #d0d0d0 } /* Name.Entity */ +.highlight .ne { color: #bbbbbb } /* Name.Exception */ +.highlight .nf { color: #447fcf } /* Name.Function */ +.highlight .nl { color: #d0d0d0 } /* Name.Label */ +.highlight .nn { color: #447fcf; text-decoration: underline } /* Name.Namespace */ +.highlight .nx { color: #d0d0d0 } /* Name.Other */ +.highlight .py { color: #d0d0d0 } /* Name.Property */ +.highlight .nt { color: #6ab825;} /* Name.Tag */ +.highlight .nv { color: #40ffff } /* Name.Variable */ +.highlight .ow { color: #6ab825; font-weight: normal } /* Operator.Word */ +.highlight .w { color: #666666 } /* Text.Whitespace */ +.highlight .mf { color: #3677a9 } /* Literal.Number.Float */ +.highlight .mh { color: #3677a9 } /* Literal.Number.Hex */ +.highlight .mi { color: #3677a9 } /* Literal.Number.Integer */ +.highlight .mo { color: #3677a9 } /* Literal.Number.Oct */ +.highlight .sb { color: #9dd5f1 } /* Literal.String.Backtick */ +.highlight .sc { color: #9dd5f1 } /* Literal.String.Char */ +.highlight .sd { color: #9dd5f1 } /* Literal.String.Doc */ +.highlight .s2 { color: #9dd5f1 } /* Literal.String.Double */ +.highlight .se { color: #9dd5f1 } /* Literal.String.Escape */ +.highlight .sh { color: #9dd5f1 } /* Literal.String.Heredoc */ +.highlight .si { color: #9dd5f1 } /* Literal.String.Interpol */ +.highlight .sx { color: #ffa500 } /* Literal.String.Other */ +.highlight .sr { color: #9dd5f1 } /* Literal.String.Regex */ +.highlight .s1 { color: #9dd5f1 } /* Literal.String.Single */ +.highlight .ss { color: #9dd5f1 } /* Literal.String.Symbol */ +.highlight .bp { color: #24909d } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #40ffff } /* Name.Variable.Class */ +.highlight .vg { color: #40ffff } /* Name.Variable.Global */ +.highlight .vi { color: #40ffff } /* Name.Variable.Instance */ +.highlight .il { color: #3677a9 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/stylesheets/styles.css b/stylesheets/styles.css new file mode 100644 index 0000000..e7b4ffc --- /dev/null +++ b/stylesheets/styles.css @@ -0,0 +1,851 @@ +@font-face { + font-family: 'OpenSansLight'; + src: url("http://github.com/fonts/OpenSans-Light-webfont.eot"); + src: url("http://github.com/fonts/OpenSans-Light-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-Light-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-Light-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-Light-webfont.svg#OpenSansLight") format("svg"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'OpenSansLightItalic'; + src: url("http://github.com/fonts/OpenSans-LightItalic-webfont.eot"); + src: url("http://github.com/fonts/OpenSans-LightItalic-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-LightItalic-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-LightItalic-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-LightItalic-webfont.svg#OpenSansLightItalic") format("svg"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'OpenSansRegular'; + src: url("http://github.com/fonts/OpenSans-Regular-webfont.eot"); + src: url("http://github.com/fonts/OpenSans-Regular-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-Regular-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-Regular-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-Regular-webfont.svg#OpenSansRegular") format("svg"); + font-weight: normal; + font-style: normal; + -webkit-font-smoothing: antialiased; +} + +@font-face { + font-family: 'OpenSansItalic'; + src: url("http://github.com/fonts/OpenSans-Italic-webfont.eot"); + src: url("http://github.com/fonts/OpenSans-Italic-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-Italic-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-Italic-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-Italic-webfont.svg#OpenSansItalic") format("svg"); + font-weight: normal; + font-style: normal; + -webkit-font-smoothing: antialiased; +} + +@font-face { + font-family: 'OpenSansSemibold'; + src: url("http://github.com/fonts/OpenSans-Semibold-webfont.eot"); + src: url("http://github.com/fonts/OpenSans-Semibold-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-Semibold-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-Semibold-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-Semibold-webfont.svg#OpenSansSemibold") format("svg"); + font-weight: normal; + font-style: normal; + -webkit-font-smoothing: antialiased; +} + +@font-face { + font-family: 'OpenSansSemiboldItalic'; + src: url("http://github.com/fonts/OpenSans-SemiboldItalic-webfont.eot"); + src: url("http://github.com/fonts/OpenSans-SemiboldItalic-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-SemiboldItalic-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-SemiboldItalic-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-SemiboldItalic-webfont.svg#OpenSansSemiboldItalic") format("svg"); + font-weight: normal; + font-style: normal; + -webkit-font-smoothing: antialiased; +} + +@font-face { + font-family: 'OpenSansBold'; + src: url("http://github.com/fonts/OpenSans-Bold-webfont.eot"); + src: url("http://github.com/fonts/OpenSans-Bold-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-Bold-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-Bold-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-Bold-webfont.svg#OpenSansBold") format("svg"); + font-weight: normal; + font-style: normal; + -webkit-font-smoothing: antialiased; +} + +@font-face { + font-family: 'OpenSansBoldItalic'; + src: url("http://github.com/fonts/OpenSans-BoldItalic-webfont.eot"); + src: url("http://github.com/fonts/OpenSans-BoldItalic-webfont.eot?#iefix") format("embedded-opentype"), url("http://github.com/fonts/OpenSans-BoldItalic-webfont.woff") format("woff"), url("http://github.com/fonts/OpenSans-BoldItalic-webfont.ttf") format("truetype"), url("http://github.com/fonts/OpenSans-BoldItalic-webfont.svg#OpenSansBoldItalic") format("svg"); + font-weight: normal; + font-style: normal; + -webkit-font-smoothing: antialiased; +} + +/* normalize.css 2012-02-07T12:37 UTC - http://github.com/necolas/normalize.css */ +/* ============================================================================= + HTML5 display definitions + ========================================================================== */ +/* + * Corrects block display not defined in IE6/7/8/9 & FF3 + */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} + +/* + * Corrects inline-block display not defined in IE6/7/8/9 & FF3 + */ +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/* + * Prevents modern browsers from displaying 'audio' without controls + */ +audio:not([controls]) { + display: none; +} + +/* + * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 + * Known issue: no IE6 support + */ +[hidden] { + display: none; +} + +/* ============================================================================= + Base + ========================================================================== */ +/* + * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units + * http://clagnut.com/blog/348/#c790 + * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom + * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ + */ +html { + font-size: 100%; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -ms-text-size-adjust: 100%; + /* 2 */ +} + +/* + * Addresses font-family inconsistency between 'textarea' and other form elements. + */ +html, +button, +input, +select, +textarea { + font-family: sans-serif; +} + +/* + * Addresses margins handled incorrectly in IE6/7 + */ +body { + margin: 0; +} + +/* ============================================================================= + Links + ========================================================================== */ +/* + * Addresses outline displayed oddly in Chrome + */ +a:focus { + outline: thin dotted; +} + +/* + * Improves readability when focused and also mouse hovered in all browsers + * people.opera.com/patrickl/experiments/keyboard/test + */ +a:hover, +a:active { + outline: 0; +} + +/* ============================================================================= + Typography + ========================================================================== */ +/* + * Addresses font sizes and margins set differently in IE6/7 + * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 + */ +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.75em; + margin: 2.33em 0; +} + +/* + * Addresses styling not present in IE7/8/9, S5, Chrome + */ +abbr[title] { + border-bottom: 1px dotted; +} + +/* + * Addresses style set to 'bolder' in FF3+, S4/5, Chrome +*/ +b, +strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/* + * Addresses styling not present in S5, Chrome + */ +dfn { + font-style: italic; +} + +/* + * Addresses styling not present in IE6/7/8/9 + */ +mark { + background: #ff0; + color: #000; +} + +/* + * Addresses margins set differently in IE6/7 + */ +p, +pre { + margin: 1em 0; +} + +/* + * Corrects font family set oddly in IE6, S4/5, Chrome + * en.wikipedia.org/wiki/User:Davidgothberg/Test59 + */ +pre, +code, +kbd, +samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; +} + +/* + * 1. Addresses CSS quotes not supported in IE6/7 + * 2. Addresses quote property not supported in S4 + */ +/* 1 */ +q { + quotes: none; +} + +/* 2 */ +q:before, +q:after { + content: ''; + content: none; +} + +small { + font-size: 75%; +} + +/* + * Prevents sub and sup affecting line-height in all browsers + * gist.github.com/413930 + */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ============================================================================= + Lists + ========================================================================== */ +/* + * Addresses margins set differently in IE6/7 + */ +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/* + * Addresses paddings set differently in IE6/7 + */ +menu, +ol, +ul { + padding: 0 0 0 40px; +} + +/* + * Corrects list images handled incorrectly in IE7 + */ +nav ul, +nav ol { + list-style: none; + list-style-image: none; +} + +/* ============================================================================= + Embedded content + ========================================================================== */ +/* + * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 + * 2. Improves image quality when scaled in IE7 + * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ + */ +img { + border: 0; + /* 1 */ + -ms-interpolation-mode: bicubic; + /* 2 */ +} + +/* + * Corrects overflow displayed oddly in IE9 + */ +svg:not(:root) { + overflow: hidden; +} + +/* ============================================================================= + Figures + ========================================================================== */ +/* + * Addresses margin not present in IE6/7/8/9, S5, O11 + */ +figure { + margin: 0; +} + +/* ============================================================================= + Forms + ========================================================================== */ +/* + * Corrects margin displayed oddly in IE6/7 + */ +form { + margin: 0; +} + +/* + * Define consistent border, margin, and padding + */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/* + * 1. Corrects color not being inherited in IE6/7/8/9 + * 2. Corrects text not wrapping in FF3 + * 3. Corrects alignment displayed oddly in IE6/7 + */ +legend { + border: 0; + /* 1 */ + padding: 0; + white-space: normal; + /* 2 */ + *margin-left: -7px; + /* 3 */ +} + +/* + * 1. Corrects font size not being inherited in all browsers + * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome + * 3. Improves appearance and consistency in all browsers + */ +button, +input, +select, +textarea { + font-size: 100%; + /* 1 */ + margin: 0; + /* 2 */ + vertical-align: baseline; + /* 3 */ + *vertical-align: middle; + /* 3 */ +} + +/* + * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet + */ +button, +input { + line-height: normal; + /* 1 */ +} + +/* + * 1. Improves usability and consistency of cursor style between image-type 'input' and others + * 2. Corrects inability to style clickable 'input' types in iOS + * 3. Removes inner spacing in IE7 without affecting normal text inputs + * Known issue: inner spacing remains in IE6 + */ +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + /* 1 */ + -webkit-appearance: button; + /* 2 */ + *overflow: visible; + /* 3 */ +} + +/* + * Re-set default cursor for disabled elements + */ +button[disabled], +input[disabled] { + cursor: default; +} + +/* + * 1. Addresses box sizing set to content-box in IE8/9 + * 2. Removes excess padding in IE8/9 + * 3. Removes excess padding in IE7 + Known issue: excess padding remains in IE6 + */ +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ + *height: 13px; + /* 3 */ + *width: 13px; + /* 3 */ +} + +/* + * 1. Addresses appearance set to searchfield in S5, Chrome + * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) + */ +input[type="search"] { + -webkit-appearance: textfield; + /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + /* 2 */ + box-sizing: content-box; +} + +/* + * Removes inner padding and search cancel button in S5, Chrome on OS X + */ +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +/* + * Removes inner padding and border in FF3+ + * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ + */ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/* + * 1. Removes default vertical scrollbar in IE6/7/8/9 + * 2. Improves readability and alignment in all browsers + */ +textarea { + overflow: auto; + /* 1 */ + vertical-align: top; + /* 2 */ +} + +/* ============================================================================= + Tables + ========================================================================== */ +/* + * Remove most spacing between table cells + */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +body { + padding: 0px 0 20px 0px; + margin: 0px; + font: 14px/1.5 "OpenSansRegular", "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #f0e7d5; + font-weight: normal; + background: #252525; + background-attachment: fixed !important; + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #2a2a29), color-stop(100%, #1c1c1c)); + background: -webkit-linear-gradient(#2a2a29, #1c1c1c); + background: -moz-linear-gradient(#2a2a29, #1c1c1c); + background: -o-linear-gradient(#2a2a29, #1c1c1c); + background: -ms-linear-gradient(#2a2a29, #1c1c1c); + background: linear-gradient(#2a2a29, #1c1c1c); +} + +h1, h2, h3, h4, h5, h6 { + color: #e8e8e8; + margin: 0 0 10px; + font-family: 'OpenSansRegular', "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: normal; +} + +p, ul, ol, table, pre, dl { + margin: 0 0 20px; +} + +h1, h2, h3 { + line-height: 1.1; +} + +h1 { + font-size: 28px; +} + +h2 { + font-size: 24px; +} + +h4, h5, h6 { + color: #e8e8e8; +} + +h3 { + font-size: 18px; + line-height: 24px; + font-family: 'OpenSansRegular', "Helvetica Neue", Helvetica, Arial, sans-serif !important; + font-weight: normal; + color: #b6b6b6; +} + +a { + color: #ffcc00; + font-weight: 400; + text-decoration: none; +} +a:hover { + color: #ffeb9b; +} + +a small { + font-size: 11px; + color: #666; + margin-top: -0.6em; + display: block; +} + +ul { + list-style-image: url("http://github.com/images/bullet.png"); +} + +strong { + font-family: 'OpenSansBold', "Helvetica Neue", Helvetica, Arial, sans-serif !important; + font-weight: normal; +} + +.wrapper { + max-width: 650px; + margin: 0 auto; + position: relative; + padding: 0 20px; +} + +section img { + max-width: 100%; +} + +blockquote { + border-left: 3px solid #ffcc00; + margin: 0; + padding: 0 0 0 20px; + font-style: italic; +} + +code { + font-family: "Lucida Sans", Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + color: #efefef; + font-size: 13px; + margin: 0 4px; + padding: 4px 6px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +pre { + padding: 8px 15px; + background: #191919; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; + border: 1px solid #121212; + -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3); + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3); + -o-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3); + overflow: auto; + overflow-y: hidden; +} +pre code { + color: #efefef; + text-shadow: 0px 1px 0px #000; + margin: 0; + padding: 0; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th { + text-align: left; + padding: 5px 10px; + border-bottom: 1px solid #434343; + color: #b6b6b6; + font-family: 'OpenSansSemibold', "Helvetica Neue", Helvetica, Arial, sans-serif !important; + font-weight: normal; +} + +td { + text-align: left; + padding: 5px 10px; + border-bottom: 1px solid #434343; +} + +hr { + border: 0; + outline: none; + height: 3px; + background: transparent url("http://github.com/images/hr.gif") center center repeat-x; + margin: 0 0 20px; +} + +dt { + color: #F0E7D5; + font-family: 'OpenSansSemibold', "Helvetica Neue", Helvetica, Arial, sans-serif !important; + font-weight: normal; +} + +#header { + z-index: 100; + left: 0; + top: 0px; + height: 60px; + width: 100%; + position: fixed; + background: url(http://github.com/images/nav-bg.gif) #353535; + border-bottom: 4px solid #434343; + -moz-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); + -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); + -o-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); +} +#header nav { + max-width: 650px; + margin: 0 auto; + padding: 0 10px; + background: blue; + margin: 6px auto; +} +#header nav li { + font-family: 'OpenSansLight', "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: normal; + list-style: none; + display: inline; + color: white; + line-height: 50px; + text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.2); + font-size: 14px; +} +#header nav li a { + color: white; + border: 1px solid #5d910b; + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #93bd20), color-stop(100%, #659e10)); + background: -webkit-linear-gradient(#93bd20, #659e10); + background: -moz-linear-gradient(#93bd20, #659e10); + background: -o-linear-gradient(#93bd20, #659e10); + background: -ms-linear-gradient(#93bd20, #659e10); + background: linear-gradient(#93bd20, #659e10); + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; + -moz-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.3), 0px 3px 7px rgba(0, 0, 0, 0.7); + -webkit-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.3), 0px 3px 7px rgba(0, 0, 0, 0.7); + -o-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.3), 0px 3px 7px rgba(0, 0, 0, 0.7); + box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.3), 0px 3px 7px rgba(0, 0, 0, 0.7); + background-color: #93bd20; + padding: 10px 12px; + margin-top: 6px; + line-height: 14px; + font-size: 14px; + display: inline-block; + text-align: center; +} +#header nav li a:hover { + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #749619), color-stop(100%, #527f0e)); + background: -webkit-linear-gradient(#749619, #527f0e); + background: -moz-linear-gradient(#749619, #527f0e); + background: -o-linear-gradient(#749619, #527f0e); + background: -ms-linear-gradient(#749619, #527f0e); + background: linear-gradient(#749619, #527f0e); + background-color: #659e10; + border: 1px solid #527f0e; + -moz-box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.2), 0px 1px 0px rgba(0, 0, 0, 0); + -webkit-box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.2), 0px 1px 0px rgba(0, 0, 0, 0); + -o-box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.2), 0px 1px 0px rgba(0, 0, 0, 0); + box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.2), 0px 1px 0px rgba(0, 0, 0, 0); +} +#header nav li.fork { + float: left; + margin-left: 0px; +} +#header nav li.downloads { + float: right; + margin-left: 6px; +} +#header nav li.title { + float: right; + margin-right: 10px; + font-size: 11px; +} + +section { + max-width: 650px; + padding: 30px 0px 50px 0px; + margin: 20px 0; + margin-top: 70px; +} +section #title { + border: 0; + outline: none; + margin: 0 0 50px 0; + padding: 0 0 5px 0; +} +section #title h1 { + font-family: 'OpenSansLight', "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: normal; + font-size: 40px; + text-align: center; + line-height: 36px; +} +section #title p { + color: #d7cfbe; + font-family: 'OpenSansLight', "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: normal; + font-size: 18px; + text-align: center; +} +section #title .credits { + font-size: 11px; + font-family: 'OpenSansRegular', "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: normal; + color: #696969; + margin-top: -10px; +} +section #title .credits.left { + float: left; +} +section #title .credits.right { + float: right; +} + +@media print, screen and (max-width: 720px) { + #title .credits { + display: block; + width: 100%; + line-height: 30px; + text-align: center; + } + #title .credits .left { + float: none; + display: block; + } + #title .credits .right { + float: none; + display: block; + } +} +@media print, screen and (max-width: 480px) { + #header { + margin-top: -20px; + } + + section { + margin-top: 40px; + } + + nav { + display: none; + } +}