LINUX網絡編程的讀書筆記


LINUX網絡編程的讀書筆記
 
第一章文件系統和進程系統
1.1文件系統的總體結構
       從文件系統的實現角度來看,按層次可以分成應用程序、系統調用、文件子系統、高速緩衝、設備驅動和具體的存儲設備等幾個層次,如下圖:
       應用程序
系統調用接口
文件子系統
硬件存儲設備
設備驅動程序
高速緩存
字符設備
塊設備

在UNIX系統中,程序不管核心按照什麼樣的格式來組織文件,只是把文件看作一個無格式的字節流來看待。對文件的存取語法是由系統定義的,數據的語義是由程序加上去的。
應用進程通過系統調用來訪問文件系統,分配給應用程序一個標準的通用接口, 便於屏蔽不同文件系統的差異。文件系統不能直接訪問硬件設備,通過調用設備驅動進程來操作具體設備。對高速設備的訪問,通常通過高速緩衝機制來提高設備和內存的數據交換。設備驅動進程用來屏蔽不同物理設備的操作差異。
 
文件系統的總體結構是:引導塊、超級塊、索引節點表,數據區。
·引導塊在文件系統的最前面,它和操作系統引導有關。有且只有一個引導塊有效。
·超級塊也叫管理塊,存放文件系統的管理信息,如文件系統大小、空閒塊大小、空閒塊鏈表節點頭等信息。
·索引節點表,每個文件都對應着一個索引節點,裏面反正用戶的存取權限、信息等。通過路徑訪問文件,內核把文件路徑經過轉換映射到索引節點表中對應節點去。
·數據區。文件系統實際存放數據的磁盤空間。
·空閒數據塊表。超級塊中空間很小,所以把空閒數據塊的信息寫在數據區中。
 
VFS(Virtual Filesystem Switch)
LINUX通過虛擬文件系統轉換來實現多文件系統的支持。LINUX把對文件操作的系統調用轉爲對不通過文件系統操作的子程序調用,這些子程序都針對具體文件系統而編寫。虛擬文件系統不是真正的文件系統,而是一種映射機制來屏蔽下層的差異爲上層提供方便。
 
 
1.2 文件結構和目錄結構
LINUX中的每個文件都對應虛擬文件系統的一個索引節點,裏面存放有直接或多級指針能夠記錄文件的數據,這樣設計是爲了存取大文件。
 
目錄也能抽象成文件,也通過索引節點表來描述,並且把目錄表中的目錄項存放在數據區中。目錄表的基本構成單位是目錄項,有“文件名-索引節點號”構成。文件節點索引表中並不包含文件名這個信息,文件名被填寫在目錄文件中。
 
·硬連接和符號(軟)連接的區別:
硬連接能實現的功能符號連接都能實現。硬連接只能用在文件(非目錄)和同一個文件系統,但是符號連接適用在目錄,也適用在不同的文件系統間。但是符號連接比硬連接更消耗內核資源,因爲符號連接的轉換規則是在內核中實現的,而硬連接則直接指向索引節點。
硬連接是文件名和索引節點的對應關係;符號連接是指向文件的路徑
 
·文件系統相關編程:
從系統的實現角度來看,文件內在表示是唯一確定的索引節點。如果從編程角度來看,文件可以通過文件描述符和文件指針來表示。UNIX I/O庫中有open,write,read,close,ioctl等系統調用來操作文件描述符。
在C庫函數中,有fopen, fprintf, fread, fwrite, fclose等文件操作函數對文件指針進行處理,它們是對系統調用的再次封裝。
 
從系統角度來說:文件句柄就是文件的一種標誌,是文件描述符表中的索引號。進程的標誌輸入、輸出和錯誤輸出的文件描述符分別是0,1,2在unistd.h中將它們定義爲STDIN_FILENO, STDOUT_FILENO和STDERR_FILENO。
從C函數庫角度來說:文件句柄是一個指向文件結構的指針。。進程的標誌輸入、輸出和錯誤輸出在stdio.h中被定義爲stdin, stdout, stderr。可以使用系統調用fileno()將一個文件指針轉爲文件描述符。
 
 
1.3 進程系統
·程序並行執行中的問題:
靜態程序的概念不能很好描述並行環境下的規律,因此引入的進程的概念。單道程序設計中,環境是封閉的,資源總被獨佔;而在並行環境中由於封閉性和資源的獨佔性被破壞,這將導致很多問題。
 
·進程和程序的區別:
程序是指令和數據的集合,是一個靜態文本,存放在一個普通的文件中,該文件在索引節點表中的文件標誌爲“可執行”。
進程是程序在一個包括指令段、系統和用戶數據的環境中,爲了完成預定的任務而運行一次的過程。進程被撤銷後就不再存在,而程序的文本依然留在系統中。
 
·進程的物理表示:
爲了描述動態變化的進程,我們把進程靜態的分爲3個部分:程序部分、數據部分、進程控制塊——統一稱爲進程映像。
 
進程的程序部分可以被多個程序所共享,共享代碼段應該被編寫成純代碼puer code,即該程序段的功能不隨着調用的程序不同而存在差異。
程序段被執行的數據區和工作單元,當執行的不是共享代碼段時,數據的一部分就被放入數據空間。
每個進程都有一個進程控制塊PCB,用來跟蹤並記錄動態變化的進程執行和調動信息的數據結構,集中體現了進程的特徵、狀態和其他進程間的關係等。
 
·進程的虛空間:
操作系統的虛空間可以分成“進程虛空間”和“系統虛空間”。
 
可執行程序的指令和數據對應着進程虛空間的地址,由操作系統把進程虛空間地址映射到物理內存上。這種映射是通過硬件寄存器和系統頁表共同實現的。
 
·用戶態和核心態:
用戶態和核心態實際上是CPU工作的兩種不同模式。所有內核對外提供的功能都是按系統調用的形式。進程進行一次系統調用,CPU將在用戶態與核心態間切換一次,系統調用工作在覈心棧,而普通用戶調用將使用用戶棧。
 
·進程上下文:
進程在生命期的所有狀態都可以通過進程上下文來描述。通常包括三個內容:
 1 用戶級上下文:包括代碼段、數據段、用戶段和共享內存段。
 2 寄存器上下文:進程運行時各寄存器的內容
 3 系統級上下文:進程控制塊、進程使用的頁表和核心棧
 
·進程轉換
stop
 
continue
 
wakeup
 
continue
 
stop
 
wakeup
 
stop
 
switch
 
switch
 
fork
 
fork
 
系統調用或中斷
 
系統調用或中斷返回
 
wait
 
sleep
 
exit
 
用戶態運行
核心態運行
就緒
暫停
睡眠
初始空間
殭屍狀態
暫停+睡眠

 
·進程調度:
核心將在幾種情況下調用調度管理器:當前進程被放入等待隊列或者系統調用結束時,以及從核心態返回到用戶態時。
(1)    LINUX支持兩類不同進程:普通與實時進程,不同之處體現在優先級和調度策略上。
(2)    如果一個實時進程處於可執行狀態,它總在任何普通進程前執行。
(3)    實時進程採用兩種調用策略:時間片輪轉和先進先出。
(4)    普通進程採用Round Robin策略。
(5)    priority進程優先級、rt_priority實時進程優先級、counter進程運行運行時間
 
·對fork的理解:
fork之後父子進程的tast_struc除了進程號,其他的數據都一樣。利用虛空間技術,共享代碼段(引用計數加1),複製數據段。fork快完成的某階段子進程被建立並保存上下文進入就緒隊列等待調度,fork完畢之後父進程上下文被保存,返回子進程的進程標識符。注意:子進程的fork調用返回是0,父進程fork調用返回是子進程的進程號。然後父子進程從fork的調用點開始分別繼續運行。
父進程退出前需要使用wait()或waipid()等待子進程執行完畢和清除殭屍進程釋放資源。
 
第二章 進程間通信和同步
前言:在linux/unix中支持多種進程間通信(IPC)的方式,主要包括:信號、信號量、消息隊列和共享內存,管道(包括無名管道和FIFO)也是進程間通信的方式。
 
