前面我們說過,對于靜态檔案的傳輸,用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.