沈陽模板 網(wǎng)站建設(shè)淘寶網(wǎng)店代運(yùn)營正規(guī)公司
概述
TCP客戶端/服務(wù)器程序示例是執(zhí)行如下步驟的一個(gè)回射服務(wù)器:
- 客戶端從標(biāo)準(zhǔn)輸入讀入一行文本,并寫給服務(wù)器。
- 服務(wù)器從網(wǎng)絡(luò)輸入讀入這行文本,并回射給客戶端。
- 客戶端從網(wǎng)絡(luò)輸入讀入這行回射文本,并顯示在標(biāo)準(zhǔn)輸出上。
TCP服務(wù)器程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <arpa/inet.h>
#include <arpa/inet.h>#define MAXLINE 4096
#define SERV_PORT 9877
#define LISTENQ 1024
#define SA struct sockaddr// 從客戶端讀入數(shù)據(jù),并把它們回射給客戶端
void str_echo(int sockfd) {ssize_t n;char buf[MAXLINE];
again:// 從套接字讀入數(shù)據(jù)// 套接字中接收緩沖區(qū)和發(fā)送緩沖區(qū)是分開的,因此讀和寫不會(huì)發(fā)生混淆while ((n = read(sockfd, buf, MAXLINE)) > 0)write(sockfd, buf, n); // 把套接字中的內(nèi)容回射給客戶端// 如果n<0表示讀取數(shù)據(jù)出錯(cuò)或到達(dá)文件末尾// 如果errno等于EINTR,表示讀取操作被信號中斷// 如果上述兩個(gè)條件同時(shí)滿足,則重新嘗試讀取數(shù)據(jù)if (n < 0 && errno == EINTR)goto again;// 如果表示文件描述符到達(dá)文件末尾else if (n < 0)printf("str_echo: read error");
}int main(int argc, char **argv)
{int listenfd, connfd;pid_t childpid;socklen_t clilen;struct sockaddr_in cliaddr, servaddr;/* --------------------------------------------- *///1) 創(chuàng)建一個(gè)TCP連接套接字listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 把服務(wù)器對應(yīng)端口綁定到套接字 bzero(&servaddr, sizeof(servaddr)); // 開辟內(nèi)存servaddr.sin_family = AF_INET; // 地址族// 指定IP地址為INADDR_ANY,這樣要是服務(wù)器主機(jī)有多個(gè)網(wǎng)絡(luò)接口,服務(wù)器進(jìn)程就可以在任意網(wǎng)絡(luò)接口上接受客戶端連接servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);if (bind(listenfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {printf("bind error");return -1;}/* --------------------------------------------- *///3) 把套接字轉(zhuǎn)換為監(jiān)聽套接字// LISTENQ表示系統(tǒng)內(nèi)核允許在這個(gè)監(jiān)聽描述符上排隊(duì)的最大客戶端連接數(shù)if(listen(listenfd, LISTENQ) < 0) {printf("listen error");return -1;}/* --------------------------------------------- *///4) 接受客戶端連接,發(fā)送應(yīng)答for ( ; ; ) {clilen = sizeof(cliaddr);// connfd為已連接描述符,用于和客戶端進(jìn)行通信connfd = accept(listenfd, (SA *) &cliaddr, &clilen);if(connfd < 0) {printf("accept error");return -1;}if ((childpid = fork()) == 0) {// 子進(jìn)程關(guān)閉監(jiān)聽套接字if (close(listenfd) == -1) {printf("child close listenfd error");return -1; }str_echo(connfd); // 子進(jìn)程處理客戶端請求exit(0); // 清理描述符 }/* --------------------------------------------- *///5) 父進(jìn)程關(guān)閉已連接套接字if (close(connfd) == -1) {printf("parent close connfd error");return -1;}}
}
TCP客戶端程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h> /* basic socket definitions */#define MAXLINE 4096
#define SERV_PORT 9877
#define SA struct sockaddr char *Fgets(char *ptr, int n, FILE *stream)
{char *rptr;// 當(dāng)遇到文件結(jié)束符或錯(cuò)誤時(shí),fgets函數(shù)將返回一個(gè)空指針,于是客戶端處理循環(huán)終止if ( (rptr = fgets(ptr, n, stream)) == NULL && ferror(stream)) {printf("fgets error");return NULL; }return (rptr);
}ssize_t readline(int fd, void *vptr, size_t maxlen)
{ssize_t n, rc;char c, *ptr;ptr = vptr;for (n = 1; n < maxlen; n++) {if ( (rc = read(fd, &c, 1)) == 1) {*ptr++ = c;if (c == '\n')break;} else if (rc == 0) {if (n == 1)return(0); /* EOF, no data read */elsebreak; /* EOF, some data was read */} elsereturn(-1); /* error */}*ptr = 0;return(n);
}
/* end readline */void str_cli(FILE *fp, int sockfd) {char sendline[MAXLINE], recvline[MAXLINE];// 從控制臺(tái)讀入一行文本while (Fgets(sendline, MAXLINE, fp) != NULL) {// 把該行文本發(fā)送給服務(wù)器if (write(sockfd, sendline, strlen(sendline)) != strlen(sendline)) {printf("writen error");return; }// 從服務(wù)器讀入回射行if (readline(sockfd, recvline, MAXLINE) < 0){printf("readline error");return; }// 把它寫到標(biāo)準(zhǔn)輸出if (fputs(recvline, stdout) == EOF) {printf("fputs error");return; }}
}int main(int argc, char **argv)
{int sockfd;char recvline[MAXLINE + 1];struct sockaddr_in servaddr;if (argc != 2)exit(1);/* --------------------------------------------- *///1) 創(chuàng)建一個(gè)TCP連接套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 指定服務(wù)器的IP地址和端口bzero(&servaddr, sizeof(servaddr)); // 初始化內(nèi)存servaddr.sin_family = AF_INET; // 地址族servaddr.sin_port = htons(SERV_PORT); // 時(shí)間獲取服務(wù)器端口為13// 注意:此處的IP和端口是服務(wù)器的IP和端口// 把點(diǎn)分十進(jìn)制的IP地址(如:206.168.112.96)轉(zhuǎn)化為合適的格式if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {printf("inet_pton error for %s", argv[1]);return -1;}/* --------------------------------------------- *///3) 建立客戶端(sockfd)與服務(wù)器(servaddr)的連接,TCP連接if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {printf("connect error");return -1;}// 完成剩余部分的客戶端處理工作str_cli(stdin, sockfd);/* --------------------------------------------- *///5) 終止程序運(yùn)行,關(guān)閉該進(jìn)程打開的所有描述符和TCP套接字exit(0);
}
正常啟動(dòng)
1)啟動(dòng)TCP服務(wù)器程序
gcc -o tcpserv tcpserv.c
gcc -o tcpcli tcpcli.c ./tcpserv &
服務(wù)器啟動(dòng)后,它調(diào)用socked、bind、listen和accept,并阻塞于accept調(diào)用。
2)啟動(dòng)TCP客戶端程序
./tcpcli 127.0.0.1// 輸入字符串
kaikaixinxinxuebiancheng
啟動(dòng)客戶端程序并指定服務(wù)器主機(jī)的IP地址??蛻舳苏{(diào)用socket和connect,后者引起TCP三次握手過程。當(dāng)三次握手完成后,客戶端中的connect和服務(wù)器中的accept均返回,連接于是被建立。
接著發(fā)生步驟如下:
- 客戶端調(diào)用str_cli函數(shù),該函數(shù)將阻塞于fgets調(diào)用,因?yàn)槲覀冞€未曾鍵入過一行文本。
- 當(dāng)服務(wù)器中的accept返回時(shí),服務(wù)器調(diào)用fork,再由子進(jìn)程調(diào)用str_echo。該函數(shù)調(diào)用readline,readline調(diào)用read,而read在等待客戶端送入一行文本期間阻塞。
- 服務(wù)器父進(jìn)程再次調(diào)用accept并阻塞,等待下一個(gè)客戶端連接。
連接建立后,不論在客戶端中輸入什么,都會(huì)回射到它的標(biāo)準(zhǔn)輸出中。
接著在終端輸入EOF字符(Ctrl+D)以終止客戶端。
此時(shí)如果立刻執(zhí)行netstat命令,則將看到如下結(jié)果:
// 服務(wù)器本地端口為9877,客戶端本地端口為42758
netstat -a | grep 9877
當(dāng)前連接的客戶端(它的本地端口號為42758)進(jìn)入了TIME_WAIT狀態(tài),而監(jiān)聽服務(wù)器仍在等待另一個(gè)客戶端連接。
正常終止
正常終止客戶端與服務(wù)器步驟:
1)當(dāng)鍵入EOF字符時(shí),fgets返回一個(gè)空指針,于是str_cli函數(shù)返回。
2)當(dāng)str_cli返回到客戶端的main函數(shù)時(shí),main通過調(diào)用exit終止。
3)進(jìn)程終止處理的部分工作是關(guān)閉所有打開的描述符,因此客戶端打開的套接字由內(nèi)核關(guān)閉。這導(dǎo)致客戶端TCP發(fā)送一個(gè)FIN給服務(wù)器,服務(wù)器則以ACK響應(yīng),這就是TCP連接終止序列的前半部分。至此,服務(wù)器套接字處于CLOSE_WAIT狀態(tài),客戶端套接字則處于FIN_WAIT_2狀態(tài)。
4)當(dāng)服務(wù)器TCP接收FIN時(shí),服務(wù)器子進(jìn)程阻塞于read調(diào)用,于是read返回0,這導(dǎo)致str_echo函數(shù)返回服務(wù)器子進(jìn)程的main函數(shù)。
5)服務(wù)器子進(jìn)程通過調(diào)用exit來終止。
6)服務(wù)器子進(jìn)程中打開的所有描述符(包括已連接套接字)隨之關(guān)閉。子進(jìn)程關(guān)閉已連接套接字時(shí)會(huì)引發(fā)TCP連接終止序列的最后兩個(gè)分節(jié):一個(gè)從服務(wù)器到客戶端的FIN和一個(gè)從客戶端到服務(wù)器的ACK。至此,連接完全終止,客戶端套接字進(jìn)入TIME_WAIT狀態(tài)(允許老的重復(fù)分節(jié)在網(wǎng)絡(luò)中消逝)。
7)進(jìn)程終止處理的另一部分內(nèi)容是:在服務(wù)器進(jìn)程終止時(shí),給父進(jìn)程發(fā)送一個(gè)SIGCHLD信號,這一點(diǎn)在上述程序示例中發(fā)生了,但是沒有在代碼中捕獲該信號,而信號的默認(rèn)行為是被忽略。既然父進(jìn)程未加處理,子進(jìn)程于是進(jìn)入僵死狀態(tài)(僵尸進(jìn)程)??梢酝ㄟ^ps命令進(jìn)行驗(yàn)證:
// 查看當(dāng)前終端編號
tty// 查看子進(jìn)程狀態(tài)
ps -t /dev/pts/0 -o pid,ppid,tty,stat,args,wchan
查看結(jié)果:
子進(jìn)程狀態(tài)表現(xiàn)為Z(表示僵死)。針對僵死進(jìn)程(僵尸進(jìn)程),必須清理。
POSIX信號處理
信號(signal)就是告知某個(gè)進(jìn)程發(fā)生了某個(gè)事件的通知,有時(shí)也稱為軟件中斷。信號通常是異步發(fā)生的,也就是說進(jìn)程預(yù)先不知道信號的準(zhǔn)確發(fā)生時(shí)刻。
注意:
1)信號可以由一個(gè)進(jìn)程發(fā)給另一個(gè)進(jìn)程(或自身)。
2)信號可以由內(nèi)核發(fā)給某個(gè)進(jìn)程。
上一小節(jié)提到的SIGCHLD信號就是由內(nèi)核在任何一個(gè)進(jìn)程終止時(shí)發(fā)給它的父進(jìn)程的一個(gè)信號。
每個(gè)信號都有一個(gè)與之關(guān)聯(lián)的處置,也稱為行為。
SIGCHLD信號處理
思考:為什么必須要處理僵死進(jìn)程?
答:因?yàn)榻┧肋M(jìn)程占用內(nèi)核空間,最終可能導(dǎo)致耗盡進(jìn)程資源。所以,無論何時(shí)針對fork出來的子進(jìn)程都得使用wait函數(shù)處理它們,以防止它們變?yōu)榻┧肋M(jìn)程。
TCP服務(wù)器程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAXLINE 4096
#define SERV_PORT 9877
#define LISTENQ 1024
#define SA struct sockaddrtypedef void Sigfunc(int); /* for signal handlers */// SIGCHLD信號處理函數(shù),防止子進(jìn)程變?yōu)榻┧肋M(jìn)程
void sig_chld(int signo)
{pid_t pid;int stat;// 等待子進(jìn)程結(jié)束,并獲取子進(jìn)程的PID和退出狀態(tài)pid = wait(&stat);// 在此處調(diào)用諸如printf這樣的標(biāo)準(zhǔn)I/O是不合適的,此處只是作為查看子進(jìn)程何時(shí)終止的診斷手段printf("child %d terminated\n", pid);return;
}Sigfunc *signal(int signo, Sigfunc *func)
{// 定義信號動(dòng)作struct sigaction act, oact;act.sa_handler = func; // 設(shè)置信號處理函數(shù)sigemptyset(&act.sa_mask); // 清空信號掩碼集act.sa_flags = 0; // 設(shè)置信號處理方式為默認(rèn)if (signo == SIGALRM) {
#ifdef SA_INTERRUPTact.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif} else {
#ifdef SA_RESTARTact.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
#endif}if (sigaction(signo, &act, &oact) < 0)return(SIG_ERR);return(oact.sa_handler);
}
/* end signal */// 捕捉指定信號并采取行動(dòng)
Sigfunc *Signal(int signo, Sigfunc *func) /* for our signal() function */
{Sigfunc *sigfunc;if ( (sigfunc = signal(signo, func)) == SIG_ERR) {printf("signal error"); }return(sigfunc);
}// 從客戶端讀入數(shù)據(jù),并把它們回射給客戶端
void str_echo(int sockfd) {ssize_t n;char buf[MAXLINE];
again:// 從套接字讀入數(shù)據(jù)// 套接字中接收緩沖區(qū)和發(fā)送緩沖區(qū)是分開的,因此讀和寫不會(huì)發(fā)生混淆while ((n = read(sockfd, buf, MAXLINE)) > 0)write(sockfd, buf, n); // 把套接字中的內(nèi)容回射給客戶端// 如果n<0表示讀取數(shù)據(jù)出錯(cuò)或到達(dá)文件末尾// 如果errno等于EINTR,表示讀取操作被信號中斷// 如果上述兩個(gè)條件同時(shí)滿足,則重新嘗試讀取數(shù)據(jù)if (n < 0 && errno == EINTR)goto again;// 如果表示文件描述符到達(dá)文件末尾else if (n < 0)printf("str_echo: read error");
}int main(int argc, char **argv)
{int listenfd, connfd;pid_t childpid;socklen_t clilen;struct sockaddr_in cliaddr, servaddr;/* --------------------------------------------- *///1) 創(chuàng)建一個(gè)TCP連接套接字listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 把服務(wù)器對應(yīng)端口綁定到套接字 bzero(&servaddr, sizeof(servaddr)); // 開辟內(nèi)存servaddr.sin_family = AF_INET; // 地址族// 指定IP地址為INADDR_ANY,這樣要是服務(wù)器主機(jī)有多個(gè)網(wǎng)絡(luò)接口,服務(wù)器進(jìn)程就可以在任意網(wǎng)絡(luò)接口上接受客戶端連接servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);if (bind(listenfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {printf("bind error");return -1;}/* --------------------------------------------- *///3) 把套接字轉(zhuǎn)換為監(jiān)聽套接字// LISTENQ表示系統(tǒng)內(nèi)核允許在這個(gè)監(jiān)聽描述符上排隊(duì)的最大客戶端連接數(shù)if(listen(listenfd, LISTENQ) < 0) {printf("listen error");return -1;}// 捕捉指定信號并采取行動(dòng)Signal(SIGCHLD, sig_chld); /* must call waitpid() *//* --------------------------------------------- *///4) 接受客戶端連接,發(fā)送應(yīng)答for ( ; ; ) {clilen = sizeof(cliaddr);// connfd為已連接描述符,用于和客戶端進(jìn)行通信connfd = accept(listenfd, (SA *) &cliaddr, &clilen);if(connfd < 0) {if (errno == EINTR) {continue; // 重啟被中斷的accept } else {printf("accept error");return -1; }}if ((childpid = fork()) == 0) {// 子進(jìn)程關(guān)閉監(jiān)聽套接字if (close(listenfd) == -1) {printf("child close listenfd error");return -1; }str_echo(connfd); // 子進(jìn)程處理客戶端請求exit(0); // 清理描述符 }/* --------------------------------------------- *///5) 父進(jìn)程關(guān)閉已連接套接字if (close(connfd) == -1) {printf("parent close connfd error");return -1;}}
}
注意:如果connect函數(shù)返回EINTR,則不能重啟,否則將立即返回一個(gè)錯(cuò)誤。當(dāng)connect被一個(gè)捕獲的信號中斷而且不自動(dòng)重啟時(shí),必須調(diào)用select來等待連接完成。
TCP客戶端程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h> /* basic socket definitions */#define MAXLINE 4096
#define SERV_PORT 9877
#define SA struct sockaddr char *Fgets(char *ptr, int n, FILE *stream)
{char *rptr;// 當(dāng)遇到文件結(jié)束符或錯(cuò)誤時(shí),fgets函數(shù)將返回一個(gè)空指針,于是客戶端處理循環(huán)終止if ( (rptr = fgets(ptr, n, stream)) == NULL && ferror(stream)) {printf("fgets error");return NULL; }return (rptr);
}ssize_t readline(int fd, void *vptr, size_t maxlen)
{ssize_t n, rc;char c, *ptr;ptr = vptr;for (n = 1; n < maxlen; n++) {if ( (rc = read(fd, &c, 1)) == 1) {*ptr++ = c;if (c == '\n')break;} else if (rc == 0) {if (n == 1)return(0); /* EOF, no data read */elsebreak; /* EOF, some data was read */} elsereturn(-1); /* error */}*ptr = 0;return(n);
}
/* end readline */void str_cli(FILE *fp, int sockfd) {char sendline[MAXLINE], recvline[MAXLINE];// 從控制臺(tái)讀入一行文本while (Fgets(sendline, MAXLINE, fp) != NULL) {// 把該行文本發(fā)送給服務(wù)器if (write(sockfd, sendline, strlen(sendline)) != strlen(sendline)) {printf("writen error");return; }// 從服務(wù)器讀入回射行if (readline(sockfd, recvline, MAXLINE) < 0){printf("readline error");return; }// 把它寫到標(biāo)準(zhǔn)輸出if (fputs(recvline, stdout) == EOF) {printf("fputs error");return; }}
}int main(int argc, char **argv)
{int sockfd;char recvline[MAXLINE + 1];struct sockaddr_in servaddr;if (argc != 2)exit(1);/* --------------------------------------------- *///1) 創(chuàng)建一個(gè)TCP連接套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 指定服務(wù)器的IP地址和端口bzero(&servaddr, sizeof(servaddr)); // 初始化內(nèi)存servaddr.sin_family = AF_INET; // 地址族servaddr.sin_port = htons(SERV_PORT); // 時(shí)間獲取服務(wù)器端口為13// 注意:此處的IP和端口是服務(wù)器的IP和端口// 把點(diǎn)分十進(jìn)制的IP地址(如:206.168.112.96)轉(zhuǎn)化為合適的格式if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {printf("inet_pton error for %s", argv[1]);return -1;}/* --------------------------------------------- *///3) 建立客戶端(sockfd)與服務(wù)器(servaddr)的連接,TCP連接if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {printf("connect error");return -1;}// 完成剩余部分的客戶端處理工作str_cli(stdin, sockfd);/* --------------------------------------------- *///5) 終止程序運(yùn)行,關(guān)閉該進(jìn)程打開的所有描述符和TCP套接字exit(0);
}
執(zhí)行流程
// 啟動(dòng)服務(wù)器程序
./tcpserv02 &// 啟動(dòng)客戶端程序
./tcpserv02 127.0.0.1
hi there
hi there
^D 鍵入EOF字符
child 16942 terminated 信號處理函數(shù)中的printf輸出
accept error:Interrupted system call main函數(shù)終止執(zhí)行
具體各步驟如下:
1)鍵入EOF字符終止客戶端??蛻舳税l(fā)送一個(gè)FIN給服務(wù)器,服務(wù)器響應(yīng)一個(gè)ACK。
2)收到客戶端的FIN導(dǎo)致服務(wù)器TCP遞送一個(gè)EOF給子進(jìn)程阻塞中的readline,從而子進(jìn)程終止。
3)當(dāng)SIGCHLD信號遞交時(shí),父進(jìn)程阻塞與accept調(diào)用。sig_chld函數(shù)(信號處理函數(shù))執(zhí)行,其wait調(diào)用渠道子進(jìn)程的PID和終止?fàn)顟B(tài),隨后是printf調(diào)用,最后返回。
4)既然該信號是在父進(jìn)程阻塞于慢系統(tǒng)調(diào)用(accept)時(shí)由父進(jìn)程捕獲的,內(nèi)核就會(huì)使accept返回一個(gè)EINTR錯(cuò)誤(被中斷的系統(tǒng)調(diào)用)。父進(jìn)程不處理該錯(cuò)誤,于是父進(jìn)程中止,無法接受新的連接。
wait和waitpid函數(shù)
問1:什么是孤兒進(jìn)程?什么是僵尸進(jìn)程?二者分別會(huì)帶來什么危害?
答:
1)孤兒進(jìn)程:如果父進(jìn)程在子進(jìn)程結(jié)束前退出,那么子進(jìn)程就會(huì)成為孤兒進(jìn)程。在這種情況下,父進(jìn)程沒有機(jī)會(huì)調(diào)用wait或waitpid函數(shù)。每當(dāng)出現(xiàn)一個(gè)孤兒進(jìn)程的時(shí)候,內(nèi)核就把孤兒進(jìn)程交給init進(jìn)程管理。即init進(jìn)程會(huì)代替該孤兒進(jìn)程的父進(jìn)程回收孤兒進(jìn)程的資源,因此孤兒進(jìn)程并不會(huì)有什么危害。
2)僵尸進(jìn)程:如果子進(jìn)程結(jié)束時(shí),父進(jìn)程未調(diào)用wait或waitpid函數(shù)回收其資源,那么子進(jìn)程就會(huì)稱為僵尸進(jìn)程。如果釋放僵尸進(jìn)程的相關(guān)資源,其進(jìn)程號就會(huì)被一致占用,但是系統(tǒng)所能使用的進(jìn)程號是有限的,如果產(chǎn)生大量的僵尸進(jìn)程,最終將會(huì)因?yàn)闆]有可用的進(jìn)程號而導(dǎo)致系統(tǒng)不能產(chǎn)生新的進(jìn)程,所以應(yīng)該避免僵尸進(jìn)程的產(chǎn)生。
問2:為什么父進(jìn)程需要在fork之前調(diào)用wait或waitpid函數(shù)等待子進(jìn)程退出?
答:父進(jìn)程使用fork函數(shù)創(chuàng)建子進(jìn)程是為了處理多個(gè)客戶端連接。fork會(huì)創(chuàng)建一個(gè)與父進(jìn)程幾乎完全相同的子進(jìn)程,包括內(nèi)存空間、文件描述符等。這樣做的好處是父進(jìn)程可以繼續(xù)監(jiān)聽新的連接請求,而子進(jìn)程可以專注于處理已接受的連接。因此,父進(jìn)程調(diào)用wait或waitpid函數(shù)主要是為了防止出現(xiàn)僵尸進(jìn)程。
wait和waitpid函數(shù):
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);返回:若成功則返回已終止的進(jìn)程ID,若出錯(cuò)則返回0或-1
函數(shù)wait和waitpid均返回兩個(gè)值:已終止的進(jìn)程ID號,以及通過statloc指針返回的子進(jìn)程終止?fàn)顟B(tài)(一個(gè)整數(shù))。
可以調(diào)用三個(gè)宏來檢查終止?fàn)顟B(tài),并辨別子進(jìn)程是正常終止、由某個(gè)信號殺死還是僅僅由作業(yè)控制停止而已。另有些宏用于接著獲取子進(jìn)程的推出狀態(tài)、殺死子進(jìn)程的信號值或停止子進(jìn)程的作業(yè)控制號值。
如果調(diào)用wait的進(jìn)程沒有已終止的子進(jìn)程,不過有一個(gè)或多個(gè)子進(jìn)程仍在執(zhí)行,那么wait將阻塞到有子進(jìn)程第一個(gè)終止為止。
wait和waitpid的區(qū)別
客戶端程序
TCP客戶端程序修改后:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h> /* basic socket definitions */#define MAXLINE 4096
#define SERV_PORT 9877
#define SA struct sockaddr char *Fgets(char *ptr, int n, FILE *stream)
{char *rptr;// 當(dāng)遇到文件結(jié)束符或錯(cuò)誤時(shí),fgets函數(shù)將返回一個(gè)空指針,于是客戶端處理循環(huán)終止if ( (rptr = fgets(ptr, n, stream)) == NULL && ferror(stream)) {printf("fgets error");return NULL; }return (rptr);
}ssize_t readline(int fd, void *vptr, size_t maxlen)
{ssize_t n, rc;char c, *ptr;ptr = vptr;for (n = 1; n < maxlen; n++) {if ( (rc = read(fd, &c, 1)) == 1) {*ptr++ = c;if (c == '\n')break;} else if (rc == 0) {if (n == 1)return(0); /* EOF, no data read */elsebreak; /* EOF, some data was read */} elsereturn(-1); /* error */}*ptr = 0;return(n);
}
/* end readline */void str_cli(FILE *fp, int sockfd) {char sendline[MAXLINE], recvline[MAXLINE];// 從控制臺(tái)讀入一行文本while (Fgets(sendline, MAXLINE, fp) != NULL) {// 把該行文本發(fā)送給服務(wù)器if (write(sockfd, sendline, strlen(sendline)) != strlen(sendline)) {printf("writen error");return; }// 從服務(wù)器讀入回射行if (readline(sockfd, recvline, MAXLINE) < 0){printf("readline error");return; }// 把它寫到標(biāo)準(zhǔn)輸出if (fputs(recvline, stdout) == EOF) {printf("fputs error");return; }}
}int main(int argc, char **argv)
{int sockfd[5];char recvline[MAXLINE + 1];struct sockaddr_in servaddr;if (argc != 2)exit(1);for (int i = 0; i < 5; i++) {/* --------------------------------------------- *///1) 創(chuàng)建一個(gè)TCP連接套接字sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 指定服務(wù)器的IP地址和端口bzero(&servaddr, sizeof(servaddr)); // 初始化內(nèi)存servaddr.sin_family = AF_INET; // 地址族servaddr.sin_port = htons(SERV_PORT); // 時(shí)間獲取服務(wù)器端口為13// 注意:此處的IP和端口是服務(wù)器的IP和端口// 把點(diǎn)分十進(jìn)制的IP地址(如:206.168.112.96)轉(zhuǎn)化為合適的格式if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {printf("inet_pton error for %s", argv[1]);return -1;}/* --------------------------------------------- *///3) 建立客戶端(sockfd)與服務(wù)器(servaddr)的連接,TCP連接if (connect(sockfd[i], (SA *) &servaddr, sizeof(servaddr)) < 0) {printf("connect error");return -1;}}// 完成剩余部分的客戶端處理工作str_cli(stdin, sockfd[0]);/* --------------------------------------------- *///5) 終止程序運(yùn)行,關(guān)閉該進(jìn)程打開的所有描述符和TCP套接字exit(0);
}
客戶端建立5個(gè)與服務(wù)器的連接,隨后在調(diào)用str_cli函數(shù)時(shí)僅用第一個(gè)連接(sockfd[0])。建立多個(gè)連接的目的是從并發(fā)服務(wù)器上派生多個(gè)子進(jìn)程,如下圖所示:
當(dāng)客戶端終止時(shí),所有打開的文件描述符由內(nèi)核自動(dòng)關(guān)閉(無需調(diào)用close,僅調(diào)用exit),且所有5個(gè)連接基本在同一時(shí)刻終止。這就引發(fā)了5個(gè)FIN,每個(gè)連接一個(gè),它們反過來使服務(wù)器的5個(gè)子進(jìn)程基本在同一時(shí)刻終止。這又導(dǎo)致差不多在同一時(shí)刻有5個(gè)SIGCHLD信號遞交給父進(jìn)程,如圖所示:
注意:如上所述,由于調(diào)用了exit函數(shù),5個(gè)連接幾乎同時(shí)產(chǎn)生SIGCHLD信號,即多個(gè)SIGCHLD信號同時(shí)遞交給服務(wù)器。
測試結(jié)果
./tcpserv & 啟動(dòng)服務(wù)器程序
./tcpcli 127.0.0.1 啟動(dòng)客戶端程序
hello
hello
^D 鍵入EOF字符
child 31591 terminated 服務(wù)器輸出
從執(zhí)行結(jié)果可以看出,只有一個(gè)printf輸出而并非5個(gè),即信號處理函數(shù)只處理了一個(gè)SIGCHLD信號,剩下四個(gè)子進(jìn)程變?yōu)榻┦M(jìn)程。
問1:為什么只處理了一個(gè)SIGCHLD信號?
答:建立一個(gè)信號處理函數(shù)并在其中調(diào)用wait并不足以防止出現(xiàn)僵尸進(jìn)程。因?yàn)樗?個(gè)信號都在信號處理函數(shù)執(zhí)行之前產(chǎn)生,而信號處理函數(shù)只執(zhí)行一次,因?yàn)閁nix信號一般不排隊(duì)。更嚴(yán)重的是,本問題是不確定的。因?yàn)楸緦?shí)驗(yàn)是在同一個(gè)主機(jī)上,信號處理函數(shù)執(zhí)行1次,留下4個(gè)僵尸進(jìn)程。但是如果客戶端程序和服務(wù)端程序不在同一個(gè)主機(jī)上,那么信號處理函數(shù)一般執(zhí)行2次:一次是第一個(gè)產(chǎn)生的信號引起的,由于另外4個(gè)信號在信號處理函數(shù)第一次執(zhí)行時(shí)發(fā)生,因此該處理函數(shù)僅僅再被調(diào)用一次,從而留下3個(gè)僵尸進(jìn)程。不過有的時(shí)候,依賴于FIN到達(dá)服務(wù)器主機(jī)的時(shí)機(jī),信號處理函數(shù)可能會(huì)執(zhí)行3次甚至4次。
問2:如何讓信號處理函數(shù)調(diào)用多次,以防止出現(xiàn)僵尸進(jìn)程?
答:調(diào)用waitpid而不是wait函數(shù)。當(dāng)在一個(gè)循環(huán)內(nèi)調(diào)用waitpid,以獲取所有已終止子進(jìn)程的狀態(tài)時(shí),必須指定WNOHANG選項(xiàng),它告知waitpid在有尚未終止的子進(jìn)程在運(yùn)行時(shí)不要阻塞。不能在循環(huán)內(nèi)調(diào)用wait,因?yàn)闆]有辦法防止wait在正運(yùn)行的子進(jìn)程尚有未終止時(shí)阻塞。
服務(wù)端程序
修改后的服務(wù)端程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAXLINE 4096
#define SERV_PORT 9877
#define LISTENQ 1024
#define SA struct sockaddrtypedef void Sigfunc(int); /* for signal handlers */// SIGCHLD信號處理函數(shù),防止子進(jìn)程變?yōu)榻┧肋M(jìn)程
void sig_chld(int signo)
{pid_t pid;int stat;// 等待子進(jìn)程結(jié)束,并獲取子進(jìn)程的PID和退出狀態(tài)while (pid = waitpid(-1, &stat, WNOHANG)) > 0) {// 在此處調(diào)用諸如printf這樣的標(biāo)準(zhǔn)I/O是不合適的,此處只是作為查看子進(jìn)程何時(shí)終止的診斷手段printf("child %d terminated\n", pid);}return;
}Sigfunc *signal(int signo, Sigfunc *func)
{// 定義信號動(dòng)作struct sigaction act, oact;act.sa_handler = func; // 設(shè)置信號處理函數(shù)sigemptyset(&act.sa_mask); // 清空信號掩碼集act.sa_flags = 0; // 設(shè)置信號處理方式為默認(rèn)if (signo == SIGALRM) {
#ifdef SA_INTERRUPTact.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif} else {
#ifdef SA_RESTARTact.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
#endif}if (sigaction(signo, &act, &oact) < 0)return(SIG_ERR);return(oact.sa_handler);
}
/* end signal */// 捕捉指定信號并采取行動(dòng)
Sigfunc *Signal(int signo, Sigfunc *func) /* for our signal() function */
{Sigfunc *sigfunc;if ( (sigfunc = signal(signo, func)) == SIG_ERR) {printf("signal error"); }return(sigfunc);
}// 從客戶端讀入數(shù)據(jù),并把它們回射給客戶端
void str_echo(int sockfd) {ssize_t n;char buf[MAXLINE];
again:// 從套接字讀入數(shù)據(jù)// 套接字中接收緩沖區(qū)和發(fā)送緩沖區(qū)是分開的,因此讀和寫不會(huì)發(fā)生混淆while ((n = read(sockfd, buf, MAXLINE)) > 0)write(sockfd, buf, n); // 把套接字中的內(nèi)容回射給客戶端// 如果n<0表示讀取數(shù)據(jù)出錯(cuò)或到達(dá)文件末尾// 如果errno等于EINTR,表示讀取操作被信號中斷// 如果上述兩個(gè)條件同時(shí)滿足,則重新嘗試讀取數(shù)據(jù)if (n < 0 && errno == EINTR)goto again;// 如果表示文件描述符到達(dá)文件末尾else if (n < 0)printf("str_echo: read error");
}int main(int argc, char **argv)
{int listenfd, connfd;pid_t childpid;socklen_t clilen;struct sockaddr_in cliaddr, servaddr;/* --------------------------------------------- *///1) 創(chuàng)建一個(gè)TCP連接套接字listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 把服務(wù)器對應(yīng)端口綁定到套接字 bzero(&servaddr, sizeof(servaddr)); // 開辟內(nèi)存servaddr.sin_family = AF_INET; // 地址族// 指定IP地址為INADDR_ANY,這樣要是服務(wù)器主機(jī)有多個(gè)網(wǎng)絡(luò)接口,服務(wù)器進(jìn)程就可以在任意網(wǎng)絡(luò)接口上接受客戶端連接servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);if (bind(listenfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {printf("bind error");return -1;}/* --------------------------------------------- *///3) 把套接字轉(zhuǎn)換為監(jiān)聽套接字// LISTENQ表示系統(tǒng)內(nèi)核允許在這個(gè)監(jiān)聽描述符上排隊(duì)的最大客戶端連接數(shù)if(listen(listenfd, LISTENQ) < 0) {printf("listen error");return -1;}// 捕捉指定信號并采取行動(dòng)Signal(SIGCHLD, sig_chld); /* must call waitpid() *//* --------------------------------------------- *///4) 接受客戶端連接,發(fā)送應(yīng)答for ( ; ; ) {clilen = sizeof(cliaddr);// connfd為已連接描述符,用于和客戶端進(jìn)行通信connfd = accept(listenfd, (SA *) &cliaddr, &clilen);if(connfd < 0) {if (errno == EINTR) {continue; // 重啟被中斷的accept } else {printf("accept error");return -1; }}if ((childpid = fork()) == 0) {// 子進(jìn)程關(guān)閉監(jiān)聽套接字if (close(listenfd) == -1) {printf("child close listenfd error");return -1; }str_echo(connfd); // 子進(jìn)程處理客戶端請求exit(0); // 清理描述符 }/* --------------------------------------------- *///5) 父進(jìn)程關(guān)閉已連接套接字if (close(connfd) == -1) {printf("parent close connfd error");return -1;}}
}
小結(jié)
問:SIGCHLD信號是怎么產(chǎn)生的,有什么作用?
答:SIGCHLD 信號是由操作系統(tǒng)產(chǎn)生的,當(dāng)一個(gè)子進(jìn)程結(jié)束(無論是正常退出還是被終止)時(shí),操作系統(tǒng)都會(huì)向父進(jìn)程發(fā)送這個(gè)信號。這個(gè)信號的目的是通知父進(jìn)程子進(jìn)程的狀態(tài)已經(jīng)改變,父進(jìn)程可以采取相應(yīng)的行動(dòng),比如回收子進(jìn)程使用的資源。
注意:父進(jìn)程調(diào)用wait函數(shù)時(shí)會(huì)阻塞整個(gè)父進(jìn)程的執(zhí)行,直到某一個(gè)或幾個(gè)子進(jìn)程結(jié)束,才會(huì)結(jié)束阻塞。上述服務(wù)器程序是通過異步調(diào)用wait函數(shù),所以看上去不是那么直觀,非異步調(diào)用wait如下:
for ( ; ; ) {clilen = sizeof(cliaddr);// connfd為已連接描述符,用于和客戶端進(jìn)行通信connfd = accept(listenfd, (SA *) &cliaddr, &clilen);if(connfd < 0) {if (errno == EINTR) {continue; // 重啟被中斷的accept } else {printf("accept error");return -1; }}if ((childpid = fork()) == 0) {// 子進(jìn)程關(guān)閉監(jiān)聽套接字if (close(listenfd) == -1) {printf("child close listenfd error");return -1; }str_echo(connfd); // 子進(jìn)程處理客戶端請求exit(0); // 清理描述符 }// 等待子進(jìn)程結(jié)束并回收子進(jìn)程資源int status;wait(&status);/* --------------------------------------------- *///5) 父進(jìn)程關(guān)閉已連接套接字if (close(connfd) == -1) {printf("parent close connfd error");return -1;}
}
UNIX網(wǎng)絡(luò)編程總結(jié):
1)當(dāng)fork子進(jìn)程時(shí),必須捕獲SIGCHLD信號。
2)當(dāng)捕獲信號時(shí),父進(jìn)程必須處理被中斷的系統(tǒng)調(diào)用,如accept函數(shù)。
3)SIGCHLD的信號處理函數(shù)必須正確書寫,并使用waitpid函數(shù)以免留下僵尸進(jìn)程。
如果需要代碼包,請?jiān)谠u論區(qū)留言!!!?
如果需要代碼包,請?jiān)谠u論區(qū)留言!!!?
如果需要代碼包,請?jiān)谠u論區(qū)留言!!!?