Unix網絡編程 3rd vol1 讀書筆記


學習筆記目的

此文檔記錄本人學習Unix Network Programming 3rd verion volumn I的一些筆記,我只將覺得重要或經過一番功夫才理解的內容記錄下來,方便以后回顧。

第二章 傳輸層TCP,UDP和SCTP

2.10 TCP端口號和並行服務器

tcp是通過一對socket(socket pair)來區分socket通訊的,可以這么理解,socket = ip + port,

socket pair = client socket + server socket。 所以,當使用一個監聽器監聽時,一旦accept到一個connect后,可以,將這個socket交給一個線程或進程執行任務,在線程中執行這個任務的同時,主線程仍然可以accept其他client socket的鏈接。因為,每個tcp connection的socket pair中的client socket是不一樣的,所以tcp是可以區分這些連接。下面這個舉個列子,如下圖所示,

clip_image002

客戶socket ,簡寫為CS1 = 206.168.112.219:1500,發起了鏈接,鏈接到服務socket ,簡寫為SS = 12.106.32.254:21。連接后,服務器fork一個進程,該進程處理CS1的相關操作。接下來,如圖:

clip_image004

客戶socket,簡寫為CS2 = 206.168.112.219:1501,發起鏈接,仍然鏈接SS,此時服務器在fork一個進程處理該鏈接,此時CS1和SS的鏈接仍然存在,並且SS還可以監聽其他鏈接。

通過以上的描述,你可以知道socket只有在accept的時候會block,accept后只要fork或采用多線程,處理請求,就可以繼續監聽其他請求,也就是說,監聽socket和accept之后的socket是可以同時存在,互相不影響的。

我以前誤以為監聽socket在accept的socket銷毀后,才能繼續監聽,否則會出現混亂,因為他們會同時使用一個端口和一個IP(上面的列子,都是12.106.32.254:21)。出現這個誤區的原因是因為我沒有理解TCP是根據socket pair區分連接,而不是根據單獨的socket。

2.11 緩存大小和限制

對於同步的socket(blocking socket),write函數的內部如何操作?

write函數會將應用程序buffer中的數據寫到Tcp buffer,這里會發生一次數據copy,因為tcp需要得到這份數據進行重傳。TCP buffer是有大小的,可以通過SO_SNDBUF來設置,如果滿了卻沒有裝滿應用程序的數據,那么應用程序進程就會sleep,也就是堵塞了,直到應用程序的數據全部copy到tcp buffer中,write程序才會返回(這里針對默認的blocking socket)。所以,可以想一下,如果應用程序每次都傳超過SO_SNDBUF的數據,那么每次至少會sleep一次,效率會大打折扣。而且,write返回后,並不意味者客戶端接收到了所有數據,他只表明應用程序中的數據全部copy到tcp buffer中,僅此而已,接下來由tcp負責將數據發送給客戶端。下圖是tcp buffer和應用程序buffer的示意圖:

clip_image006

第三章 套接字介紹

3.2 Socket地址結構

每一種協議都有地址結構,這些地址結構都是以“sockaddr_XXX”的形式命名,比如IP v4的地址結構為sockaddr_in,IP v6為sockaddr_in6,鏈路協議sockaddr_dl等等。

IP v4結構定義

struct in_addr

{

in_addr_t s_addr; // 32比特的IP v4地址,網絡字節序

// 需要函數將點分十進制的地址轉化為該值

};

struct sockaddr_in

{

uint8_t sin_len; // 結構長度,應用程序開發人員無需關系

sa_family_t sin_family; // 恆為AF_INET

in_port_t sin_port; // 16比特TCP或UDP端口,網絡字節序

struct in_addr sin_addr; // 32比特的IP v4地址,網絡字節序

char sin_zero[8]; // 沒有使用

};

總結一下:

l 都是網絡字節序

l 只需要關心16位的port和32位的IP address

l sin_family恆為AF_INET

3.3 “值-結果”參數(value-result arguments)

值-結果參數的意思就是in-out參數,即作為函數的參數,有作為函數的一部分結果,

線面的代碼就是值結果參數

1. struct sockaddr_un cli; /* Unix domain */

2. socklen_t len;

3.

4. len = sizeof(cli); /* len is a value */

5. getpeername(unixfd, (SA *) &cli, &len);

6. /* len may have changed */

如果cli是sockaddr_in(或sockaddr_in6),那么len恆為為16(或28),而sockadr_un的長度是會變化的,地址結構的可以參見下圖:

clip_image008

3.4 字節序函數

字節序

對於單字節數據類型如char來說,無所謂字節數序的問題,但是對於兩個或兩個以上的數據而言,字節序就很重要,如果字節序不統一,將會出現嚴重的數據兼容問題。比如short,是由兩個字節組成,究竟是高位字節存在內存低位,還是低位字節存在內存低位(前者稱為big-endian byte order,后者稱為little-endian byte order)。答案是,不確定。不同操作系統自己定義。我們可以做的是保持我們的字節序與環境一致,而不需要關心到底是big-endian還是little-endian。下圖表示不同字節序在內存中的結構:

clip_image010

網絡字節序

網絡字節序規定數據在網絡中傳輸的字節數序,是統一的,每一個協議實現的廠家必須遵守該字節序,否則無法與其他協議通訊。由於歷史原因,網絡字節序使用big-endian byte order(高位數據存在內存低位,與手寫整數的順序類似)。

本機字節序

本機字節序沒有統一的規定,不同操作系統廠家會只有自己的本機字節序,下面的代碼可以判斷當前OS本機字節序。

#include <iostream>

using namespace std;

int main()

{

union {

short s;

char bytes[2];

}un;

short sSample = 0x0102;

un.s = sSample;

if (sizeof(un.s) == 2)

{

if (un.bytes[0] == 1 && un.bytes[1] == 2)

{

cout << "big-endian" << endl;

}

else if (un.bytes[0] == 2 && un.bytes[1] == 1)

{

cout << "little-endian" << endl;

}

else

{

cout << "unknow" << endl;

}

}

else

{

cout << "size of short is not 2, but " << sizeof(short) << endl;

}

return 0;

}

字節序函數

