阻塞/非阻塞讀寫總結、tcp網路程式設計的本質、muduo::Buffer設計簡介

s1mba發表於2013-11-07

一、阻塞/非阻塞讀寫總結


1、對於read 呼叫,如果接收緩衝區中有 20位元組,請求讀 100個位元組,就會返回 20;對於 write呼叫,如果請求寫 100個位元組,而傳送緩衝區中只有 20個位元組的空閒位置,那麼 write會阻塞,直到把 100個位元組全部交給傳送緩衝區才返回。但如果 socket檔案描述符有 O_NONBLOCK標誌,則 write不阻塞,直接返回 20;此時非阻塞地read 也直接返回20。

2、read 沒有一點資料可讀或 write 沒有一點空間可以寫入,如果disable O_NONBLOCK 則會阻塞,如果enable O_NONBLOCK 則會返回-1,errno = EAGAIN | EWOULDBLOCK 錯誤。

3、阻塞模式下可以用setsockopt設定SO_RCVTIMEO(超時時間),即如果在超時時間內接收緩衝區都沒有一點資料到來,那麼返回-1,errno = EAGAIN | EWOULDBLOCK 錯誤。同理,還有SO_SNDTIMEO 選項,在超時時間內傳送緩衝區都沒有足夠記憶體存放資料,也是返回-1,errno = EAGAIN | EWOULDBLOCK 錯誤。

4、recv的第四個引數若為MSG_WAITALL,則在阻塞模式下不等到指定數目的資料不會返回,除非超時時間到。當然如果對方關閉了,即使超時時間未到,recv 也返回0。/usr/include/i386-linux-gnu/bits/socket.h     MSG_WAITALL = 0x100


5、在多執行緒環境中,某個執行緒的阻塞不會引起程式的阻塞,除非程式中的所有執行緒都被阻塞。(pthread)


二、TCP網路程式設計的本質


TCP網路程式設計最本質是的處理三個半事件(來自:muduo manual.pdf)

1. 連線的建立,包括服務端接受(accept) 新連線和客戶端成功發起(connect) 連接。TCP 連線一旦建立,客戶端和服務端是平等的,可以各自收發資料。

2. 連線的斷開,包括主動斷開(close 或shutdown) 和被動斷開(read(2) 返回0)。

3. 訊息到達,檔案描述符可讀。這是最為重要的一個事件,對它的處理方式決定了網路程式設計的風格(阻塞還是非阻塞,如何處理分包,應用層的緩衝如何設計等等)。

3.5 訊息傳送完畢,這算半個。對於低流量的服務,可以不必關心這個事件;另外,這裡“傳送完畢”是指將資料寫入作業系統的緩衝區,將由TCP 協議棧負責數據的傳送與重傳,不代表對方已經收到資料。


1、下圖是根據muduo庫中對讀寫事件的處理畫出的草圖:




2、Echoser 類圖:(muduo/example/simple/Echo.h、Echo.cc)


使用基於物件風格實現,詳見這裡


3、什麼都不做的EventLoop

one loop per thread意思是說每個執行緒最多只能有一個EventLoop物件,這種執行緒即“reactor"(mainReactor & subReactor)。剩下一些存在於threadpool 的執行緒主要用於做計算(decode, compute, encode),並不是IO執行緒。

EventLoop物件構造的時候,會檢查當前執行緒是否已經建立了其他EventLoop物件,如果已建立,終止程式(LOG_FATAL)

EventLoop建構函式會記住本物件所屬執行緒(threadId_)。

建立了EventLoop物件的執行緒稱為IO執行緒,其功能是執行事件迴圈(EventLoop::loop)


三、muduo::Buffer設計簡介


所有muduo 中的IO 都是帶緩衝的IO (buffered IO),你不會自己去read() 或write() 某個socket,只會操作TcpConnection 的input buffer 和output buffer。更確切的說,是在onMessage() 回撥裡讀取input buffer;呼叫TcpConnection::send()來間接操作output buffer,一般不會直接操作output buffer。

TcpConnection 會有兩個Buffer 成員,input buffer 與output buffer。

• input buffer,TcpConnection 會從socket 讀取資料,然後寫入input buffer(其實這一步是用Buffer::readFd() 完成的);客戶程式碼從input buffer 讀取數據。
• output buffer,客戶程式碼會把資料寫入output buffer (其實這一步是用TcpConnection::send() 完成的);TcpConnection 從output buffer 讀取資料並寫入socket。

其實,input 和output 是針對客戶程式碼而言,客戶程式碼從input 讀,往output 寫。TcpConnection 的讀寫正好相反。


兩個indices 把vector 的內容分為三塊:prependable、readable、writable,各塊的大小是(公式一):
prependable = readIndex
readable = writeIndex - readIndex
writable = size() - writeIndex

Muduo Buffer 裡有兩個常數kCheapPrepend 和kInitialSize,定義了prependable的初始大小和writable 的初始大小,readable 的初始大小為0。在初始化之後,Buffer 的資料結構如下:括號裡的數字是該變數或常量的值。


關於Buffer::readFd():

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 結合棧上的空間,避免記憶體使用過大,提高記憶體使用率
// 如果有5K個連線,每個連線就分配64K+64K的緩衝區的話,將佔用640M記憶體,
// 而大多數時候,這些緩衝區的使用率很低
ssize_t Buffer::readFd(int fd, int *savedErrno)
{
    // saved an ioctl()/FIONREAD call to tell how much to read
    // 節省一次ioctl系統呼叫(獲取有多少可讀資料)
    char extrabuf[65536];
    struct iovec vec[2];
    const size_t writable = writableBytes();
    // 第一塊緩衝區
    vec[0].iov_base = begin() + writerIndex_;
    vec[0].iov_len = writable;
    // 第二塊緩衝區
    vec[1].iov_base = extrabuf;
    vec[1].iov_len = sizeof extrabuf;
    const ssize_t n = sockets::readv(fd, vec, 2);
    if (n < 0)
    {
        *savedErrno = errno;
    }
    else if (implicit_cast<size_t>(n) <= writable)  //第一塊緩衝區足夠容納
    {
        writerIndex_ += n;
    }
    else        // 當前緩衝區,不夠容納,因而資料被接收到了第二塊緩衝區extrabuf,將其append至buffer
    {
        writerIndex_ = buffer_.size();
        append(extrabuf, n - writable);
    }
    // if (n == writable + sizeof extrabuf)
    // {
    //   goto line_30;
    // }
    return n;
}

具體做法是,在棧上準備一個65536 位元組的stackbuf,然後利用readv() 來讀取資料,iovec 有兩塊,第一塊指向muduo Buffer 中的writable 位元組,另一塊指向棧上的stackbuf。這樣如果讀入的資料不多,那麼全部都讀到Buffer 中去了;如果長度超過Buffer 的writable 位元組數,就會讀到棧上的stackbuf 裡,然後程式再把stackbuf 裡的資料append 到Buffer 中。


參考:
《UNP》
muduo manual.pdf
《linux 多執行緒伺服器程式設計:使用muduo c++網路庫》


相關文章