星期三, 十一月 22, 2006

http://pl.atyp.us/content/tech/servers.html

Introduction

The purpose of this document is to share some ideas that I've developed over the years about how to develop a certain kind of application for which the term "server" is only a weak approximation. More accurately, I'll be writing about a broad class of programs that are designed to handle very large numbers of discrete messages or requests per second. Network servers most commonly fit this definition, but not all programs that do are really servers in any sense of the word. For the sake of simplicity, though, and because "High-Performance Request-Handling Programs" is a really lousy title, we'll just say "server" and be done with it.

I will not be writing about "mildly parallel" applications, even though multitasking within a single program is now commonplace. The browser you're using to read this probably does some things in parallel, but such low levels of parallelism really don't introduce many interesting challenges. The interesting challenges occur when the request-handling infrastructure itself is the limiting factor on overall performance, so that improving the infrastructure actually improves performance. That's not often the case for a browser running on a gigahertz processor with a gigabyte of memory doing six simultaneous downloads over a DSL line. The focus here is not on applications that sip through a straw but on those that drink from a firehose, on the very edge of hardware capabilities where how you do it really does matter.

Some people will inevitably take issue with some of my comments and suggestions, or think they have an even better way. Fine. I'm not trying to be the Voice of God here; these are just methods that I've found to work for me, not only in terms of their effects on performance but also in terms of their effects on the difficulty of debugging or extending code later. Your mileage may vary. If something else works better for you that's great, but be warned that almost everything I suggest here exists as an alternative to something else that I tried once only to be disgusted or horrified by the results. Your pet idea might very well feature prominently in one of these stories, and innocent readers might be bored to death if you encourage me to start telling them. You wouldn't want to hurt them, would you?

The rest of this article is going to be centered around what I'll call the Four Horsemen of Poor Performance:

  1. Data copies
  2. Context switches
  3. Memory allocation
  4. Lock contention

There will also be a catch-all section at the end, but these are the biggest performance-killers. If you can handle most requests without copying data, without a context switch, without going through the memory allocator and without contending for locks, you'll have a server that performs well even if it gets some of the minor parts wrong.

Data Copies

This could be a very short section, for one very simple reason: most people have learned this lesson already. Everybody knows data copies are bad; it's obvious, right? Well, actually, it probably only seems obvious because you learned it very early in your computing career, and that only happened because somebody started putting out the word decades ago. I know that's true for me, but I digress. Nowadays it's covered in every school curriculum and in every informal how-to. Even the marketing types have figured out that "zero copy" is a good buzzword.

Despite the after-the-fact obviousness of copies being bad, though, there still seem to be nuances that people miss. The most important of these is that data copies are often hidden and disguised. Do you really know whether any code you call in drivers or libraries does data copies? It's probably more than you think. Guess what "Programmed I/O" on a PC refers to. An example of a copy that's disguised rather than hidden is a hash function, which has all the memory-access cost of a copy and also involves more computation. Once it's pointed out that hashing is effectively "copying plus" it seems obvious that it should be avoided, but I know at least one group of brilliant people who had to figure it out the hard way. If you really want to get rid of data copies, either because they really are hurting performance or because you want to put "zero-copy operation" on your hacker-conference slides, you'll need to track down a lot of things that really are data copies but don't advertise themselves as such.

The tried and true method for avoiding data copies is to use indirection, and pass buffer descriptors (or chains of buffer descriptors) around instead of mere buffer pointers. Each descriptor typically consists of the following:

  • A pointer and length for the whole buffer.
  • A pointer and length, or offset and length, for the part of the buffer that's actually filled.
  • Forward and back pointers to other buffer descriptors in a list.
  • A reference count.

Now, instead of copying a piece of data to make sure it stays in memory, code can simply increment a reference count on the appropriate buffer descriptor. This can work extremely well under some conditions, including the way that a typical network protocol stack operates, but it can also become a really big headache. Generally speaking, it's easy to add buffers at the beginning or end of a chain, to add references to whole buffers, and to deallocate a whole chain at once. Adding in the middle, deallocating piece by piece, or referring to partial buffers will each make life increasingly difficult. Trying to split or combine buffers will simply drive you insane.

I don't actually recommend using this approach for everything, though. Why not? Because it gets to be a huge pain when you have to walk through descriptor chains every time you want to look at a header field. There really are worse things than data copies. I find that the best thing to do is to identify the large objects in a program, such as data blocks, make sure those get allocated separately as described above so that they don't need to be copied, and not sweat too much about the other stuff.

This brings me to my last point about data copies: don't go overboard avoiding them. I've seen way too much code that avoids data copies by doing something even worse, like forcing a context switch or breaking up a large I/O request. Data copies are expensive, and when you're looking for places to avoid redundant operations they're one of the first things you should look at, but there is a point of diminishing returns. Combing through code and then making it twice as complicated just to get rid of that last few data copies is usually a waste of time that could be better spent in other ways.

Context Switches

Whereas everyone thinks it's obvious that data copies are bad, I'm often surprised by how many people totally ignore the effect of context switches on performance. In my experience, context switches are actually behind more total "meltdowns" at high load than data copies; the system starts spending more time going from one thread to another than it actually spends within any thread doing useful work. The amazing thing is that, at one level, it's totally obvious what causes excessive context switching. The #1 cause of context switches is having more active threads than you have processors. As the ratio of active threads to processors increases, the number of context switches also increases - linearly if you're lucky, but often exponentially. This very simple fact explains why multi-threaded designs that have one thread per connection scale very poorly. The only realistic alternative for a scalable system is to limit the number of active threads so it's (usually) less than or equal to the number of processors. One popular variant of this approach is to use only one thread, ever; while such an approach does avoid context thrashing, and avoids the need for locking as well, it is also incapable of achieving more than one processor's worth of total throughput and thus remains beneath contempt unless the program will be non-CPU-bound (usually network-I/O-bound) anyway.