在進行網絡編程時,我們不需要關心機器字節序和網絡字節序到底是little-endian還是big-endian,我們只需要知道數據在當前機器上的進程處理時,需要使用本機字節序,當數據在網絡上傳遞時,需要使用網絡字節序。通過下面這四個函數,可以方便的進行本機與網絡字節序之間的轉換:

#include <netinet/in.h>

uint16_t htons(uint16_t host_16_bit_value);

uint32_t htonl(uint32 _t host_32_bit_value);

both return: value in network byte order

uint16_t ntohs(uint16_t net_16_bit_value);

uint32_t ntohl(uint32_t net_32_bit_value);

both return: value in host byte order

記這些函數的技巧:

l h代表host,即本機;

l n代表net,即網絡;

l l代表long,即32位;

l s代表short,即16位。

3.5 字節操作函數

Unix下有兩組字節操作函數,一組以b(byte)開頭,是socket庫提供的自己操作函數,一種以mem(memory)開頭,由ANSI C提供。

#include <strings.h> // 注意這里不是<string.h>,多了一個s

void bzero(void* dest, size_t nbyte);

void bcopy(const void* src, void* dest, size_t nbyte);

int bcmp(const void* ptr1,void* ptr2, size_t nbyte); // 返回第一個不想等的byte比較結果

/*

總結:

l 無法修改的都是用const修飾,

l 長度都是size_t,指的的src的長度

l bzero比較好用,因為只有兩個參數

*/

#include <string.h>

void* memset(void* dest, int c, size_t len);

void* memcpy(void* dest, const void* src, size_t nbyte);

void* memcmp(const void* ptr1, const void* ptr2, size_t nbytes);

/*

總結:

l memcpy的記憶方式, dest = src

l memset的記憶方式, 長度在最后面

l 據說bcopy會正確處理越界(overlap)情況,而memcpy不行,不知如何正確處理

*/

3.6 函數inet_aton,inet_addr和inet_ntoa

這幾個函數都是用於點分十進制IP和網絡字節序IP相互轉換。

#include <arpa/inet.h>

/*

* 注意:

* 1 下面一對函數,相互轉換,a代表字符串,n代表網絡字節序

* 2 沒有長度參數,因為點分十進制額字符串長度比較固定,程序可以分析

* 3 inet_ntoa接收的參數是值,而不是指針,比較少見,而且返回的值是在靜態內容中,

* 且運行修改,因為不是const

* 4 inet_aton 返回1代表成功轉換,0代表失敗,可以理解為true和false,

* 與一般的0標識成功有點不同

*/

int inet_aton(const char* strptr, struct in_addr* addrptr);

char* inet_ntoa(struct in_addr inaddr);

/*

* 注意:

* 此方法過時,因為文檔不全,更主要的是對於“255.255.255.255”轉換得到結果與錯誤碼相同,有缺陷,所以不建議使用。雖然使用起來,比inet_aton方便,但是隱患較多,所以最好不要使用。

*/

in_addr_t inet_addr(const char* strptr);

3.7 函數inet_pton和inet_ntop

這一對函數與inet_aton/inet_ntoa類似,但是支持Ipv6,添加了一個family參數接受AF_INET對應IPV4,AF_INET6對應Ipv6。需要解釋一下”p”和”n”分別代表presentation和numeric。inet_pton/inet_ntop的函數接口更為一致。上一節提到過,inet_ntoa傳遞的是in_addr值,而不是指針,這與一般的方法使用上不是很一致。隨着IP v6的普及,使用這一對函數,應該是大勢所趨。下面來看看他們的接口定義:

#include <arpa/inet.h>

/*

* 參數的順序很好記憶

* numic =>prensentation 所以地址在前,字符串在后

*

* 此函數不提供內置的靜態空間存儲地址,需要用戶提供地址,好在buffer的長度使用了

* 兩個常量定義

* #include<netinet/in.h>

* #define INET_ADDRSTRLEN       16       // for IPv4 dotted-decimal 
* #define INET6_ADDRSTRLEN      46       // for IPv6 hex string 

* 返回的地址與傳入的地址一樣提供的地址,如果轉換過程出現問題,將會返回NULL

* 實力代碼

char szIP[INET_ADDRSTRLEN];

if (inet_ntop(AF_INET, &foo.sin_addr, szIP, sizeof(szIP)) == NULL)

{

cout << “failed to convert sin_addr”<< endl;

}

*/

const char* inet_ntop(int family,const void*addrptr, char* addrstr, size_t len );

/*

* 參數的順序很好記憶

* presention =>numeric 所以字符串在前,地址在后

* 在這里IP字符串地址使用const修飾的,所以不需要長度。

* 成功返回1,格式錯誤返回0,其他錯誤返回-1(在linux下,發現任意一個為null,均會core dump,報“segment fault”,C/C++程序操作NULL很容易core dump,所以使用這些函數時,不要心存僥幸不會接受到NULL指針,一定要驗證是否是NULL)

*/

int inet_pton(int family, const char* szIP, void* addrptr);


第四章 TCP套接字基礎

4.2 函數socket

socket函數應該是tcp socket中使用十分頻繁的函數,它用於創建套接字socket,所有的網絡相關的操作都與該函數的返回值有關,所以這個函數十分重要。

#include <sys/socket.h>

/*

* 創建套接字對象

* @param family: 地址族,如AF_INET, AF_INET6等

* @param type: 什么樣的socket,子啊IP v4上,可以理解為是TCP或是UDP

* @param protocol: 什么協議,為0時,選擇系統默認選項,一般都為0.

* @return: 成功返回非負數fd(file descriptor),失敗返回-1

*P.S. : 如果socket被close了,說明該socket的狀態到達終結狀態,資源被系統回收,不能

* 再使用了。

* 使用示例代碼:

// 創建Ipv4的TCP套接字

int iSock = socket(AF_INET, SOCK_STREAM, 0);

if (iSock < 0)

{

cout << “failed to create a tcp socket” << endl;

exit(1);

}

*/

int socket(int family, int type, int protocol);

下面是一些常用的套接字創建組合

clip_image012

4.3 函數connect

