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