静态代码扫描,借用一段网上的原文解释一下(这里叫静态检查):“静态测试包括代码检查、静态结构分析、代码质量度量等。它可以由人工进行,充分发挥人的逻辑思维优势,也可以借助软件工具自动进行。代码检查代码检查包括代码走查、桌面检查、代码审查等,主要检查代码和设计的一致性,代码对标准的遵循、可读性,代码的逻辑表达的正确性,代码结构的合理性等方面;可以发现违背程序编写标准的问题,程序中不安全、不明确和模糊的部分,找出程序中不可移植部分、违背程序编程风格的问题,包括变量检查、命名和类型审查、程序逻辑审查、程序语法检查和程序结构检查等内容。”。
我看了一系列的静态代码扫描或者叫静态代码分析工具后,总结对工具的看法:静态代码扫描工具,和编译器的某些功能其实是很相似的,他们也需要词法分析,语法分析,语意分析 ...但和编译器不一样的是他们可以自定义各种各样的复杂的规则去对代码进行分析。
以下将会列出的静态代码扫描工具,会由于实现方法,算法,分析的层次不同,功能上会差异很大。有的可以做SQL注入的检查,有的则不能(当然,由于时间问题还没有对规则进行研究,但要检查复杂的代码安全漏洞,是需要更高深分析算法的,所以有的东西应该不是设置规则库就可以检查到的,但在安全方面的检查,一定程度上也是可以通过设置规则进行检查的 )。
以下我在网上搜集到的分析工具,我整理了以下挑了一些出来,这里只是一部分,另外一些可以到参考链接上看一下:
| 工具名 | 静态扫描语言 | 开源/付费 | 厂商 | 介绍 | 主页网址 |
| ounec5.0 | VB.Net、C、C++和C#, 还支持Java。 |
付 费 | Ounce Labs | \ | http://www.ouncelabs.com/ |
| Coverity Prevent | C/C++,C#,JAVA | 付费 | Coverity | 还有其他辅助工具: 1.Coverity Thread Analyzer for Java 2.Coverity Software Readiness Manager for Java 3.Coverity Architecture Analyzer |
http://www.coverity.com/index.html |
| @stake SmartRisk™ Analyzer |
C/C++,Java | 付费 | Symantec Corporation |
@stake SmartRisk™ Analyzer harnesses the power of static analysis of binary executables (C, C++, and Java) to identify, categorize and prioritize security。 注:在Symantec没有搜到此产品?! |
http://www.symantec.com/business/index.jsp |
| Rational Purify | C/C++,Java | 付费 | IBM | Provides memory leak and memory corruption detection for Windows,Runtime?! |
http://www-01.ibm.com/software/awdtools/purify/ |
| PREfix | \ | \ | microsoft | 微软用的静态分析工具,但暂时没有找到下载, 现在好像在考虑发布中! |
\ |
| Jtext | Java | 付费 | parasoft | 同时还有其他静态分析代码的产品,如:C++Test... 详细请查询官网 |
http://www.parasoft.com/jsp/cn/support.jsp |
| flawfinder | C/C++ | 开源 | \ | 用Python编写的c、c++程序安全审核工具, 可以检查潜在的安全风险。 |
http://www.dwheeler.com/flawfinder/ |
| Static Code Analyzer |
C/C++,C#,JAVA | 付费 | Fortify | \ | http://www.fortify.com/ |
| Klocwork Insight | C/C++ ,Java | 付费 | Klocwork | \ | http://www.klocwork.com/products/insight.asp |
| PolySpace Client/Server |
C/C++、Ada语言 | 付费 | MathWorks | \ | http://www.mathworks.cn/ |
| rats | C/C++, Python, Perl, PHP代码进行安全审核的工具 |
开源 | \ | \ | http://www.fortify.com/security-resources/rats.jsp |
| LAPSE | Java | 开源 | \ | LAPSE stands for a Lightweight Analysis for Program Security in Eclipse. LAPSE is designed to help with the task of auditing Java J2EE applications for common types of security vulnerabilities found in Web applications. LAPSE was developed by Benjamin Livshits as part of the Griffin Software Security Project. |
http://www.owasp.org/index.php/Category:OWASP_LAPSE_Project |
| Fluid | java | 开源 | \ | We have explored properties including: * race conditions and locking policies, * unique references and other programmer-significant aliasing properties, * effects, * appropriate typing, * realtime threading policies, and * single-threading policies. |
http://www.fluid.cs.cmu.edu:8080/Fluid |
| Splint | C | 开源 | University of Virginia, Department of Computer Science |
静态检测针对C语言的安全工具和漏洞检测。 | http://www.splint.org/ |
| cqual | C/C++ | 开源 | 马里兰大学 | 轻量级的静态扫描器,在类Linux系统下运行。 | http://www.cs.umd.edu/~jfoster/cqual/ |
| MOPS | C | 开源 | berkeley大学 | MOPS is a tool for finding security bugs in C programs and for verifying conformance to rules of defensive programming |
http://www.cs.berkeley.edu/~daw/mops/ |
| BOON | C | 开源 | berkeley大学 | BOON is a tool for automatically finding buffer overrun vulnerabilities in C source code. Buffer overruns are one of the most common types of security holes, and we hope that BOON will enable software developers and code auditors to improve the quality of security-critical programs. |
http://www.cs.berkeley.edu/~daw/boon/ |
| BLAST | C | 开源 | The BLAST 2.0 Team |
BLAST is a software model checker for C programs. The goal of BLAST is to be able to check that software satisfies behavioral properties of the interfaces it uses. BLAST uses counterexample-driven automatic abstraction refinement to construct an abstract model which is model checked for safety properties. The abstraction is constructed on-the-fly, and only to the required precision. |
http://mtc.epfl.ch/software-tools/blast/ |
| SpikeWAMP | Php | 开源 | \ | for analyzing PHP programs | http://developer.spikesource.com/wiki/index.php/SpikeWAMP |
| Pixy | Php | 开源 | \ | Finding XSS and SQLI vulnerabilities | http://pixybox.seclab.tuwien.ac.at/pixy/ |
| Mike | Java | 开源 | \ | Java source code security scanner built on top of Orizon. They are connected to OWASP. |
http://milk.sourceforge.net/download.html |
| Smatch | C | 开源 | \ | \ | http://smatch.sourceforge.net/ |
| Oink | C++ | 开源 | \ | C++ Static Analysis Tools | http://www.cubewano.org/oink |
| Frama-C | C | 开源 | \ | static analyzers for the C language. | http://frama-c.cea.fr/ |
| RTL-check | \ | 开源 | \ | RTL-check is an extensible and powerful abstract interpretation framework for static analysis of programs from a safety and security perspective |
http://rtlcheck.sourceforge.net/ |
| PMD | Java | 开源 | \ | PMD scans Java source code and looks for potential problems like: * Possible bugs - empty try/catch/finally/ switch statements * Dead code - unused local variables, parameters and private methods * Suboptimal code - wasteful String/StringBuffer usage * Overcomplicated expressions - unnecessary if statements, for loops that could be while loops * Duplicate code - copied/pasted code means copied/pasted bugs |
http://pmd.sourceforge.net/ |
| FindBugs | Java | 开源 | 马里兰大学 | uses static analysis to look for bugs in Java code. 注意:提供Eclipse插件。 |
http://findbugs.sourceforge.net/ |
| ITS4 | C\C++ | 开源 | \ | Cigital developed ITS4 to help automate source code review for security. |
http://www.cigital.com/its4/ |
| QJ-Pro | Java | 开源 | \ | QJ-Pro is a comprehensive software inspection tool targeted towards the software developer. QJ-Pro checks: * conformance to coding standards, * misuse of the Java language, * best practice conformence * code structure and * potential bugs at the earliest stages of development. 注意:提供各种IDE插件! |
http://qjpro.sourceforge.net/ |
| Jint | Java | 开源 | \ | Jlint will check your Java code and find bugs, inconsistencies and synchronization problems by doing data flow analysis and building the lock graph. |
http://artho.com/jlint/ |
| Hammurapi | Java | 开源 | \ | code review system captures coding best practices and delivers them to developers' fingertips. It also generates consolidated reports for lead developers, architects, and managers to monitor codebase quality and evolution. |
http://www.hammurapi.biz/hammurapi-biz/ef/xmenu/hammurapi-group/index.html |
| DoctorJ | Java | 开源 | \ | Among what it detects: * misspelled words * parameter and exception names: o missing o misordered o misspelled * Javadoc tags: o invalid o misordered o missing expected arguments o invalid arguments o missing descriptions * undocumented classes, methods, fields, parameters |
http://www.incava.org/projects/java/doctorj/index.html |
| Dependency Finder | Java | 开源 | \ | Dependency Finder is a suite of tools for analyzing compiled Java code. At the core is a powerful dependency analysis application that extracts dependency graphs and mines them for useful information. This application comes in many forms for your ease of use, including command-line tools, a Swing-based application, a web application ready to be deployed in an application server, and a set of Ant tasks. |
http://depfind.sourceforge.net/ |
| Checkstyle | Java | 开源 | \ | Checkstyle is a development tool to help programmers write Java code that adheres to a coding standard. It automates the process of checking Java code to spare humans of this boring (but important) task. This makes it ideal for projects that want to enforce a coding standard. 注意:提供多种IDE的插件。 |
http://checkstyle.sourceforge.net/ |
| Classycle | Java | 开源 | \ | Classycle's Analyser analyses the static class and package dependencies in Java applications or libraries. |
http://classycle.sourceforge.net/ |
| JDepend | Java | 开源 | \ | JDepend traverses Java class file directories and generates design quality metrics for each Java package. JDepend allows you to automatically measure the quality of a design in terms of its extensibility, reusability, and maintainability to manage package dependencies effectively. |
http://www.clarkware.com/software/JDepend.html |
| JCSC | Java | 开源 | \ | JCSC is a powerful tool to check source code against a highly definable coding standard and potential bad code. |
http://jcsc.sourceforge.net/ |
......
以下是直接提供代码检查/相关帮助的厂商:
Fortify:
ASPECT:
http://www.aspectsecurity.com/
OWASP:
http://www.owasp.org/index.php/Main_Page
securitycompass:
http://www.securitycompass.com/resources.shtml
参考资料:
1. http://www.dwheeler.com/flawfinder/
2. http://www.java2s.com/Product/Java/Byte-Source-Code/Source-Analysis-Diagram.htm
3. http://www.softwarelist.cn/?fsid=53&cid=530&cpath=ABAN
4. http://www.hacker.com.cn/article/view_14804.html
5. http://www.cs.cmu.edu/~aldrich/courses/654/tools/
注:以上链接列举了大量相关工具
新特性一览表:
Swing
新增 JLayer 类,是一个灵活而且功能强大的Swing组件修饰器,使用方法:How to Decorate Components with JLayer.
Nimbus Look and Feel 外观从 com.sun.java.swing 包移到 javax.swing 包中,详情:javax.swing.plaf.nimbus
更轻松的重量级和轻量级组件的混合
支持透明窗体以及非矩形窗体的图形界面,请看 How to Create Translucent and Shaped Windows
JColorChooser 类新增 HSV tab.
网络
新增 URLClassLoader.close 方法,请看 Closing a URLClassLoader.
支持 Sockets Direct Protocol (SDP) 提供高性能网络连接,详情请看 Understanding the Sockets Direct Protocol.
集合
新增 TransferQueue 接口,是 BlockingQueue 的改进版,实现类为 LinkedTransferQueue
RIA/发布
拖拽的小程序使用一个默认或者定制的标题进行修饰,详情:Requesting and Customizing Applet Decoration in Draggable Applets.
JNLP 文件做了如下方面的增强,详情请看 JNLP File Syntax:
The os attribute in the information and resources elements can now contain specific versions of Windows, such as Windows Vista or Windows 7.
Applications can use the install attribute in the shortcut element to specify their their desire to be installed. Installed applications are not removed when the Java Web Start cache is cleared, but can be explicitly removed using the Java Control Panel.
Java Web Start applications can be deployed without specifying the codebaseattribute; see Deploying Without Codebase
可直接在 HTML 中嵌入 JNLP 文件:Embedding JNLP File in Applet Tag.
可在 JavaScript 代码中检查 Applet 是否已经加载完成:Handling Initialization Status With Event Handlers.
可在 Applet 从快捷方式启动或者拖出浏览器时对窗口样式和标题进行控制:Requesting and Customizing Applet Decoration in Developing Draggable Applets.
XML
包含 Java API for XML Processing (JAXP) 1.4.5, 支持 Java Architecture for XML Binding(JAXB) 2.2.3, 和 Java API for XML Web Services (JAX-WS) 2.2.4.
java.lang 包
消除了在多线程环境下的非层次话类加载时导致的潜在死锁,详情:Multithreaded Custom Class Loaders in Java SE 7.
Java 虚拟机
支持非 Java 语言: Java SE 7 引入一个新的 JVM 指令用于简化实现动态类型编程语言
Garbage-First Collector 是一个服务器端的垃圾收集器用于替换 Concurrent Mark-Sweep Collector (CMS).
提升了 Java HotSpot 虚拟机的性能
Java I/O
java.nio.file 包以及相关的包 java.nio.file.attribute 提供对文件 I/O 以及访问文件系统的全面支持,请看 File I/O (featuring NIO.2).
目录 <Java home>/sample/nio/chatserver/ 包含使用 java.nio.file 包的演示程序
目录 <Java home>/demo/nio/zipfs/ 包含 NIO.2 NFS 文件系统的演示程序
安全性
新的内置对多个基于 ECC 算法(ECDSA/ECDH)的支持,详情请看:Sun PKCS#11 Provider's Supported Algorithms in Java PKCS#11 Reference Guide.
禁用了一些弱加密算法,详情请看 Appendix D: Disabling Cryptographic Algorithms in Java PKI Programmer's Guide and Disabled Cryptographic Algorithms in Java Secure Socket Extension (JSSE) Reference Guide.
Java 安全套接字扩展中对 SSL/TLS 的增强
并发
fork/join 框架,基于 ForkJoinPool 类,是 Executor 接口的实现,设计它用来进行高效的运行大量任务;使用 work-stealing 技术用来保证大量的 worker 线程工作,特别适合多处理器环境,详情请看 Fork/Join
目录<Java home>/sample/forkjoin/ 包含了 fork/join 框架的演示程序
ThreadLocalRandom 类class 消除了使用伪随机码线程的竞争,请看 Concurrent Random Numbers.
Phaser 类是一个新的同步的屏障,与 CyclicBarrier 类似.
Java 2D
一个新的基于 XRender 的 Java 2D 渲染管道支持现在的 X11 桌面,改善了图形性能,请看 System Properties for Java 2D Technology 中的 xrender .
JDK 可枚举并显示出已安装的 OpenType/CFF 字体,通过GraphicsEnvironment.getAvailableFontFamilyNames 方法 See Selecting a Font.
TextLayout 类支持西藏语脚本
libfontconfig, 是一个字体配置 api ,see Fontconfig.
国际化
支持 Unicode 6.0.0
目录 <Java home>/demo/jfc/Font2DTest/ 包含 Unicode 6.0 的演示程序
Java SE 7 可容纳在 ISO 4217 中新的货币,详情请看 Currency 类.
Java 编程语言特性
二进制数字表达方式
使用下划线对数字进行分隔表达,例如 1_322_222
switch 语句支持字符串变量
泛型实例创建的类型推断
使用可变参数时,提升编译器的警告和错误信息
try-with-resources 语句
同时捕获多个异常处理
JDBC 4.1
支持使用 try-with-resources 语句进行自动的资源释放,包括连接、语句和结果集
支持 RowSet 1.1
流是一种强大的数据处理抽象机制,它允许你调用泛型的读/写函数,不必关心数据从什么地方来、到什么地方去。使用流,同样的代码可从控制台、文件、套接字等地方读取数据。STL通常不是线程安全的,本文提出了如何在线程安全的方式下使用流的方案。
C++将流的强大能力与运算符重载合并到一起,为我们提供了>>和<<运算符,以便从流中读取,以及向流中写入,如清单A所示。
向流中写入通常不是“线程安全”(thread-safe)的。事实上,即便是fread和fwrite这样的基元函数,也不要求是线程安全的。不过,使用正确实现的标准模板库(STL)流,不仅能继续发挥流简单易用的特点,还能保证代码的线程安全。另外,你以前使用了<<运算符的代码仍可继续使用,所有线程安全问题都会在幕后解决。
STL流为什么默认不是线程安全的
C++标准没有规定向流中写入是否线程安全,但它通常都不是。这主要是考虑到效率问题。如果向流中的写入是线程安全的,就要求所有public(以及一些protected)函数也是线程安全的。根据STL文档,basic_ostream<>类(所有输出类的基类)有大量public函数。例如以下代码:
for (int idx = 0; idx < 1000000; ++idx) std::cout << idx << ' ';
假如向std::cout的写入是线程安全的,就需要至少200万个锁,这无疑会造成巨大的性能瓶颈。
另外,假如流使用了流缓冲(stream buffers),则缓冲也必须是线程安全的。对一个STL流类进行扩展,还会造成更大的线程安全性能问题。最后,即使STL流是线程安全的,如清单B所示的代码也不能担保生成你所希望的结果。
从表面看,清单B应该在控制台上打印以下结果:
message from thread 1
message from thread 2
但它实际输出的是:
message from message from thread 2
thread 1
这是因为在第一个线程成功打印message和from之后,第二个线程将接管控制权,打印它的完整的消息。之后,第一个线程继续打印它尚未完成打印的内容。这个简单的例子演示了假如代码不是线程安全的,可能有什么后果。
记录要比写入容易
流允许定位,这类似于文件访问。你可直接访问一个流的中部(如果流允许的话),并从那里开始写入。然而,记录信息时,采用的总是“追加”(append)方式。这时不需要任何定位——肯定会从尾部开始。为了简化问题,我们的日志记录类根本不允许定位。
刷新和std::endl
我们知道,要用<<运算符向一个流中写入,比如:
std::cout << val1 << val2 << …等等
在内部,你写入的一切其实都保存在一个流缓冲中。只有当程序员调用flush()成员函数时,或者因缓冲满而自动刷新时,才会实际地写入目的地。
但是,完全可以采用一种简单的方式来要求一个流进行刷新,这就是向它写入std::endl。这等价于向流中写入\n并刷新它。
为简化问题和便于理解,我们约定:当一个流刷新时,当前消息会终止,并开始一条新的消息,如以下代码所示:
// 写100条消息
for ( int idx = 0; idx < 100; ++idx)
get_log() << "message " << idx << " - just for testing" << std::endl;
为了保证“写入消息”过个操作是线程安全的,需要按以下5个步骤操作:
- 创建一个底层的流U,你希望以线程安全的方式来访问它。
- 创建一个流S,它含有对U的一个引用。
- 确定每个线程都有它自己的流S。
- 向流S写入时,将数据暂存在一个缓冲的内部,直至刷新(flush)。
- 流S被刷新之后(当前消息终止,开始一条新的消息),应该以一种线程安全的方式,将它的缓冲写入底层的流U。然后,流S应该刷新它的缓冲。
以下代码演示了上述步骤:
thread_safe_log log = safe_cout(); // 返回std::cout的线程安全版本
for (int idx = 0; idx < 1000000; ++idx) log << idx << ' ';
log << std::endl;
对刷新过程进行监视并不像表面上那么容易。你必须覆盖流缓冲的sync()函数。清单C展示了basic_message_handler_log类,它负责处理所有这些细节。要记住,你可创建自己的日志类,并覆盖on_new_message函数;该函数会在一条新消息向它写入时进行调用。
临时变量的技巧
这里稍微总结一下:每个线程都应该有它自己的流S,S容纳了对U的一个引用(要求对U进行线程安全的访问)。为了使每个线程都有它自己的流S,最简单的办法是使用临时变量。临时变量天生便是线程安全的,因为只有创建变量的线程才有权访问它。这里采用的一个技巧是,我们使用一个名为get_log的函数,它内部含有一个静态变量(底层的流U),返回的则是一个临时变量(流S),如清单D 所示。
除了要比其他大多数备选方案都简单之外,清单D展示的技术也是最有效的。只有在流刷新时,才会发生锁定。
方案1
thread_safe_log类派生自如清单C所示的basic_message_handler_log,并覆盖了on_new_message,它可采取“线程安全”的方式向底层日志写入。现在的问题是,应该在哪里容纳critical_section对象呢?要将thread_safe_log分离出来,需要创建另一个名为internal_thread_safe_log的类,它专门负责线程处理问题。
下面是一个get_log函数的基本形式:
thread_safe_log get_log()
{
static underlying_stream_type U( args);
static internal_thread_safe_log log( U);
return log;
}
请记住internal_thread_safe_log和thread_safe_log类;后文还会经常提到它们。
清单E演示了上述技术。注意,清单E的代码要使用由清单F提供的CriticalSection.h文件,否则无法正常工作。
你应该注意以下要点:
- 无论internal_thread_safe_log还是thread_safe_log,都是它们的basic_*版本的typedef。这遵循了目前通行的编程经验法则:因为std::ostream是std::basic_ostream<char>的一个typedef,而std::streambuf是std::basic_streambuf<char>的一个typedef,以此类推。
- 在对get_log()的每个访问之后,都跟有一个.ts()。这是必需的,因为某些流操作要求流是一个左值(lvalue)。简单地说,左值是可以放在operator=左侧的一个操作数。临时变量是右值,所以必须转变成左值。
- 临时变量流S在构造时,通过copy_state_to从流U获得它的状态;并在析构时,将那个状态拷贝回U。可将流的状态想象成一些特殊信息,它们规定了特定数据(比如填充字符、locale和其他格式化信息等等)应该如何写入。
如果忽视这些问题,可能造成严重后果。假定U已被设为German locale(所以5.235要写成5,235)。如果S在打印数字时使用默认locale,对输出的解释就是完全错误的。
清单E也包括一个测试(要运行它,你需要清单C和清单F)。这个测试使用out.txt作为底层流U。它创建200个并发线程,每个线程都写500条消息,消息的形式是:
- "writing double 5.23"(第一种类型的消息)
- "message <idx> from thread <thread>"(第二种类型的消息)
索引10的线程写第11条消息时,U的locale更改为German。后续所有消息都会变成"writing double 5,23",而不是"writing double 5.23"。
方案2
对于清单E的方案来说,它的问题在于,假如100个线程同时试图写入,在任何给定的时间,只有一个会成功,其他线程必须等着获得锁。取决于底层的流,执行m_underlyingLog << str;可能会花很长的时间,这会使其他所有线程都处于停顿状态。这很快就会成为一个严重的瓶颈。 在internal_thread_safe_log::write_message函数中,消息追加到一个队列上。一个专用线程不停地从这个队列中读取,并将消息写到底层的流中。internal_thread_safe_log::write_message函数现在只负责在队列中添加一个指针,这几乎不花任何时间,因此避免了瓶颈。清单G展示了更新过的方案。
下面列出了两个版本的区别:
- internal_thread_safe_log有一个writer对象。
- 每个writer对象(thread_safe_log_writer)都有自己的专用线程,后者负责将消息写入底层的流U。
- 每个writer对象都需要使用一个线程管理器(threading manager)来创建自己的线程。
线程处理管理器的角色是将独立于平台的线程处理问题(比如新线程的创建)抽离出来。我们要到后面才会讲解线程管理器,目前,你只需知道现在使用的线程管理器是win32_thread_manager。最终的方案还将支持其他线程管理器。
方案3
取决于应用程序要使用的日志和/或线程数目,方案2也许并不恰当。多个日志可能共享同一个线程,由这个线程分别向每个日志中写入。方案3实现了这一功能,它具有以下关键特性:
- 一个writer线程
- 多个日志,每个日志都向writer线程注册。
- 在writer线程中,每个日志都有一个指定的优先级(日志的构造函数新增了一个日志优先级参数)。根据这个优先级,writer线程可能优先在某些日志中写入。
例如,假定有3个日志:log1的优先级是6,log2的优先级是3,log3的优先级是1(它们的优先级累加起来是10 = 6 + 3 + 1)。对于writer线程,它每进行10次写入,有6次会写入log1,有3次会写入log2,有1次会写入log3。假如试图向一个日志写入,但日志的队列中没有消息,这一次写入就会被忽略。
清单H对测试进行了修改,包括了以下特性:
- 新测试包括10个日志:out0.txt,out1.txt,…,out9.txt。
- 对于索引为<idx>的日志来说,它的优先级公式是10 * (idx + 1)^2。例如,日志out3.txt具有优先级10 * 4 * 4 = 160。
- 有20个线程向out4.txt写入。
运行清单H的代码时,注意由于索引编号较大的日志优先级较高,所以填充速度会比其他日志快一些。
方案4
方案4允许你任意选择方案2和方案3,具体由应用程序的需求来决定。为了从一种方案切换到另一种方案,需要更改get_log()函数中的一、两行代码。对于这个支持多种格式的方案来说,它必须具有以下特性:
- 要有两个internal_thread_safe_log类:internal_thread_safe_log_ownthread,它与单独一个专用的日志相匹配;以及internal_thread_safe_log_sharethread,它由多个日志共享。
- internal_thread_safe_log_sharethread需要一个对应的thread_safe_log_writer_sharethread,它是日志需要共享的线程。
清单I证明了internal_thread_safe_log_ownthread和internal_thread_safe_log_sharethread的相互切换有多么容易。清单J则给出了方案4的完整实现。
清单J包括以下增补特性:
- internal_thread_safe_log类现在接受一个额外的模板参数,即thread_manager,它将独立于平台的线程处理问题抽离出来,后文还会具体解释。
- 清单J不像清单F那样还需要CriticalSection.h;因为CriticalSection.h封装在thread_manager中。
- 每次刷新流,都会开始一条新消息,这意味着最后一条消息永远不会刷新。所以,必须对最后一条消息进行特殊处理。我们修改了message_handler_log.h(如清单K所示),允许处理最后一条消息。
忘记刷新也许是一个错误,如清单L所示。所以,我们强制在每个临时变量析构之前都进行刷新,如清单M所示。做到这一点很容易,请仔细研究thread_safe_log::on_last_message。
在thread_safe_log的析构函数中,我们防止处理无效的引用。由于要处理临时变量,所以必须解决一系列特殊问题:怎样在它析构之后使用它([temp-destructed])。
测试也作了一些修改:
- 有10个日志(日志0-9)来自上一次测试(它们共享一个线程)。
- 还有10个日志(日志10-19)分别有它自己的线程。
- 有10个线程向一个指定的日志写入(线程4,24,44,…,184向第4个日志写入)。
最后讨论一下线程管理器。线程管理器是一个特殊的类,它规定如何解决一些必要的线程处理问题。它必须提供:
- thread_obj_base:该类应在创建一个线程时作为参数传递;它的重载的operator()要针对其他线程而执行。
- sleep( nMillisecs):当前线程将休眠nMillisecs毫秒。
- create_thread( thread_obj_base & obj):它创建一个线程,并为其执行obj.operator()。
- critical_section和auto_lock_unlock classes:两者的行为类似于CCriticalSection和CAutoLockUnlock(参见清单F)。
我提供了两个线程管理器:
- win32_thread_manager:这是Win32应用程序的线程管理器。
- boost_thread_manager:这是在你使用boost线程([boost])时的线程管理器。
在你的代码中,可以加#define USE_WIN32_THREAD_MANAGER语句,指定第一个管理器是默认管理器;或者添#define USE_BOOST_THREAD_MANAGER语句,将第二个管理器作为默认管理器。除此之外,还可设计自己的线程管理器,然后添加#define DEFAULT_THREAD_MANAGER your_threading_manager_class语句。
运行清单J(记住,它需要由清单K提供的最新的message_handler_log.h)。注意清单J包含数量相当多的类。这正是要把它们分解成多个文件的原因。
使用thread_safe_log和internal_thread_safe_log_*类,你可采用自己最熟悉的方式来记录日志,这样做既高效,又能保证线程安全。从此以后,使用了STL流的代码、辅助函数和类都能以线程安全的方式使用。对现有的应用程序进行重构也变得更容易。线程安全虽然是一个难以掌握的主题,但在本文的帮助下,再加上你的少许努力,就能透彻理解它,并真正体验到它的巨大好处。
把握重点
本文说明了vector 容器使用时应该注意的内存分配问题,原理说的比较详细,对于初学者比较适用。
本文描述的是一种很常见的情况:当你在某个缓存中存储数据时,常常需要在运行时调整该缓存的大小,以便能容纳更多的数据。本文将讨论如何使用 STL 的
vector 进行内存的再分配。
这里描述的是一种很常见的情况:当你在某个缓存中存储数据时,常常需要在运行时调整该缓存的大小,以便能容纳更多的数据。传统的内存再分配技术非常繁琐,而且容易出错:在
C 语言中,一般都是每次在需要扩充缓存的时候调用 realloc()。在 C++ 中情况更糟,你甚至无法在函数中为 new
操作分配的数组重新申请内存。你不仅要自己做分配处理,而且还必须把原来缓存中的数据拷贝到新的目的缓存,然后释放先前数组的缓存。本文将针对这个问题提供一个安全、简易并且是自动化的
C++ 内存再分配技术——即使用 STL 的 vector。
用 STL vector
对象取代内建的数组来保存获取的数据,既安全又简单,并且是自动化的。
进一步的问题分析
在提出解决方案之前,我先给出一个具体的例子来说明
C++ 重新分配内存的弊病和复杂性。假设你有一个编目应用程序,它读取用户输入的 ISBNs,然后将之插入一个数组,直到用户输入 0
为止。如果用户插入的数据多于数组的容量,那么你必须相应地增加它的大小:
| #include
<iostream> using namespace std; int main() { int size=2; // 初始化数组大小;在运行时调整。 int *p = new int[size]; int isbn; for(int n=0; ;++n) { cout<< "enter an ISBN; press 0 to stop "; cin>>isbn; if (isbn==0) break; if (n==size) // 数组是否到达上限? reallocate(p, size); p[n]=isbn; // 将元素插入扩容的数组 } delete [] p; // 不要忘了这一步! } |
注意上述这个向数组插入数据的过程是多么的繁琐。每次反复,循环都要检查缓存是否达到上限。如果是,则程序调用用户定义的函数
reallocate(),该函数实现如下:
| #include
<algorithm> // for std::copy int reallocate(int* &p, int& size) { size*=2; // double the array''s size with each reallocation int * temp = new int[size]; std::copy(p, p+(size/2), temp); delete [] p; // release original, smaller buffer p=temp; // reassign p to the newly allocated buffer } |
reallocate() 使用 STL std::copy()
算法对缓存进行合理的扩充——每次扩充都放大一倍。这种方法可以避免预先分配过多的内存,从量上减少需要重新分配的内存。这个技术需要得到充分的测试和调试,当初学者实现时尤其如此。此外,reallocate()
并不通用,它只能处理整型数组的情形。对于其它数据类型,它无能为力,你必须定义该函数额外的版本或将它模板化。幸运的是,有一个更巧妙的办法来实现。
创建和优化
vector
每一个 STL
容器都具备一个分配器(allocator),它是一个内建的内存管理器,能自动按需要重新分配容器的存储空间。因此,上面的程序可以得到大大简化,并摆脱
reallocator 函数。
第一步:创建 vector
用 vector
对象取代内建的数组来保存获取的数据。main() 中的循环读取 ISBN,检查它是否为 0,如果不为 0 ,则通过调用 push_back()
成员函数将值插入
| vector:
#include <iostream> #include <vector> using namespace std; int main() { vector <int> vi; int isbn; while(true) { cout << "enter an ISBN; press 0 to stop "; cin >> isbn; if (isbn==0) break; vi.push_back(isbn); // insert element into vector } } |
在 vector 对象构造期间,它先分配一个由其实现定义的默认的缓存大小。一般 vector 分配的数据存储初始空间是 64-256
存储槽(slots)。当 vector 感觉存储空间不够时,它会自动重新分配更多的内存。实际上,只要你愿意,你可以调用 push_back()
任何多次,甚至都不用知道一次又一次的分配是在哪里发生的。
为了存取 vector 元素,使用重载的 [] 操作符。下列循环在屏幕上显示所有
vector 元素:
| for
(int n=0; n<vi.size(); ++n) { cout<<"ISBN: "<<vi[n]<<endl; } |
第二步:优化
在大多数情况下,你应该让 vector
自动管理自己的内存,就像我们在上面程序中所做的那样。但是,在注重时间的任务中,改写默认的分配方案也是很有用的。假设我们预先知道 ISBNs 的数量至少有
2000。那么就可以在对象构造期间指出容量,以便 vector 具有至少 2000 个元素的容量:
| vector <int> vi(2000); // 初始容量为 2000 个元素 |
除此之外,我们还可以调用 resize() 成员函数:
| vi.resize(2000);// 建立不小于 2000 个元素的空间 |
这样,便避免了中间的再分配,从而提高了效率。
摘要:本文列出几个基本的STL map和STL set的问题,通过解答这些问题讲解了STL关联容器内部的数据结构,最后提出了关于UNIX/LINUX自带平衡二叉树库函数和map, set选择问题,并分析了map, set的优势之处。对于希望深入学习STL和希望了解STL map等关联容器底层数据结构的朋友来说,有一定的参考价值。
STL map和set的使用虽不复杂,但也有一些不易理解的地方,如:
或许有得人能回答出来大概原因,但要彻底明白,还需要了解STL的底层数据结构。
C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构操作。vector封装数组,list封装了链表,map和set封装了二叉树等,在封装这些数据结构的时候,STL按照程序员的使用习惯,以成员函数方式提供的常用操作,如:插入、排序、删除、查找等。让用户在STL使用过程中,并不会感到陌生。
C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般的平衡二叉树(有些书籍根据作者姓名,Adelson-Velskii和Landis,将其称为AVL-树),所以被STL选择作为了关联容器的内部结构。本文并不会介绍详细AVL树和RB树的实现以及他们的优劣,关于RB树的详细实现参看红黑树: 理论与实现(理论篇)。本文针对开始提出的几个问题的回答,来向大家简单介绍map和set的底层数据结构。
为何map和set的插入删除效率比用其他序列容器高?
大部分人说,很简单,因为对于关联容器来说,不需要做内存拷贝和内存移动。说对了,确实如此。map和set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。结构图可能如下:
A
/ \
B
C
/ \ / \
D E F G
因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点就OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。
为何每次insert之后,以前保存的iterator不会失效?
看见了上面答案的解释,你应该已经可以很容易解释这个问题。iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。
为何map和set不能像vector一样有个reserve函数来预分配数据?
我以前也这么问,究其原理来说时,引起它的原因在于在map和set内部存储的已经不是元素本身了,而是包含元素的节点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。例如:
map<int, int, less<int>, Alloc<int> > intmap;
这时候在intmap中使用的allocator并不是Alloc<int>, 而是通过了转换的Alloc,具体转换的方法时在内部通过Alloc<int>::rebind重新定义了新的节点分配器,详细的实现参看STL中的Allocator。其实你就记住一点,在map和set内面的分配器已经发生了变化,reserve方法你就不要奢望了。
当数据元素增多时(10000和20000个比较),map和set的插入和搜索速度变化如何?
如果你知道log2的关系你应该就彻底了解这个答案。在map和set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。你明白这个道理后,就可以安心往里面放入元素了。
最后,对于map和set Winter还要提的就是它们和一个c语言包装库的效率比较。在许多unix和linux平台下,都有一个库叫isc,里面就提供类似于以下声明的函数:
void
tree_init(void
**tree);
void
*tree_srch(void **tree,
int (*compare)(),
void *data);
void tree_add(void **tree, int (*compare)(), void *data, void (*del_uar)());
int tree_delete(void **tree, int (*compare)(), void *data,void (*del_uar)());
int tree_trav(void **tree, int (*trav_uar)());
void tree_mung(void **tree, void (*del_uar)());
许多人认为直接使用这些函数会比STL map速度快,因为STL map中使用了许多模板什么的。其实不然,它们的区别并不在于算法,而在于内存碎片。如果直接使用这些函数,你需要自己去new一些节点,当节点特别多,而且进行频繁的删除和插入的时候,内存碎片就会存在,而STL采用自己的Allocator分配内存,以内存池的方式来管理这些内存,会大大减少内存碎片,从而会提升系统的整体性能。Winter在自己的系统中做过测试,把以前所有直接用isc函数的代码替换成map,程序速度基本一致。当时间运行很长时间后(例如后台服务程序),map的优势就会体现出来。从另外一个方面讲,使用map会大大降低你的编码难度,同时增加程序的可读性。何乐而不为?

