系列文章導航:《Unix 網路程式設計》筆記
ECHO-Application 結構如下:
除此之外,還有:
#include "unp.h"
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
// 建立 Socket
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
// 初始化連線引數
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
// 繫結
Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
// 開始監聽
Listen(listenfd, LISTENQ);
for (;;)
{
clilen = sizeof(cliaddr);
// 伺服器阻塞, 等待請求
connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);
if ((childpid = Fork()) == 0)
{ /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
#include "unp.h"
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ((n = read(sockfd, buf, MAXLINE)) > 0)
Writen(sockfd, buf, n);
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
err_sys("str_echo: read error");
}
#include "unp.h"
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd); /* do it all */
exit(0);
}
#include "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (Fgets(sendline, MAXLINE, fp) != NULL) {
Writen(sockfd, sendline, strlen(sendline));
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
}
當我們把伺服器和使用者端都啟動後,可以通過命令檢視網路的情況:
[root@centos-5610 Unix_Network]# netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost:9877 localhost:38160 ESTABLISHED
tcp 0 0 localhost:38160 localhost:9877 ESTABLISHED
連線的正常斷開
我們在使用者端輸入 EOF (Ctrl + D),之後會發生一系列事情:
(上述如通訊端的操作其實是在核心完成的,這裡為了簡便所以標在了對應的執行緒上)
如下,可以看到使用者端的 TIME_WAIT 狀態持續了一段時間
[root@centos-5610 Unix_Network]# netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost:38160 localhost:9877 TIME_WAIT
[root@centos-5610 Unix_Network]# netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
背景
在上述程式中,其實子程序結束後,會向父程序傳送一個 SIGCHLD
訊號,我們這裡沒有捕捉,預設行為為被忽略。
既然父程序未加處理,子程序於是進入僵死狀態,如下狀態 Z 所示:
[root@centos-5610 Unix_Network]# ps -t pts/0 -o pid,ppid,stat,tty,args,wchan
PID PPID STAT TT COMMAND WCHAN
2008 1771 S pts/0 ./tcpserv01 inet_csk_accept
2382 2008 Z pts/0 [tcpserv01] <defunct> do_exit
或如下所示:
[root@centos-5610 tcpcliserv]# ps
PID TTY TIME CMD
1771 pts/0 00:00:00 bash
2008 pts/0 00:00:00 tcpserv01
2382 pts/0 00:00:00 tcpserv01 <defunct>
2555 pts/0 00:00:00 tcpserv01 <defunct>
2654 pts/0 00:00:00 tcpserv01 <defunct>
2886 pts/0 00:00:00 tcpserv01 <defunct>
3238 pts/0 00:00:00 tcpserv01 <defunct>
6685 pts/0 00:00:00 ps
為什麼會有僵死程序
設定僵死的目的是維護子程序的資訊,以便父程序在以後某個時候獲取這些資訊(包括程序 ID、終止狀態、資源利用情況)
父程序終止了,還有人管這些僵死程序嗎
如果父程序也終止了,而其有處於僵死狀態的子程序,那麼子程序的父程序會被設定為 1(init 程序的 ID),init 程序會清理他們(wait,後續講解)
僵死程序的壞處
他們佔用核心的空間,最終可能導致我們耗盡處理資源,所以我們必須處理僵死程序。
訊號就是告知某個程序發生了某個事件的通知,有時也稱為軟體中斷。
訊號的來源
訊號的處理
通過呼叫 sigaction
函數設定一個訊號的處理,並有三種選擇:
SIG_IGN
來忽略它。同樣,上述兩個訊號不能被忽略SIG_DFL
來啟用他的預設處置。預設處置通常是終止程序sigaction 函數太過於複雜,所以一般我們會呼叫 signal 函數。
但是 signal 函數由於歷史和標準的原因在不同的系統上實現不一致,所以我們實現自己的 signal 方法。其簽名如下:
void (*signal(int signo, void (*func)(int)))(int);
我們會做一些處理,簡化其表示:
typedef void Sigfunc(int);
Sigfunc *signal(int signo, Sigfunc * func);
signal 函數如下:
#include "unp.h"
Sigfunc *signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM)
{
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif
}
else
{
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return (SIG_ERR);
return (oact.sa_handler);
}
建立一個俘獲 SIGCHLD 訊號的訊號處理常式,在函數體中呼叫 wait(後面會提到):
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminated\n", pid);
return;
}
在 Listen 方法後呼叫:(必須在 fork 前呼叫,且只能執行一次)
Listen(listenfd, LISTENQ);
Signal(SIGCHLD, sig_chld);
此時就不會再出現僵死程序了。
慢系統呼叫
如 accept 等函數,如果沒有使用者連線,將一直阻塞下去,把這樣的系統呼叫稱為慢系統呼叫。
滿系統呼叫的中斷
如前一節我們處理 SIGCHLD 訊號時,當系統阻塞於一個慢系統呼叫時,而該程序又捕獲了一個訊號,且相應的訊號處理常式返回時,該系統呼叫可能會返回一個 IENTER 錯誤。
有些系統可能會自動重啟某些被中斷的系統呼叫,但是出於對程式的可移植性考慮,我們應該對此有所準備。
for (;;) {
clilen = sizeof(cliaddr);
if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0) {
if (errno == EINTER) {
continue;
}
else {
err_sys("XXX");
}
}
}
這種方式對 accept
以及諸如 read、write、select、open 之類的函數來說都是合適的,但是如前面所說,connect 函數不能重啟。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
相同之處
均返回已終止子程序的 ID,以及通過 statloc 指標返回的子程序終止狀態
一些宏 WIFEXIST、WEXITSTATUS 可以用來檢視其資訊
不同之處
如果呼叫 wait 的程序沒有已終止的子程序,則阻塞至第一個現有子程序終止為止
而 waitpid 可以通過 pid 和 options 引數來進行更多的控制
wait 的問題
如果我們用多臺使用者端傳送請求,然後同時終止,如下:
多個 SIGCHLD 訊號會到達,但是 wait 只會被執行一次,導致會留下 4 個僵死程序,如果是在不同的機器上執行的,則更為不確定。
用 waitpid 可以解決這個問題:
#include "unp.h"
void sig_chld(int signo) {
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
連線剛剛建立,使用者端就傳送一個 RST。
什麼樣的場景下會發生這種事情?我在網上簡單檢索了一下,但是沒有找到典型的發生場景。
書上給出的例子是 Web 伺服器比較繁忙
如何處理這種情況取決於具體的實現。
這裡指的是伺服器的子程序,也就是提供具體服務的那個程序。
我們先把 server/client 啟動,然後把子程序關閉掉,觀察現象:
[root@centos-5610 tcpcliserv]# ./tcpcli01 127.0.0.1
>>1
str_cli: server terminated prematurely
過程解釋
本例的問題在於:
這正是後文 select 和 poll 這兩個函數的目的之一;後文經過修改,即可讓程式立刻對伺服器的 FIN 進行處理
SIGPIPE 訊號
例如伺服器宕機了,這種情況下伺服器來不及交代「遺言」就掛掉了。
發生的事情
ETIMEDOUT
,如果被路由器判定不可達,則返回 EHOSTUNREACH
或 ENETUNREACH
改進
SO-KEEPALIVE
通訊端選項readline
設定一個超時如果伺服器重啟
ECONNRESET
錯誤Unix 系統關機時,會「先禮後兵」:
SIGTERM
訊號給所有程序,在一段時間後再傳送 SIGKILL
訊號SIGTERM
程序一般會進行一些善後操作,如果程序不捕獲這個訊號,那他的預設行為就是終止程序SIGKILL
會讓所有程序終止,自然也會釋放通訊端等資訊由於如下的問題:
所以通過通訊端傳輸二進位制資料是不明智的。
解決方法有: