谈谈writev的问题
轉載地址:http://blog.lucode.net/linux/talk-about-the-problem-of-writev.html
POSIX提供了一個比write函數更加高級的writev,在很多場景下,它相對于write有一定的優勢。
APUE一書將writev的介紹放在了Advanced I/O部分,個人拙見,它和write應該是屬于同層次的IO,談不上Advanced。
最近,我在重寫HTTP解析器的時候用到了writev,發現它并不如想象中的方便,甚至相當坑,有必要記錄一下,或許對大家有所幫助。
使用場景
為什么說writev并不比write高級?拋開具體的實現,它和write只不過是應用場景不同,嚴格而言,不應該用Advanced加以區分。APUE對于writev的描述也就2頁,老套路:函數原型、參數介紹、性能測試,實在看不出所謂的Advanced體現在哪里。
大體而言,write面向的是連續內存塊,writev面向的是分散的數據塊,兩個函數的最終結果都是將內容寫入連續的空間。
假設我們需要將N個key-value組合dump到文件中。
已知每個pair的空間是單獨分配的,那么在這個場景下,如果想要使用write完成任務,有如下2種做法:
在數據量不是太大的情況下,方案1比方案2要高效,應為syscall的開銷是很可觀的,當每個pair的數據都比較大時,首選方案2。怎么判斷這里的臨界值?對于磁盤IO,考慮pagesize,這個臨界值很可能是N*pagesize(N>=1),當然我沒有具體考證。
這個時候,我們就會期望能夠有一個函數可以做到:
顯然,它就是writev了,其函數原型如下:
writev(int fd, struct iovect *iov, int iovcnt);struct iovec {void *iov_base;size_t iov_len; };iov_base就是每個pair的基址,iov_len則是長度,不用包含“0”。
APUE中指出writev的固有開銷比write大,因此對于小內存的寫而言,很可能也沒有copy+write高效,具體參考APUE(en) P.522。另外,iovcnt不應超過IOV_MAX,Linux上的IOV_MAX=1024,且iov_len的總和不應溢出,雖然超過限制的情況很少出現,但應該考慮到。
除此之外,另一種比較高效的做法是,分配一大塊內存,想辦法讓所有的pair連續,這樣就不需要考慮write了,但是很多情況下你可能根本無法預先獲知總長度,例如從socket中讀取的數據,為了能夠容納所有的數據,就不得不分配適當大的數據,當不夠用的時候再realloc。
如此一來,會造成空間利用率稍低的問題,且realloc很可能帶來潛在的內存拷貝開銷。
writev的實現
在Linux2.2之前,由于IOV_MAX過于小,glibc會提供一個wrapper function,代碼如下:
/*Write data pointed by the buffers described by VECTOR, which is a vector of COUNT 'struct iovec's, to file descriptor FD.The data is written in the order specified.Operates just like 'write' (see <unistd.h>) except that the dataare taken from VECTOR instead of a contiguous buffer.*/ ssize_t __writev(int fd, const struct iovec *vector, int count) {/*Find the total number of bytes to be written.*/size_t bytes = 0;for (int i=0; i<count; ++i) {/*Check for ssize_t overflow. */if (SSIZE_MAX - bytes < vector[i].iov_len) {__set_errno(EINVAL);return -1;}bytes += vector[i].iov_len;}/*Allocate a temporary buffer to hold the data. We should normallyuse alloca since it's faster and does not require synchronizationwith other threads. But we cannot if the amount of memory required is too large.*/char *buffer;char *malloced_buffer __attribute__ ((__cleanup__ (ifree))) = NULL;if (__libc_use_alloca(bytes))buffer = (char *) __alloca(bytes);else {malloced_buffer = buffer = (char *)malloc (bytes);if (buffer == NULL) /*XXX I don't know whether it is acceptable to try writingthe data in chunks. Probably not so we just fail herr.*/return -1;}/*Copy the data into BUFFER. */size_t to_copy = bytes;char *bp = buffer;for (int i=0; i<count; ++i) {size_t copy = MIN(vector[i].iov_len, to_copy);bp = __mempcpy((void *) bp, (void *)vector[i].iov_base, copy);to_copy -= copy;if (to_copy == 0) break;}ssize_t bytes_written = __write(fd, buffer, bytes);return bytes_written; } weak_alias(__writev, writev)? 大致流程就是:
? ? ? ?1.計算總長度
? ? ? ? 2.分配空間(棧/堆)
? ? ? ?3.拷貝數據
? ? ? ?4.使用write
這么做完全是權宜之計,并沒有體現writev的優點,如果沒有一次寫完,那么就需要多次復制。
內核中的實現則是有點類似分別對內存塊write,只不過由于已經位于內核空間,自然沒有什么syscall的開銷了,也能使用更加直接的方式,比如直接寫buf,代碼在fs/read_write.c,感興趣的讀者可以挖掘下。
writev的問題
writev的出發點是好的,并且看起來似乎也比較美好,因此很受推崇。
通過這兩天的使用情況來看,我個人認為writev在設計上可能存在一定的問題,產生這些問題的具體場景為socket IO,令人遺憾的是,盡管很多人推崇writev,但是google相關內容,資料卻少得可憐。。。
對于socket IO而言,write經常不能夠一次寫完,好在它會返回已經寫了多少字節,如果繼續寫,此時就會阻塞;對于非阻塞socket而言,write會在buf不可寫時返回的EAGAIN,那么在下一次write時,便可通過之前返回的值重新確定基址和長度。
manual中對于writev的相關描述為:和write類似。也就是說,它也會返回已經寫入的長度或者EAGAIN(errno)。千萬不可天真地認為,每次傳同樣的iovec就能解決問題,writev并不會為你做任何事情,重新處理iovec是調用者的任務。
問題是,這個返回值“實用性”并不高,因為參數傳入的是iovec數組,計量單位是iovcnt,而不是字節數,用戶依舊需要通過遍歷iovec來計算新的基址,另外寫入數據的“結束點”可能位于一個iovec的中間某個位置,因此需要調整臨界iovec的io_base和io_len。
可以通過如下代碼確認:
while (iov_iter_count(iter)) {struct iovec iovec = iov_iter_iovec(iter);ssize_t nr;nr = fn(filp, iovec.iov_base, iovec.iov_len, ppos);if (nr < 0) {if (!ret) ret = nr;break;}ret += nr;if (nr != iovec.iov_len) break;iov_iter_advance(iter, nr); }個人認為這個設計和write并不是一個風格,write使用很方便,writev卻很繁瑣,難道僅從參數和返回的類型就能確定一套API的風格了?
個人認為下述方案或許更好:
在do_loop_readv_writev中可以直接加入相關邏輯,對于do_iter_readv_writev或許會麻煩點,但是在回調中應該不難解決問題。好吧,說再多也只是紙上談兵。相信這并不是實現者的問題,而是POSIX在制定接口時就欠缺考慮。
總結
對于磁盤IO,可以放心使用writev,對于socket,尤其是非阻塞socket,還是盡可能避免的好,實現連續的內存塊反而可以簡化實現。
?
?
總結
以上是生活随笔為你收集整理的谈谈writev的问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java web项目在tomcat中以调
- 下一篇: HTTP 状态码常用对照表