Archive for the ‘C++’ Category

在我们的项目里,需要实时采集web服务器产生的日志,进行分析。由于PV较高,且每个PV会产生多条日志,故系统整体吞吐量较大。由于第一手日志的读写都是由web服务器完成的,故本文主要关注PHP程序写日志,以及logagent进程实时读和网络推送日志,是否会对性能产生影响。

C函数与缓存

PHP内核是由C实现的,所以要理解其写日志的性能,首先需要了解相关的C函数。C函数与缓存的关系如下图,其中application memory和clib buffer都是在当前进程的用户内存空间,而page cache是内核内存空间,为多进程共享。

需要关注的几个Notes:

  • Note that fclose only flushes the user space buffers provided by the C library. To ensure that the data is physically  stored  on disk the kernel buffers must be flushed too, e.g. with sync(2) or fsync(2).
  • Note  that  fflush() only flushes the user space buffers provided by the C library.  To ensure that the data is physically stored on disk the kernel buffers must be flushed too, e.g. with sync(2) or fsync(2).
  • fsync  copies  all  in-core parts of a file to disk, and waits until the device reports that all parts are on stable storage.  It also updates metadata stat information. It does not necessarily ensure that the entry in the directory containing  the  file  has also reached disk.  For that an explicit fsync on the file descriptor of the directory is also needed.
  • In case the hard disk has write cache enabled, the data may not really be on permanent storage when fsync/fdatasync return.When an ext2 file system is mounted with the sync option, directory entries are also implicitly synced by fsync. On kernels before 2.4, fsync on big files can be inefficient.  An alternative might be to use the O_SYNC flag to open(2).
  • A successful return from write does not make any guarantee that data has been committed to disk.  In fact, on some  buggy  implementations(注:一种针对较大块内存分配算法),  it  does  not  even guarantee that space has successfully been reserved for the data(注:可能会被LRU掉).  The only way to be sure is to call fsync(2) after you are done writing all your data.
  • Opening a file with O_APPEND set causes each write on the file to be appended to the end. (注:对于单次写,有原子性保证)

 PHP对C函数调用

通过对PHP的fwrite源码和gdb跟入,确定针对普通本地文件的调用链如上图所示,即并未使用标准Lib库的IO函数,而是直接调用了系统函数open、write,每次调用都需要经历用户态与内核态的切换,有一定的资源消耗,而且也没用使用到C lib库的cache层。

同时,还需要注意的是,虽然系统write函数保证了原子性,但PHP对其进行了封装,根据stream的chunk_size拆分为多个write调用。故在buf字节数超过chunk_size的情况下,无法保证原子性。其中chunk_size在main/php_network.h中定义:#define PHP_SOCK_CHUNK_SIZE 8192。

再看下file_put_contents如下图,多次调用系统函数进行open、可选的flock、write、fflush和close操作。其中可能破坏原子性的地方有两处:

  • 若传入一维数组,会针对每一行调用php_stream_write方法
  • 每次php_stream_write方法,若待写入数据超过chunk_size,会分多次调用write()

另外,常用的error_log方法,在目标文件是普通本地文件时,也只是对php_stream_open_wrapper、php_stream_write、php_stream_close的封装。

写日志实现方式

基本思路见 封装php的Log类,考虑3种方式:

  • 每次直接调用file_put_contents
  • 每次直接调用fwrite
  • 每次先写入应用层缓存,触发条件或request结束时批量file_put_contents

完整实现代码已上传至:https://github.com/flykobe/php-log

性能测试

小块日志写入

3种调用方式,分别调用10w次,每行日志1024B,总体写入1GB数据。在mac本上,php 5.3.24,多次运行,平均测试结果:

_fwrite 2647.6ms

_write_with_buf 2659.9ms

_file_put_contents 10322.6ms

可以看到 耗时:_file_put_contents > _write_with_buf > _fwrite。

大块日志写入

3种调用方式,分别调用10w次,每行日志8192B,总体写入8GB数据。在mac本上,php 5.3.24,多次运行,平均测试结果:

_fwrite 4300

_write_with_buf 5575

_file_put_contents 11600

_fwrite和_write_with_buf的耗时上涨比例都较大。且两者差距拉大,主要由于此时_write_with_buf会加锁,以保证原子性。_file_put_contents反而变化比例不大。

优化_write_with_buf如下:

  • self::$_arrBuf[$strType][] = &$strMsg; // 将buf数组改为引用的方式,以避免PHP内核进行数据copy。没啥效果,甚至耗时还略微上涨
  • self::$_intBufMaxNum = $arrConfig[‘writebufmax’]; // 传入writebufmax = 100, 默认值是20行。略微有效果,下降200ms左右,但多次运行波动较大。增大该值,还会获得微小提升,但对于web系统而言,不太实用。

原子性测试

同时运行两个./atomic_test.php 100000 8196  脚本,分别写入8196个a或b。使用perl进行验证,a和b不能同时出现在一行里:

perl -ne ‘$ma = m#1234567\t.*a#; $mb = m#1234567\t.*b#; if ($ma && $mb){print;}’ log/20150317/notice.log | wc -l

发现有大量错行的情况!而如果将写入字符串大小改为1024、4096等,即使加上自动添加的日志信息,也远小于chunk_size,则没有错行情况。

使用_write_with_buf,写入大小为8196B的日志,没有错行!改为4096,同样没有错行。

使用_file_put_contents,写入大小为8196B的日志,也有错行!改为4096B,同样没有错行。

即,如果日志大小(包括填充的前后数据)超过chunk_size,则仅依赖PHP底层无法保证原子性!而_write_with_buf在应用层按需向file_put_contents传入LOCK_EX参数加锁,故保证了原子性。

持久化考虑

在默认情况下,系统调用write()仅是写入到内核的page cache,等待调度被刷到磁盘上。如果在此期间出现系统或硬件故障,可能会导致数据丢失。如果在单机情况下,要保证100%的可靠,则需要手工调用fsync(),或在open()时,设置O_DIRECT flag。但这两者,无疑都是较为耗时的,因为写入耗时里真正包含了到磁盘的时间。在log类里,一般不会达到这么高的要求,所以不予开启。

参考资料

http://www.ibm.com/developerworks/cn/linux/l-cn-directio/

http://www.dbabeta.com/2009/io-performence-02_cache-and-raid.html

http://php.net/manual/zh/function.stream-set-write-buffer.php

http://blog.chinaunix.net/uid-27105712-id-3270102.html

类似PHP、Nginx之类,需要频繁申请与释放内存的软件,一般都提供了用户态的内存管理。原因是,直接调用malloc/free会导致用户态到内核态的切换,较为耗时。那么,两者的区别到底是什么呢?

内核态与用户态的定义

CPU只会运行在以下两种状态:

  1. Kernel ModeIn Kernel mode, the executing code has complete and unrestricted access to the underlying hardware. It can execute any CPU instruction and reference any memory address. Kernel mode is generally reserved for the lowest-level, most trusted functions of the operating system. Crashes in kernel mode are catastrophic; they will halt the entire PC.
  2. User ModeIn User mode, the executing code has no ability to directly access hardware or reference memory. Code running in user mode must delegate to system APIs to access hardware or memory. Due to the protection afforded by this sort of isolation, crashes in user mode are always recoverable. Most of the code running on your computer will execute in user mode.
不同硬件的实现方式可能也不同,x86是通过0-3的4层protection rings硬件来划分的。据称Linux仅使用ring 0作为内核态,ring 3作为用户态,未涉及ring 1-2;而windows中部分drivers会使用ring 1-2。

何时会发生切换

Typically, there are 2 points of switching:

  1. When calling a System Call: after calling a System Call, the task voluntary calls pieces of code living in Kernel Mode
  2. When an IRQ (or exception) comes: after the IRQ an IRQ handler (or exception handler) is called, then control returns back to the task that was interrupted like nothing was happened.

IRQ全称为Interrupt Request,即是“中断请求”的意思,IRQ的作用就是在我们所用的电脑中,执行硬件中断请求的动作。

