## 1. 經典的回射程序
### 1) 服務器程序srv.c
~~~
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <errno.h>
#define MAXLINE 1024
#define SA struct sockaddr
void str_echo(int sockfd);
int main(int argc, char **argv)
{
int listenfd, connfd;
int buff[MAXLINE];
pid_t pid;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t cliLen;
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(9877);
bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
listen(listenfd, 5);
for ( ; ; ){
cliLen = sizeof(cliaddr);
connfd = accept(listenfd, (SA *)&cliaddr, &cliLen);
if ((pid = fork()) == 0){
close(listenfd);
str_echo(connfd);
_exit(0);
}
if (waitpid(pid, NULL, 0) != pid){
printf("waitpid error\n");
exit(1);
}
close(connfd);
}
return 0;
}
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ((n = read(sockfd, buf, MAXLINE)) > 0){
buf[n] = '\0';
write(sockfd, buf, n);
}
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
printf("str_echo:read error\n");
}維護子進程的信息,以便父進程在以后某個時候獲取。這些信息包括子進程的進程ID,終止狀
~~~
### 2) 客戶端程序cli.c
~~~
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <fcntl.h>
#define MAXLINE 1024
#define SA struct sockaddr
void str_cli(FILE *fp, int sockfd);
int main(int argc, char **argv)
{
int sockfd, n;
struct sockaddr_in servaddr;
char buff[MAXLINE + 1];
struct sockaddr_in cliaddr;
socklen_t cliLen;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd, (SA *)&servaddr, sizeof(servaddr));
str_cli(stdin, sockfd);
return 0;
}
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (fgets(sendline, MAXLINE, fp) != NULL){
write(sockfd, sendline, strlen(sendline));
if (read(sockfd, recvline, MAXLINE) == 0){
printf("str_cli:server terminated prematurely\n");
return;
}
fputs(recvline, stdout);
}
}
~~~
### 3)程序運行
### (1)服務器后臺啟動
~~~
leichaojian@ThinkPad-T430i:~$ ./srv &
[1] 3932
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
tcp 0 0 *:9877 *:* LISTEN
~~~
### (2)啟動客戶端,并且鍵入一行文本
客戶端:
~~~
leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1
hello world
hello world
~~~
服務端:
~~~
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
tcp 0 0 *:9877 *:* LISTEN
tcp 0 0 localhost:9877 localhost:43399 ESTABLISHED
tcp 0 0 localhost:43399 localhost:9877 ESTABLISHED
~~~
### (3)客戶端終止
~~~
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
tcp 0 0 *:9877 *:* LISTEN
tcp 0 0 localhost:43399 localhost:9877 TIME_WAIT
~~~
## 2. 處理信號
### 1) POSIX信號處理
?? 信號就是告知某個進程發生了某個事件的通知,有時也稱為軟件中斷。信號通常是異步發生的,也就是說進程預先不知道信號的準確發生時刻。
?? 信號可以:
(1)由一個進程發給另一個進程。
(2)由內核發給某個進程。
?? 每個信號都有一個與之關聯的處置,也稱為行為:
(1)我們可以提供一個函數,只要有特定信號發生它就會被調用。這樣的函數稱為信號處理函數,這種行為稱為捕獲信號。有兩個信號不能被捕獲,它們是SIGKILL和SIGSTOP。信號處理函數由信號值這個單一的整數參數來調用,且沒有返回值,其函數原型如下:
~~~
void handler( int signo );
~~~
(2)我們可以把某個信號的處置設定為SIG_IGN來忽略它。SIGKILL和SIGSTOP這兩個信號不能被忽略。
(3)我們可以把某個信號的處置設定為SIG_DFL來啟用它的默認處置。
### 2) 處理SIGCHLD信號
?? 設置僵尸狀態的目的是維護子進程的信息,以便父進程在以后某個時候獲取。這些信息包括子進程的進程ID,終止狀態以及資源利用信息。如果一個進程終止,而該進程有子進程處于僵尸狀態,那么它的所有僵尸子進程的父進程ID將被重置為1(init進程)。繼承這些子進程的init進程將清理它們。
?? 而僵尸進程出現時間是在子進程終止后,但是父進程尚未讀取這些數據之前。所有解決之道就是保證父進程處理這些數據,我們可以通過wait或者waitpid函數來達到這個要求。
?? 由于子進程的終止必然會產生信號SIGCHLD信號,所以重寫TCP服務器程序最終版本:
### (1)服務器程序srv.c
~~~
#include <stdio.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <signal.h>
#include <errno.h>
#define MAXLINE 1024
#define SA struct sockaddr
void sig_chld(int signo);
typedef void Sigfunc(int);
Sigfunc *Signal(int signo, Sigfunc *func);
void str_echo(int sockfd);
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in servaddr, cliaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
listen(listenfd, 5);
Signal(SIGCHLD, sig_chld);
for ( ; ; ){
clilen = sizeof(cliaddr);
if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0){
if (errno == EINTR)
continue;
else{
printf("accept error\n");
exit(-1);
}
}
if ((childpid = fork()) == 0){
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
return 0;
}
void sig_chld(int signo)
{
pid_t pid;
int stat;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
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;
#endif
} else {
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART;
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return (SIG_ERR);
return (oact.sa_handler);
}
void str_echo(int sockfd)
{
char buff[MAXLINE];
int n;
for ( ; ; ){
if ((n = read(sockfd, buff, MAXLINE)) > 0){
buff[n] = '\0';
write(sockfd, buff, n);
}
else if (n < 0 && errno == EINTR)
continue;
else if (n < 0){
printf("str_echo:read error\n");
return;
}
else if (n == 0){
break;
}
}
}
~~~
### (2)客戶端測試程序cli.c
~~~
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <fcntl.h>
#define MAXLINE 1024
#define SA struct sockaddr
void str_cli(FILE *fp, int sockfd);
int main(int argc, char **argv)
{
int sockfd[5], n;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
int i;
for (i = 0; i < 5; i++){
sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd[i], (SA *)&servaddr, sizeof(servaddr));
}
str_cli(stdin, sockfd[0]);
return 0;
}
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (fgets(sendline, MAXLINE, fp) != NULL){
write(sockfd, sendline, strlen(sendline));
if (read(sockfd, recvline, MAXLINE) == 0){
printf("str_cli:server terminated prematurely\n");
return;
}
fputs(recvline, stdout);
}
}
~~~
程序輸出如下:
客戶端:
~~~
leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1
hello world
hello world
^C
~~~
服務端:
~~~
leichaojian@ThinkPad-T430i:~$ ./srv
child 9831 terminated
child 9835 terminated
child 9832 terminated
child 9833 terminated
child 9834 terminated
^C
~~~
### 3) 測試僵尸進程的產生
test.c:
~~~
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
int main( void )
{
pid_t pid;
if ( ( pid = fork() ) == 0 ){
printf("child:%d\n", getpid());
exit(0);
}
sleep( 20 );
if ( pid > 0 ){
printf("parent:%d\n", getpid() );
}
return 0;
}
~~~
程序運行:
~~~
leichaojian@ThinkPad-T430i:~$ ./a.out
child:14447
parent:14446
~~~
在顯示child:14447而尚未顯示parent:14446(即20秒的睡眠時間),我們執行如下命令:
~~~
leichaojian@ThinkPad-T430i:~$ ps -eo state,pid,cmd | grep '^Z'
Z 14447 [a.out] <defunct>
~~~
?? 發現子進程14447果真稱為僵尸進程。但是過了20秒后,再次執行時候,則沒有任何數據,說明僵尸進程已經被父進程殺死了(就是父進程讀取了子進程的數據)
### 4) 服務器進程終止
具體操作如下:
### 1)運行服務器程序,運行客戶端程序:
服務端:
~~~
leichaojian@ThinkPad-T430i:~$ ./tcpserv
~~~
客戶端:
~~~
leichaojian@ThinkPad-T430i:~$ ./tcpcli 127.0.0.1
hello
hello
~~~
監視端:
~~~
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
tcp 0 0 *:9877 *:* LISTEN
tcp 0 0 localhost:37935 localhost:9877 ESTABLISHED
tcp 0 0 localhost:9877 localhost:37935 ESTABLISHED
~~~
### 2) 終止服務器程序(先終止服務器程序,然后執行監視端,再執行客戶端,再執行監視端)
監視端:
~~~
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
tcp 0 0 localhost:9877 localhost:37953 FIN_WAIT2
tcp 1 0 localhost:37953 localhost:9877 CLOSE_WAIT
~~~
客戶端:
~~~
leichaojian@ThinkPad-T430i:~$ ./tcpcli 127.0.0.1
hello
hello
world
str_cli error
~~~
監視端:(無任何輸出,說明客戶端進程已經終止,這里終止是產生了信號,強行終止)
來自UNP上的解釋是:當一個進程向某個已收到RST的套接字執行寫操作時,內核向該進程發送一個SIGPIPE信號。該信號的默認行為是終止進程,因此進程必須捕獲它以免不情愿的被終止。
~~~
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
~~~
### 3) 問題出在哪里?
?? 當服務端的FIN到達套接字時,客戶正阻塞與fgets調用上。客戶實際上在應對兩個描述符--套接字和用戶輸入,它不能單純阻塞在這兩個源中的某個特定源的輸入上(正如目前編寫的str_cli函數所為),而是應該阻塞在其中任何一個源的輸入上,這正是select和poll這兩個函數的目的之一。