The first thing that a "thread-frugal" program has to do is figure out how it's going to make one thread handle multiple connections at once. This usually implies a front end that uses select/poll, asynchronous I/O, signals or completion ports, with an event-driven structure behind that. Many "religious wars" have been fought, and continue to be fought, over which of the various front-end APIs is best. Dan Kegel's C10K paper is a good resource is this area. Personally, I think all flavors of select/poll and signals are ugly hacks, and therefore favor either AIO or completion ports, but it actually doesn't matter that much. They all - except maybe select() - work reasonably well, and don't really do much to address the matter of what happens past the very outermost layer of your program's front end.

The simplest conceptual model of a multi-threaded event-driven server has a queue at its center; requests are read by one or more "listener" threads and put on queues, from which one or more "worker" threads will remove and process them. Conceptually, this is a good model, but all too often people actually implement their code this way. Why is this wrong? Because the #2 cause of context switches is transferring work from one thread to another. Some people even compound the error by requiring that the response to a request be sent by the original thread - guaranteeing not one but two context switches per request. It's very important to use a "symmetric" approach in which a given thread can go from being a listener to a worker to a listener again without ever changing context. Whether this involves partitioning connections between threads or having all threads take turns being listener for the entire set of connections seems to matter a lot less.

Usually, it's not possible to know how many threads will be active even one instant into the future. After all, requests can come in on any connection at any moment, or "background" threads dedicated to various maintenance tasks could pick that moment to wake up. If you don't know how many threads are active, how can you limit how many are active? In my experience, one of the most effective approaches is also one of the simplest: use an old-fashioned counting semaphore which each thread must hold whenever it's doing "real work". If the thread limit has already been reached then each listen-mode thread might incur one extra context switch as it wakes up and then blocks on the semaphore, but once all listen-mode threads have blocked in this way they won't continue contending for resources until one of the existing threads "retires" so the system effect is negligible. More importantly, this method handles maintenance threads - which sleep most of the time and therefore dont' count against the active thread count - more gracefully than most alternatives.

Once the processing of requests has been broken up into two stages (listener and worker) with multiple threads to service the stages, it's natural to break up the processing even further into more than two stages. In its simplest form, processing a request thus becomes a matter of invoking stages successively in one direction, and then in the other (for replies). However, things can get more complicated; a stage might represent a "fork" between two processing paths which involve different stages, or it might generate a reply (e.g. a cached value) itself without invoking further stages. Therefore, each stage needs to be able to specify "what should happen next" for a request. There are three possibilities, represented by return values from the stage's dispatch function:

  • The request needs to be passed on to another stage (an ID or pointer in the return value).
  • The request has been completed (a special "request done" return value)
  • The request was blocked (a special "request blocked" return value). This is equivalent to the previous case, except that the request is not freed and will be continued later from another thread.

Note that, in this model, queuing of requests is done within stages, not between stages. This avoids the common silliness of constantly putting a request on a successor stage's queue, then immediately invoking that successor stage and dequeuing the request again; I call that lots of queue activity - and locking - for nothing.

If this idea of separating a complex task into multiple smaller communicating parts seems familiar, that's because it's actually very old. My approach has its roots in the Communicating Sequential Processes concept elucidated by C.A.R. Hoare in 1978, based in turn on ideas from Per Brinch Hansen and Matthew Conway going back to 1963 - before I was born! However, when Hoare coined the term CSP he meant "process" in the abstract mathematical sense, and a CSP process need bear no relation to the operating-system entities of the same name. In my opinion, the common approach of implementing CSP via thread-like coroutines within a single OS thread gives the user all of the headaches of concurrency with none of the scalability.