一般而言,系统调用是用户主动发起的,例如调用fork函数,会间接调用系统函数sys_fork,从而陷入内核态。而IRQ的发生也有用户“主动”和“被动”两种形式:例如用户调用malloc申请内存,可能会导致缺页异常,引发IRQ陷入内核态;或者我们需要读取硬盘中的一段数据时,当数据读取完毕,硬盘就通过IRQ来通知系统,相应的数据已经写到指定的内存中了。

切换成本

从用户态到内核态,本质上都是响应中断。因为系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本上也是一致的。其切换过程如下:

  1. 从当前进程的描述符中提取其内核栈的ss0及esp0信息。
  2. 使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
  3. 将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。

内核态到用户态需要将保存的进程信息予以恢复。

从上面的步骤可以看到,mode switch涉及大量数据的复制,还需要硬件配合,故耗时较大。而在不发生mode switch时,cpu只需顺序执行指令即可。所以应该尽量减少mode switch!

参考资料

http://blog.codinghorror.com/understanding-user-and-kernel-mode/

http://www.linfo.org/kernel_mode.html

http://www.tldp.org/HOWTO/KernelAnalysis-HOWTO-3.html

http://jakielong.iteye.com/blog/771663

http://os.ibds.kit.edu/downloads/publ_1995_liedtke_ukernel-construction.pdf

http://os.inf.tu-dresden.de/pubs/sosp97/

经常我们谈到Nginx比Apache快,一些topic里也就范范的解释为由于Nginx使用了epoll,而apache使用select。我也一直没有深究,直到最近讨论到这个问题,才想起真的问一句,所有的场景,我们都应该选择epoll吗?如果真是这样,为什么select没有被淘汰掉呢?

找到国外一位名叫George的人写的文章,以下根据自己的理解对其进行翻译和解释。

原文链接:http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/

select / poll / epoll: practical difference for system architects

select/poll/epoll:在系统架构实践中的区别

When designing a high performance networking application with non-blocking socket I/O, the architect needs to decide which polling method to use to monitor the events generated by those sockets. There are several such methods, and the use cases for each of them are different. Choosing the correct method may be critical to satisfy the application needs.

当我们在做非网络IO阻塞型的高性能网络应用架构时,需要决定应该使用何种polling方法,来监控sockets产生的事件。已经存在不少polling方法,且它们的适用场景也各有差别。而选择合适的polling方法,是满足高性能要求的关键!

This article highlights the difference among the polling methods and provides suggestions what to use.

这篇文章关注了几种polling方法的区别,并提供一些使用建议。

 

Contents

  • 1 Polling with select()
  • 2 Polling with poll()
  • 3 Polling with epoll()
  • 4 Polling with libevent

Polling with select()

 

Old, trusted workforce from the times the sockets were still called Berkeley sockets. It didn’t make it into the first specification though since there were no concept of non-blocking I/O at that moment, but it did make it around eighties, and nothing changed since that in its interface.

Select是古老的polling方法,从sockets还被称为伯克利sockets时代就存在了。由于那时还没有非阻塞型IO一说,它没能成为规范。但select从其诞生至今,接口一直没有发生过改变。

To use select, the developer needs to initialize and fill up several fd_set structures with the descriptors and the events to monitor, and then call select(). A typical workflow looks like that:

要使用select,开发者需要初始化一些fd_set结构体,包括其中的fd(描述符),以及该fd_set对应的监听事件(读、写、异常),然后调用select()。常见示例代码如下:

fd_set fd_in, fd_out;
structtimeval tv;
// Reset the sets
FD_ZERO( &fd_in );
FD_ZERO( &fd_out );
// Monitor sock1 for input events
FD_SET( sock1, &fd_in );
// Monitor sock2 for output events
FD_SET( sock1, &fd_out );
// Find out which socket has the largest numeric value as select requires it
intlargest_sock = sock1 > sock2 ? sock1 : sock2;
// Wait up to 10 seconds
tv.tv_sec = 10;
tv.tv_usec = 0;
// Call the select
intret = select( largest_sock, &fd_in, &fd_out, NULL, &tv );
// Check if select actually succeed
if( ret == -1 )
    // report error and abort
elseif( ret == 0 )
    // timeout; no event detected
else
{
    if( FD_ISSET( sock1, &fd_in ) )
        // input event on sock1
    if( FD_ISSET( sock2, &fd_out ) )
        // output event on sock2
}

When the select interface was designed and developed, nobody probably expected there would be multi-threaded applications serving many thousands connections. Hence select carries quite a few design flaws which make it undesirable as a polling mechanism in the modern networking application. The major disadvantages include:

在select出现的时候,还没有料到后来会由支持成百上千请求的多线程应用出现,所以select存在一些设计缺陷,使其不适用于现代的网络应用。突出的弊端有:

  • select modifies the passed fd_sets so none of them can be reused. Even if you don’t need to change anything – such as if one of descriptors received data and needs to receive more data – a whole set has to be either recreated again (argh!) or restored from a backup copy via FD_COPY. And this has to be done each time theselect is called.
  • select()方法会修改传入的fd_sets,所以这些变量不可以被复用。即使fd_sets的内容没有改变,例如本次被触发的fd还需要接收更多的数据所以无需删除它,整个儿fd_sets还是需要被重新传递给select()方法,或者重新生成fd_sets,或者从一个副本里复制过来。而且,每次select被调用之后,都需要重新做这件事情。
  • To find out which descriptors raised the events you have to manually iterate through all the descriptors in the set and call FD_ISSET on each one of them. When you have 2,000 of those descriptors and only one of them is active – and, likely, the last one – you’re wasting CPU cycles each time you wait.
  • 当有事件发生select()方法返回后,通过其返回值,开发者只能知道有没有ready的事件,还需要依次遍历所有的fds调用FD_ISSET,才可以知道事件发生在哪个fd上。在最坏的情况下,如果有2000个fds,且最后一个ready了,就非常浪费CPU了。
  • Did I just mention 2,000 descriptors? Well, select cannot support that much. At least on Linux. The maximum number of the supported descriptors is defined by the FD_SETSIZE constant, which Linux happily defines as 1024. And while some operating systems allow you to hack this restriction by redefining the FD_SETSIZEbefore including the sys/select.h, this is not portable. Indeed, Linux would just ignore this hack and the limit will stay the same.
  • 实际上select是无法支持2000个fds的,至少在linux上是这样。最大可支持的fds数目由Linux常量FD_SETSIZE指定,是1024。虽然一些系统允许通过hack的方式,在include sys/select.h头文件之前,重新定义FD_SETSIZE,但Linux会忽略并维持1024的限制。
  • You cannot modify the descriptor set from a different thread while waiting. Suppose a thread is executing the code above. Now suppose you have a housekeeping thread which decided that sock1 has been waiting too long for the input data, and it is time to cut the cord. Since this socket could be reused to serve anotherpaying working client, the housekeeping thread wants to close the socket. However the socket is in the fd_setwhich select is waiting for.
    Now what happens when this socket is closed? man select has the answer, and you won’t like it. The answer is, “If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”.
  • 不支持跨线程修改被监听的fd_sets。假设有一个线程在执行上面那段代码,这时有另外一个清理进程发现sock1已经等待输入数据过久需要被回收。由于应用逻辑决定了这个socket没有办法被其他client复用,所以清理进程希望关闭它。但是请千万别这样做!通过man select页面可以找到答案:被select()方法监听的fd如果被其他线程关闭了,那么select()的行为是未定义的。
  • Same problem arises if another thread suddenly decides to send something via sock1. It is not possible to start monitoring the socket for the output event until select returns.
  • 如果另外一个线程决定要通过sock1发送数据也同样存在问题。在select()方法返回前,都没有办法启动对sock1写事件的监听(之前监听的是读事件)。(两个进程可以select监听同样的fd、相同or不同的读写事件吗?如果允许,有哪些限制呢?)
  • The choice of the events to wait for is limited; for example, to detect whether the remote socket is closed you have to a) monitor it for input and b) actually attempt to read the data from socket to detect the closure (readwill return 0). Which is fine if you want to read from this socket, but what if you’re sending a file and do not care about any input right now?
  • 监听的事件有限制。例如,为了监听远端socket是否被关闭,开发者需要首先监听该fd的读事件,然后尝试read接收数据以发现关闭(read会返回eof)。如果本身就需要从这个fd里读取数据,那还ok。但如果应用本来只是关注该端口上的写事件,压根不想read任何数据呢?
  • select puts extra burden on you when filling up the descriptor list to calculate the largest descriptor number and provide it as a function parameter.
  • 一个额外的小负担,需要找到监听读/写/异常fds里的最大值,从而给select()方法提供第一个参数(highest-numbered descriptor)