·2,2信號的捕獲和處理:
#inlucde <signal.h> //參見POSIX.1中定義
相關函數:
sigaction(int signo, const struct sigaction *act, struct sigaction *oact); //設置信號處理器
struct sigaction{
       void (*sa_handler)();
       sigset_t sa_mask;
       int sa_flags;
};
(1) 信號處理器函數指針 (2)進程屏蔽的信號集合 (3)信號處理器的標誌(查閱手冊)
 
int sigemptyset(sigset-t *set);              //信號集合清空
int sigfillset(sigset_t *set);            //設置包含所有信號的全集
int sigaddset(sigset_t *set, int signo);    //把一個信號加入信號集合
int sigdelset(sigset_t *set, int signo);     //把一個信號從集合裏刪除
int sigismember(const sigset_t *set, int signo);    //判斷信號是否包含在給定集合中
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);     //設置進程中斷屏蔽碼
how = [SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK], *oset對設置前屏蔽碼做備份
 
使用信號處理器基本方法:
1.       編寫信號處理函數handler_sigproc();
//信號處理函數執行完畢的最後,記得要清堵塞的信號
//sigaddset(&blockmask, SIGINT);     //信號處理器缺省堵塞的信號
//sigaddset(&blockmask, SIGTERM); //信號處理器處理的信號
//sigprocmask(SIG_BLOCK, &blockmask, NULL); //清堵塞信號
2.       設置信號處理器struct action act;
act.sa_handler = handler_sigproc;
sigemptyset(act.sa_mask);
sigaddset(&act.sa_mask, SIGTERM); //信號處理器執行期間堵塞相應的信號
sigaction(SIGTERM, &act, NULL);//將(kill產生)終止信號加入act信號處理器
 
快系統調用、慢系統調用都可能被信號打斷,POSIX.1把被中斷的系統調用返回-1,errno設置爲EINTER,只要不是“原子操作”都可能被打斷,注意對這類問題的容錯處理:
ret = read(fd, buf, 255);
if (ret == -1 && errno == EINTER) //如果 (系統調用是由中斷引起的執行失敗) 則……
 
·2.3 信號量
有名信號量是全局,只要知道它的名字就可以使用它;
無名信號量是局部,只能通過繼承才能使用它;
 
相關函數:
頭文件:<sys/types.h>, <sys/ipc.h>, <sys/sem.h>
int semget(key_t key, int nsems, int semflg);              //創建或取得一個信號量組
int semctl(int sem_id, int semnum, int cmd);     //信號量控制函數(取值/刪除/設置等)
int semop(int semid, struct sembuf *sops, int nsops); //信號量操作函數
(1)    信號量組ID (2)進行怎樣操作(3)操作次數
struct sembuf{
       short sem_num;     //對信號量組第sem_num個進行操作
       short sem_op;        //對信號量sem_value執行 -1是P操作,+1是V操作
       short sem_flg;        //通常取0,如果使用SEM_UNDO退出進程後,信號量值變爲0
};
使用信號量基本流程:
1. sem_id = semget(SEM_KEY,0,0); //SEM_KEY自定義,要確保唯一性
2. if (sem_id != -1) //如果信號量組不存在
              sem_id = semget(SEM_KEY, SEM_NUM, IPC_CREAT|IPC_EXCL|0666)
        ...//創建資源爲SEM_NUM個的一個信號量組,權限爲0666(可讀寫)
else 初始化信號量組的信號量資源個數
3實現P和V操作函數:
void P(int sem_num, int sem_id)//對信號量組sem_id的第sem_num個信號量操作
{
       struct sembuf sem[1];
       sem[0].sem_num=sem_num; sem[0].sem_op = -1; sem[0].sem_flg = 0;
       if (semop(sem_id, sem, 1) == -1) //... 執行一次P操作,V操作類似
}
4 semctl(sem_id, sem_index, IPC_RMID); //手動刪除信號量組
//注意“信號量組”和“信號量值”的區別!
 
·2.4 消息隊列
#include <sys/msg.h>
int msgget(key_t key, int msgflg)); //創建或取得消息隊列的ID,和信號量組類似
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
int msgsnd(int msqid, void *msgp, size_t msgsz, int msgflg));
int msgrcv(int msqid, void *msgp, size_t msgsz, long int msgtyp, int msgflg)); //接收消息
//msgtyp=0:返回第一個消息 >0:返回第一個值=msgtyp的消息 <0:返回第一個值<=-msgtyp
 
 
消息隊列使用基本原理:
子進程child發送首次登記的標誌FLAG(msgtyp>0)和child進程號到服務器進程server註冊,在server段使用msgrcv(Q_MSG_KEY, &recv_buf, sizeof(Message)-sizeof(long), FLAG, 0)接收,Message正文不包括消息頭的標誌。然後server端發送server進程號,接收消息標誌爲子進程號的Message到子進程表示接收到先前消息。
 
―――――――――――――――――――――――――――――――――――――――
Message send_msg;              //首次登記並提交本進程的ID
send_msg.m_type = FLAG;
send_msg.process_id = getpid();
send_len = sizeof(long)+sizeof(int);
ret = msgsnd(msq_key, &send_msg, send_len, 0);
Message recv_msg, reply_msg;
//接收標誌爲FLAG的消息
ret = msgrcv(msq_key, &recv_msg, sizeof(Message)-sizeof(long), getpid(),FLAG,0);
reply_msg.m_type = recv_msg.process_id;   //向子進程發送反饋消息
reply_msg.process_id = getpid()          //告訴子進程服務端server進程號,準備建立交互
 
 
消息隊列通過消息標誌(即進程號)進行通信,如果客戶/服務端進程有任何一方退出,則可能會出現消息丟失。即把退出一方的進程號作爲標誌的消息不會被任何進程接收,因爲其他的進程號和消息標誌不匹配。
 
建立連接開始數據通信
回覆反饋信息
發送註冊信息
Server
Client
消息隊列通信

·2.5 共享內存
共享內存就是多個進程共享一端物理內存空間,通過把一段物理內存地址映射不同的虛空間來實現,而消息隊列是把數據從應用緩衝區到核心緩衝區往返復制。因此共享內存的通信使用效率比消息隊列高,但存在複雜的同步互斥關係。
 
函數:
int shmget((key_t key, int size, int shmflg));                      //創建或取得一塊共享內存
int shmctl((int shmid, int cmd, struct shmid_ds *buf));             //共享內存操作
void *shmat((int shmid, const void *shmaddr, int shmflg));       //獲取共享內存的指針
int shmdt((const void *shmaddr));                                   //將共享內存塊從進程中分離/刪除
 
 
·小結:
消息隊列可以進行多路複用,進程間同步不需要複雜的同步互斥。數據以流的方式傳遞,
各消息都是獨立且可以區分的。信息的具體語義需要收發兩端的進程自己去定義和解釋。消
息隊列的缺點是需要進行兩次數據複製,從用戶空間到核心,在從核心到用戶空間。
共享內存不需要多次拷貝,在數據量大的進程間同步中,用共享內存方式可以提高效率,
但是會需要複雜的進程間同步互斥控制。
 
 
第三章 TCP/IP協議
·3.1 OSI參考模型、協議和服務
物理層實現在通信信道的0、1比特傳輸;
數據鏈路層加強了比特傳輸功能,將01比特組織成數據幀實現可靠傳輸;
網絡層主要實現路由選擇,確定端到端的傳輸路徑;
傳輸層實現點對點的無差錯數據傳輸;
會話層主要實現用戶會話關係和同步的管理;
表示層消除信息的語法和語義的差別;
應用層面向不同需求,實現不同功能。
 
·3.2 TCP和UDP的比較
TCP實現了面向連接的、端對端的可靠流傳輸。TCP爲其可靠性做的最重要的工作有:確認和超時重發、以及流量控制等。適合在傳輸數據可靠性較高的應用。
UDP建立在IP協議上,利用IP包提供一種無連接的高效服務。但它不考慮數據包的正確和可靠性,需要應用程序自己來處理。適合在實時、數據量較小或網絡通信可靠時的應用。
 