connect函數觸發tcp三次握手,沒有指定客戶端一定需要在connect函數調用之前使用bind函數,但是如果客戶端使用了bind,那么客戶段就指定了自己的端口和ip。否則內核會自動為客戶端socket分配ip(多網卡需要選擇ip)和端口。

#include <sys/socket.h>

/*

* @param iSock:一個可以用的socket描述符

*@param sa: 通用的地址,使用時要強行轉換

*@param len: sa的長度。

*@return : 0標識正常,-1標識失敗

*/

int connect(int iSock, struct sockaddr * sa, int len);

對於connect,成功說明三次握手成功,與對方建立了套接字鏈接,但是失敗有下列常見情況
1 找不到對應IP,鏈接超時,會等到超市后,才報錯。會產生ETIMEDOUT錯誤

2 對方相關端口沒有打開,通常是“對方積極拒絕(connection refused)”,錯誤代碼是ECONNREFUSED

3 對方IP不可到達,產生EHOSTUNREACHENETUNREACH錯誤代碼。

在局域網中編寫socket程序,一般前兩個情況比較常見。

4.4 函數bind

bind是將socket與特定的端口或地址進行綁定,相當於為端口和套接字設定指定的IP和端口。一般服務器會bind端口,而IP使用通配符(wildcard)。而客戶端socket不用bind端口和IP地址。但是,這並不意味着客戶端socket不能bind,只是這樣做沒有多大意義,因為如果port為0時,內核會自動分配一個沒有使用的端口給客戶端socket。Ipv4的IP通配符為INADDR_ANY,使用時最好使用htonl將其轉為網絡字節序並付給sockaddr_in.sin_addr,這樣內核會自動為服務器socket指定IP。為什么需要指定IP呢,因為一台機器可以裝多個網卡,這樣就會有多個IP,所以有時候需要指定IP進行綁定。下面,來看看bind的函數接口:

#include <sys/socket.h>

/*

* @param sockfd 套接字

* @param sockaddr 被綁定的地址結構

* @param len: 地址長度

* @return:0標識成功,-1標識失敗

*/

int bind(int sockfd, const sturct sockaddr* sa, socklen_t len);

bind的常見錯誤是“端口重綁定”,也就是bind到一個已經使用的端口,有時候也有可能是前一個socket雖然被進程close了,但是內核還沒有完全釋放資源,導致無法綁定,這樣可以設置socket參數SO_REUSEADDR,用來重用該地址。如果不實用bind,系統會自動設定ip和地址,使用了就會更具進程指定的端口和ip綁定。一般只會指定端口,而IP使用通配符,讓內核幫你指定。除非有特別需求,才會指定需要綁定的IP。

4.5 函數listen

此函數的作用是將socket轉化為被動socket,也就是服務器socket,接受客戶端socket的請求。listen函數調用順序實在socket,bind和accept之間。下面看看listen接口,

#include <sys/socket.h>

/*

* @param sockfd 套接字描述符

* @param queue 請求等待的隊列

* @return: 0正常,-1異常

*/

int listen(int sockfd, int queue);

在這個里面,queue這個參數需要解釋一下。這個參數是標識最大的排隊的客戶socket的數目,在排隊的客戶socket有兩種狀態:1)正在鏈接,也就是正在三次握手;2)已經鏈接,也就是三次握手結束。下面的參考圖,可以幫助理解這個現象:

clip_image014

4.6 函數accept

accept函數將等待隊列中以鏈接好的socket去除,作為返回值返回,同時還可以得到客戶端的地址結構,accept是會堵塞的,如果等待隊列中沒有准備好的socket,進程將會sleep直到有鏈接的socket,內核才會將進程喚醒。accept返回的socket與發起connect的socket對應,通過這個socket可以與客戶端通訊。而accept的參數中的socket是listen socket,專門用於監聽客戶socket。一般而言,服務器始終保持一個監聽socket。下面看看accept的接口描述:

#include <sys/socket.h>

/*

* @param sockfd: 監聽socket,也就是調用了listen后的socket

* @param saaddr: 客戶socket地址,如果不關心,可以傳NULL,但是len也必須NULL,

否則會有異常發生,有可能coredump,這點應該是可以保證的

* @param len: saaddr的長度,值-結果參數,如果為NULL,saaddr也必須為NULL

*/

int accept(int sockfd, struct sockaddr * saaddr, socklen &len );

4.7 函數fork和exec****

fork函數用於產生一個子進程,執行其他任務。初次使用時,估計比較難以理解,因為調用一次fork,卻返回兩次,一次在主進程,此時返回子進程ID;一次在子進程,此時返回0。所以,可以通過判斷返回值來判斷在子進程,還是在父進程,或是出錯誤。為什么需要這樣設計呢,因為父進程可能產生很多子進程,這種使父進程可以方便的維護子進程的ID,而子進程可以通過調用getppid和getpid,方便的獲取父進程的ID和本身的ID,返回0只是讓程序可以判斷當前實在子進程,可以執行子進程的邏輯。父進程在調用fork之前打開的file decriptor(例如socket, fd等等)均會在子進程中共享。下面看看如何fork函數的接口:

#include <uinstd.h>

/*

* 創建一個子進程,調用一次返回兩次

* @return: 0 in child, non-negative id of child in parent, -1 for error

*/

pid_t fork()

exec***系列函數有六個,其中有5個是封裝了參數,最后還是調用了系統函數execve,可以看看下面的圖示:

clip_image016

exec****調用的效果是,將當前進程上下文取代為參數中指定的應用程序,控制流重main開始。如果調用失敗,控制流還是返回到調用進程。此函數只是用一個程序取代當前進程,而不是創建一個新進程。調用exec****之前的文件描述符任然是開着的,但是可以通過fcntl設置FD_CLOEXEC來關閉這些文件描述符。使用exec***后,應為程序會中main開始,而不是像fork一樣從fork調用的地方開始,這樣原來打開的fd無法獲取,所以可以通過main參數的形式以字符串的形式傳過來,因為socket其實只是一個整數。

4.8 並發服務器