Of course the operating system developers recognized those drawbacks and addressed most of them when designing the poll method. Therefore you may ask, is there is any reason to use select at all? Why don’t just store it in the shelf of the Computer Science Museum? Then you may be pleased to know that yes, there are two reasons, which may be either very important to you or not important at all.

当然操作系统的开发者也发现了这些弊端并通过poll方法解决了大部分。那select是否还有用武之地呢?为什么不废弃select方法?这儿至少有两个原因会导致选用select。

The first reason is portability. select has been around for ages, and you can be sure that every single platform around which has network support and nonblocking sockets will have a working select implementation while it might not have poll at all. And unfortunately I’m not talking about the tubes and ENIAC here; poll is only available on Windows Vista and above which includes Windows XP – still used by the whooping 34% of users as of Sep 2013 despite the Microsoft pressure. Another option would be to still use poll on those platforms and emulate it with select on those which do not have it; it is up to you whether you consider it reasonable investment.

第一个原因是可移植性:select已经存在太久了,所有的支持网络和非阻塞sockets的平台都由select的实现,而它们可能没有poll方法。这样的平台不是指像tubes和ENIAC这样的古董,例如只有windows vista和更新的版本才支持poll,xp也没有poll。或者另一个选择是自己适配跨平台接口,针对不存在poll的OS,利用select封装出poll接口。

The second reason is more exotic, and is related to the fact that select can – theoretically – handle the timeouts withing the one nanosecond precision, while both poll and epoll can only handle the one millisecond precision. This is not likely to be a concern on a desktop or server system, which clocks doesn’t even run with such precision, but it may be necessary on a realtime embedded platform while interacting with some hardware components. Such as lowering control rods to shut down a nuclear reactor – in this case, please, use select to make sure we’re all stay safe!

第二个原因比较特殊,select的timeout时间理论上可以精确到纳秒,而poll和epoll只能到毫秒。在桌面系统和小型机上这可能不成问题,它们系统时钟也没用那么高的精确度。但嵌入式实时操作系统可能有这样的需求,例如核反应堆的控制系统。

The case above would probably be the only case where you would have to use select and could not use anything else. However if you are writing an application which would never have to handle more than a handful of sockets (like, 200), the difference between using poll and select would not be based on performance, but more on personal preference or other factors.

上面两个场景是必须使用select的原因。但当然,如果你的应用压根不会处理超过200个sockets,那select和poll的性能差别就不会那么大,完全可以出于个人喜好或其他原因进行选择了。

Polling with poll()

poll is a newer polling method which probably was created immediately after someone actually tried to write the high performance networking server. It is much better designed and doesn’t suffer from most of the problems which select has. In the vast majority of cases you would be choosing between poll and epoll/libevent.

出于高性能网络服务器的需求,poll()作为一种较新的polling方法出现了。它设计更优秀,解决了select的大部分问题。在很多场景下应该选择poll或者epoll/libevent。

To use poll, the developer needs to initialize the members of struct pollfd structure with the descriptors and events to monitor, and call the poll(). A typical workflow looks like that:

要用poll,开发者需要初始化pollfd结构体,包括fds和每个fd对应的监听事件,然后调用poll()方法。常见示例代码如下:

// The structure for two events
struct pollfd fds[2];
// Monitor sock1 for input
fds[0].fd = sock1;
fds[0].events = POLLIN;
// Monitor sock2 for output
fds[1].fd = sock2;
fds[1].events = POLLOUT;
// Wait 10 seconds
int ret = poll( &fds, 2, 10000 );

// Check if poll actually succeed
if ( ret == -1 )
    // report error and abort
else if ( ret == 0 )
    // timeout; no event detected
else
{
    // If we detect the event, zero it out so we can reuse the structure
    if ( fds[0].revents & POLLIN )
        fds[0].revents = 0;
        // input event on sock1

    if ( fds[1].revents & POLLOUT )
        fds[1].revents = 0;
        // output event on sock2
}

poll was mainly created to fix the pending problems select hadso it has the following advantages over it:

poll主要是为了解决select的问题,所以它具备以下优点:

  • There is no hard limit on the number of descriptors poll can monitor, so the limit of 1024 does not apply here.
  • 没有fd数量的硬性规定。
  • It does not modify the data passed in the struct pollfd data. Therefore it could be reused between the poll() calls as long as set to zero the revents member for those descriptors which generated the events. The IEEE specification states that “In each pollfd structure, poll() shall clear the revents member, except that where the application requested a report on a condition by setting one of the bits of events listed above, poll() shall set the corresponding bit in revents if the requested condition is true“. However in my experience at least one platform did not follow this recommendation, and man 2 poll on Linux does not make such guarantee either (man 3p poll does though).
  • poll()方法不会修改传入的pollfd参数,所以pollfd参数可以在多次poll调用中被重复使用,只要每次处理完把当前pollfd的revents字段重新置为0即可。
  • It allows more fine-grained control of events comparing to select. For example, it can detect remote peer shutdown without monitoring for read events.
  • 相对select而言,更可控。例如,可以检测到对端关闭事件,而无需监听读事件。

There are a few disadvantages as well, which were mentioned above at the end of the select section. Notably,poll is not present on Microsoft Windows older than Vista; on Vista and above it is called WSAPoll although the prototype is the same, and it could be defined as simply as:

如前所述,poll也有一些弊端。比vista更老的windows系统上没有poll方法,vista和更新的系统里它被实现为WASPoll,接口是一致的,可以被适配如下:

#if defined (WIN32)
static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); }
#endif

And, as mentioned above, poll timeout has the 1ms precision, which again is very unlikely to be a concern in most scenarios. Nevertheless poll still has a few issues which need to be kept in mind:

另外,如前所述,poll的timeout只能精确到1ms的精度,不过在大多数场景下都不是个事儿。但poll仍然有一些其他需要注意的问题:

  • Like select, it is still not possible to find out which descriptors have the events triggered without iterating through the whole list and checking the revents. Worse, the same happens in the kernel space as well, as the kernel has to iterate through the list of file descriptors to find out which sockets are monitored, and iterate through the whole list again to set up the events.
  • 和select()一样,开发者也需要遍历整个pollfds数组,才可以知道哪些fds被什么事件触发了。更糟的是,同样的事情也会发生在内核态,也就意味着操作系统也需要遍历所有fds来发现哪些sockets ready了,还需要遍历以set up监听事件。
  • Like select, it is not possible to dynamically modify the set or close the socket which is being polled (see above).
  • 和select()一样,也无法动态修改fds或关闭正在被监听的socket。

Often  those are important when implementing most client networking applications – the only exception would be a P2P client software which may handle thousands of connections. It may not be important even for some server applications. Therefore poll should be your default choice over select unless you have specific reasons mentioned above. More, poll should be your preferred method even over epoll if the following is true:

通常来说,在实现大多数客户端网络应用时,需要重点考虑poll的这些弊端,但如果是处理上千连接的p2p客户端软件就不同了。对某些服务端应用来说(poll的弊端)可能也不重要。所以除非有上面提到的特殊原因,相较于select而言,poll应该是默认的选择。甚至,如果有以下原因,也不应该选择epoll,而得使用poll:

  • You need to support more than just Linux, and do not want to use epoll wrappers such as libevent (epoll is Linux only);
  • 需要支持跨平台,而且不想使用类似libevent之类的适配方法(仅Linux支持epoll)
  • Your application needs to monitor less than 1000 sockets at a time (you are not likely to see any benefits from using epoll);
  • 仅需监听少于1000的网络连接(这时epoll没有什么优势)
  • Your application needs to monitor more than 1000 sockets at a time, but the connections are very short-lived (this is a close case, but most likely in this scenario you are not likely to see any benefits from using epoll because the speedup in event waiting would be wasted on adding those new descriptors into the set – see below)
  • 虽然需要监听多于1000的网络连接,但这些连接的生命周期都很短(虽然不是绝对,但该场景的大多数情况下,epoll没啥效果。因为事件等待节省的时间,都被浪费于将新建立的连接加入监听sets上了-下面有更详细的解释)
  • Your application is not designed the way that it changes the events while another thread is waiting for them (i.e. you’re not porting an app using kqueue or IO Completion Ports).
  • 非事件通知型的应用,即一个应用改变事件状态,另一个线程等待该事件的发生(例如你的应用没有使用kqueue 或 IO Completion Ports)