·3.3 傳輸層端口
       傳輸層和網絡層最重要的區別是提供了“進程到進程的”通信能力,而網絡層只能將IP包尋找到主機。實現進程間通信,除了主機地址還需要進程標誌。
       TCP, UDP提出了協議端口的概念,此端口是軟件端口不同於硬件端口。TCP/IP實現中對端口操作被設計成如同一般文件的操作。TCP和UDP的端口是完全獨立的,即使在同一個進程裏使用如9999/TCP和9999/UDP的端口號,它們是不會起衝突的。
       端口分配有兩種:1 全局分配,即集中控制方式,由權威機構根據需要統一分配;2 本地分配,進程需要傳輸服務時向系統動態申請,操作系統根據當前系統端口使用情況返回本地唯一的端口號。由於端口的唯一性,也可以來標示一個進程。
       TCP/IP把端口分爲兩部分:1 少量的保留端口 2 自由端口,由進程進行通信前申請。
 
·3.4 域名系統和名字服務器
       域名解析就是實現IP地址和主機名字間一一對應關係。正向解析是從域名映射到IP地址,反向解析是從IP地址得到域名。在TCP/IP中,名字和地址間轉換是由一組相互獨立和協作的服務器軟件來完成,即名字服務器。
 
·3.5 TCP協議
1. TCP的確認機制
       TCP傳輸數據是以字節流方式,流中的數據是一個字節構成的序列,序列結構由應用程序解釋,TCP的基本傳輸單元是TCP數據段。當接收端收到數據後,如果數據正確TCP將發送確認信息給發送端,確認值是下一個字節的序列ACK。表明發送端的ACK之前的序列都已被正確接收。
 
2. TCP的超時重傳機制
TCP在發送一個數據包後,數據信息還保留在緩衝區中,直到接收端發送確認信息後才刪除它們。如果一段時間後沒有收到接收端確認,那麼發送端將重發該數據包然後等待再次確認。如果超時重發到達一定次數,那麼發送方認爲對端不可到達,斷開TCP連接。
       TCP採用一種自適應的確定定時時長算法。定義RTT(round trip time)爲發出數據包到數據包確認之間的時間長度。TCP檢查每一連接的性能,根據變化重新計算RTT值:
Timeout = β× RTT
RTT = α×old_RTT + (1-α) × new_RTT_Sample
其中α決定RTT對時延變化反映的速度,如果α接近1,則時間變化不影響RTT值,如果接近0那麼RTT將隨時延快速變化。
 
       TCP數據段數據結構,略;
 
3. TCP的滑動窗口協議
       TCP通過滑動窗口協議實現擁擠控制,即發送方最多隻能發送控制窗口大小的數據,當有接收方發送來的數據確認,發送才繼續進行。控制窗口的大小由兩個因素決定:一個是發送方自身的擁塞窗口控制;而是控制窗口大小是發送和接收兩方中的最小值。
 
4. TCP的“慢啓動”策略
當TCP發現丟失數據則認爲網絡擁擠,擁塞窗口大小就減半。當TCP認爲擁塞結束,就使用“慢啓動”策略:每收到一個數據包擁塞窗口加1,直到窗口數達到上次發生擁塞時窗口一半時候,這是隻有發送出去的所有數據包都得到迴應,擁擠窗口才加1。
 
5. 小結
TCP通過確認和超時重傳機制保證數據包的可靠性;利用滑動窗口協議和“慢啓動”策略進行流量控制。UDP協議沒有上述功能,所以實時性好,但可靠性差。
 
·3.6 TCP的狀態轉移過程
       一個TCP連接在它的生命週期中,將經歷一系列狀態:LISTEN, SYN-SENT, SYN-RECEIVED, ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT, CLOSED。
 
       TCP連接建立過程,由A端的TCP發送請求,對端B的TCP迴應:
(1)    A --> B SYN my sequence number is X       <SEQ=X><CTL=SYN>
(2)    A <-- B ACK your sequence number is X     
(3)    A --> B SYN my sequence number is Y        <SEQ=Y><ACK=X><CTL=SYN,ACK>
(4)    A <-- B ACK your sequence number is Y       <SEQ=X+1><ACK=Y+1><CTL=SYN,ACK>
A和B發送自己的同步SYN信息給對方後,在SYN中包括本端初始的數據序列號,並且需要接收對方對自身發從的SYN的ACK確認。這個過程稱爲“三次握手”,共發送了3個數據包傳遞了4個信息。
 
       TCP連接關閉前設置了TIME-WAIT狀態,TCP將在等待2MSL(Maximum Segment Lifetime)時間後進入CLOSED狀態。其中MSL是數據段在網絡中最大生存時間。對端TCP在發送FIN數據端進行關閉確認時候,由於IP協議不可靠傳輸,可能主動方發送的確認數據包還未到達對端,對端就開始進行超時重發,如果這時主動端關閉TCP連接,那麼TCP協議會認爲發生網絡連接錯誤,將發送RST舊連接數據段。因此主動端關閉TCP前等待2MSL時間,將確保發送和接收端的數據包在網絡中消失。由A端主動發起連接斷開狀態圖:
TCP A                                           TCP B
   ESTABLISHED                                    ESTABLISHED
     (主動關閉)
   FIN-WAIT-1 --><SEQ=X><ACK=Y><CTL=FIN,ACK>  -->CLOSE-WAIT
   FIN-WAIT-2 <--<SEQ=Y><ACK=X+1><CTL= ACK>   <--CLOSE-WAIT
                                                       (被動關閉)
   TIME-WAIT <--<SEQ=Y><ACK=X+1><CTL=FIN,ACK><--LAST-ACK
   TIME-WAIT --><SEQ=X+1><ACK=Y+1><CTL=FIN,ACK>--> CLOSED
     (等待2MSL)
   CLOSED
 
       IP數據包格式,略
       ICMP協議產生的控制報文放在IP數據包裏,通過IP數據包發送到制定地點。
 
       ACK q+1
FIN q
ACK p
FIN p
………………
DATA, 帶回確認
DATA, 帶回確認
DATA
ACK=b+1
SYN=b, ACK=a+1,
MSS=1460
SYN=a, MSS=1460
一個正常的TCP通信過程
connect主動請求發送後狀態:SYN-SEND
Client
Server
LISTEN正在傾聽accept
發送後狀態SYN-RVCD
ESTABLISH
connect返回
ESTABLISH
accept返回
write數據
read數據,
處理數據,
write寫回結果
read數據
close主動要求關閉連接:
FIN-WAIT-1
被動關閉
CLOSE-WAIT
FIN-WAIT-2
(等待2MSL)
CLOSED
TIME-WAIT
close
LAST-ACK
CLOSED
TCP最初需要進行MSS(Maxium Segment Size)協商,否則數據包可能被分段後再重組

 
 
 
 
 
 
 
 
第四章 基本套接字編程
·4.1 基本套接字函數族
頭文件:<sys/types.h>, <sys/socket.h>
主要函數:
int socket(int domain, int type, int protocol);              //創建socket描述符
[domain=AF_UNIX,AF_INET,AF_ISO; type=SOCK_STREAM,SOCK_DGRAM,SOCK_RAW;]
 
int connect(int sockfd, struct sockaddr* servaddr, int addrlen); //向服務器發送連接請求
int bind(int sockfd, struct sockaddr* myaddr, int addrlen);     //向系統登記一個固定端口
int listen(int sockfd, int backlog);                                        //被動傾聽套接字的指定端口
int accept(int sockfd, struct sockaddr* addr, int addrlen); //從完全連接隊列接收一個連接套接
[如果完全連接隊列爲空隊列則堵塞,或直接返回-1,accept成功返回一個(繼承傾聽套接字屬性的)連接套接字描述符]
int close(int sockfd);                                         //關閉套接字描述符
int shutdown(int sockfd, int howto);                             //關閉套接字描述符讀寫信道
[howto = SHUT_RD, SHUT_WR, SHUT_RDWR]
 
網絡字序轉換函數族:
頭文件:<netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);            //host to network long 字序轉換
unsigned long int htons(unsigned long int hostlong);           //host to network short 字序轉換
unsigned long int ntohl(unsigned long int hostlong);            //network to host long 字序轉換
unsigned long int ntohs(unsigned long int hostlong);      //network to host short 字序轉換
 
IP地址轉換函數族
頭文件:<sys/socket.h>, <netinet/in.h>, <arpa/inet.h>
主要函數:
int inet_aton(const char *cp, struct in_addr *inp);      //將字符串表示IP地址轉struct in_addr表示
unsigned long int inet_addr(const char *cp);    //將字符串表示IP地址轉32 bits表示
char *inet_ntoa(struct in_addr in);                  //將struct in_addr表示IP地址轉字符串表示
 