前面幾章中編寫的服務器是非並發,每一次服務器只能處理一個客戶端請求,其他客戶端請求不得不在listen queue中排隊,等待accept返回,如果多個客戶端同時請求,並且服務器對一個請求處理的時間相對較長,listen queue很快會滿,導致許多請求被拋棄。這一點是可以優化的,因為監聽請求和處理請求這可以並行,並不矛盾,所以可以通過並發的方式處理請求,這樣相比之前可以處理更多客戶端鏈接,提高服務器處理能力。並發服務器可以使用多線程實現,也可以使用多進程實現。前者實現相對復雜,因為子線程core dump會引起父線程core dump,導致整個服務器宕機。而多進程而不同,子進程與父進程相對獨立,子進程core dump不會影響父進程。下面的偽代碼演示了一個典型的多進程並發服務器:

listenFd = Socket(…);

Bind(listenFd,…);

Listen(listenFd,…);

while(true)

{

connFd = Accept(listenFd, …);

if ((pid = fork()) == 0) // 子進程

{

Close(listenFd); // 關閉監聽fd,因為子進程不監聽請求

Process(connFd); // 子進程處理請求

Close(connFd); // 使用完后,關閉connfd

exit(0);

}

Close(connFd); // 父進程, 關閉confd不會立刻關閉,只會將引用計數減一

}

….

上一章提到過,子進程會分享父進程調用fork之前的文件描述符(fd,socket等),那么你會看到父進程馬上關閉了connFd,這樣不會影響子進程嗎?答案是不會,內核會為每一個文件描述符維護一個引用計數,當fork成功后,內核會為共享的fd添加引用計數的值,所以父進程調用Close只會使connFd的引用計數減一,而不會真正的關閉連接,影響子進程,但是如果父進程不關閉connfd,那么打開socket不會在子進程中“真正”關閉,這樣socket使用完后沒有被內核回收,最后達到文件描述符上限,無法正確Accept,而且更重要的是所有的鏈接都沒有關閉,占用了機器的網絡資源,並且會影響客戶端那邊的響應(這個可以試一試)。而在子進程中,其實可以不用調用close,因為listen只會打開一次,而且整個服務器程序只有在結束的時候才會釋放,exit函數會自動釋放該進程打開的所有的文件描述符,所以可以不用顯示調用。子進程中顯示的調用Close是一種良好的編程習慣,標識你沒有忘記需要Close,釋放資源。

4.9 函數close

close不經作用與socket,也作用與file,但是行為不一樣,這里只描述針對socket的行為:

1) 標識不可用,即該socket再也不能用於connet,recv,send等操作

2) 將socket中沒有送完的數據全部送出去,並執行4此tcp握手斷開鏈接

3) 減少引用計數,如果該socket被多次打開

#include <unistd.h>

/*

* @return: 0 if ok, -1 if error

*/

int close (int sockfd);

4.10 函數getsockname和getpeername

這兩個函數用於獲取鏈接的socket兩端的地址結構,包括getsockname獲取本機地址,getpeername獲取對方地址,無論是服務器程序還是客戶端程序都是這樣。這里,使用name是可能有點歧義,因為其實返回的是地址結構(address structure),而不是name,下面來看看具體的接口描述:

#include <sys/socket.h>

/*

* @param sockfd: 如果connect成功的socket,可以得到內核任意分配的IP地址和端口,如果沒有connect,但是調用了bind,同樣可以通過此方法得到IP地址和端口,但是無論如何,調用此方法都是可以得到socket的family類型。此socket必須是accept返回的socket,而不是listen socket.

* @param addptr: 地址結構

* @param len : in-out參數,輸入時是addptr指向的數據結構長度,返回時是真正的數據長度

*return : 0 for ok, -1 for error

*/

int getsockname(int sockfd, struct sockaddr * addrptr, socklen_t len);

/*

* @param sockfd: 鏈接成功socket(可以嘗試一下傳入沒有鏈接的socket,不過一般不會將沒有鏈接的socket傳進去)

* @param

* @param addptr: 地址結構

* @param len : in-out參數,輸入時是addptr指向的數據結構長度,返回時是真正的數據長度

*return : 0 for ok, -1 for error

*/

int getpeername(int sockfd, struct sockaddr* addrptr, socklen_t* len);

第五章 TCP客戶端/服務器示例

5.1 章節簡介

本章通過一個簡單的echo服務器,介紹了TCP客戶端/服務器模型。實現十分簡單,主要是需要弄清楚一些邊界情況以及如何處理,這些邊界情況如下:

l 客戶端/服務器啟動時發生了什么?

l 客戶端正常結束時發生了什么?

l 服務器進程在客戶端完成之前停止會發生什么?

l 服務器崩潰時客戶端會發生什么?

5.7 正常結束

僵屍進程由於子進程的結束和父進程的運行是一個異步過程,即父進程永遠無法預測子進程到底什么時候結束。那么不會因為父進程太忙來不及wait子進程,或者說不知道子進程什么時候結束,而丟失子進程結束時的狀態信息呢? 不會。因為UNIX提供了一種機制可以保證只要父進程想知道子進程結束時的狀態信息,就可以得到。這種機制就是: 在每個進程退出的時候,內核釋放該進程所有的資源,包括打開的文件,占用的內存等。但是仍然為其保留一定的信息(包括進程號process ID,退出狀態termination status of the process,運行時間the amount of CPU time taken by the process等),直到父進程通過wait / waitpid來取時才釋放。但這樣就導致了問題,如果父進程不調用wait / waitpid的話,那么保留的那段信息就不會釋放,其PID就會一直被占用,但是系統所能使用的PID是有限的,如果大量的產生 僵死進程,將因為沒有可用的PID而導致系統不能產生新的進程。這個進程號沒有被釋放的進程,即為僵屍進程。僵屍進程的存在,是UNIX提供的一種父進程了解子進程結束的機制,使的父進程可以知道子進程是怎么“死的”,相當與子進程留下的“遺言”。只有父進程閱讀(調用wait或waitpid)了該“遺言”,Unix才會將器釋放。

正常開始流程

clip_image018

TCP正常連接需要3次握手,如上圖所示,這里echo客戶端發起鏈接:

l 客戶端調用connect函數,發送SYN分節

l 服務器接到后發送SYN和ACK兩個分節發給客戶端

l 客戶段接受后,connect函數返回,並發送一個ACK分節,socket處於ESATBLISHED狀態

l 服務器接收到ACK分節后,accept返回。