Polling with epoll()

epoll is the latest, greatest, newest polling method in Linux (and only Linux). Well, it was actually added to kernel in 2002, so it is not so new. It differs both from poll and select in such a way that it keeps the information about the currently monitored descriptors and associated events inside the kernel, and exports the API to add/remove/modify those.

epoll是Linux下最新最好的polling方法,也仅有Linux支持epoll。不过它其实在2002年已经被加入内核了,所以也不年轻了。它与select和epoll最大的区别是,它将当前被监听fds和事件相关的信息维持在内核空间里,并暴露增删改的API。

To use epoll, much more preparation is needed. A developer needs to:

要使用epoll,需要多做些准备工作。开发者需要:

  • Create the epoll descriptor by calling epoll_create;
  • 调用epoll_create生成epoll描述符
  • Initialize the struct epoll structure with the wanted events and the context data pointer. Context could be anything, epoll passes this value directly to the returned events structure. We store there a pointer to our Connection class.
  • 初始化epoll结构体,设置待监听事件和上下文数据指针。上下文数据可以是任意的,当事件触发时,epoll直接将它作为返回值的一部分。在下面的例子里,我们使用Connection对象的指针作为上下文。
  • Call epoll_ctl( … EPOLL_CTL_ADD ) to add the descriptor into the monitoring set
  • 调用epoll_ctl(…EPOLL_CTL_ADD )将描述符添加到监控set里(传入的参数包括 前面create的 epoll fd,待监听的端口fd,epoll结构体)
  • Call epoll_wait() to wait for 20 events for which we reserve the storage space. Unlike previous methods, this call receives an empty structure, and fills it up only with the triggered events. For example, if there are 200 descriptors and 5 of them have events pending, the epoll_wait will return 5, and only the first five members of the pevents structure will be initialized. If 50 descriptors have events pending, the first 20 would be copied and 30 would be left in queue, they won’t get lost.
  • 调用epoll_wait方法等待最多20个事件的返回。该方法接受一个空结构体pevents作为参数,并仅在有事件被触发时填充其值。例如,如果一共有200个被监听fds,只有5个被事件触发,那么epoll_wait返回5,且只有pevents的前5个槽位被填充了。而如果有50个事件,那么前20个事件被copy到pevents里,后30个被维持在epoll的queue里,不会丢失。
  • Iterate through the returned items. This will be a short iteration since the only events returned are those which are triggered.
  • 遍历返回的数据。这个遍历相对较短,因为仅包含了被触发的事件(不像之前select和poll,需要遍历所有的被监听fds)。

A typical workflow looks like that:

常见代码示例如下:

// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
// The function argument is ignored (it was not before, but now it is), so put your favorite number here
int pollingfd = epoll_create( 0xCAFE ); 

if ( pollingfd < 0 )
 // report error

// Initialize the epoll structure in case more members are added in future
struct epoll_event ev = { 0 };

// Associate the connection class instance with the event. You can associate anything
// you want, epoll does not use this information. We store a connection class pointer, pConnection1
ev.data.ptr = pConnection1;

// Monitor for input, and do not automatically rearm the descriptor after the event
ev.events = EPOLLIN | EPOLLONESHOT;