服務器應答
客戶發送數據
客戶請求建立連接
socket()
套接字編程基本流程
bind()
listen()
accept()
堵塞等待請求
read()
write()
close()
socket()
connect()
write()
read()
close()
Client
Server
NOTE:
通常客戶端不需要bind端口,系統動態分配

示例代碼:
listen_fd = socket(AF_INET, SOCK_STREAM, 0); //創建Internet協議簇,流類型套接字
if (listen_fd == -1)        error_proc();
bzero(&serv_addr, sizeof(serv_addr)); //初始化服務器地址結構
serv_addr.sin_family = AF_INET;   //使用Internet協議簇
serv_addr.sin_port = htons(端口號); //設置端口號,並轉換爲網絡字序
/*如果是客戶端執行
 *ret = inet_aton(“127.0.0.1”, &serv_addr.sin_addr); //爲socketaddr地址結構設置IP地址
 *然後bind();指定端口,接着connect(); 服務器端請求建立連接,開始“三次握手”協議
 *如果是服務器端則執行 */
serv_addr.sinaddr.s_addr = htonl(INADDR_ANY); //允許任何網絡設備接口連接並處理
ret = bind(listen_fd, (struct sockaddr*)&serv_addr,sizeof(serv_addr)); //套接字綁定指定端口
if (ret < 0) error_proc();
listen(listen_fd, 傾聽隊列長度);    //轉爲傾聽套接字,設置傾聽隊列長度
//從完全連接隊列中接受一個新連接,返回連接套接字描述符
conn_fd = accept(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
 
·併發服務器模式
 /*
       0 服務器端套接字初始化,
       1服務器端的父進程用傾聽套接字X,傾聽來自客戶端的請求連接,
2當accept一個連接套接字Y時候fork子進程專門處理客戶端的數據處理,
3子進程關閉傾聽套接字X不使用,處理客戶端請求,完畢後關閉Y,退出,
4而父進程關閉連接套接字描述符Y不使用,繼續傾聽新的客戶端連接(轉過程2)。
   */
do{
       /*過程2; 3; 4; 的實現參考代碼*/
//從完全傾聽隊列中接收一個連接套接字描述符
       conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, sizeof(cli_addr));
       if (conn_fd < 0)
error_proc();        //錯誤處理
       switch ( ret = fork() ){
       case -1:
              error_proc();        //錯誤處理
    case 0:
              close(listen_fd);      //關閉傾聽套接字描述符
              serv_for(conn_fd);    //爲客戶端提供服務
              exit(0);
default:
       close(conn_fd);       //關閉連接套接字描述符
}
} while (continue);
 
 
 
第五章 無堵塞套接字和單進程輪詢服務器
·5.1 無堵塞套接字
       堵塞套接字在等待輸入/輸出時會進入睡眠,不能繼續其他的操作。在併發服務器模式下這一缺點並不明顯,但在一些複雜應用中可能需要在單進程中爲多個連接服務,這時堵塞套接字會大大降低效率。另外,進程可能一直被堵塞。比如服務器端崩潰,而客戶端並不知道,此時客戶端進程將一直堵塞。
       無堵塞套接字會對讀、寫、建立連接、接收連接過程產生影響。總的來說就是不等待所有資源到齊,而立即操作並返回,這點在和處理無堵塞套接字上有一些區別。如果本已堵塞而由於使用無堵塞套接字,那麼errno將返回EWOULDBLOCK,通過下面語句可以判斷:
ret = accept(...);
if (ret < 0 & errno != EWOULDBLOCK) //如果錯誤返回並且錯誤原因不是無堵塞
 
       無堵塞套接字的兩種實現:
int (flags = fcntl(sock_fd, F_GETFL, 0) < 0) error_proc();
flags |= O_NONBLOCK;
if (fcntl(sock_fd, F_SETFL, flags) < 0) error_proc(); //這種方法是POSIX標準定義方式
 
int b_on = 1; //在ioctl函數中使用FIONBIO命令
ioctl(sock_fd, FIONBIO,&b_on);
 
·5.2 單進程輪詢服務器模式
make_null(serv_slot, maxlen); //serv_slot[]是連接套接字描述符數組,本進程爲其提供服務
listen(listen_fd, MAXSIZE);   //建立傾聽套接字
do{
//從完全傾聽隊列中接收一個連接套接字描述符
       conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, sizeof(cli_addr));
       if (conn_fd < 0 && errno != EWOULDBLOCK)
error_proc();                     //錯誤處理
       else  if (conn_fd >= 0)                //接收到新的連接套接字描述符
              create_new_connect(conn_fd, serv_slot, &maxlen);   //建立新連接
for (i = 0; i < maxlen; ++i)      //從0到maxlen都是有效連接,進程輪流爲其服務
        serve_for(serv_slot[i]);             //本進程爲第i個連接服務
} while (continue);
       使用單進程輪詢服務器模式仍然無法避免客戶端的某些意外(比如非正常斷線)或惡意行爲造成失效,而且如果客戶數量增加,服務器端的相應時延也會加大。因此我們仍然使用併發服務器模式來提供並行的服務,因爲一個服務器子進程失效不會影響到其他進程的工作。
 
 
第六章 帶外數據與多路複用、信號驅動的輸入/輸出模型
·6.1 多路複用的輸入/輸出模型
       多路複用的概念:進程不是主動詢問套接字情況,而是希望對監視的套接字向系統登記,而後採用被動的態度等待。當監視的套接字上發生了事件,進程去檢查發生的狀況然後做相應的處理。在這種工作方式下,進程是在已經知道在套接字上發生了事件纔去檢測,在沒有發生事件的時候進入睡眠狀態。
 
頭文件:<sys/time.h> <unistd.h>  [<signal.h>(pselect使用)]         
主要函數:
int select (int maxfd, fd_set *rdset, fd_set *wrset, fdset *exset, struct timeval *timeout);
[maxfd是需要監視的最大文件描述符值+1,即系統監視從0到maxfd-1的文件描述符;
rdset, wrset, exset是對應需要檢測的可讀、可寫和異常文件描述符集合;
timeout內沒有發生事件,函數返回0]
 
int pselect(int maxfd, fd_set *rdset, fd_set *wrset, fdset *exset, struct timespec *timeout,const sigset_t sigmask); //POSIX中對select函數的增強,參數sigmask是執行後對堵塞信號恢復
 
文件描述符集合:
FD_ZERO(fd_set *fdset);            //清空初始化
FD_SET(int fd, fd_set *fdset);        //增加
FD_CLR(int fd, fd_set *fdset);        //刪除
FD_ISSET(int fd, fd_set *fdset);       //判斷包含
 
·套接字的讀、寫和異常就緒條件
       讀就緒:傾聽套接字完全連接隊列建立新連接;連接套接字的讀緩衝區超過讀下限、讀管道關閉和套接字異常。
       寫就緒:連接套接字的寫緩衝區空閒小於某下限、寫管道被關閉和套接字異常。
       異常就緒:套接字上到達外帶數據。異常就緒連帶觸發讀、寫就緒。
上面列出部分常用就緒條件,具體參考幫助手冊。
 
基本用法:
FD_ZERO(&r_set);               //初始化
FD_SET(listen_fd, &r_set);         //加入可讀文件描述符集合
ret = select(listen_fd+1, &r_set, NULL, NULL, NULL); //對傾聽套接字進行就緒判斷
 
 
·6.2 信號驅動的輸入/輸出模型
       信號驅動通常用於接收緊急數據。進程先向系統登記,然後系統檢測到數據到達後會向接收者發生SIGIO信號,然後接收者在信號處理器中接收數據。這種方式通常用在接收緊急的控制數據場合。
 