A contemporary example of the staged-execution idea evolved in a saner direction is Matt Welsh's SEDA. In fact, SEDA is such a good example of "server architecture done right" that it's worth commenting on some of its specific characteristics (especially where those differ from what I've outlined above).

  1. SEDA's "batching" tends to emphasize processing multiple requests through a stage at once, while my approach tends to emphasize processing a single request through multiple stages at once.
  2. SEDA's one significant flaw, in my opinion, is that it allocates a separate thread pool to each stage with only "background" reallocation of threads between stages in response to load. As a result, the #1 and #2 causes of context switches noted above are still very much present.
  3. In the context of an academic research project, implementing SEDA in Java might make sense. In the real world, though, I think the choice can be characterized as unfortunate.

Memory Allocation

Allocating and freeing memory is one of the most common operations in many applications. Accordingly, many clever tricks have been developed to make general-purpose memory allocators more efficient. However, no amount of cleverness can make up for the fact that the very generality of such allocators inevitably makes them far less efficient than the alternatives in many cases. I therefore have three suggestions for how to avoid the system memory allocator altogether.

Suggestion #1 is simple preallocation. We all know that static allocation is bad when it imposes artificial limits on program functionality, but there are many other forms of preallocation that can be quite beneficial. Usually the reason comes down to the fact that one trip through the system memory allocator is better than several, even when some memory is "wasted" in the process. Thus, if it's possible to assert that no more than N items could ever be in use at once, preallocation at program startup might be a valid choice. Even when that's not the case, preallocating everything that a request handler might need right at the beginning might be preferable to allocating each piece as it's needed; aside from the possibility of allocating multiple items contiguously in one trip through the system allocator, this often greatly simplifies error-recovery code. If memory is very tight then preallocation might not be an option, but in all but the most extreme circumstances it generally turns out to be a net win.

Suggestion #2 is to use lookaside lists for objects that are allocated and freed frequently. The basic idea is to put recently-freed objects onto a list instead of actually freeing them, in the hope that if they're needed again soon they need merely be taken off the list instead of being allocated from system memory. As an additional benefit, transitions to/from a lookaside list can often be implemented to skip complex object initialization/finalization.

It's generally undesirable to have lookaside lists grow without bound, never actually freeing anything even when your program is idle. Therefore, it's usually necessary to have some sort of periodic "sweeper" task to free inactive objects, but it would also be undesirable if the sweeper introduced undue locking complexity or contention. A good compromise is therefore a system in which a lookaside list actually consists of separately locked "old" and "new" lists. Allocation is done preferentially from the new list, then from the old list, and from the system only as a last resort; objects are always freed onto the new list. The sweeper thread operates as follows:

  1. Lock both lists.
  2. Save the head for the old list.
  3. Make the (previously) new list into the old list by assigning list heads.
  4. Unlock.
  5. Free everything on the saved old list at leisure.

Objects in this sort of system are only actually freed when they have not been needed for at least one full sweeper interval, but always less than two. Most importantly, the sweeper does most of its work without holding any locks to contend with regular threads. In theory, the same approach can be generalized to more than two stages, but I have yet to find that useful.

One concern with using lookaside lists is that the list pointers might increase object size. In my experience, most of the objects that I'd use lookaside lists for already contain list pointers anyway, so it's kind of a moot point. Even if the pointers were only needed for the lookaside lists, though, the savings in terms of avoided trips through the system memory allocator (and object initialization) would more than make up for the extra memory.

Suggestion #3 actually has to do with locking, which we haven't discussed yet, but I'll toss it in anyway. Lock contention is often the biggest cost in allocating memory, even when lookaside lists are in use. One solution is to maintain multiple private lookaside lists, such that there's absolutely no possibility of contention for any one list. For example, you could have a separate lookaside list for each thread. One list per processor can be even better, due to cache-warmth considerations, but only works if threads cannot be preempted. The private lookaside lists can even be combined with a shared list if necessary, to create a system with extremely low allocation overhead.

Lock Contention

Efficient locking schemes are notoriously hard to design, because of what I call Scylla and Charybdis after the monsters in the Odyssey. Scylla is locking that's too simplistic and/or coarse-grained, serializing activities that can or should proceed in parallel and thus sacrificing performance and scalability; Charybdis is overly complex or fine-grained locking, with space for locks and time for lock operations again sapping performance. Near Scylla are shoals representing deadlock and livelock conditions; near Charybdis are shoals representing race conditions. In between, there's a narrow channel that represents locking which is both efficient and correct...or is there? Since locking tends to be deeply tied to program logic, it's often impossible to design a good locking scheme without fundamentally changing how the program works. This is why people hate locking, and try to rationalize their use of non-scalable single-threaded approaches.

Almost every locking scheme starts off as "one big lock around everything" and a vague hope that performance won't suck. When that hope is dashed, and it almost always is, the big lock is broken up into smaller ones and the prayer is repeated, and then the whole process is repeated, presumably until performance is adequate. Often, though, each iteration increases complexity and locking overhead by 20-50% in return for a 5-10% decrease in lock contention. With luck, the net result is still a modest increase in performance, but actual decreases are not uncommon. The designer is left scratching his head (I use "his" because I'm a guy myself; get over it). "I made the locks finer grained like all the textbooks said I should," he thinks, "so why did performance get worse?"

In my opinion, things got worse because the aforementioned approach is fundamentally misguided. Imagine the "solution space" as a mountain range, with high points representing good solutions and low points representing bad ones. The problem is that the "one big lock" starting point is almost always separated from the higher peaks by all manner of valleys, saddles, lesser peaks and dead ends. It's a classic hill-climbing problem; trying to get from such a starting point to the higher peaks only by taking small steps and never going downhill almost never works. What's needed is a fundamentally different way of approaching the peaks.

The first thing you have to do is form a mental map of your program's locking. This map has two axes:

  • The vertical axis represents code. If you're using a staged architecture with non-branching stages, you probably already have a diagram showing these divisions, like the ones everybody uses for OSI-model network protocol stacks.
  • The horizontal axis represents data. In every stage, each request should be assigned to a data set with its own resources separate from any other set.

You now have a grid, where each cell represents a particular data set in a particular processing stage. What's most important is the following rule: two requests should not be in contention unless they are in the same data set and the same processing stage. If you can manage that, you've already won half the battle.

Once you've defined the grid, every type of locking your program does can be plotted, and your next goal is to ensure that the resulting dots are as evenly distributed along both axes as possible. Unfortunately, this part is very application-specific. You have to think like a diamond-cutter, using your knowledge of what the program does to find the natural "cleavage lines" between stages and data sets. Sometimes they're obvious to start with. Sometimes they're harder to find, but seem more obvious in retrospect. Dividing code into stages is a complicated matter of program design, so there's not much I can offer there, but here are some suggestions for how to define data sets:

  • If you have some sort of a block number or hash or transaction ID associated with requests, you can rarely do better than to divide that value by the number of data sets.
  • Sometimes, it's better to assign requests to data sets dynamically, based on which data set has the most resources available rather than some intrinsic property of the request. Think of it like multiple integer units in a modern CPU; those guys know a thing or two about making discrete requests flow through a system.
  • It's often helpful to make sure that the data-set assignment is different for each stage, so that requests which would contend at one stage are guaranteed not to do so at another stage.

If you've divided your "locking space" both vertically and horizontally, and made sure that lock activity is spread evenly across the resulting cells, you can be pretty sure that your locking is in pretty good shape. There's one more step, though. Do you remember the "small steps" approach I derided a few paragraphs ago? It still has its place, because now you're at a good starting point instead of a terrible one. In metaphorical terms you're probably well up the slope on one of the mountain range's highest peaks, but you're probably not at the top of one. Now is the time to collect contention statistics and see what you need to do to improve, splitting stages and data sets in different ways and then collecting more statistics until you're satisfied. If you do all that, you're sure to have a fine view from the mountaintop.

