Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2039084
  • 博文数量: 369
  • 博客积分: 10093
  • 博客等级: 上将
  • 技术积分: 4271
  • 用 户 组: 普通用户
  • 注册时间: 2005-03-21 00:59
文章分类

全部博文(369)

文章存档

2013年(1)

2011年(2)

2010年(10)

2009年(16)

2008年(33)

2007年(146)

2006年(160)

2005年(1)

分类: LINUX

2007-10-31 00:12:27

目前,大多数程序的开发,执行效率一般都要向开发效率妥协,因为市场瞬息万变,时机不等人,在有限的人力和物力资源下,只能采用先开发出原型,然后逐渐完善的产品发布模式。这非常奏效,ESR在他的《UNIX程序设计艺术》一书中也极力推崇。并且按照摩尔定律,硬件的更新换代所带来的执行效率的提升往往比单纯的软件优化要奏效,有时成本更加低廉。事实也的确如此,但是作为程序员可不能把这个当成懒惰的借口,尤其是系统程序员,应该时时刻刻把执行效率记在心间,相信程序员这个特殊的群体中很多人也一定和我一样,属于完美主义者。

对效率孜孜不倦地追求,不放过每一个可以优化的点,这是很多系统程序员的“通病”。对于时间和空间,他们可比葛朗台都要吝啬,任何优化手段,只要能获得效率的提升,他们无不用其极,那怕可能带来诡异和晦涩的代码,虽然他们心中也信奉KISS的UNIX程序设计哲学。

网络程序,特别是服务器程序,对执行效率的要求很高,特别是处在CPU的频率提升变缓而网络速度突飞猛进的时代,如何降低网络程序对服务器CPU造成的负担就显得特别重要。又因为内存的速度还远不能和CPU速度向匹敌,如何尽量少地访问内存,提高CPU内部Cache的命中率,减少Cache的刷新次数就成为了提高程序效率的关键。另外,随着多核 /多处理器的逐渐普及,如何充分利用多个处理器,换句话说就是如何在多个处理单元间进行负载均衡、减少同步开销也是一个挑战。

零拷贝技术就是通过避免数据在内存间的拷贝来去除拷贝内存的开销,从而提高运行效率的一种程序优化方法。

本文将以笔记的形式详细记述我在网络程序的零拷贝方面所做的尝试,其中不免有些弯路,错路,不妨记述下来,我相信有的时候过程可能比结果更加有用,尤其是对于blog这种日志文体来说。

尝试一:用tmpfs和sendfile来提高网络程序的数据发送效率

先介绍一下什么是tmpfs。tmpfs是Linux内核实现的内存文件系统,所谓内存文件系统就是说文件并没有象通常人们所认为的那样处于硬盘上,而是驻留于物理内存中,当然如果你开启了交换分区,文件或者文件的部分内容还是有可能在某些时候被交换到交换分区(一般位于硬盘上)的。把文件放在物理内存里的优势也是相当明显的--提高文件的访问速度。与ramdisk(内存块设备)相比,tmpfs具有更好的伸缩性,它的大小不是固定的,而是按需分配。这并不意味着它的大小无限,通常如果你不在加载(mount)文件系统的时候指定它所被允许的最大大小的话,那么它的最大值就是一半物理内存大小。有时,这种按需分配的原则并不怎么友好,设想一下,系统内存不足,那么就可能无法在内存文件系统里面创建文件,或者是写文件,而固定大小的ramdisk因为是预先分配内存则没有这个问题。

sendfile系统调用用于将一个文件的内容“直接”发送到指定套接字,而无须将数据从内核空间拷贝到用户空间,然后再从用户空间拷贝回内核空间,甚至通过增加内存页面的引用计数配合技术,真正的可以做到从内核的文件cache到网卡数据发送的0拷贝,从而提高文件发送的效率,其原型如下所示:

#include <sys/sendfile.h>

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);


如果out_fd(即套接字)是非阻塞的,那么当套接字的发送缓冲区已满,则会立即返回-1,并将errno置为EAGAIN。注意,因为Linux文件操作实际上并不直接支持异步(可能是因为select/poll系列的系统调用不符合文件的语意),所以调用还是有可能被阻塞的,虽然时间可能并不是很长,但是这对于异步的网络服务器来说,却是相当不好的一个消息。一种解决方案就是结合上面提到的内存文件系统,通过Linux的异步文件IO将文件先缓存到tmpfs文件系统中,然后再通过sendfile将其发出。就是这么做的,不过它没有在tmpfs文件系统中为那些文件做cache,如果附以以一定的cache替换算法,那么lighttpd的性能应该还会有所提升。

