前面我们说过,对于静态文件的传输,用sendfile可以减少系统调用,现在我们看看动态的数据应该如何处理。
首先,如果数据足够小(小于1024)且只有唯一的一个buffer,我们直接用 send/write 就可以了。
通常的情况下,程序可能会在多个地方产生不同的buffer,如 nginx,第一个phase里都可能会产生buffer,放进一个chain里,
如果对每个buffer调用一次send,系统调用的个数将直接等于buffer的个数,对于多buffer的情况会很糟。
可能大家会想到重新分配一个大的buffer, 再把数据全部填充进去,这样其实只用了一次系统调用了。
又或者在一开始就预先分配一块足够大的内存。
这两种情况是能满足要求,不过都不足取,前一种会浪费内存,后一种方法对phase的独立性有影响。
linux 有一个writev可以支持这种情况,先看下函数声明:
ssize_t writev (int fd, __const struct iovec * iov, int count)
相关的结构:
struct iovec { char *iov_base; /*缓冲区起始地址*/ size_t iov_len; /*缓冲区长度*/ };
函数声明很明显的告诉我们可以同时发送多个buffer。
不妨看一下nginx的用法:
ngx_chain_t *cl;
struct iovec *iov, headers[NGX_HEADERS];
for (cl = in; cl && send < limit; cl = cl->next) {
iov = ngx_array_push(&header);
iov->iov_base = (void *) cl->buf->pos;
iov->iov_len = (size_t) size;
}
writev(c->fd, header.elts, header.nelts);
一开始创建一下 struct iovec 数组,将每个元素的 iov_base指向 单个要发送的buffer,iov_len 则是等于长度。
最后调用 writev一齐发送。
接着我们看一下函数posix定义:
ssize_t writev(int fd, const struct iovec *vecs, size_t count){ ssize_t bytes = sys_writev(fd, vecs, count); RETURN_AND_SET_ERRNO(bytes); }
内核函数sys_writev:
ssize_t sys_writev(unsigned long fd, const struct iovec __user *vec, unsigned long vlen) { struct file *file; ssize_t ret = -EBADF; int fput_needed; file = fget_light(fd, &fput_needed); if (file) { ret = vfs_writev(file, vec, vlen, &file->f_pos); fput_light(file, fput_needed); } return ret; }
我们看到实际上是调用 vfs_writev:
ssize_t vfs_writev(struct file *file, const struct iovec __user *vec, unsigned long vlen, loff_t *pos ) { if (!(file->f_mode & FMODE_WRITE)) return -EBADF; if (!file->f_op || (!file->f_op->writev && !file->f_op->write)) return -EINVAL; return do_readv_writev(WRITE, file, vec, vlen, pos); }
发现 readv, writev 其实都是用 do_readv_writev 来do的:
static ssize_t do_readv_writev(int type, struct file *file, const struct iovec __user * uvector, unsigned long nr_segs, loff_t *pos)
这个函数比较比较长,我们拣重点分析:
struct iovec iovstack[UIO_FASTIOV]; struct iovec *iov=iovstack, *vector;
内核 创建数据结构。
copy_from_user(iov, uvector, nr_segs*sizeof(*uvector));
将用户空间的数据考贝到内核空间,注意只是拷贝了 struct iovec 结构,里面的 iov_base 指定的内容没有拷贝。
fnv = file->f_op->writev; if (fnv) { ret = fnv(file, iov, nr_segs, pos); goto out; }
如果fs 实现了 file_operation 结构体中的 writev 函数,就直接调用它,否则才会调用下面:
while (nr_segs > 0) { void __user * base; size_t len; ssize_t nr; base = vector->iov_base; len = vector->iov_len; vector++; nr_segs--; nr = file->f_op->write(file, base, len, pos); //报错处理代码 省略 }
不过可惜的是,目前主流文件系统的驱动层fs_operation都不支持 writev 函数,以 ext4为例:
const struct file_operations ext4_file_operations = { .llseek = generic_file_llseek, .read = do_sync_read, .write = do_sync_write, .aio_read = generic_file_aio_read, .aio_write = ext4_file_write, .unlocked_ioctl = ext4_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = ext4_compat_ioctl, #endif .mmap = ext4_file_mmap, .open = ext4_file_open, .release = ext4_release_file, .fsync = ext4_sync_file, .splice_read = generic_file_splice_read, .splice_write = generic_file_splice_write, };
的确没有对 writev 进行实现。
所以对于writev目前的做法是内核是循环write的,但是比用户层的循环节省了切换的开销,因此效率上还是会好一些,但也好不了多少,不过有理由相
信未来的文件系统会实现 file_operation 的writev.