數據接收者設置:
#include <fcntl.h>
int fcntl(int fd, int cmd,...);  
//使用命令F_SETOWN,第三個參數如果是正整數表示進程號,負整數表示進程組接收
 
 
·6.3 系統I/O模型的總結
       本書講述了“堵塞方式、非堵塞方式、多路複用和信號驅動”四種I/O模型。
       1 堵塞方式:
              廣泛使用在併發服務器上,當套接字不滿足操作條件立即堵塞等待資源。
       2 非堵塞方式:
              廣泛使用在單進程輪詢服務器上,浪費較大CPU資源使用場合較少。
       3 多路複用方式:
              廣泛使用在單進程進行多客戶端服務上,比非堵塞方式在輪詢中節約CPU時間。
       4 信號驅動方式:
              廣泛使用在接收緊急數據場合。
 
·6.4 帶外數據的接收和發送
       帶外數據就是指在正常數據流信道之外傳輸的數據,通常用在對遠端進程的同步和控制。它和信號驅動方式幾乎相同,但是發送的是SIGURG信號,不是SIGIO。
 
頭文件:<sys/types.h>, <sys/socket.h>
主要函數:
int send(int sockfd, void *buf, int len, int flags); //使用MSG_OOB控制選項發送帶外數據
int recv(int sockfd, void *buf, int len, int flags); //使用MSG_OOB控制選項接收帶外數據
 
       帶外數據一次只允許發送一個字節,如send(sock_fd, “bc”, 2, MSG_OOB),TCP只認爲最後一個是帶外數據,之前都是普通數據。在特殊情況下,帶外數據包優先被接收方接受。
接收方在缺省情況下(使用ioctl函數可以改變)使用一個字節的外帶數據緩衝區接收外帶數據,並且外帶數據段和普通套接字數據段字符集不同,可以區分外帶數據和普通數據。     
如果接收方收到多個帶外數據段,TCP會和先前一次收到的數據段中數據做比較,如果其值相同則認爲它們是同個帶外數據段。由於發送方可能發送多個外帶數據段,接收外帶數據是必須做容錯處理。
接收方同一時刻只允許有一個字節的外帶數據,先到者如果沒有被及時處理,那麼任何後來的外帶數據段都將覆蓋它。帶外數據被覆蓋後成爲普通數據。
       收到外帶數據將觸發異常就緒。直到讀指針大於帶外數據標示(緊急)指針後解除異常。
       服務器端接收外帶數據可以使用:
1 多路複用方式,要點:
檢測異常就緒和讀就緒套接字先後順序不同,結果也不同。
2 異步信號驅動方式,要點:
設計SIGURG信號處理器,處理前後注意屏蔽/堵塞信號。
3 檢測帶外數據標記方式,要點:
       //套接字設置成SO_OOBINLINE,即外帶數據看成普通數據存放,on=1
 setsockopt(conn_fd,SOL_SOCKET,SO_OOBINLINE,&on,sizeof(on));
 ioctrl(conn_fd, SIOCATMARK,&n_data); //檢測讀指針是否和帶外數據標示指針重合
 if (n_data == 1) //帶外數據到達
 
注意:被覆蓋的帶外數據將保留繼續保留在讀緩衝區裏,而後當成普通數據讀入。如果使用過的帶外數據沒有及時得從緩衝區裏刪除,該帶外數據可能會被當場普通數據讀入,如sleep()系統調用可能導致這類需要緊急處理的過程產生詭異的行爲!
 
 
 
 
第七章 UDP數據報
·7.1 UDP數據報
UDP源端口、UDP目的端口標示進程;UDP數據報長度;校驗和,可選。
UDP非面向連接,不可靠傳輸,具有較小傳輸時延。
 
·7.2 UDP傳輸過程
SERVER: socket() --> bind() --> recvfrom() --> sendto()-->close()
CLIENT : socket() --> sendto() --> recvfrom() --> close()
服務器端和TCP協議相比少了listen()和accept()兩個過程,而客戶端不需要建立連接
 
       頭文件:<socket.h>
       發送和接收函數:
       int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);      //參數to是指定接收方地址
       int recvfrom(int sockfd, const void *buf, int len, unsigned int flags, const struct sockaddr *from, socklen_t *addrlen);   //參數from是保存發送方的地址
       int send(int sockfd, void *buf, int flags);      //使用connect綁定後,發送到缺省地址
       int recv(int sockfd, void *buf, int flags);      //使用connect綁定後,從缺省地址接收
    //socket(AF_INET, SOCK_DGRAM, 0) 創建一個UDP套接字
 
·7.3 UDP服務器和TCP服務器的比較
       使用UDP協議的服務器通常是非面向連接的,因此不用listen和accept的。UDP服務器只需要在其端口上等待客戶機發來的數據報即可。
       TCP服務器需要和客戶端進行連接然後,獨佔一個連接套接字爲其服務;而UDP服務器實際並不和客戶機進行連接,UDP服務器僅是接收報文,處理並返回結果。
       UDP協議並不關心數據報的可靠性和次序,而希望應用程序保證這些。
       TCP服務器中,服務器可以調用getsockpeer函數獲取客戶機地址信息和端口號,使用getsockname獲取套接字對應的IP地址和端口號。
UDP服務器中,可以在recvfrom函數中獲取數據報的源地址,使用getsockname獲取數據報的端口。如果該UDP服務器有多個IP地址,則無法判斷是哪個IP地址獲取該數據報。因爲UDP協議非面向連接,沒有記錄接收方的IP地址,此時如果需要明確知道是哪個IP地址收到該數據報,在服務器端需要爲每個建立多個UDP套接字綁定在不同網絡接口上,通過getsockname獲取對應的IP號。
 
·7.4 UDP的“連接”
       UDP是非面向連接,但是也可以調用connect函數對套接字進行綁定到缺省IP地址上。
       UDP服務器調用conncet後,並不啓用“三次握手”,僅僅記住目的地址和端口。在使用send函數時候會自動按照缺省情況來填寫數據報頭;同時也可以用sendto函數發送指定位置。
但在接收時候,如果套接字已綁定一個地址和端口,那麼UDP服務器只接收該套接字上源地址和端口相同的數據報。如果一個數據報源地址和端口和該套接字設置不同,則丟棄它。而沒有綁定地址和端口的UDP套接字可以接收任意來源的數據報。
可以多次調用connect函數修改UDP套接字的綁定地址和端口設置。如果調用connect(AF_UNSPEC, NULL, 0)可以取消對套接字的綁定。
 
·7.4 UDP應用程序性能改進
       1 解決報文的無序問題:
對UDP報文設計一個數據序列號,接收到報文先進行排序後再轉交給數據處理程序。
       2 解決報文的流量控制問題:
 如果客戶端接收能力遠高於服務器端發送速率,則會讓網絡傳輸性能大大下降;
如果客戶端接收能力遠低於服務器端發送速率,則會讓UDP報文大量丟失,消耗服務器端資源。因此需要匹配雙方的發送和接收速率,有利用提高整體性能。我們需要在應用程序中建立一種端對端的流量控制反饋機制,由客戶端給服務器端發送接收能力的反饋,讓服務器端動態調整發送速率。
應用緩衝區
核心緩衝區
應用程序處理套接字緩衝的速率
應用程序讀取套接字緩衝的速率
應用程序發送緩衝區
應用程序接收緩衝區
單個系統發送緩衝區
UDP套接字接收緩衝區
服務器端發送速率
根據客戶端程序接收緩衝的佔用情況給服務器端提供反饋
服務器端
客戶端

 
 
第八章 域名系統和通用套接字選項
·8.1 域名系統
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname); //指定域名地址來獲取IP地址
struct hostent *gethostbyaddr(const char *addr, size_t len, int family); //指定IP地址獲取域名
struct hostent{
       char *h_name;              //主機名
       char **h_alias;       //主機別名列表
       int h_addrtype;       //主機地址類型
       int h_length;           //主機地址長度
       char **h_addr_list;//主機IP地址列表
};
 
int gethostname(char *name, size_t len); //返回本機域名地址 <unistd.h>
int uname(struct utsname *name); //同上,#Include <sys/utsname.h>
struct servent *getservbyname(const char *servname, const char *protoname);//服務名獲取端口
struct servent *getservbyport(int port, const char *protoname); //由端口名獲取服務信息
 