Linux系统中的sendfile系统调用在2.6.9版之后就强制要求in_fd必须指向一个支持mmap类系统调用的文件,而out_fd必须指向套接字。这也就意味着我们不能通过把in_fd指向一个socket,然后用sendfile系统调用在两个socket之间互相传递数据。

那么我们如何把文件和缓冲区联系起来,从而提高缓冲区数据的发送效率呢?

这就要靠另外一个系统调用mmap了:

#include <sys/mman.h>

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

int munmap(void *start, size_t length);


他能将文件映射到进程的用户空间地址上,如果这个文件存在于内存文件系统tmpfs中,那么这段内存将直接和存储文件内容的内存页面相对应,也就是说写这块地址空间对应的内存就是同步写相应的tmpfs文件的内容。这样文件和缓冲区就联系起来了。

不再卖官司,代码伺候:

typedef struct _page
{
    int fd, flags;
    void *start;
    size_t size;
} page;


以上代码定义了一个名字叫作page的结构体:
  • fd: 指向tmpfs中一个具体文件的文件描述符
  • flags: 内存映射的标志位
  • start: 映射内存的起始地址
  • size: 映射内存的大小

#define _ALIGN(x, aln) (((x) + (aln) - 1) & ~((aln) - 1))
#define PAGE_ALIGN(x) _ALIGN(x, sysconf(_SC_PAGESIZE))

int __alloc_page(page *p, size_t size, int flags)
{
        char buf[] = "/dev/shm/pageXXXXXX";

        assert(p != NULL);
        assert(n > 0);

        p->fd = mkstemp(buf);
        if (p->fd < 0)
                return p->fd;
        if (unlink(buf) != 0)
                goto err;
        size = PAGE_ALIGN(size);
        if (ftruncate(p->fd, size) != 0)
                goto err;
        flags |= MAP_SHARED;
        p->start = mmap(NULL, size, PROT_READ | PROT_WRITE, flags, p->fd, 0);
        if (p->start == NULL)
                goto err;
        p->size = size;

        return 0;

err:
        while (close(p->fd) < 0 && errno == EINTR)
                ;

        return -1;
}


__alloc_page申请大小为size的内存。其中用到了一个小技巧:创建完临时文件并顺利打开后立刻删除相应文件,因为文件仍处于打开状态,所以你还可以通过文件描述符访问它,直到你关闭它所对应的文件描述符后,它才真正被删除;但是,因为它已经从文件系统上消失,所以你不能再次通过文件名打开它。这么做的好处就是:当进程意外退出的时候,不会将临时文件遗留在文件系统上,从而保持了系统的整洁。

注意:mmap要求映射的内存必须是按页面大小对齐的,所以才有了后面的ftruncate和PAGE_ALIGN操作。

static inline void __free_page(page *p)
{
    assert(p != NULL);

    munmap(p->start, p->size);
    while (close(p->fd) < 0 && errno == EINTR)
        ;
}


释放内存:取消映射并关闭相应的文件描述符。

static inline int send_page(int out_fd, page *page, off_t *offset, size_t size)
{
    assert(page != NULL);

    return sendfile(out_fd, page->fd, offset, size);
}


发送由offset和size指定的内存到套接字out_fd。

警告:请谨慎使用以上三个函数,因为sendfile的返回只表示指定的数据已经被提交到套接字的缓冲区,并不意味着数据已经发送完毕,如果这个时候你修改“发送过”的地址对应的数据,那么数据将会遭到破坏,这肯定不是你想看到的!所以,一定确保不会发生此类错误,并且当所有数据都被提交到套接字后,及时用__free_page“释放”内存。

测试的结果显示:发送同样的数据sendfile比write少用了1/3时间,也就是说发送速度提高了1/2。当然实际的应用中,因为上段所讲的限制,__alloc_page和__free_page的开销会使效率有所打折,甚至抵消或者超过拷贝所带来的开销,所以应用程序必须小心地设计,并充分测试!
阅读(3619) | 评论(4) | 转发(0) |
给主人留下些什么吧!~~

xiaosuo2009-09-15 17:00:44

确实需要把数据copy到page中,但是如果是只读的数据,那么只copy一次即可,上面提到的办法也只是在数据是一次copy多次send的时候效果明显。目前包括vmsplice在内,还没有特别好的办法处理用户空间动态数据的发送。

chinaunix网友2009-09-15 16:09:21

这里发送数据的话,需要把数据copy到page中? sendfile的本意是直接在内核中处理文件的发送吧

chinaunix网友2008-05-08 09:54:22

能否把例子一起贴出来

xiaosuo2008-04-02 10:17:35

出于安全方面的考量,内核返回给用户空间的内存页都是经过清0处理的,所以__alloc_page的开销很可能抵消它所带来的好处。