Other Stuff

As promised, I've covered the four biggest performance problems in server design. There are still some important issues that any particular server will need to address, though. Mostly, these come down to knowing your platform/environment:

  • How does your storage subsystem perform with larger vs. smaller requests? With sequential vs. random? How well do read-ahead and write-behind work?
  • How efficient is the network protocol you're using? Are there parameters or flags you can set to make it perform better? Are there facilities like TCP_CORK, MSG_PUSH, or the Nagle-toggling trick that you can use to avoid tiny messages?
  • Does your system support scatter/gather I/O (e.g. readv/writev)? Using these can improve performance and also take much of the pain out of using buffer chains.
  • What's your page size? What's your cache-line size? Is it worth it to align stuff on these boundaries? How expensive are system calls or context switches, relative to other things?
  • Are your reader/writer lock primitives subject to starvation? Of whom? Do your events have "thundering herd" problems? Does your sleep/wakeup have the nasty (but very common) behavior that when X wakes Y a context switch to Y happens immediately even if X still has things to do?

I'm sure I could think of many more questions in this vein. I'm sure you could too. In any particular situation it might not be worthwhile to do anything about any one of these issues, but it's usually worth at least thinking about them. If you don't know the answers - many of which you will not find in the system documentation - find out. Write a test program or micro-benchmark to find the answers empirically; writing such code is a useful skill in and of itself anyway. If you're writing code to run on multiple platforms, many of these questions correlate with points where you should probably be abstracting functionality into per-platform libraries so you can realize a performance gain on that one platform that supports a particular feature.

The "know the answers" theory applies to your own code, too. Figure out what the important high-level operations in your code are, and time them under different conditions. This is not quite the same as traditional profiling; it's about measuring design elements, not actual implementations. Low-level optimization is generally the last resort of someone who screwed up the design.

  1. 10K并发连接问题

此文是 http://www.kegel.com/c10k.html 的翻译与理解。

已经是WEB服务器解决10K并发连接的时候了,你不这么认为吗?毕竟,WEB是现拥有最大的地盘。


现在的的计算机也大了。1千200美元左右可以买到1G的机器,装有2G的内存与千兆网卡。我们看看,如果有20K客户端,那么每个客户端分得50KHz CPU,100K字节内存,50Kbits/sec的带宽。那就意味着对于20K客户端中的一个不应该花费比每秒从磁盘读取4K字节并发送到网络更多的马力。(这种情况下每个客户端花费8美分。那些每客户端100美元的服务器许可证费用看上去贵了点)。所以,硬件不再是瓶颈。(老外就是细致,这都算的这么清楚!)


在1999年,最忙的一台FTP站点,cdrom.com, 实际上已经通过G比特以太网并发处理了10K的连接。到了2001年,相同的速度的服务已经在几个ISP中提供,它们希望能够为规模不断扩大的商业用户提供服务。


瘦客户端模型的计算模型在形式上又回来了,这时网络上工作的服务器,服务者数以千计的客户。


我们要知道,这里是一些如何配置操作系统以及写代码支持以千计客户端的笔记。讨论集中在类Unix操作系统,那也是我个人感兴趣的领域,但是Windows也有所涉及。



目录

  • 10K连接问题
  • 相关站点
  • 首先该读的书
  • I/O框架
  • I/O策略
    1. 每一线程的多服务多客户端,使用非阻塞I/O与读触发通知
      • 传统select()方法
      • 传统poll()方法
      • /dev/poll方法(Solaris 2.7+)
      • kqueue方法(FreeBSD, NetBSD)
    2. 每一线程的多服务客户端,是用非阻塞I/O与读更改通知
      • epoll(Linux 2.6+)
      • Polyakov's Kevent(Linux 2.6+)
      • Drepper's New Network Interface(proposal for Linux 2.6+)
      • Realtime Signals(Linux 2.4+)
      • Signal-per-fd
      • kqueue(FreeBSD,NetBSD)
    3. 每一线程的多服务器客户端,使用异步I/O与完成通知
    4. 每一服务线程服务一个客户端
      • Linux/Threads(Linux 2.0+)
      • NGPT(Linux 2.4+)
      • NPTL (Linux 2.6, Red Hat 9)
      • FreeBSD threading support
      • NetBSD threading support
      • Solaris threading support
      • Java threading support in JDK 1.3.x and earlier
      • Note: 1:1 threading vs. M:N threading
    5. 建立服务器代码到内核
  • 注释
  • 打开文件句柄的限制
  • 线程限制
  • Java问题
  • 其它提示
    • 零拷贝
    • sendfile()系统调用可实现零拷贝网络操作
    • 杜绝小侦数据使用writev or TCP CORK
    • 获益于非Posix线称的程序
    • 缓存你的数据有时也会赢得胜利
  • 其它限制
  • 内核问题
  • 测量服务器性能
  • 事例
    • 对select()基础的服务器的兴趣
    • 对/dev/poll基础的服务器的兴趣
    • 对kqueue()基础的服务器的兴趣
    • 对实时信号基础的服务器的兴趣
    • 对线称基础的服务器的兴趣
    • 对内核内服务器的兴趣
  • 其它感兴趣连接


相关站点