PS: 服務器調用listen后,客戶端既可以鏈接了,調用accept只是去除鏈接。

正常結束流程:

clip_image020

TCP正常斷開需要進程四次握手,如上圖所示,這里假設echo客戶端發起的tcp結束:

l 客戶端調用close(),發出一個FIN分節(segment)

l 服務器接受到FIN分節后,read函數返回0,結束讀取循環,並發送一個ACK分節

l 客戶端接受到ACK分節后,進入FIN_WAIT_2狀態

l 服務器子進程exit時,關閉所有描述符,也就是調用close,引發最后兩分節的收發

l 服務器發送FIN分節

l 客戶端接受FIN分節並發送ACK分節響應

l 如此這樣,TCP四次握手(4個分節的收發)完成,TCP斷開

l 在這里,由於echo server沒有調用wait函數,子進程“含冤而死”,變成僵屍進程

P.S.: 可以通過殺死僵死父進程而殺死所有的僵死進程

5.8 POSIX信號處理

當某件事情發生了,使用一個事件來告知進程,這個就是信號機制。信號有時又稱之為“軟件中斷”(PS:信號signal信號量semaphore是不同的)。信號是異步發生的,發生時可以通過回調函數執行指定操作。所以,進程是無法知道信號何時發生,它只知道信號發生了可以做些什么。信號的傳遞方式:

l A進程發給B進程(或自己)

l 內核發給進程

信號的三種處理方式,通過sigaction/signal函數注冊信號處理函數

l 自定義回調函數,

void signal_hander(int signal_no)

PS: SIGKILLSIGSTOP 無法被捕獲t

l 默認處理SIG_DFL

l 忽略處理SIG_IGN

sigaction()是POSIX的信號接口,而signal()是標准C的信號接口(如果程序必須在非POSIX系統上運行,那么就應該使用這個接口)
signal
是設置某信號的響應函數.
sigaction
可以設置新的信號處理函數act同時保留該信號原有的信號處理函數oldact
http://hi.baidu.com/zengzhaonong/blog/item/b046b8ef106b4917fdfa3c97.html

  #include <signal.h>
  typedef void (*sighandler_t)(int);
  sighandler_t signal(int signum, sighandler_t handler);

 
 


  int sigaction(int signum, const struct sigaction *act,
  struct sigaction *oldact);
  struct sigaction {
  void (*sa_handler)(int);
  void (*sa_sigaction)(int, siginfo_t *, void *);
  sigset_t sa_mask;
  int sa_flags;
  void (*sa_restorer)(void);
  };

 
 

5.9 處理信號SIGCHLD

The purpose of the zombie state is to maintain information about the child for the parent to fetch at some later time. This information includes the process ID of the child, its termination status, and information on the resource utilization of the child (CPU time, memory, etc.). If a process terminates, and that process has children in the zombie state, the parent process ID of all the zombie children is set to 1 (the init process), which will inherit the children and clean them up (i.e., init will wait for them, which removes the zombie). Some Unix systems show the COMMAND column for a zombie process as <defunct>.

所以,通過上面的論述可以知道:如果子進程被kill后,父進程仍在,子進程變成僵屍進程,這是UNIX的機制,無法改變。這些僵屍進程占用內存,更為嚴重的是占用進程ID資源,這樣導致后面無法fork,因為進程ID是有上限的。父進程需要自己清理這些僵屍進程,父進程可以通過注冊SIGCHLD信號,這樣可以在子進程被殺死時立刻收到通知,然后執行清理工作(調用waitwaitpid方法)。

處理被打斷的系統調用(Handling Interrupted System Calls

正如上面所說的,為了避免僵屍進程,需要通過注冊SIGCHLD消息。消息觸發,相當於發生了一次系統中斷,如果此時進程在系統調用(如accept,write,read),這些系統調用就被打斷。返回EINTR錯誤碼(該錯誤碼標識調用該函數時,被打斷)。有些unix系統會自動重啟該系統調用(如linux),而有些系統不會,所以如果你需要編寫跨平台的程序時,需要處理EINTR錯誤嗎,手動restart系統調用,如下面的實例代碼:

for ( ; ; ) {
     clilen = sizeof (cliaddr);
     if ( (connfd = accept (listenfd, (SA *) &cliaddr, &clilen)) < 0) {
         if (errno == EINTR)
             continue;         /* back to for () */
         else
             err_sys ("accept error");
}

5.9 wait和waitpid函數

unp中說道linux不會對信號排隊,所以多個信號同時過來,會丟失信號,所以使用了下面的代碼,來循環,盡可能的等待所有的僵死子進程。

void

sig_chld(int signo)

{

pid_t pid;

int stat;

while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)

printf("child %d terminated\n", pid);

return;

}

但是,這里有一點疑問,如果當主進程程序走到return這段代碼,有一個進程結束並觸發SIGCHLD信號,由於信號不排隊,所以這個信號丟失了,那么任然會出現僵屍進程。

但是,從運行tcpserv04中,發現這個現象並沒有出現。沒有僵死進程。首先,我認為是同時結束的子進程太少,在第一次while輪訓中,waitpid全部處理完。所以,我將tcpclit04中的5個子進程該成了100個。希望這樣在while一次不能全部處理完。

實驗證明,確實第一次while沒有處理完,而是經過多次while,最后waitpid了全部的子進程,沒有僵屍進程。

所以,我認為由於一次性結束的進程較多,在第一次while后,只執行了return就結束了當前的SIGCHLD事件處理函數,這樣后面SIGCHLD事件就可以被監聽到,只要一個被捕捉,通過while輪訓,最后還是可以waitpid所有的結束進程的。所以,最后沒有僵屍進程。

但是,我仍然想弄出一個出現僵屍進程的場景,我就將上面的代碼修改如下:

void

sig_chld(int signo)

{

pid_t pid;

int stat;

int iKilledCount = 0;

while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)

{

printf("child %d terminated\n", pid);

++iKilledCount;

}

sleep(3); // sleep 1 second so that it will not kill all zombie child

printf("Total of Kill Zombie Child : %d\n", iKilledCount);

return;

}