·8.2 套接字選項
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *potval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, cosnt void *potval, socklen_t *optlen);
[通用套接字選項,參考手冊,略。]
 
 
第九章高級套接字函數編程
·9.1 發送和接收函數的高級用法
頭文件:<sys/types.h>, <sys/socket.h>
int send(int sockfd, void *buf, int len, int flags);
[flags=MSG_OOB, MSG_DONTWAIT, MSG_DONTROUTE]
int recv(int sockfd, void *buf, int len, int flags);  
[flags=MSG_OOB, MSG_PEEK, MSG_WAITALL, MSG_DONTROUTE]
 
 
頭文件:<sys/uio.h>
int readv(int fd, struct iovec *iov, int iovlen);   //將套接字緩衝區數據讀到多個應用緩衝區中
int writev(int fd, struct iovec *iov, int iovlen); //將多個應用緩衝區寫到套接字緩衝區數據中
struct iovec{
 void *iov_base; .//指向應用緩衝區結構體的數組
 size_t iov_len;   //緩衝區的個數
};
 
頭文件:<sys/types.h>, <sys/socket.h>
int recvmsg(int sockfd, struct msghdr *msg, int flag);   //常用在UNIX域套接字中對
int sendmsg(int sockfd, struct msghdr *msg, int flag); //進程間發送/接收文件描述符使用
struct msghdr{
       void *msg_name;      //發送端的地址信息
       int msg_namelen;      //  
       struct iovec *msg_iov; //緩衝區結構體指針
       int msg_iovlen;       //緩衝區個數
       void *msg_control;    //控制信息
       int msg_controllen;    //控制信息長度
       int msg_flags;       
};
使用這兩個函數發送附加消息時候通常需要定義以下結構體:
union{
       struct cmsghdr cm;
       char control[CMSG_SPACE(sizeof(附加段長度))];
};
操作宏,頭文件: <sys/socket.h>, <sys/param.h>
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msghdrptr);//指向第一個cmsghdr結構指針
struct cmsghdr *CMSG_NEXTHDR(struct msghdr *msghdrptr, struct cmsghdr *cmsgptr);
unsigned char *CMSG_DATA(struct cmsghdr *cmsghdr); //返回指向cmsghdr結構第一個字節
unsigned CMSG_LEN(unsigned int lenght); //獲取cmsghdr中存放數據字節數
 
 
 
 
 
第十章守護進程和超級服務器inetd
·10.1 守護進程的原理
       只要系統沒有關機或者崩潰,守護進程將在系統中不間斷運行。關鍵是如何把守護進程的運行環境和其他進程的運行環境隔離。步驟:
1 第一次fork和setsid函數調用建立新會話組
       這個操作的主要目的是爲了讓一個進程和控制終端脫離,這樣來自終端的信號就不會影響到守護進程。setsid()函數的功能是讓創建一個新的會話過程,並讓調用setsid()的進程成爲該會話過程的領頭進程。但是setsid()的調用條件是這個進程不是一個進程組的主進程。因此我們先fork()一個子進程並終止該進程組主進程的運行,在子進程中調用setsid()讓子進程成爲不帶控制終端的新會話過程的領頭進程。於是這個進程就和原控制終端脫離,併成爲新會話組的領導進程。
 
2 第二次fork和setpgrp函數調用建立新進程組
       調用setsid()之後,雖然在新的會話組中的領導進程不帶控制終端。但是如果這個進程打開了一個終端,那麼整個會話組將又重新的控制終端。因此我們需要fork()一個新的子進程繼續運行,並終止該會話組領頭進程的運行。此時這個新進程不是會話組領導進程,所以即使打開一個終端也不會成爲整個會話組的控制終端。
但由於父進程是會話組的領頭進程,如果讓父進程退出而子進程繼續運行,那麼將發送SIGHUP(中斷和掛起信號)給這個會話組中所有進程。因此我們要先忽略信號SIGHUP,然後再fork,接着退出父進程而子進程繼續運行。
但是這個新進程仍然和已退出父進程同在一個進程組中,仍然會受到同個進程組中的信號影響。所以我們要使用setpgrp()讓這個進程成爲新的進程組的領導者。
 
3 關閉所有文件描述符
       fork()時候子進程將繼承父進程的文件描述符。我們需要在守護進程中關閉先前打開的文件描述符,以免對其他進程造成影響。使用sysconf(_SC_OPEN_MAX)函數獲取系統中每個進程可以打開文件的最大數目,然後使用close()關閉它們。
 
4 消除umask的影響
       每個進程都有一個umask同它關聯。umask指定進程創建文件的保護掩碼,是系統提供的一種安全機制,限制進程創建文件的權限。比如進程創建一個文件的權限爲0777,進程umask爲0277,那麼實際文件權限爲0777-0277=0500。此時如果其他進程去讀寫守護進程創建的文件可能會受到文件掩碼的影響。使用umask(0)系統調用清除舊的文件掩碼。
 
5 改變守護進程的當前目錄
       每個進程都有一個當前目錄,當進程產生錯誤時可以將錯誤信息記錄在當前目錄的core文件中供以後分析錯誤使用。如果不把守護進程修改到一個安全的目錄,那麼守護進程的當前目錄是不確定的,並可能影響到其他系統的管理工作。我們可以使用chdir(“/”);將守護進程當前目錄修改到根目錄下。
 
6 對標準I/O描述符重定向
       由於守護進程不連接任何控制終端,並且所有文件描述符都關閉。那麼如果意外調用printf, perror等輸出將導致出錯。我們把標準I/O重定向到無傷害設備(harmless device)描述符上,那麼對標準I/O的操作都將忽略避免人爲意外使用出錯。
 
7 使用syslog記錄守護進程的錯誤
       syslogd本身就是一個守護進程,使用UDP 514端口。應用程序可以發送UDP報文記錄錯誤信息。void syslog(int priority, const char *message, ...); //#incluse <syslog.h>
 
8 文件鎖和控制守護進程副本互斥運行
       爲了避免運行多個相同的守護進程產生的干擾,因此使用鎖文件的方式記錄守護進程的工作情況。linux系統的鎖採用諮詢鎖,只是系統將告知文件鎖定情況而不阻止進程對文件的操作。我們可以使用int flock(int fd, int operation); //#include <sys/file.h>
[operation = LOCK_SH共享鎖, LOCK_EX互斥鎖,LOCK_UN解鎖,LOCK_NB進程獲取鎖失敗不堵塞,缺省爲進程堵塞]
 
 
示例代碼:
int init_daemon(const char *pathname, int facility);
{
       struct sigaction act;
       int max_fd, i, ret;
       int lock_fd;
       char buf[10];
       //打開一個文件鎖
       lock_fd = open(LOCKFILE, O_RDWR | O_CREAT, 0640);
       if (lock_fd < 0)
              error_proc();
       //加鎖
       ret = flock(lock_fd, LOCK_EX LOCK_NB);
       if (ret < 0)
              error_proc();
       //第一次fork
       ret = fork();
       if (ret < 0)
              error_proc();
       else if (ret != 0)
              exit(0);    //關閉進程組的主進程,子進程繼續運行      
       ret = setsid(); //這個子進程成爲新會話組的領導進程
       if (ret < 0)
              error_proc();
       //忽略SIG_IGN
       act.sa_handler = SIG_IGN;
       sigemptyset(&act.sa_mask);
       act.sa_flag = 0;
       sigaction(SIGHUP, &act, NULL);
       //第二次fork
       ret = fork();
       if (ret < 0)
              error_proc();
       else
              exit(0); //關閉進程組的主進程,子進程繼續運行
       chdir(“/”);    //改變守護進程的當前目錄
       umask(0);    //清除舊的文件掩碼
       setpgrp();    //讓這個進程成爲新進程組的領導進程
       sprintf(buf, “%6d/n”, getpid()); //獲取守護進程的ID號,保存進buf數組中
       write(lock_fd, buf, strlen(buf)); //把守護進程ID號保存進鎖文件中
       max_fd = sysconf(_SC_OPEN_MAX);   //獲取進程最大打開的描述符
       for (i = 0; i < max_fd; ++i)
              close(i);                                       //逐個關閉
       open(“dev/null”, O_RDWR);          //打開無傷害設備
       dup(1);    //
       dup(2);     //標準I/O描述符重定向
       openlog(pathname, LOG_PID,facility); //打開syslogd記錄文件
       return 0;   //守護進程標準創建過程結束
}
 
  1. if (srv->srvconf.pid_file->used) {  
  2.         if (-1 == (pid_fd = open(srv->srvconf.pid_file->ptr, O_WRONLY | O_CREAT | O_EXCL | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))) {  
  3.             struct stat st;  
  4.             if (errno != EEXIST) {  
  5.                 log_error_write(srv, __FILE__, __LINE__, "sbs",  
  6.                     "opening pid-file failed:", srv->srvconf.pid_file, strerror(errno));  
  7.                 return -1;  
  8.             }  
  9.   
  10.             if (0 != stat(srv->srvconf.pid_file->ptr, &st)) {  
  11.                 log_error_write(srv, __FILE__, __LINE__, "sbs",  
  12.                         "stating existing pid-file failed:", srv->srvconf.pid_file, strerror(errno));  
  13.             }  
  14.   
  15.             if (!S_ISREG(st.st_mode)) {  
  16.                 log_error_write(srv, __FILE__, __LINE__, "sb",  
  17.                         "pid-file exists and isn't regular file:", srv->srvconf.pid_file);  
  18.                 return -1;  
  19.             }  
  20.   
  21.             if (-1 == (pid_fd = open(srv->srvconf.pid_file->ptr, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))) {  
  22.                 log_error_write(srv, __FILE__, __LINE__, "sbs",  
  23.                         "opening pid-file failed:", srv->srvconf.pid_file, strerror(errno));  
  24.                 return -1;  
  25.             }  
  26.         }  
  27.     }  