// Add the descriptor into the monitoring list. We can do it even if another thread is // waiting in epoll_wait - the descriptor will be properly added
if ( epoll_ctl( epollfd pollingfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )
    // report error

// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)
struct epoll_event pevents[ 20 ];

// Wait for 10 seconds
int ready = epoll_wait( pollingfd, pevents, 20, 10000 );

// Check if epoll actually succeed
if ( ret == -1 )
    // report error and abort
else if ( ret == 0 )
    // timeout; no event detected
else
{
    // Check if any events detected
    for ( int i = 0; i < ret; i++ )
    {
        if ( pevents[i].events & EPOLLIN )
        {
            // Get back our connection pointer
            Connection * c = (Connection*) pevents[i].data.ptr;
            c->handleReadEvent();
         }
    }
}

附 EPOLLONESHOT 的解释,摘自 man epoll_ctl:

EPOLLONESHOT

Sets the One-Shot behaviour for the associated file descriptor.

It  means  that after an event is pulled out with epoll_wait(2)

the associated file descriptor is internally  disabled  and  no

other  events will be reported by the epoll interface. The user

must call epoll_ctl(2) with EPOLL_CTL_MOD to re-enable the file

descriptor with a new event mask.

Just looking at the implementation alone should give you the hint of what are the disadvantages of epoll, which we will mention firs. It is more complex to use, and requires you to write more code, and it requires more library calls comparing to other polling methods.

只是看看上面的示例,就会直观的感觉到epoll的第一个弊端,相对其他polling方法而言,它太复杂了,需要写更多的代码,更多的函数调用。

However epoll has some significant advantages over select/poll both in terms of performance and functionality:

不过epoll确实在性能和功能上有一些优势:

  • epoll returns only the list of descriptors which triggered the events. No need to iterate through 10,000 descriptors to find that one which triggered the event!
  • epoll仅返回被触发事件的列表。无需遍历所有的监听描述符,只为了定位一个被触发的事件。
  • You can attach meaningful context to the monitored event instead of socket file descriptors. In our example we attached the class pointers which could be called directly, saving you another lookup.
  • 可以在被触发的事件粒度,附加上下文数据(即针对一个fd,使用不同的事件定义不同的epoll结构体,并add到监听set)。在上面的例子里,使用可以被直接调用的对象指针,进一步节省了找到合适回调方法的消耗。
  • You can add sockets or remove them from monitoring anytime, even if another thread is in the epoll_waitfunction. You can even modify the descriptor events. Everything will work properly, and this behavior is supported and documented. This gives you much more flexibility in implementation.
  • 可以随时增删被监听的端口,即使有其他线程被阻塞在epoll_wait()。甚至可以随时修改监听端口对应的事件。这些都是被支持的,且文档化的。这样在实现层面就增加了灵活性。
  • Since the kernel knows all the monitoring descriptors, it can register the events happening on them even when nobody is calling epoll_wait. This allows implementing interesting features such as edge triggering, which will be described in a separate article.
  • 由于内核有所有被监听端口的信息,所以即使没有任何用户调用epoll_wait,内核也可以注册已触发的事件。这些可以用于实现有趣的功能,例如edge triggering(边沿触发),会在单独的文章里介绍。
  • It is possible to have the multiple threads waiting on the same epoll queue with epoll_wait(), something you cannot do with select/poll. In fact it is not only possible with epoll, but the recommended method in the edge triggering mode.
  • 允许多个线程使用epoll_wait等待相同的epoll queue(拥有相同的epollfd),这些是select和poll无法做到的。事实上,当使用edge triggering这是建议的方式。

However you need to keep in mind that epoll is not a “better poll”, and it also has disadvantages when comparing to poll:

但是需要谨记的是,epoll不是万能的,相对poll而言,它也有其不足:

  • Changing the event flags (i.e. from READ to WRITE) requires the epoll_ctl syscall, while when using poll this is a simple bitmask operation done entirely in userspace. Switching 5,000 sockets from reading to writing withepoll would require 5,000 syscalls and hence context switches (as of 2014 calls to epoll_ctl still  could not be batched, and each descriptor must be changed separately), while in poll it would require a single loop over thepollfd structure.
  • 改变监听事件(例如从读改为写)需要调用epoll_ctl系统方法,而poll时这仅是一个用户空间的bit位设置。如果需要将5000个端口从监听读改为监听写,就需要5000次独立的系统调用(截止2014年,epoll_ctl还不支持批量,每个fd需要被独立设置),而poll里仅是针对pollfd结构体的一次遍历。
  • Each accept()ed socket needs to be added to the set, and same as above, with epoll it has to be done by calling epoll_ctl – which means there are two required syscalls per new connection socket instead of one for poll. If your server has many connections which do not send much information, epoll will likely take longer than poll to serve them.
  • 每一个accept()ed 端口都得被添加到监听set里,需要调用epoll_ctl系统函数 — 即每个连接需要两次系统调用(一次listen()ed端口,一次accept()ed端口),而poll只需要一次()。所以,如果你的服务器维持了很多连接,且都只交互少量数据,那epoll的耗时反而会大于poll。
  • epoll is exclusively Linux domain, and while other platforms have similar mechanisms, they are not exactly the same – edge triggering, for example, is pretty unique (FreeBSD’s kqueue supports it too though).
  • 仅Linux支持epoll,虽然其他操作系统也有类似的机制,但都略有不同。例如epoll的edge triggering就相当独特(虽然FreeBSD的kqueue也支持)。
  • High performance processing logic is more complex and hence more difficult to debug, especially for edge triggering which is prone to deadlocks if you miss extra read/write.
  • 高性能的逻辑实现更复杂,也就更难调试,尤其是在edge triggering时遗漏额外读写而导致的死锁。

Therefore you should only use epoll if all following is true:

所以,仅当以下条件都成立时,你才必须选择epoll:

  • Your application runs a thread poll which handles many network connections by a handful of threads. You would lose many benefits in a single-threaded application, and most likely it won’t outperform poll.
  • 应用中包含一个处理一些网络连接的线程池。不过你必定会损失单线程的一堆好处,而且性能也不一定比得上poll。
  • You expect to have a reasonably large number of sockets to monitor (at least 1,000); with a smaller number epoll is not likely to have any performance benefits over poll and may actually have worse performance;
  • 应用需要管理相当多的连接(至少1000)。少量连接情况下,epoll的性能可能还会弱于poll。
  • Your connections are relatively long-lived; as stated above epoll will be slower than poll in a situation when a new connection sends a few bytes of data and immediately disconnects because of extra system call required to add the descriptor into epoll set;
  • 网络连接的生命周期较长。如前所述,如果一个新的连接只是发送很少的数据,然后就关闭了,那epoll会由于额外系统调用(而导致的内核态切换)变得较慢。
  • Your app depends on other Linux-specific features (so in case portability question would suddenly pop up, epoll wouldn’t be the only roadblock), or you can provide wrappers for other supported systems. In the last case you should strongly consider libevent.
  • 应用还依赖其他Linux相关特性(所以就不需要考虑移植性了),或者开发者愿意针对其他平台提供适配方法。后者建议使用libevent。

If all the items above aren’t true, you should be better served by using poll instead.

如果以上都不成立的话,那还是用poll吧。

Polling with libevent

libebent is a library which basically wraps the polling methods listed in this article (and some others) in an uniform API.Its main advantage is that it allows you to write the code once and compile and run it on many operating systems without the need to change the code. It is important to understand that libevent it is just a wrapper built on top of the existing polling methods, and therefore it inherits the issues those polling methods have. It will not make select supporting more than 1024 sockets on Linux or allow epoll to modify the polling events without a syscall/context switch. Therefore it is still important to understand each method’s pros and cons.

libevent针对以上提到的polling方法和其他一些特性,提供了统一的API。它最大的好处是,允许写一份代码,然后编译执行于多个操作系统。但需要谨记的是,libevent也只是基于现有polling方法的封装,并没有改变任何问题。例如它不会使select支持多于1024个监听端口,也不会使epoll免于修改监听事件导致的内核态上下文切换。所以,还是需要理解每个polling方法的优劣。

Having to provide access to the functionality from the dramatically different methods, libevent has a rather complex API which is much more difficult to use than poll or even epoll. It is however easier to use libevent than to write two separate backends if you need to support FreeBSD (epoll and kqueue). Hence it is a viable alternative which should be considered if:

为了配置底层各种千差万别的polling方法,libevent的API更为复杂,但这也好过为了不同操作系统开发不同的代码。所以,如果以下条件满足,libevent不失为一个不错的选择:

  • Your application requirements indicate that you must use epoll, and using just poll would not be enough (ifpoll would satisfy your needs, it is extremely unlikely libevent would offer you any benefits)
  • 应用强烈需要用epoll(如果poll就够用了,那无需使用libevent)。
  • You need to support other OS than Linux, or may expect such need to arise in future. Again, this depends on other features of your application – if it is tied up to many other Linux-specific things you’re not going to achieve anything by using libevent instead of epoll
  • 现在或未来需要支持Linux和其他更多操作系统。当然这也依赖于应用所需的其他功能,如果很多linux特殊功能,那libevent也无法全部解决。

 

附man下各命令对应的领域:

1  用户命令,  可由任何人启动的。
2  系统调用,  即由内核提供的函数。
3  例程,   即库函数。
4  设备,   即/dev目录下的特殊文件。
5  文件格式描述,  例如/etc/passwd。
6  游戏,   不用解释啦!
7  杂项,   例如宏命令包、惯例等。
8  系统管理员工具, 只能由root启动。
9  其他(Linux特定的), 用来存放内核例行程序的文档。
n  新文档,  可能要移到更适合的领域。
o  老文档,  可能会在一段期限内保留。
l  本地文档,  与本特定系统有关的。

今年做的重要项目之一,就是对一个核心Web系统重构,使之达到了99.999% 的高可用性。在此过程中,积累了一些系统架构、自动故障发现与解决、代码健壮性等方面的经验,予以记录。

业务背景介绍

该web系统是一个大型互联网系统商业运营侧的核心web系统,PHP语言实现,之前的可用性方面存在一定问题,在历史上也出过不少事故。由于是商业系统,其PV仅是中等规模,但复杂度相对较高,体现在其所涉及的网络交互、DB调用、Cache交互较多。

需要解决的问题

  1. 所依赖的非核心上游服务不可用时,及时发现并自动降级
  2. 所依赖的上游服务部分节点不可用时,及时发现并自动摘除故障节点
  3. 通过Lib库,封装网络交互中的重试、连接和读写超时、异常日志和以上各种功能,使之对业务层透明

整体架构设计

该系统采用集中式+分布式相结合的异常发现和处理方案。由浅蓝色业务层和深蓝色基础服务设施一起,识别并处理异常攻击、网络、系统和上游服务问题。为了最大程度的解耦,它们交互的方式,就是规范化的文本日志和INI配置。

之所以采用集中+分布的混合方式,是由于所处系统环境的复杂度导致的:

  1. 对于异常流量,由于目前集群规模较大,单点不可能阈值配置过大(规则),也无法收集到全局信息(策略),只有集中式才可以综合判断;
  2. 对于上游服务的整体故障,同样的道理,单点也不可能阈值配置过小,否则很容易产生抖动,而集中式可以全局收集信息,全局联动;
  3. 但集中式的服务降级,无法很好解决以下两种问题,所以需要通过每个业务节点通过节点健康检查来处理。
    1. 不可降级的核心服务交互
    2. 点对点的非核心服务故障,例如上游服务部分机房故障,或某台服务器故障

全局故障监控与降级

故障采集:为了降低侵入性,采取了由业务模块打印warn、error log,各业务节点上的守护进程阻塞read日志文件,并发往多个(目前配置为2个)同构监控与调度服务的方式。之所以采用多写的方式,是由于一期实现时,复用了日志ETL流,走的是公司的消息队列和流式计算平台,而它们都是强一致性的,当网络出现抖动或拥塞时,故障日志甚至会延迟数小时之久!所以,最终我们决定采用最简单的方式,对于这种对实时性要求极高的消息交互,采用CS直接透传的方式。而交叉多写可以保证只要不是所有机房间链路都发生故障,就可以在秒级别完成消息传递。多个监控与调度服务完全同构,都会接收异常日志并予以计算和处理,由于重复降级不会产生副作用,所以没有做maser、slave角色的划分。

故障判别:出于扩展性和复用角度的考虑,故障判别的规则是基于Protobuf格式配置的,基本原理就是配置:某一种类型的异常日志在一段时间内到达指定阈值后,需要将什么配置项改为什么值。由于异常日志的数量一般不会太大,所以每个监控与调度服务都是独立运行的,不存在数据同步的问题,时间窗口、频率等信息都在内存中直接计算。当需要横向扩展时,可以与故障采集配合,根据业务模块、异常日志类型进行纵向划分,映射到不同的监控节点上。仅有一点需要注意的是,为了防止网络等问题导致消息延迟到达,在计算频率时,会过滤掉超时的消息。

故障处理:同样为了降低业务侵入性和耦合度,故障处理是通过修改业务模块的配置文件实现的。通过在PHP业务节点上部署我们的zk_agent,监听zk文件变化并修改业务模块的ini配置对应项,和复用C++进程的基于zk的热加载功能,监控与调度模块只需与zookeeper集群通信,修改指定配置项,而无需知晓业务模块的具体信息。上面提到的多实例方式,也可以保证只要有一个监控与调度模块与zk集群通信成功,就可以成功完成降级指令的发布。这里需要注意的是,由于机器规则总有其不完善处,引入了保险栓的方式,保证在开启保险栓的情况下,人工干预的优先级最高。

当然,这套流程也有其不完善处,例如为了避免网络抖动,我们是通过设置一个较大的阈值来实现的,而没有做更为精细化的处理。同时,其配置恢复是手工的,因为监控与调度模块为了降低耦合度和复杂度,没有主动去探测故障的恢复情况。

单点故障监控与摘除

作为自动降级的补充,该功能着眼于发现点对点的问题,并予以处理。其原理也是:该业务服务器上,在一段时间内,对某上游节点的调用若失败超过阈值,则屏蔽该上游节点。这里直接通过真实请求的失败进行计数,没有开启独立进程,也是为了降低复杂度,提升易用性。

采用策略模式,实现了APC和File两种存储介质的封装,并支持扩展。由于APC也是跨进程的,所以可以在单机所有PHP进程间共享失败次数、故障节点信息。经过测试,APC的性能优于File,所以一般推荐使用APC模式。但为了兼容那些未安装APC module的业务模块,所以也保留了File的方式。

由于大中型系统的点对点故障是频繁发生的,所以这里采用屏蔽超时自动恢复的方式,虽然可能会造成锯齿状的耗时波动,但无需人工干预,是综合而言最优的方式。

由于请求间可能有相互依赖关系,单点是无法handle降级的,所以一旦检查到某服务的上游节点全部不可用,则重新置位,设为全部可用。

PHP基础lib库

以上及其他未提及的稳定性保障,对于业务层而言略显复杂,且由每个业务开发者来保证这些也是不可靠的,所以必须有一套对业务透明的php Lib库,予以封装。我们实现了这样一套Lib,首先细致处理了Mysql、Memcache、Curl、Socket、Redis等网络交互时的重试、连接和读写超时。其次,以上网络组件又是基于代码层的负载均衡、节点健康检查、服务降级、配置组件,从而对上屏蔽稳定性细节。同时,该Lib充分考虑易用性,例如通过对多种Cache的封装,仅暴露一个简单的Call接口和规范化配置,就可以使业务层完成旁路cache的使用。

总结

通过以上方式,该系统在上线3个月内,自动处理了2000多次故障,实现了按请求成功率计算,99.999%以上的可用性。感触最深的是,很多做业务开发的PHPer,简单的认为稳定大多是运维同学的责任,但想要达到高稳定性,工作其实从架构设计就开始了,更深深渗透在每一行代码里!

printf(“(char)48 = %c\n”, (char)48);
printf(“(int)’0′ = %d\n”, (int)’0′);
printf(“convert ‘0’ to 0: %d\n”, b_atoi(‘0′));
printf(“convert 0 to ‘0’: %c\n”, b_itoa(0));

在不严谨的情况下,可以如上写。

有的时候,我们会希望能够把1、 2 、 3这样的数字作为字符直接存储到字符串中。如果直接(char)2的话,你得到的不会是字符’2’,它更像一些乱码。

由于字符’0’对应int型的48,所以可以利用这个特性来进行int和字符字面上的转换。

zz from: http://www.ibm.com/developerworks/cn/aix/library/au-unixtools.html

计算机编程的最新技术将一种特殊的人性与一组特殊的工具结合在一起,用以生产出对其他人非常有帮助的一种神奇的产品,即软件。计算机程序员是一群注重细节的人,他们可以处理计算机中各种各样的困难。计算机的要求非常苛刻,并且不能容忍其中存在任何的偏差。毫无疑问,无论您的个性如何以及在工作中使用了何种辅助工具,计算机程序的编写都是非常困难的。

在 UNIX® 和 Linux® 中,任何事物都是文件。您可以认为,UNIX 和 Linux 编程实际上是编写处理各种文件的代码。系统由许多类型的文件组成,但目标文件具有一种特殊的设计,提供了灵活和多样的用途。

目标文件是包含带有附加地址和值的助记符号的路线图。这些符号可以用来对各种代码段和数据段进行命名,包括经过初始化的和未初始化的。它们也可以用来定位嵌入的调试信息,就像语义 Web,非常适合由程序进行阅读。

计算机编程中使用的工具包括代码编辑器,如 vi 或 Emacs,您可以使用这些工具输入和编辑希望计算机在完成所需任务时执行的指令,以及编译器和连接器,它们可以生成真正实现这些目标的机器代码。

高级的工具,称为集成调试环境 (IDE),它以统一的外观集成了不同工具的功能。IDE 使得编辑器、编译器、连接器和调试器之间的界限变得很模糊。因此,为了更深入地研究和了解系统,在使用集成的套件之前,最好先单独地使用这些工具。(注意:IDE 也通常被称为集成开发环境。)

编译器可以将您在代码编辑器中创建的文本转换为目标文件。最初,目标文件被称为代码的中间表示形式,因为它用作连接编辑器(即连接器)的输入,而连接编辑器最终完成整个任务并生成可执行的程序作为输出。

从代码到可执行代码的转换过程经过了良好的定义并实现了自动化,而目标文件是这个链中有机的连接性环节。在这个转换过程中,目标文件作为连接编辑器所使用的映象,使得它们能够解析各种符号并将不同的代码和数据段连接在一起形成统一的整体。

计算机编程领域中存在许多著名的目标文件格式。DOS 系列包括 COMOBJ 和 EXE 格式。UNIX 和 Linux 使用 a.outCOFFELF。Microsoft® Windows® 使用可移植的执行文件 (PE) 格式,而 Macintosh 使用 PEFMach-O 和其他文件格式。

最初,各种类型的计算机具有自己独特的目标文件格式,但随着 UNIX 和其他在不同硬件平台上提供可移植性的操作系统的出现,一些常用的文件格式上升为通用的标准。其中包括 a.outCOFF 和 ELF 格式。

要了解目标文件,需要一组可以读取目标文件中不同部分并以更易于读取的格式显示这些内容的工具。本文将讨论这些工具中比较重要的方面。但首先,您必须创建一个工作台,并在其中建立一个研究对象。

启动一个 xterm 会话,让我们先创建一个空白的工作台,并开始对目标文件进行研究。下面的命令创建了一个目录,可以将目标文件放到该目录中进行研究:

cd
mkdir src
cd src
mkdir hw
cd hw

然后,使用您最喜欢的代码编辑器,在 $HOME/src/hw 目录中输入清单 1 中的程序,并命名为 hw.c

#include <stdio.h>

int main(void)
{
  printf("Hello World!\n");
  return 0;
}

要使用 UNIX 工具库中提供的各种工具,可以将这个简单的“Hello World”程序作为研究的对象。您将学习构建和查看目标文件的输出,而不是使用任何快捷方法直接创建可执行文件(的确有许多这样的快捷方法)。

C 编译器的正常输出是用于您所指定的目标处理器的汇编代码。汇编代码是汇编器的输入,在缺省情况下,汇编器将生成所有目标文件的祖先,即 a.out 文件。这个名称本身表示汇编输出 (Assembler Output)。要创建 a.out 文件,可以在 xterm 窗口中输入下面的命令:

cc hw.c

注意:如果出现了任何错误或者没有创建 a.out 文件,那么您可能需要检查自己的系统或源文件 (hw.c),以找出其中的错误。还需要检查是否已将 cc 定义为运行您的 C/C++ 编译器。

最新的 C 编译器将编译和汇编步骤组合成一个步骤。您可以指定不同开关选项以查看 C 编译器的汇编输出。通过输入下面的命令,您可以看到 C 编译器的汇编输出:

cc -S hw.c

这个命令生成了一个新的文件 hw.s,其中包含您通常无法看到的汇编输入文本,因为编译器在缺省情况下将生成 a.out 文件。正如所预期的,UNIX 汇编程序可以对这种输入文件进行汇编,以生成 a.out 文件。

假定编译过程一切顺利,那么在该目录中就有了一个 a.out 文件,下面让我们来对其进行研究。有许多可用于研究目标文件的有价值的工具,下面便是其中一组:

  • nm:列出目标文件中的符号。
  • objdump:显示目标文件中的详细信息。
  • readelf:显示关于 ELF 目标文件的信息。

列表中的第一个工具是 nm,它可以列出目标文件中的符号。如果您输入 nm 命令,您将注意到在缺省情况下,它会寻找一个名为a.out 的文件。如果没有找到该文件,这个工具会给出相应的提示。然而,如果该工具找到了编译器创建的 a.out 文件,它将显示类似清单 2 的清单。

08049594 A __bss_start
080482e4 t call_gmon_start
08049594 b completed.4463
08049498 d __CTOR_END__
08049494 d __CTOR_LIST__
08049588 D __data_start
08049588 W data_start
0804842c t __do_global_ctors_aux
0804830c t __do_global_dtors_aux
0804958c D __dso_handle
080494a0 d __DTOR_END__
0804949c d __DTOR_LIST__
080494a8 d _DYNAMIC
08049594 A _edata
08049598 A _end
08048458 T _fini
08049494 a __fini_array_end
08049494 a __fini_array_start
08048478 R _fp_hw
0804833b t frame_dummy
08048490 r __FRAME_END__
08049574 d _GLOBAL_OFFSET_TABLE_
         w __gmon_start__
08048308 T __i686.get_pc_thunk.bx
08048278 T _init
08049494 a __init_array_end
08049494 a __init_array_start
0804847c R _IO_stdin_used
080494a4 d __JCR_END__
080494a4 d __JCR_LIST__
         w _Jv_RegisterClasses
080483e1 T __libc_csu_fini
08048390 T __libc_csu_init
         U __libc_start_main@@GLIBC_2.0
08048360 T main
08049590 d p.4462
         U puts@@GLIBC_2.0
080482c0 T _start

这些包含可执行代码的段称为正文段。同样地,数据段包含了不可执行的信息或数据。另一种类型的段,称为 BSS 段,它包含以符号数据开头的块。

对于 nm 命令列出的每个符号,它们的值使用十六进制来表示(缺省行为),并且在该符号前面加上了一个表示符号类型的编码字符。常见的各种编码包括:A 表示绝对 (absolute),这意味着不能将该值更改为其他的连接;B 表示 BSS 段中的符号;而 C 表示引用未初始化的数据的一般符号。

可以将目标文件中所包含的不同的部分划分为段。段可以包含可执行代码、符号名称、初始数据值和许多其他类型的数据。有关这些类型的数据的详细信息,可以阅读 UNIX 中 nm 的 man 页面,其中按照该命令输出中的字符编码分别对每种类型进行了描述。

在目标文件阶段,即使是一个简单的 Hello World 程序,其中也包含了大量的细节信息。nm 程序可用于列举符号及其类型和值,但是,要更仔细地研究目标文件中这些命名段的内容,需要使用功能更强大的工具。

其中两种功能强大的工具是 objdump 和 readelf 程序。通过输入下面的命令,您可以看到目标文件中包含可执行代码的每个段的汇编清单。对于这么一个小的程序,编译器生成了这么多的代码,真的很令人惊异!

objdump -d a.out

这个命令生成的输出如清单 3 所示。每个可执行代码段将在需要特定的事件时执行,这些事件包括库的初始化和该程序本身主入口点。

对于那些着迷于底层编程细节的程序员来说,这是一个功能非常强大的工具,可用于研究编译器和汇编器的输出。细节信息,比如这段代码中所显示的这些信息,可以揭示有关本地处理器本身运行方式的很多内容。对该处理器制造商提供的技术文档进行深入的研究,您可以收集关于一些有价值的信息,通过这些信息可以深入地了解内部的运行机制,因为功能程序提供了清晰的输出。

类似地,readelf 程序也可以清楚地列出目标文件中的内容。输入下面的命令,您将可以看到这一点:

readelf -all a.out

这个命令生成的输出如清单 4 所示。ELF Header 为该文件中所有段入口显示了详细的摘要。在列举出这些 Header 中的内容之前,您可以看到 Header 的具体数目。在研究一个较大的目标文件时,该信息可能非常有用。

正如从该输出中看到的,简单的 a.out Hello World 文件中包含了大量有价值的细节信息,包括版本信息、柱状图、各种符号类型的表格,等等。通过使用本文中介绍的这几种工具分析目标文件,您可以慢慢地对可执行程序进行研究。

除了所有这些段之外,编译器可以将调试信息放入到目标文件中,并且还可以显示这些信息。输入下面的命令,仔细分析编译器的输出(假设您扮演了调试程序的角色):

readelf --debug-dump a.out | less

这个命令生成的输出如清单 5 所示。调试工具,如 GDB,可以读取这些调试信息,并且当程序在调试器中运行的同时,您可以使用该工具显示更具描述性的标记,而不是对代码进行反汇编时的原始地址值。

在 UNIX 中,可执行文件 目标文件,并且您可以像对 a.out 文件那样对它们进行分析。可以进行一次有益的练习,更改到 /bin 或 /local/bin 目录,然后针对一些您最常用的命令,如 pwdpscat 或 rm,运行 nmobjdump 和 readelf。通常,在您编写需要某种功能的程序时,如果标准的工具已经提供了这个功能,那么通过运行 objdump -d <command>,可以查看这些工具究竟如何完成这项任务。

如果您倾向于使用编译器和其他的语言工具,那么您可以对组成计算机系统的各种目标文件进行仔细研究,并且您将会发现这项工作是非常值得的。UNIX 操作系统具有许多层次,那些通过工具查看目标文件所公开的层次,非常接近底层硬件。通过这种方式,您可以真实地接触到系统。

另外,还有一篇文章也涉及到segfault调试的问题:

http://hi.baidu.com/liheng_2009/blog/item/c065e880cc09129ef603a6e8.html

error number是由三个字位组成的,从高到底分别为bit2 bit1和bit0,所以它的取值范围是0~7.
bit2: 值为1表示是用户态程序内存访问越界,值为0表示是内核态程序内存访问越界
bit1: 值为1表示是写操作导致内存访问越界,值为0表示是读操作导致内存访问越界
bit0: 值为1表示没有足够的权限访问非法地址的内容,值为0表示访问的非法地址根本没有对应的页面,也就是无效地址

我的segfault log为:

Nov  8 15:37:37 tj1clnxweb0004 madservice[27711]: segfault at 0000000000000018 rip 00002b80ddfc6be5 rsp 00000000410010a8 error 4

最终定位到,是针对C++的map类型erase时出错导致的:

正确代码:

for (spi=mad_spammers.begin(); spi!=mad_spammers.end();) {
if (spi->second < stamp) {
adr.s_addr = ((client_t)spi->first).ip;
ret = blacklist_remove_item(adr.s_addr);
mad_spammers.erase(spi++);
} else {
spi++;
}

错误代码:

for (spi=mad_spammers.begin(); spi!=mad_spammers.end();spi++) {

if (spi->second < stamp) {

adr.s_addr = ((client_t)spi->first).ip;

ret = blacklist_remove_item(adr.s_addr);

mad_spammers.erase(spi);

}

}

但是很怪异的是,进行以下测试发现:将spi++放置在位置1,正确;放置在位置2,依然内存出错。怀疑可能是编译器做了优化。

for (spi=mad_spammers.begin(); spi!=mad_spammers.end();) {

if (spi->second < stamp) {

adr.s_addr = ((client_t)spi->first).ip;

ret = blacklist_remove_item(adr.s_addr);

mad_spammers.erase(spi);

// 位置1

log_msg(“spammer cleanup, remove %s from blacklist %d”, inet_ntoa(adr), ret);

// 位置2

} else {

spi++;

}

在网上没有找到合适的开源collaborative filtering的C语言实现,所以决定自己动手写一个。

目前网络上的cf资源请参考前文:Collaborative Filtering Resources在动手之前,试用了SUGGEST库,这是一个库文件,没有开放源代码,但是性能和推荐结果都挺不错的,我没有妄图超越它。

计划写的内容(不一定按照计划来):

窥探SUGGEST库

实现user-based cf算法:使用三元组存储稀疏矩阵

实现user-based cf算法:计算用户间距离

实现user-based cf算法:根据相似用户进行推荐

实现item-based cf算法

根据item相关性,改进cf算法

由于我目前只初步实现了user-based cf算法,所以只能保证前4条。之后,我希望可以把源码开放出来,给同学们做一个参考。也希望更多更好的cf算法能够开源。

SCWS是一款简单的开源中文分词系统,其网址为:http://www.ftphp.com/scws/

在代码中添加了一些输出,以便探究其分词的算法:

[yicheng@chengyi cli]$ ./scws -c utf-8 -d dict.utf8.xdb -r rules.utf8.ini -D -i ‘中国人民在中国人民大学上大学’ -t 10
No. WordString               Attr  Weight(times)
————————————————-
[0,0]Ox11(中)
[0,1]Ox83(中国)
[0,2]Ox81(中国人)
[1,1]Ox31(国)
[1,2]Ox81(国人)
[2,2]Ox31(人)
[2,3]Ox81(人民)
[3,3]Ox21(民)
[4,4]Ox1(在)
[5,5]Ox11(中)
[5,6]Ox83(中国)
[5,7]Ox83(中国人)
[5,10]Ox81(中国人民大学)
[6,6]Ox31(国)
[6,7]Ox81(国人)
[7,7]Ox31(人)
[7,8]Ox83(人民)
[7,10]Ox81(人民大学)
[8,8]Ox21(民)
[9,9]Ox31(大)
[9,10]Ox81(大学)
[10,10]Ox21(学)
[11,11]Ox1(上)
[12,12]Ox11(大)
[12,13]Ox81(大学)
[13,13]Ox21(学)
PATH by keyword = 中国人, (weight=8.9964):
中国人 民
PATH by keyword = 中国, (weight=219.7764):
中国 人民
PATH by keyword = 国人, (weight=0.0218):
中 国人 民
PATH by keyword = 人民, (weight=219.7764):
中国 人民
01. 中国人民大学       nt    10.64(1)
02. 中国                   ns    6.26(1)
03. 人民                   n     4.41(1)
04. 大学                   n     4.23(1)
+–[lt-scws(scws-cli/1.1.2)]———-+
| TextLen:   42                  |
| Prepare:   0.0074    (sec)     |
| Segment:   0.0010    (sec)     |
+——————————–+

以下简单记录今天看代码的收获。

typedef struct

{

xdict_t d;

rule_t r;

unsigned char *mblen;

unsigned int mode;

unsigned char *txt;

int zis;

int len;

int off;

int wend;

scws_res_t res0;

scws_res_t res1;

word_t **wmap;

struct scws_zchar *zmap;

}       scws_st, *scws_t;

是很核心的结构体。其中,使用txt保存用户输入的待分词字符串,使用off记录当前待处理的字节位置,res0为返回的分词结果,wmap就是上面输出的二维数组,zmap为wmap中每个词在txt中的index位置,d是字典,r是rule。

1、在scws_get_result函数中,首先按照最简单的规则,对文字进行分段,比如字母、数字、标点、换行符等,都被当做token来分段。

2、 对于每一段,首先初始化wmap二维数组,在它的对角线上存储每个字。

3、 然后依次访问wmap的对角线,向后匹配,在字典中查找,若找到匹配结果,则存储在wmap的行里。比如

[0,0]Ox11(中)
[0,1]Ox83(中国)
[0,2]Ox81(中国人)

就是从对角线的第一个元素,依次匹配第二个、第三个元素的结果。

4、 根据rule,进行规则过滤

5、 对wmap中的元素遍历,根据tf、idf计算weight,找出weight最大的path,作为最优分词结果。

6、 清理内存,返回。

scws_get_result的返回结果是二维的。每一次返回的是当前段的处理结果,是一个链表。该结果的next,是下一段的处理结果。

所以需要

while (res = cur = scws_get_result(s))

{

while (cur != NULL)

{

printf(“Word: %.*s/%s (IDF = %4.2f)\n”,

cur->len, text+cur->off, cur->attr, cur->idf);

cur = cur->next;

}

scws_free_result(res);

printf(“\n———————\n”);

}

当父类是一个抽象类,即它具备至少一个virtual函数的时候,如果没有virtual的析构函数,gcc在打开-Wall的时候,会给出警告:

abstract.cc:5: 警告:‘class Base’ 有虚函数却没有虚析构函数
abstract.cc:10: 警告:‘class Child’ 有虚函数却没有虚析构函数
为了规避这个警告,可以给父类增加一个virtual的析构函数。那么这个析构函数到底有什么用处呢?
下面的代码的运行结果为:
child call
child call
Child destroy
Base destroy
Child destroy
Base destroy
可以看到,子类析构的时候,编译器会自动调用父类的析构函数。

 C++ |  copy code |? 
01
02
  #include <iostream>
03
 
04
  using namespace std;
05
 
06
  class Base {
07
      public:
08
          virtual void call() = 0;
09
          virtual ~Base(){
10
              cout << "Base destroy" << endl;
11
          }
12
  };
13
 
14
  class Child : public Base{
15
      public:
16
          void call(){
17
              cout << "child call" << endl;
18
          }
19
          ~Child(){
20
              cout << "Child destroy" << endl;
21
          }
22
  };
23
 
24
  int main()
25
  {
26
      Child c;
27
      c.call();
28
 
29
      Base* b = new Child();
30
      b->call();
31
      delete b;
32
 
33
      return 0;
34
  }
35

但是,如果把Base析构函数的virtual去掉,gcc仍然会给出警告,运行结果:

child call

child call

Base destroy

Child destroy

Base destroy

这代表着,当显示操作子类Child的时候,会先调用Child的析构函数,然后编译器自动调用父类Base的析构函数。但是,当使用父类的指针操作Child的时候,就仅调用了父类Base的析构函数!

所以,《C++大学教程》里有这样的两段话:

1、如果您打算让别人从您的类中派生子类,并希望能够通过父类指针多态的删除它们,就把父类的析构函数定义为虚函数。

2、如果您想创建一个父类,可以被派生,但是不要多态的删除它们,那么就把父类的析构函数定义为protected的非虚函数。这样,子类仍然可以隐式调用父类的析构函数,但是其他函数,由于无法访问父类的非public函数,就无法直接调用父类的析构函数了。

考虑下面的代码,父类P没有无参构造函数,当调用子类C的有参构造函数时,由于没有采用C(int i):P(i)的形式,先调用父类的有参构造函数,所以P的无参构造函数会首先被调用!这样就会引起编译器报错。

 C++ |  copy code |? 
01
  class P{
02
      public:
03
          P(int i):val(i){}
04
 
05
      private:
06
          int val;
07
  };
08
 
09
  class C : public P{
10
      public:
11
          C(int i){
12
              P(i);
13
          }
14
  };
15
 
16
  int main(){
17
      C *c = new C(1);
18
      return 0;
19
  }

但是,如果子类C的有参构造函数,必须对它的参数先进行处理,然后才能初始化父类中的值,该怎么办呢?

答案是:为父类P添加一个无参构造函数,和一个接受参数的init函数。

 C++ |  copy code |? 
01
  class P{
02
      public:
03
          P(int i):val(i){}
04
          P(){}
05
 
06
          void init(int i){
07
              val = i;
08
          }
09
 
10
      private:
11
          int val;
12
  };
13
 
14
  class C : public P{
15
      public:
16
          C(int i){
17
              init(i);
18
          }
19
  };
20
 
21
  int main(){
22
      C *c = new C(1);
23
      return 0;
24
  }

那么结论是不是c++里子类想调用父类的构造函数,是不是只能在初始化列表里调用呢?这样不会太麻烦了吗?为什么这样设计呢?

另外,记得在某个地方看到过,如果一个类有可能成为别的类的父类,那么就必须要有无参构造函数。现在明白为什么了!