在2003年10月,Felix von Leitner 整理了优秀的关于网络可测量性的网页和介绍,完成了比较多种网络系统调用和操作系统的数据的标键。他报告中的一个观点就是linux 2.6的内核实际上打败了2.4的内核,但是还有很多,许多好的图像将会给开发者们食物用于考虑一段时间!(参看Slashdot的注释,主要关注于是否任何人都会跟随基准提高Felix的结果!

首先阅读的书

如果你还没有读它,就立即出发购买一份. Unix Network Programming:Networking APIs. Sockets and Xit(Volume1),Unix 网络编程, 网络接口,套接字与Xit。 W. Richard Stevens的著作. 他描述了许多I/O策略与写高性能服务器的相关缺陷. 他甚至讨论了 "thundering herd" 问题, 当你度过之后,开始阅读 Jeff Darcy's的高性能服务器设计的笔记.

另外一本可能有所帮助的书籍对于使用者比编写Web服务器的人的更为重要, 建立扩展性网站!

I/O框架

抽象了一些技术的预打包的库是实现了一些I/O框架,它们可以隔离你的代码与服务器,并且使代码更容易移植.

  • ACE 重量级的C++ I/O框架,包含了面向对象的一些I/O策略的实现以及其它许多有价值的东西。具体一些 它的Reactor是以OO的方式来处理非阻塞I/O,Proactor是以OO的方式处理异步I/O.
  • ASIO 将成为BOOST库成员的C++ I/O框架. 它像是ACE在STL时代的升级.
  • libevent Niels Provos的 轻量级的 C I/O框架, 支持kqueue与select, 很快将会支持poll与epoll. 它仅仅是触发, 作者认为,那同时具有好坏两方面. Niels 的处理时间所花时间的漂亮的图像作为了连接数量的功能. 它显示出kqueue 与 sys_epoll很明显获胜.
  • 作者自己的轻量级框架(遗憾的是,并不是最新的)
    • Pooler 是轻量级的 C++ I/O库,它实现了触发读API,采用了底层的你想要的读操作API(poll, select, /dev/poll kqueue, or sigio). 被不同API性能的比较结果是有价值的,此文档链接的Pooler子类举例说明了不同读操作API的用法
    • rn是轻量级的 C I/O操作框架,是作者在Poller之后的另一个尝试. lgpl的版权(所以,方便使用在商业应用中)
  • MattWelsh 在2000年4月写的论文. 描述了如何在工作线称与事件驱动技术之间取得平衡在建立可扩展服务器的时候.该论文描述了他的Sandstorm的I/O框架.
  • CoryNelson's Sacle! Library 为WIndows提供的异步Socket, 文件,与管道I/O库!

I/O策略

代码静态分析工具PC-LINT安装配置--step by step[转]

http://www.yuanma.org/data/2006/0529/article_512.htm

PC-Lint是C/C++软件代码静态分析工具,你可以把它看作是一种更加严格的编译器。它不仅可以检查出一般的语法错误,还可以检查出那些虽然符合语法要求但不易发现的潜在错误。 C语言的灵活性带来了代码效率的提升,但相应带来了代码编写的随意性,另外C编译器不进行强制类型检查,也带来了代码编写的隐患。PCLint识别并报告C语言中的编程陷阱和格式缺陷的发生。它进行程序的全局分析,能识别没有被适当检验的数组下标,报告未被初始化的变量,警告使用空指针,冗余的代码,等等。软件除错是软件项目开发成本和延误的主要因素。PClint能够帮你在程序动态测试之前发现编码错误。这样消除错误的成本更低。 使用PC-Lint在代码走读和单元测试之前进行检查,可以提前发现程序隐藏错误,提高代码质量,节省测试时间。并提供编码规则检查,规范软件人员的编码行为。由于PC-LINT对于一般程序员来说可能比较陌生,有好多人安装了也不知道怎样配置和使用。 下面我就根据自己的安装和配置心得对PC-Lint的安装、配置及使用进行下详细说明.本人主要介绍了将PC-Lint集成到VC++6.0和SourceInsight的方法和步骤。
(一)Windows下C/C++开发工具中,VC6使用较为普遍,因此这里先讲下VC6.0环境中集成pclint的步骤.
首先, 当然要下载软件,正版软件要200多$呢,买不起!所以只好网上找免费的拉。从http://www.61ic.com/down/othe/pclint.rar处可以下载到一个8.0版本的pclint.
1.将pclint.rar解压至c:\, 这样lint文件就位与c:\pclint(安装目录)下了。2.将c:\pclint\lnt 下的3个文件lib-w32.lnt,env-vc6.lnt,co-msc60.lnt拷贝至c:\pclint下, 再在安装目录下创建std.lnt和options.lnt两个文件,其中std.lnt的内容如下
// contents of std.lnt
c:\pclint\co-msc60.lntc:\pclint\lib-w32.lntc:\pclint\options.lnt -si4 -sp4-i"D:\Program Files;D:\Program Files\Microsoft Visual Studio\VC98\Include"
//end
其中-i后面的路径名为VC的安装路径和VC Include 文件路径,根据自己的修改便可。
options.lnt 内容可为空,为定制内容,以后需要时再添加。
准备工作做完了,下一步就是要将pclint集成到VC6中去,先配置lint使之能对单个C或C++文件进行检查。
1.打开VC6,tools--->customize-->tools 新建一个名为pclint的项,在下面填入command: C:\pclint\lint-nt.exearguments: -u c:\pclint\std.lnt c:\pclint\env-vc6.lnt "$(FilePath)"
Use Output Window 打上勾close 完成。 这个在你VC窗口tools菜单下应该多了一个pclint选项,可以用它来运行lint程序,对你的c/c++代码进行静态检查了。
现在就可以用个小程序测试一下pclint了//test1.cpp#include
class X
{
int *p;
public:
X()
{ p = new int[20]; }
void init()
{ memset( p, 20, 'a' ); }
~X()
{ delete p; }
};
编译这个文件,看下你的编译器给你多少警告,再运行下lint, 可以自己对比一下。我的机器上,VC产生0 errors 0 warnings, 而lint程序产生了如下8条警告信息,有些还是很有用处的提示,这里就不一一分析了.
test.cpp(12): error 783: (Info -- Line does not end with new-line)test.cpp(7): error 1732: (Info -- new in constructor for class 'X' which has no assignment operator)test.cpp(7): error 1733: (Info -- new in constructor for class 'X' which has no copy constructor) { memset( p, 20, 'a' ); }test.cpp(9): error 669: (Warning -- Possible data overrun for function 'memset(void *, int, unsigned int)', argument 3 (size=97) exceeds argument 1 (size=80) [Reference: test.cpp: lines 7, 9])test.cpp(7): error 831: (Info -- Reference cited in prior message)test.cpp(9): error 831: (Info -- Reference cited in prior message) { delete p; }test.cpp(11): error 424: (Warning -- Inappropriate deallocation (delete) for 'new[]' data)
--- Wrap-up for Module: test.cpp
test.cpp(2): error 753: (Info -- local class 'X' (line 2, file test.cpp) not referenced)Tool returned code: 8
2.通常一个VC项目中包含多个C或C++文件,有时需要同时对这一系列的文件进行lint检查,我们可以通过配置一个pclint_project来达到目的。
和前面第一步中的方法基本一样,不过这里我们需要用到unix中的find等命令来查找当前目录下的C和C++文件,然后再将它们送给lint程序处理,所以得先从http://www.weihenstephan.de/~syring/win32/UnxUtils.zip下载UnxUtils.zip.接着按下列步骤进行:
(i)解压UnxUtils.zip至c:\unix下, 可以看到C:\unix\usr\local\wbin有很多unix下的命令,等下会用到
(ii)打开VC6,tools--->customize-->tools 新建一个名为pclint_project的项,只不过下面的commands和arguments内容不同。
commands: C:\unix\usr\local\wbin\find.exearguments: $(FileDir) -name *.c -o -name *.cpp C:\unix\usr\local\wbin\xargs lint-nt -i"c:\unix\usr\local" -u c:\pclint\std.lnt c:\pclint\env-vc6.lnt
(iii)Use Output Window打上勾,close退出。好了,这时VC tools菜单下应该又多了一个pclint_project项了,你以后可以用它来对一个VC项目运行lint检查程序了.
(二)SourceInsight中集成pclint程序的方法.
Windows平台下也有好多人都喜欢用SourceInsight编辑C/C++程序,如果将pclint集成到SourceInsight中,那就相当于给SourceInsight增加了一个C/C++编译器,而且它的检查更严格,能发现一些编译器发现不了的问题,可以大大减少程序中潜伏的BUG。这样的话,相信更多人会喜欢SourceInsight这个工具了。
下面简要地介绍下pclint集成到SourceInsight中的方法
有了上面VC中集成pclint的经验, 下面的事情就应该比较轻松了,(a)打开你的SourceInsight, 选择Options-->Custom Commands-->Add, 输入pclint(当然名字可以随便).
(b) Run中输入: c:\pclint\lint-nt -u c:\pclint\std.lnt c:\pclint\env-vc6.lnt %f
(c)Dir留空,将Iconic Window, Capture Output, Parse Links in OutPut, File,then Line 四项前打上勾。
(d)然后点右侧 Menu--->Menu-->View-->, 右侧Insert, OK.
(e)此时在SourceInsight中的View菜单下多了个pclint选项,可以用它来对单个C/C++文件进行静态检查。
用类似的方法可以配置对一个SourceInsight工程文件的lint检查。
(a)打开你的SourceInsight, 选择Options-->Custom Commands-->Add, 输入pclint_project(当然名字可以随便).
(b) Run中输入: C:\unix\usr\local\wbin\find.exe %d -name *.c -o -name *.cpp C:\unix\usr\local\wbin\xargs lint-nt -i"C:\unix\usr\local" -u c:\pclint\std.lnt c:\pclint\env-vc6.lnt
(c)Dir留空,将Iconic Window, Capture Output, Parse Links in OutPut, File,then Line 四项前打上勾。
(d)然后点右侧 Menu--->Menu-->View-->, 右侧Insert, OK.
(e)此时在SourceInsight中的View菜单下多了个pclint_project选项,可以用它来一个工程中的C/C++文件进行静态检查。
本文主要对pclint集成到VC及SourceInsight环境中的方法根据本人安装和使用心得做了较详细介绍,希望对以前没使用过pclint的朋友们能有所帮助,不足之处还请多指正!

星期五, 十月 20, 2006

正则表达式的一些文章与工具

http://www.cnblogs.com/Laser_Lu/archive/2005/04/21/142605.aspx

http://aspxboy.com/private/508/default.aspx

http://www.codeproject.com/dotnet/regextutorial.asp

http://www.codeproject.com/dotnet/expresso.asp

http://www.radsoftware.com.au/regexdesigner/

星期二, 十月 17, 2006

维基百科

维基百科今天下午好像解除封锁,庆贺! http://www.wikipedia.org/
后来又刷新了一下,发现不行,还在封锁中

P2P 赢利模式分析

在刚才浏览的一篇涉及网络安全的文章中,提到了网络安全所要面临的新的问题,没有深究作者的观点,无非是流行的P2P技术,如何带来安全隐患,如何给网络安全带来新的挑战等等。

可以肯定的是P2P技术存在大量的TCP/UDP连接,和大量打开的端口,与传统网络安全中尽量少开端口的得理念相背离。基于P2P的产品试图突破防火墙的控制,大量使用了80,443,22,21,25等知名端口,重这一点来说,一方宣布智能穿透防火墙,令一方宣布专业封杀P2P。大有水火不容的态势。

这一切在我看来,P2P产品没有一个标准的缘故,P2P行业的自律性也差了点,P2P服务目前所看到的利益来自于人气,这一点类是于传统网站,在P2P赢利模式不清楚的情况下,只有用户多了,才有赚钱的可能,使用P2P技术强制使用个人/单位/服务商的带宽,带宽就是钱啊,这么看来,P2P有点“抢钱”的意思,话在说回来,想要使用P2P提供的优质服务,总的付出点代价。这样,电信/网通就急了。那些被P2P占用带宽的公司也急了!

问题的解决,需要多方利益的统一,说起来容易做起来难。内容提供商,服务运营商,基础设施(主要是网络),用户以及这个链条中的其它环节,现在的利益还没有统一,彼此之间也不负责,大部分是服务运营商在唱独角戏。脚指头想都知道,有限的风投花完了该怎么办,出路在哪里?那个惨啊,可想而知。

P2P行业就这么完了吗?当初一点点小的技术创新,惹得互联网波涛汹涌的气势都哪里去了?泡沫过后,大家冷静下来的时候,也就是该有人站出来的时候了!会是谁呢,谁来整合这个残局?如果行业平静了,其实还是有很多人可以出来充当这一角色,他所要解决的问题就是拣贝壳,从贝壳中拿出那颗受过苦难的贝壳中孕育出的那颗珍珠。价值体现出来了,那些小贝壳们又出现了希望。传统媒体(主要是电视台),网络运营商(电信/网通),门户网站都可以充当这个拣贝壳的角色,充当P2P运营模式中的主流。非主流的情况应该是作为增值模块来提供某一服务,不过这个应该是个小头。

Google Video在目前我所在的区域拒绝提供服务,也无从评判现有P2P新媒体服务的好坏,当各种因素平衡的时候,P2P的春天会来临,仍在P2P行业里面奋斗的人们,请坚持你们的理想,保持这种创新的活力!

写道这里已经严重跑题,本来是想讨论P2P与网络安全的问题,却成了发表感慨。

不说了,兄弟我拭目以待!

星期一, 十月 16, 2006

Apache 2.0性能优化—MPM的选择与配置

转: http://docs.chinalinuxpub.com/read.php?wid=719

  Apache 2.0在性能上的改善最吸引人。在支持POSIX线程的Unix系统上,Apache可以通过不同的MPM运行在一种多进程与多线程相混合的模式下,增强部分配置的可扩充性能。相比于Apache 1.3,2.0版本做了大量的优化来提升处理能力和可伸缩性,并且大多数改进在默认状态下即可生效。但是在编译和运行时刻,2.0也有许多可以显著提高性能的选择。本文不想叙述那些以功能换取速度的指令,如HostnameLookups等,而只是说明在2.0中影响性能的最核心特性:MPM(Multi -Processing Modules,多道处理模块)的基本工作原理和配置指令。

  毫不夸张地说,MPM的引入是Apache 2.0最重要的变化。大家知道,Apache是基于模块化的设计,而Apache 2.0更扩展了模块化设计到Web服务器的最基本功能。服务器装载了一种多道处理模块,负责绑定本机网络端口、接受请求,并调度子进程来处理请求。扩展模块化设计有两个重要好处:

  ◆ Apache可以更简洁、有效地支持多种操作系统;

  ◆ 服务器可以按站点的特殊需要进行自定制。

  在用户级,MPM看起来和其它Apache模块非常类似。主要区别是在任意时刻只能有一种MPM被装载到服务器中。

  指定MPM的方法

  下面以Red Hat Linux 9为平台,说明在Apache 2.0中如何指定MPM (Apache采用2.0.45)。先解压缩源代码包httpd-2.0.45.tar.gz,生成httpd-2.0.45目录(Apache 1.3源代码包的命名规则是apache_1.3.NN.tar.gz,而2.0版则是httpd-2.0.NN.tar.gz,其中NN是次版本号)。

  进入httpd-2.0.45目录,运行以下代码:

 $ ./configure --help|grep mpm



  显示如下:

--with-mpm=MPM
Choose the process model for Apache to use.
MPM={beos|worker|prefork|mpmt_os2| perchild|leader|threadpool}



  上述操作用来选择要使用的进程模型,即哪种MPM模块。Beos、mpmt_os2分别是BeOS和OS/2上缺省的MPM, perchild主要设计目的是以不同的用户和组的身份来运行不同的子进程。这在运行多个需要CGI的虚拟主机时特别有用,会比1.3版中的SuExec 机制做得更好。leader和threadpool都是基于worker的变体,还处于实验性阶段,某些情况下并不会按照预期设想的那样工作,所以 Apache官方也并不推荐使用。因此,我们主要阐述prefork和worker这两种和性能关系最大的产品级MPM ( 有关其它的MPM详细说明,请参见Apache官方文档:http://httpd.apache.org/docs-2.0/mod/)

  prefork的工作原理及配置

  如果不用“--with-mpm”显式指定某种MPM,prefork就是Unix平台上缺省的MPM。它所采用的预派生子进程方式也是 Apache 1.3中采用的模式。prefork本身并没有使用到线程,2.0版使用它是为了与1.3版保持兼容性;另一方面,prefork用单独的子进程来处理不同的请求,进程之间是彼此独立的,这也使其成为最稳定的MPM之一。

  若使用prefork,在make编译和make install安装后,使用“httpd -l”来确定当前使用的MPM,应该会看到prefork.c(如果看到worker.c说明使用的是worker MPM,依此类推)。再查看缺省生成的httpd.conf配置文件,里面包含如下配置段:


StartServers 5
MinSpareServers 5
MaxSpareServers 10
MaxClients 150
MaxRequestsPerChild 0




  prefork的工作原理是,控制进程在最初建立“StartServers”个子进程后,为了满足MinSpareServers设置的需要创建一个进程,等待一秒钟,继续创建两个,再等待一秒钟,继续创建四个……如此按指数级增加创建的进程数,最多达到每秒32个,直到满足 MinSpareServers设置的值为止。这就是预派生(prefork)的由来。这种模式可以不必在请求到来时再产生新的进程,从而减小了系统开销以增加性能。

  MaxSpareServers设置了最大的空闲进程数,如果空闲进程数大于这个值,Apache会自动kill掉一些多余进程。这个值不要设得过大,但如果设的值比MinSpareServers小,Apache会自动把其调整为MinSpareServers+1。如果站点负载较大,可考虑同时加大MinSpareServers和MaxSpareServers。

  MaxRequestsPerChild设置的是每个子进程可处理的请求数。每个子进程在处理了“MaxRequestsPerChild” 个请求后将自动销毁。0意味着无限,即子进程永不销毁。虽然缺省设为0可以使每个子进程处理更多的请求,但如果设成非零值也有两点重要的好处:

  ◆ 可防止意外的内存泄漏;

  ◆ 在服务器负载下降的时侯会自动减少子进程数。

  因此,可根据服务器的负载来调整这个值。笔者认为10000左右比较合适。

  MaxClients是这些指令中最为重要的一个,设定的是Apache可以同时处理的请求,是对Apache性能影响最大的参数。其缺省值 150是远远不够的,如果请求总数已达到这个值(可通过ps -ef|grep http|wc -l来确认),那么后面的请求就要排队,直到某个已处理请求完毕。这就是系统资源还剩下很多而HTTP访问却很慢的主要原因。系统管理员可以根据硬件配置和负载情况来动态调整这个值。虽然理论上这个值越大,可以处理的请求就越多,但Apache默认的限制不能大于256。如果把这个值设为大于256,那么 Apache将无法起动。事实上,256对于负载稍重的站点也是不够的。在Apache 1.3中,这是个硬限制。如果要加大这个值,必须在“configure”前手工修改的源代码树下的src/include/httpd.h中查找 256,就会发现“#define HARD_SERVER_LIMIT 256”这行。把256改为要增大的值(如4000),然后重新编译Apache即可。在Apache 2.0中新加入了ServerLimit指令,使得无须重编译Apache就可以加大MaxClients。下面是笔者的prefork配置段:


StartServers 10
MinSpareServers 10
MaxSpareServers 15
ServerLimit 2000
MaxClients 1000
MaxRequestsPerChild 10000




  上述配置中,ServerLimit的最大值是20000,对于大多数站点已经足够。如果一定要再加大这个数值,对位于源代码树下server/mpm/prefork/prefork.c中以下两行做相应修改即可:

#define DEFAULT_SERVER_LIMIT 256
#define MAX_SERVER_LIMIT 20000



  worker的工作原理及配置

  相对于prefork,worker是2.0 版中全新的支持多线程和多进程混合模型的MPM。由于使用线程来处理,所以可以处理相对海量的请求,而系统资源的开销要小于基于进程的服务器。但是, worker也使用了多进程,每个进程又生成多个线程,以获得基于进程服务器的稳定性。这种MPM的工作方式将是Apache 2.0的发展趋势。

  在configure -with-mpm=worker后,进行make编译、make install安装。在缺省生成的httpd.conf中有以下配置段:


StartServers 2
MaxClients 150
MinSpareThreads 25
MaxSpareThreads 75
ThreadsPerChild 25
MaxRequestsPerChild 0




  worker的工作原理是,由主控制进程生成“StartServers”个子进程,每个子进程中包含固定的ThreadsPerChild 线程数,各个线程独立地处理请求。同样,为了不在请求到来时再生成线程,MinSpareThreads和MaxSpareThreads设置了最少和最多的空闲线程数;而MaxClients设置了所有子进程中的线程总数。如果现有子进程中的线程总数不能满足负载,控制进程将派生新的子进程。

  MinSpareThreads和MaxSpareThreads的最大缺省值分别是75和250。这两个参数对Apache的性能影响并不大,可以按照实际情况相应调节。

  ThreadsPerChild是worker MPM中与性能相关最密切的指令。ThreadsPerChild的最大缺省值是64,如果负载较大,64也是不够的。这时要显式使用 ThreadLimit指令,它的最大缺省值是20000。上述两个值位于源码树server/mpm/worker/worker.c中的以下两行:

#define DEFAULT_THREAD_LIMIT 64
#define MAX_THREAD_LIMIT 20000



  这两行对应着ThreadsPerChild和ThreadLimit的限制数。最好在configure之前就把64改成所希望的值。注意,不要把这两个值设得太高,超过系统的处理能力,从而因Apache不起动使系统很不稳定。

  Worker模式下所能同时处理的请求总数是由子进程总数乘以ThreadsPerChild值决定的,应该大于等于MaxClients。如果负载很大,现有的子进程数不能满足时,控制进程会派生新的子进程。默认最大的子进程总数是16,加大时也需要显式声明ServerLimit(最大值是20000)。这两个值位于源码树server/mpm/worker/worker.c中的以下两行:

#define DEFAULT_SERVER_LIMIT 16
#define MAX_SERVER_LIMIT 20000



  需要注意的是,如果显式声明了ServerLimit,那么它乘以ThreadsPerChild的值必须大于等于MaxClients,而且MaxClients必须是ThreadsPerChild的整数倍,否则Apache将会自动调节到一个相应值(可能是个非期望值)。下面是笔者的 worker配置段:


StartServers 3
MaxClients 2000
ServerLimit 25
MinSpareThreads 50
MaxSpareThreads 200
ThreadLimit 200
ThreadsPerChild 100
MaxRequestsPerChild 0




  通过上面的叙述,可以了解到Apache 2.0中prefork和worker这两个重要MPM的工作原理,并可根据实际情况来配置Apache相关的核心参数,以获得最大的性能和稳定性。



其它更详细的写以到http://httpd.apache.org/docs-2.0/