if (srv->srvconf.pid_file->used) {
		if (-1 == (pid_fd = open(srv->srvconf.pid_file->ptr, O_WRONLY | O_CREAT | O_EXCL | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))) {
			struct stat st;
			if (errno != EEXIST) {
				log_error_write(srv, __FILE__, __LINE__, "sbs",
					"opening pid-file failed:", srv->srvconf.pid_file, strerror(errno));
				return -1;
			}

			if (0 != stat(srv->srvconf.pid_file->ptr, &st)) {
				log_error_write(srv, __FILE__, __LINE__, "sbs",
						"stating existing pid-file failed:", srv->srvconf.pid_file, strerror(errno));
			}

			if (!S_ISREG(st.st_mode)) {
				log_error_write(srv, __FILE__, __LINE__, "sb",
						"pid-file exists and isn't regular file:", srv->srvconf.pid_file);
				return -1;
			}

			if (-1 == (pid_fd = open(srv->srvconf.pid_file->ptr, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))) {
				log_error_write(srv, __FILE__, __LINE__, "sbs",
						"opening pid-file failed:", srv->srvconf.pid_file, strerror(errno));
				return -1;
			}
		}
	}


·10.2 超級服務器inetd的工作原理
       由於服務器套接字初始化方式非常類似,所以可以設計一個專門的服務器負責初始化工作,並且它將根據接入端口不同調用相應的服務程序進行工作,這些服務程序在未被接入前都處於睡眠等待狀態。採用超級服務器的方式可以讓服務器程序採用統一方式管理。
 
       超級服務器將採用select的方式併發檢測在文件/etc/inetd.conf中說明的TCP/UDP端口,一旦發現有客戶接入就創建一個子進程。超級服務器inetd是服務接入者,它在創建字進程時候調用exec()載入具體的服務程序。在子進程中關閉傾聽套接字,父進程中關閉連接套接字,於是父進程繼續檢測,子進程開始爲客戶端進行服務。對於wait服務程序,超級服務器inet載入它時候將其在檢測集合中刪除,等待該服務結束後才能接入下次服務。服務程序完畢後將發送SIGCHLD信號,超級服務器將其繼續加入檢測集合。當系統管理員修改超級服務器配置文件後將發送SIGHUP信號,超級服務器將重新初始化。
      
 
 
第十一章 數據結構的傳輸和XDR標準
·11.1 數據結構的傳送
       網絡數據結構傳遞可能存在以下問題:網絡字序問題、浮點數傳輸、指針處理
       自定義手工處理方式:
將待發送數據結構轉換以後放入應用的發送緩衝區;
將應用的接收緩衝區中數據結構轉換以後再進行數據處理。
       代碼示例:
       void send_int32_2buf(char *buf, unit32_t n)
       {
              n = htonl(n);
              bcopy((char *)&n, buf, sizeof(unit32_t));
       }
       void send_string_2buf(char *buf, char*str);
       {
              bcopy(str, buf, strlen(str));
       }
       void send2_buf(char *buf, struct u_data *ptr)
       {
              send_int32_2buf(buf, ptr->aInt);
              buf += sizeof(unit32_t);
              send_string_2buf(buf, ptr->str);
              buf += strlen(ptr->str); // * sizeof(char)
}
void recv_int32_from(char *buf, unit32_t *n)
{
       bcopy(buf, (void*)n, sizeof(unit32_t));
       *n = ntohl(*n);
}
void recv_from_buf(char *buf, struct u_data *ptr)
{
       recv_int32_from_buf(buf, &(ptr->aInt));
       buf += sizeof(uint32_t);
}
 
·XDR標準和實現原理
XDR數據結構傳輸標準是SUN公司設計的,已經成爲大多數客戶機/服務器應用中的事實上的標準。
      
XDR對各種數據類型規定了編碼方式。初始化函數是
#include <rpc/xdr.h>
extern void xdrmem_create((XDR *xdrs, const caddr_t addr, u_int size, enum xdr_op xop)); //xdrs是創建後XDR流指針,addr是存放XDR流發送緩衝區
       XDR的流轉換方式和上面自定義方式類似,但是對各種數據類型的處理做了統一規定。應用程序設計配對的接收和發送,分別處理每個數據項。
 
       XDR有內存流和I/O流兩種。可以使用內存流,進行套接字緩衝區間的數據結構傳輸;使用I/O流將編解碼的結果輸出到文件流中。
 
       XDR和TCP都是流的抽象,所以兩者可以很好結合。另外XDR提供了面向記錄的XDR抽象,應用在UDP傳輸。
 
       XDR轉化函數所做操作決定於XDR流本身性質。如果XDR是編碼流時,轉化函數就做數據編碼,如果是解碼流時,轉換函數就做數據解碼。
 
第十二章 RPC遠程過程調用原理和實現
·RPC的原理
       使用XDR協議可以讓數據結構無差別的在網絡傳輸,使用RPC(remote procedure call)遠程過程調用可以使函數在不同主機上運行。RPC所要達到目的是將網絡通信功能和應用的需求分開。
       RPC的中心是優先考慮應用的分析,在模塊功能劃分完畢後將其分離出來。這些模塊在不同主機上運行,RPC保證模塊分離前後的語義不變。
      
函數調用的4個原則:
1 把函數所需參數準備好,通過某種方式可以讓調用函數訪問到。
2 必須包含函數的返回信息
3 能夠確定調用函數的位置
4 爲調用函數創建可以運行的環境
 
其中“本地調用”這是上述四個原則的一種實現,1 通過堆棧段 2 函數返回地址 3 PC指針指向函數入口地址 4 局部變量在函數棧分配,其他數據共享進程數據段
 
RPC遠程調用模擬本地調用:
我們可以使用定義好的信息格式保存調用過程參數,在信息格式中說明如何去找被調用者(通常是某種標誌)。然後通過網絡將信息報文發送到被調用所在機器上,然後調用者等待被調用方發回的調用結果。【條件1和3】
被調用主機上應有一個分派器控制所有遠程調用過程。收到報文後通過其中的標誌知道需要調用哪個,取出被調用者需要的參數然後傳入。【條件2】
被調用過程在它所在環境中運行,並將運行結果寫在信息格式中,最後網絡將調用結果返回。【條件4】
準備函數傳入參數
說明函數如何返回
確定被調用函數位置
創建被調用函數環境
被調用函數運行
準備運行結果
切換回調用者的環境
返回被調用者的結果

·RPC的實現
遠程過程標示:(程序號,遠程調用過程版本號,遠程過程序號)
 
端口的動態映射:每一個遠程調用過程都對應占用一個有操作系統動態分配的傳輸層端口。調用方需要調用一個遠程調用過程,它會向端口映射器發送一個請求,然後端口映射器通過查表返回相應遠程調用過程的端口號。然後它向遠程調用過程發起調用請求。
 
