在网络编程中,如果我们想要提供文件传输的功能,最简单的方法就是用read将数据从磁盘上的文件中读取出来,再将其用write写入到socket中,通过网络协议发送给客户端。
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
但是就是这两个简单的操作,却带来了大量的性能丢失。
例如我们的服务器需要为客户端提供一个下载操作,此时的操作如下:
从上图可以看出,虽然仅仅只有这两行代码,但是却在发生了四次用户态和内核态的上下文切换,以及四次数据拷贝,也就是在这个地方产生了大量不必要的损耗。
那么为什么会发生这些操作呢?
上下文切换
由于read和recv是系统调用,所以每次调用该函数我们都需要从用户态切换至内核态,等待内核完成任务后再从内核态切换回用户态。
数据拷贝
上面也说了,由于数据的读取与写入都是由系统进行的,那么我们就得将数据从用户的缓冲区中拷贝到内核:
第一次拷贝:将磁盘中的数据拷贝到内核的缓冲区中
第二次拷贝:内核将数据处理完,接着拷贝到用户缓冲区中
第三次拷贝:此时需要通过socket将数据发送出去,将用户缓冲区中的数据拷贝至内核中socket的缓冲区中
第四次拷贝:把内核中socket缓冲区的数据拷贝到网卡的缓冲区中,通过网卡将数据发送出去。
所以要想优化传输性能,就要从减少数据拷贝和用户态内核态的上下文切换下手,这也就是零拷贝技术的由来。
零拷贝
零拷贝的主要任务就是避免CPU将数据从一块存储中拷贝到另一块存储,主要就是利用各种技术,避免让CPU做大量的数据拷贝任务,以此减少不必要的拷贝。或者借助其他的一些组件来完成简单的数据传输任务,让CPU解脱出来专注别的任务,使得系统资源的利用更加有效。
sendfile
sendfile函数的作用是直接在两个文件描述符之间传递数据。由于整个操作完全在内核中(直接从内核缓冲区拷贝到socket缓冲区),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝。
需要注意的是,in_fd必须是一个支持类似mmap函数的文件描述符,不能是socket或者管道,而out_fd必须是一个socket,由此可见sendfile是专门为了在网络上传输文件而实现的函数。
#include <sys/sendfile.h>
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, size<count);
if (written < 0) {
desc->error = 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);
}
可见源文件数据自始至终只在内核中传递,并没有经过用户态。