從上面的代碼可以看出,在return之前,使主進程睡三秒,在這三秒中內,應該可以將其他所有的SIGCHLD事件全部掩蓋掉(因為不排隊)。這樣,就可以出現僵死進程了,就可以證明這段代碼還是存在缺陷。但是,經過實驗證明,3秒過后,觸發了第二次SIGCHLD事件,處理了所有的子進程,最后沒有一個僵屍進程,:(。

ORZ,坑爹嗎?不是信號不會排隊嗎,這里不是明顯的排隊了嗎?

5.12 服務器進程結束(Termination of Server Process)

注意:這里是Termination of Server Process,不是Termination of Server Host。兩種現象的處理方式不同。

模擬此現象十分簡單

1 啟動服務器tcpserv04

2 使用tcpcli01鏈接服務器

3 鍵入一句話,確認一切OK

4 找到與tcpcli01鏈接的服務器子進程,並殺掉

5 采用的tcpserv04服務器版本,所以可以正確的處理僵屍進程

6 服務器子進程結束后,會關掉socket fd,這樣會發送一個FIN給客戶端,並收到一個ACK。由於客戶端正在堵塞在fgets方法,也就是等待用戶輸入,所以沒有意識到tcp收到了FIN 並發送了ACK。

7 當用戶輸入了一段畫並發送后,tcpcli01任然可以將這端話write給server,此時tcpserv04會直接返回一個RST分節給tcpcli01,標識socket fd已經關閉,此時tcpcli01的read會立刻返回,並報錯。

這里的主要問題是:

tcpcli01其實處理了兩個IO——one for network and one for user input。但是,程序程序確實線性的處理這兩個IO,當堵塞在user IO時,network IO發生了事情,進程也不會立刻察覺。這也就是為什么需要使用select和poll方法,引入IO多路歸並,這樣可以同時監聽多個IO的狀態變化,做到立刻反饋。

5.13 信號SIGPIPE

進程可以向收到FIN分節的socket寫入數據,導致另外一端發送RST分節,此時,進程如果向已經收到了RST分節的socket寫入,會導致SIGPIPE信號觸發(像一個已經關閉的socket寫入數據會導致SIGPIPE)。該信號的默認行為是結束進程,但是你可以通過忽略該信號,或者注冊SIGPIPE信號處理函數並返回,這樣就可以避免進程結束。如果你這樣做了,當調用write時,會返回EPIPE。此時,你知道的事實是:對方已經發送了一個RST分節,你不能在發送任何數據。知道這些信息后,你可以根據這些信息,處理你的業務邏輯。

5.15 服務器crash后重啟

如標題,如果客戶端在不知道情的情況下,服務器crash后重啟,由於之前的socket信息全部丟失,服務器的TCP會返回一個RST分節,標識socket,關閉,此時,客戶端調用write會返回錯誤碼ECONNRESET。所以,客戶端需要有一些邏輯處理服務器重啟。

5.16 服務器關機

當服務器關閉時,init線程會給所有其他線程發送一個SIGTERM信號,然后過5到10秒,發送SIGKILL信號(此信號無法注冊處理事件,必殺線程),所以每個進程有5到10秒的時間善后,然后被信號SIGKILL”一擊必殺”。

一點感悟(2011-11-19)

目前讀到這里(第五章結束),發現Linux內核其實為我們完成了許多異步的事情,第一個是listen函數。調用它之后,內核會為我們(這里指客戶進程)異步的管理其他接入的鏈接。第二個是信號處理,內核為我們管理信號的發生,發送和信號處理函數調用。第三個是TCP處理,內核幫我們完成了重傳,RST分節的的收發,我們用戶進程並不知曉。所以,其實處理多用戶並發請求,並不是一定要大量使用多線程或多進程,因為內核其實我們做了這些工作,我們要做的是高效的處理應用程序業務邏輯,合理的利用內核為我們提供的這些異步機制

上面的論點可能存在錯誤,希望通過后面的學習,在回過頭來看這個感悟時,我成長了。

第六章 I/O多路歸並,poll和select函數

6.2 I/O模型

下面概念的區別(參見http://www.cppblog.com/converse/archive/2009/05/13/82879.html):

l 同步/異步:被通知

l 阻塞/非阻塞:不被通知,自己詢問

上面的概念比較學院派,其實最主要的還是需要清除幾種I/O模型的執行原理,然后能夠正確高效的在設計應用程序時選取最適合的模型,這才是王道…

Unix提供的5種I/O模型

l 堵塞(Block I/O)

l 非堵塞(Nonblocking I/O)

l I/O多路歸並(I/O Multiplexing) ----select and poll

l 信號驅動I/O(Signal Driven I/O) ----SIGIO

l 異步I/O(Asynchronous I/O)----this POSIX aio_ functions

輸入操作的兩個階段

l 等待數據(socket就是數據到達,文件就是磁盤將數據加載到內存)

l 將數據重內核拷貝到應用程序進程

1 Block I/O Model

clip_image022

從上面的截圖,可以看出來,堵塞I/O模型中,進程調用recvfrom后,被堵塞,數據到達,並且拷貝到進程后,進程恢復,處理數據。輸入截斷的兩個過程全部被堵塞了,無法做任何事情,這個時候進程被內核sleep了。如果在這個過程,進程被信號打斷,有些unix系統不會重啟該過程,而返回錯誤碼,進程需要自己重啟這個過程。但是,有些unix系統會重啟該過程,應用程序不用關心被打斷后,重啟該系統調用。

2 Nonblocking I/O Model

clip_image024

從上圖可以看出,非堵塞的I/O模型中,recvfrom調用后,如果數據沒有准備好,會立刻返回一個錯誤碼,標識數據沒有達到。一般的應用程序,可以在一個while循環中不斷調用recvfrom,不端詢問,知道數據准備好,將數據讀取出來。並且在輪訓的間隙中,可以做一些其他的事情,這樣也不會浪費CPU。CPU使用效率比同步IO模型高。

3 I/O Multiplexing Model

clip_image026

I/O多路歸並結合了堵塞和非堵塞模型(可以設定),並且添加了多個I/O監控的特性,省了應用程序輪訓多個IO的操作,而且這些事情是內核完成,效率和質量上應該更有保證。

(還有一種模型類似此模型,就是使用多線程機制,每個線程調用堵塞的IO模型,這樣就可以實現同時監聽多個IO,但是引入多線程就有可能會引如使用多線程的麻煩,如線程同步,線程通信等等)

4 Signal Driven I/O Model

clip_image028

相比於非堵塞I/O,信號I/O省去了輪訓的過程,當數據准備好后,在回調函數中,將數據拷貝到進程,並處理數據。但是,只能監聽一個IO。

5 Asynchronous I/O Model

clip_image030

與信號IO類似,通過回調函數處理數據,唯一不同的是,但處理數據時,該數據已經在進程中了,而信號IO調喲昂回調函數時,數據還在內核中。異步IO的效率據說比信號IO高。

以上5種I/O模型的比較

clip_image032

前面四種均是在數據准備好后,才能處理。只有一邊IO是在數據拷貝到進程后,才處理數據。根據POSIX定義的同步與異步模型:

POSIX defines these two terms as follows:

· A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.

· An asynchronous I/O operation does not cause the requesting process to be blocked.

Using these definitions, the first four I/O models—blocking, nonblocking, I/O multiplexing, and signal-driven I/O—are all synchronous because the actual I/O operation (recvfrom) blocks the process. Only the asynchronous I/O model matches the asynchronous I/O definition.

只有異步IO屬於異步,前面始終全部屬於同步。

6.4 函數str_cli

unpv13e/select/tcpcli01.c 中有個bug,鏈接的端口是“7”而不是SERV_PORT(9877)。

修改后,重新連接,發現,一旦殺死服務器鏈接繼承,相比於5.12節的例子,客戶端會立刻響應,並輸出響應的錯誤信息。這一點是十分有意義的,因為在某些應用中,如果用戶輸入了很多,花費了很多時間,可是在輸入過程中,鏈接斷開,客戶端不能即時響應,當用戶輸入完畢並提交到服務器后,發現無法連接,這樣會很傷害用戶感情的。所以,I/O多路歸並在某些場合十分有用的。但是如果只有一個IO FD,感覺用I/O multiplexing沒有多大意義。

6.6 函數shutdown

此函數功能與close類似,但是可以更細節的控制close的行為,主要不同如下:

l 引用計數:close會降低引用計數,一但為0,才會關閉socket,而shutdown不會考慮引用計數,一旦調用立刻開啟結束的四次握手

l 單方關閉:shutdown提供參數進行單向關閉,比如關閉read或關閉write或全部關閉,通過SHUT_RD,SHUT_WR和SHUT_RDWR來指定。

shutdown函數

函數原型如下:

#include <sys/socket.h>

int shutdown(int sockfd, int howto);

Returns:0 if OK, -1 on error

howto參數可以為下面之中的一個

SHUT_RD socket讀的一半鏈接被關閉,具體現象:調用該函數后,不能接受任何數據任何處於接受buffer中的數據將被丟棄。函數調用之后,進程不能再正對該socket調用任何“讀”相關的函數。

SHUT_WR socket寫的一半鏈接被關閉,此現象也稱之為“半關閉”(half-close,注意只針對write half close),具體現象:任何寫buffer的數據將被發送,發送完后,將發送一個FIN分節。正如上面提到的,socket的應用計數不減一。任何“寫”相關的操作將不能作用於該socket。

SHUT_WRRD 相當於第一次調用“SHUT_RD”,然后調用“SHUT_WR”。

從上面的描述,可以看出,“關閉讀一半”與“關閉寫一半”還不能完全等價。

6.8 Tcp回射服務器

本節通過使用select的IO多路復用方式,使用單線程實現了回射服務器,但是發現單線程的實現會遭受Denial-of-Service攻擊,因為處理echo是通過堵塞的方式處理,惡意用戶可以發送單字節數據永遠堵塞服務器進程,其他用戶就無法使用服務了。所以,要么使用異步,要么采用多線(進)程,才能解決此問題。

6.10 Poll函數

poll函數的接口比select好用,因為使用select時,需要考慮系統的最大file descritor與fd max size的大小,而且如果fd數據很大,即時只有一個fd,貌似也要檢測所有的借點,這樣是不有點效率不高。但是poll卻不同,poll通過pollfd這樣一個數據結構的數組來維護需要檢查數據,所以設計得更好用,更合理一點。而且,使用pollfd,中的revents是out參數,events是in參數,不容易使用錯誤,不像select,fdset是一個in-out參數。

第七章 Socket可選項

7.2 函數‘getsockopt’和‘setsockopt’

這兩個函數的用途用來設置或讀取socket選項,函數接口如下:

#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname, void* optval, socklen_t *optlen);

int setsockopt(int sockfd, int level, int opnname, const void* optval, socklen_t optlen);

Both return: 0 if OK, -1 on error

參數簡單描述一下:

l sockfd: 被設置的socket file descriptor

l level: level specifies the code in the system that interprets the option: the general socket code or some protocol-specific code (e.g., IPv4, IPv6, TCP, or SCTP).具體值,見下表。

l optname: 選項常量,見下表

l optval: 被設置的值或讀取值

l optlen: 上面數據結構的大小

clip_image034

7.3 檢測socket選項是否支持並獲取默認值

這一節中學習到的最大技巧是c/c++可以這么簡介的初始化結構體,這樣就可以輕松的編寫數據驅動的測試用例,如下為代碼片段:

struct StartEndWithTestInfo

{

string m_sText;

string m_sFlag;

BOOL m_bWith;

} arrStartWithTestData[] ={

/**

* 空字符測試

*/

{"", "", M_TRUE},

{"some text start with", "", M_TRUE},

{"", "dfs", M_FALSE},

/**

* 成功的測試

*/

{"some text start with", "some", M_TRUE},

{"some", "some", M_TRUE},

/**

* 失敗的測試

*/

{"some text start with", "slfjjl", M_FALSE},

{"some", "some longger", M_FALSE}

},

arrEndWithTestData[] = {

/**

* 空字符測試

*/

{"", "", M_TRUE},

{"some text start with", "", M_TRUE},

{"", "dfs", M_FALSE},

/**

* 成功的測試

*/

{"some text start with", "with", M_TRUE},

{"some", "some", M_TRUE},

/**

* 失敗的測試

*/

{"some text start with", "wsfsfith", M_FALSE},

{"some", "some longger", M_FALSE},

};

可以看到,通過成員定義的順序,在大括號中初始化成員,great

7.4 通用的socket選項

The basic principle here is that a successful return from close, with the SO_LINGER socket option set, only tells us that the data we sent (and our FIN) have been acknowledged by the peer TCP. This does not tell us whether the peer application has read the data. If we do not set the SO_LINGER socket option, we do not know whether the peer TCP has acknowledged the data.

One way for the client to know that the server has read its data is to call shutdown (with a second argument of SHUT_WR) instead of close and wait for the peer to close its end of the connection. We show this scenario in Figure 7.10.

調用close的原則:close返回0,只能標識對方tcp接受到了完整的數據,但是不能保證應用程序進程獲得了所有的數據,這樣有可能應用程序進程在收到數據之前,就死掉了。

clip_image036

有一種方式可以保證客戶端知道服務器程序是否接受了最后的數據,調用shutdown。

clip_image038

7.11 函數fcntl

此函數用於設置socket或其他文件描述的屬性,比如可以將socket設置為non-blocking或者是signal IO。此函數的接口如下:

#include <fcntl.h>

int fcntl(int fd, int cmd, … /* int arg */);

return : depends on cmd if ok, -1 on error

典型的用法是先去處原來的設置,然后通過邏輯或將需要的設置添加上去,而不是直接設置需要的值:

正確的做法

int     flags;
 
/* Set a socket as nonblocking */
if  ( (flags = fcntl (fd, F_GETFL, 0)) < 0)
    err_sys("F_GETFL error");
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0)
    err_sys("F_SETFL error");

錯誤的做法:

/* Wrong way to set a socket as nonblocking */
if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0)
    err_sys("F_SETFL error");

