零拷贝技术-内核源码剖析

在网络编程中,如果我们想要提供文件传输的功能,最简单的方法就是用read将数据从磁盘上的文件中读取出来,再将其用write写入到socket中,通过网络协议发送给客户端。

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

但是就是这两个简单的操作,却带来了大量的性能丢失。

例如我们的服务器需要为客户端提供一个下载操作,此时的操作如下:

零拷贝技术-内核源码剖析_第1张图片

从上图可以看出,虽然仅仅只有这两行代码,但是却在发生了四次用户态和内核态的上下文切换,以及四次数据拷贝,也就是在这个地方产生了大量不必要的损耗。

那么为什么会发生这些操作呢?

上下文切换

由于read和recv是系统调用,所以每次调用该函数我们都需要从用户态切换至内核态,等待内核完成任务后再从内核态切换回用户态。

数据拷贝

上面也说了,由于数据的读取与写入都是由系统进行的,那么我们就得将数据从用户的缓冲区中拷贝到内核:

第一次拷贝:将磁盘中的数据拷贝到内核的缓冲区中

第二次拷贝:内核将数据处理完,接着拷贝到用户缓冲区中

第三次拷贝:此时需要通过socket将数据发送出去,将用户缓冲区中的数据拷贝至内核中socket的缓冲区中

第四次拷贝:把内核中socket缓冲区的数据拷贝到网卡的缓冲区中,通过网卡将数据发送出去。

所以要想优化传输性能,就要从减少数据拷贝和用户态内核态的上下文切换下手,这也就是零拷贝技术的由来。

零拷贝

零拷贝的主要任务就是避免CPU将数据从一块存储中拷贝到另一块存储,主要就是利用各种技术,避免让CPU做大量的数据拷贝任务,以此减少不必要的拷贝。或者借助其他的一些组件来完成简单的数据传输任务,让CPU解脱出来专注别的任务,使得系统资源的利用更加有效。

sendfile

sendfile函数的作用是直接在两个文件描述符之间传递数据。由于整个操作完全在内核中(直接从内核缓冲区拷贝到socket缓冲区),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝。

需要注意的是,in_fd必须是一个支持类似mmap函数的文件描述符,不能是socket或者管道,而out_fd必须是一个socket,由此可见sendfile是专门为了在网络上传输文件而实现的函数。

零拷贝技术-内核源码剖析_第2张图片
#include 

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

参数: out_fd : 待写入内容的文件描述符 in_fd : 待读出内容的文件描述符 offset : 文件的偏移量4. DMA拷贝 count : 需要传输的字节数 返回值: 成功:返回传输的字节数 失败:返回-1并设置errno

内核源码简读

sendfile系统调用分32位接口与64位接口,原理都是一样的,这里直接看32位的(inux/fs/read_write.c):

asmlinkage ssize_t sys_sendfile(int out_fd, int in_fd, off_t __user *offset, size_t count)
{
    loff_t pos;
    off_t off;
    ssize_t ret;

    if (offset) {
        if (unlikely(get_user(off, offset)))
            return -EFAULT;
        pos = off;
        ret = do_sendfile(out_fd, in_fd, &pos, count, MAX_NON_LFS);
        if (unlikely(put_user(pos, offset)))
            return -EFAULT;
        return ret;
    }

    return do_sendfile(out_fd, in_fd, NULL, count, 0);
}

殊途同归,都是调用了do_sendfile:

static ssize_t do_sendfile(int out_fd, int in_fd, loff_t *ppos,
               size_t count, loff_t max)
{
    // 一些检查...
    
    // 调用struct file_operations中的sendfile
    retval = in_file->f_op->sendfile(in_file, ppos, count, file_send_actor, out_file);

    if (retval > 0) {
        current->rchar += retval;
        current->wchar += retval;
    }
    current->syscr++;
    current->syscw++;
}

实际调用的是这个接口:

ssize_t generic_file_sendfile(struct file *in_file, loff_t *ppos,
             size_t count, read_actor_t actor, void *target)
{
    read_descriptor_t desc;

    if (!count)
        return 0;

    desc.written = 0;
    desc.count = count;
    desc.arg.data = target;
    desc.error = 0;

    // 读文件,读完通过actor接口发送
    do_generic_file_read(in_file, ppos, &desc, actor);
    if (desc.written)
        return desc.written;
    return desc.error;
}

回到上一步注入actor的地方,找到file_send_actor的实现:

int file_send_actor(read_descriptor_t * desc, struct page *page, unsigned long offset, unsigned long size)
{
    ssize_t written;
    unsigned long count = desc->count;
    struct file *file = desc->arg.data;

    if (size > count)
        size = count;

    // 调用struct file_operations的sendpage接口
    written = file->f_op->sendpage(file, page, offset,
                       size, &file->f_pos, sizeerror = written;
        written = 0;
    }
    desc->count = count - written;
    desc->written += written;
    return written;
}

此时的file是一个socket文件,

ssize_t sock_sendpage(struct file *file, struct page *page,
              int offset, size_t size, loff_t *ppos, int more)
{
    struct socket *sock;
    int flags;

    sock = SOCKET_I(file->f_dentry->d_inode);

    flags = !(file->f_flags & O_NONBLOCK) ? 0 : MSG_DONTWAIT;
    if (more)
        flags |= MSG_MORE;

    // 调用struct proto的发送接口, 比如udp
    return sock->ops->sendpage(sock, page, offset, size, flags);
}

可见源文件数据自始至终只在内核中传递,并没有经过用户态。

你可能感兴趣的:(C语言,Linux,linux,网络,服务器)