天天看點

大并發伺服器不得不說的技術--writev

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

繼續閱讀