第八章 UDP套接字基礎

8.2函數recvfrom和sendto

這兩個函數與tcp套接字的recv/send相似,但是多了一些參數,可以用下面的比喻來加深理解:

recvfrom = recv + accpet

sendto = send + connect

下面看看具體的函數接口:

#include <sys/socket.h>

/**

* @param flags 配置,如果不關心,可以設置為0

* @param from [out] 客戶socket的地址

* @param addrlen [in-out] 客戶端地址的長度

* from 與 addrlen同時為NULL時,表明不關系客戶端地址

*/

ssize_t recvfrom(int sock, void* buff, size_t nbytes, int flags,

struct sockaddr* from, socklen_t* addrlen);

/**

* 與上面的類似,更具函數名稱自己可以推斷

*/

ssize_t sendto(int sock, const void* buf, size_t nybtes, int flags,

const struct sockaddr* to, sockelen_t addrlen);

Both Return: number of data recv or send if OK, or -1 on error

8.8 認證接受的響應

由於UDP沒有鏈接,所以client接收一個來自其他server的datagram,因為只要任何一個server知道了client的臨時port,都可以向他發包。所以,可以通過recvfrom的最后兩個參數,用於和發送地址比對,如果相同,則說明是服務器發送過來的,否則不是。但是,這仍然有一個問題,那就是server的IP使用通配符綁定的,如果一個server有多個網卡時,那么就有可能通過不同的IP來發送echo,那么client的這種策略就失效了,會出現誤判。