RPC的報文:RPC使用XDR語言定義應用的報文。
RPC開發工具:由於ONC RPC協議規程非常複雜,因此係統提供了專門用於開發RPC的工具。注意包括:XDR庫函數,RPC運行時間庫函數,一些程序的自動生成工具,產生一個構件RPC分佈式程序需要的C程序文件,這些程序主要是屏蔽底層通信對應用的影響。
 
客戶端主要實現:
       向端口映射器發送請求,並從端口映射器接收回應;形成CALL報文,向真正的遠程調用過程發送調用請求,接收來自服務器的調用結果。
服務器段主要實現:
       在提供服務之前向端口映射器註冊自己實際端口;將一個調用分派到具體調用程序中的一個調用過程。
 
 
 
第十三章 UNIX域套接字和併發服務器的預創建技術
·UNIX域套接字
linux操作系統提供了一種UNIX域協議的進程間通信方式,它不能應用在網絡中,能使用在本機兩進程間的通信中。它能方便的向兩個非親屬關係的進程間傳遞文件描述符,效果類似於在父子進程間傳遞一樣。UNIX域套接字在和本地進程進行交互時候效率更高,因爲它不需要處理網絡異常可能。
 
地址結構:
struct sockaddr_un{
       short int sun_family;
       char sun_path[104];
};
使用示例:
listen_fd = socket(AF_UNIX,SOCK_STREAM,0);           //UNIX域套接字
serv_addr.sun_family = AF_UNIX;                        //UNIX域協議簇
strcpy(serv_addr.sun_path, “/tmp/myfile”);                  //創建綁定文件
unbind(serv_addr.sun_path);                             //先解除指定文件的綁定
ret = bind(listen_fd,(struct sockaddr*)&serv_addr,
strlen(serv_addr.sun_path)+sizeof(serv_add.sun_family);   //綁定指定文件路徑
       listen(listen_fd, 傾聽隊列長度);
 
·使用sendmsg和recvmsg傳遞和接收文件描述符
       主要使用UNIX域套接字,sendmsg和recvmsg兩個函數實現。在發送附加數據之前需要自己定義一個聯合體。這樣附加數據將在內存中置於struct cmsghdr結構之後,同cmsghdr相關宏定義才能正常工作。
示例代碼:
void unix_send_fd(int unix_fd, int conn_fd)
{
       struct msghdr msg;
       struct iovec iov[1];
       char c;
       int ret;
       union{
              struct cmsghdr cm;
              char control[CMSG_SPACE(sizeof(int))];
       }control_un; //定義聯合體,將附加數據置於struct cmsghdr結構之後
       struct cmsghdr *cmptr;
       //報文套接字使用字段,對於流類型套接字,地址域填爲空。
       msg.msg_name = NULL;
       msg.msg_namelen = 0;
       //填寫普通數據的緩衝區
       iov[0].iov_base = &c;
       iov[0].iov_len = 1;
       //執行普通數據緩衝區
       msg.msg_iov = iov;
       msg.msg_iovlen = 1;
       //設置附加數據的指針
       msg.msg_control = control_un.control;
       msg.msg_controllen = sizeof(control_un.control);
       //獲取結構struct cmsghdr的指針
       cmptr = CMSG_FIRSTHDR(&msg);
       //獲取該結構的長度
       cmptr->cmsg_len = CMSG_LEN(sizeof(int));
       //填寫控制數據的層次
       cmptr->cmsg_level = SOL_SOCKET;
       //填寫控制數據的類型
       cmptr->cmsg_type = SCM_RIGHTS;
       //填寫控制數據的內容
       *(int*)CMSG_DATA(cmptr)=conn_fd;
       ret = sendmsg(unix_fd, &msg,0);
       if (ret < 0)
              error_proc();
       //接收就是上面逆向的過程,先預設接收緩衝,然後cmptr= CMSG_FIRSTHDR(&msg);
       //最後return *(int*)CMSG_DATA(cmptr);
}
 
·併發服務器的預創建技術
       預創建技術可以爲併發服務器解決以下幾個顯著缺點:
       1 爲新連接上的客戶儘快提供服務,消除新建進程的時延
       2 不必爲每個客戶服務進程都需要建立/釋放資源
       3 子進程重複利用率高
       4 通過傳遞文件描述符,對子進程動態管理
 
       UNIX套接字對
       int socketpari(int family, int type, int protocol, int sockfd[2]);
       //建立兩個連接好的套接字對,可以像管道一樣使用
       使用套接字和管道相比有“全雙工、可以將文件描述符傳遞給任何進程”的優點
 
 
第十四章原始套接字
·14.1 原始套接字
       類型爲0的原始套接字
ICMP類型原始套接字
EGP類型原始套接字
ICMP協議實體
EGP協議實體
UDP/TCP處理模塊
UDP/TCP的IP包
承載ICMP報文IP包
承載EGP報文IP包

       原始套接字的使用:
       1 ip_fd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP); //只允許超級用戶建立和使用
              //SOCK_RAW創建原始套接字,第三個參數是協議,可以使用0或其他
 
2         bind和connect函數對原始套接字的影響
原始套接字工作在傳輸層以下所以沒有端口的概念。bind一個原始套接字的地址後,核心只把目的地址是該原始套接字的IP包發送給它,忽略其它。調用connect後,核心將傳遞源地址是connect地址的IP包給這個原始套接字。如果沒有bind和connect原始套接字,核心將把所有協議匹配的IP包傳遞給它。
      
IP套接字選項
       getsockopt()函數:IP_TTL獲取生存期,IP_HDRINCL自行填充IP包頭(可僞造)
 
第十五章 多線程編程
·15.1 線程的概念
線程這樣一種實體,它被包含在進程實體中,具有自己的運行線索,可以完成一定的任務。它和進程中其他進程共享所有的共享數據以及部分環境,並且可以和其他線程協同完成一定任務。線程常常被成爲輕量級進程。
 
多個線程將共享同個進程虛空間的環境包括代碼段和大部分數據,所以fork需要建立的大量複製創建就不需要,不同線程可以通過共享變量進行數據傳輸,不需要複雜的IPC機制。
 
同個進程的線程將併發運行。線程有獨立的:線程ID、寄存器組值、線程堆棧、錯誤返回碼變量errno、線程信號屏蔽碼、線程優先級。
線程ID
32位屏蔽碼
優先級
信號
調度器
進程虛空間
 
....................
....................
....................
 
大部分數據
代碼段
程序計數器
堆棧基址指針
堆棧棧頂指針
    .......
寄存器組

 
 
·15.2 線程的分類
1 用戶線程:
用戶線程的實現是通過運行時間系統代碼來實現。這部分代碼的功能是將線程恢復運行時需要保存的寄存器組信息和其他信息保存在一個結構中,然後將這個結構放在線程調度隊列中,然後在線程隊列中選擇一個優先級最高的線程,而後將結構中保存的信息回覆運行這個線程。
 
2 內核線程:
       內核線程切換是通過內核調度器來實現,和普通進程一樣是核心調度器實體。
 
·15.3 用戶線程和內核線程的比較
       1 CPU時間的分配
用戶線程是通過在進程中連接用戶級線程包來實現,所以這個進程的多個線程將競爭CPU分配給這個進程的時間。用戶級線程不能在多個CPU環境中同時運行
內核線程和其他進程一起在覈心調度器中競爭CPU時間,可以在多CPU環境運行更具良好的併發性
      
2 線程的併發性
除非一個運行中的用戶級線程放棄CPU時間去運行進程內的線程切換代碼,否則其他線程將不能獲取CPU時間。因此一些慢系統調用將引起線程堵塞,而其他線程卻無法切換。因此在用戶級線程中,可能堵塞的系統調用將被無堵塞的版本取代。也因此這些原因和CPU的時間分配,大大限制了用戶級線程的併發性
內核線程在發生堵塞時候,將由核心調度器把CPU時間分配給其他進程或核心線程。
 
3線程調度的開銷
     用戶級線程的調度在進程內所有開銷很小;內核線程調度和進程調度類似,所以開銷較大。內核線程的良好併發性是用較大的開銷換取,在對於併發性要求不是很高的應用時,用戶級線程更加合適。
 
·線程函數庫的使用
       略
·線程同步
       採用無名信號量、互斥鎖、條件變量和信號變量、信號處理器等。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章