8.9 不運行服務器

如果在運行服務器的情況下,直接運行客戶端,會發生什么情況呢?

客戶端會永遠停留在recefrom上。因為udp發送了數據之后,成功返回,並不知道包是否已近到達了目的地,所以及時沒有到,客戶端也不知道。但是,ICMP會發送一個異步的error。這個error是不會通知給客戶端的,必須通過調用connect,才能顯示的被通知。

8.11 UDP的connect函數

connect函數對於udp socket,不會有三次握手,更不會建立鏈接,但是與不掉用connect的udp socket有下面三點不同

l 發送數據不需要使用sendto,而是write,send或sendmsg,地址不需要傳

l 接受數據不需要使用recvfrom,而是read,recv或recvmsg,地址不需要(可以避免誤接受)

l 可以同步接受異步錯誤

好處

l 數據包不會發錯,也就是上面8.8節的問題不會出現,因為datagram中記錄了兩端的IP和PORT,所以UDP可以區分,

l 編寫程序時,不需要總是傳輸重復的目標IP和端口

l 效率變高,目標地址和端口只需要一次拷貝,

8.13 UDP沒有流量控制機制

UDP沒有流浪控制機制,也就是說,如果一個客戶端發包速度過快,UDP socket的緩存裝滿后,多的包會被UDP丟棄掉。但是可以通過設置第七章談到的socket receiving buffer,加大緩存大小,這樣可以稍微緩解丟包,但是不可能重根本上避免丟包現象。

第十三章 守護進程和超級服務器inetd

13.1 介紹

守護進程沒有與終端關聯,默默的在后台運行。這樣就不會被終端打擾(也就是用戶),如關閉終端時會自動關閉該終端開啟的非守護進程的相關程序。

13.5 守護進程“inetd”

inetd是一個通用的並行服務器,為每個服務器程序處理了並行(fork,daemon)和網絡(socket,bind,accept),並且將socket通信重定向到fd 0,1,2,這樣編寫服務器程序就想編寫一般的程序一樣,只是輸入和輸出都是與遠端的socket通訊。inetd有點像是一個服務器‘容器’,只是功能過於簡單。

第十四章 高進I/O函數

14.2 Sockets超時

三種方式設置socket超時

1. 信號SIGALARM:通過signal函數,注冊SIGALRM信號處理函數,通過alarm函數,設定超時。由於使用信號機制,不建議在多線程環境下使用,因為會十分復雜。

2. Select函數:調用select函數

3. Socket選項SO_RCVTIMEO 和 SO_SNDTIMEO:每個fd只需要設置一次,但是有點平台不兼容,不是每個平台至此這個選項

第十五章 Unix Domain協議

15.1 介紹

Unix Domain協議是一個特殊的IP/TCP協議,用於本地服務區客戶端通過網絡API通訊,可以作為IPC的一種方式,但是效率傳統的IP/TCP的2倍,同時它可以傳輸fd,並且有額外的安全檢測。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM