UNIX网络编程-TCP套接字编程(实战)

概述


TCP客户端/服务器程序示例是执行如下步骤的一个回射服务器:

  1. 客户端从标准输入读入一行文本,并写给服务器。
  2. 服务器从网络输入读入这行文本,并回射给客户端。
  3. 客户端从网络输入读入这行回射文本,并显示在标准输出上。

TCP服务器程序

#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// 从客户端读入数据,并把它们回射给客户端
void str_echo(int sockfd) {ssize_t n;char    buf[MAXLINE];
again:// 从套接字读入数据// 套接字中接收缓冲区和发送缓冲区是分开的,因此读和写不会发生混淆while ((n = read(sockfd, buf, MAXLINE)) > 0)write(sockfd, buf, n);    // 把套接字中的内容回射给客户端// 如果n<0表示读取数据出错或到达文件末尾// 如果errno等于EINTR,表示读取操作被信号中断// 如果上述两个条件同时满足,则重新尝试读取数据if (n < 0 && errno == EINTR)goto again;// 如果表示文件描述符到达文件末尾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) 创建一个TCP连接套接字listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 把服务器对应端口绑定到套接字 bzero(&servaddr, sizeof(servaddr));     // 开辟内存servaddr.sin_family      = AF_INET;     // 地址族// 指定IP地址为INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任意网络接口上接受客户端连接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) 把套接字转换为监听套接字// LISTENQ表示系统内核允许在这个监听描述符上排队的最大客户端连接数if(listen(listenfd, LISTENQ) < 0) {printf("listen error");return -1;}/* --------------------------------------------- *///4) 接受客户端连接,发送应答for ( ; ; ) {clilen = sizeof(cliaddr);// connfd为已连接描述符,用于和客户端进行通信connfd = accept(listenfd, (SA *) &cliaddr, &clilen);if(connfd < 0) {printf("accept error");return -1;}if ((childpid = fork()) == 0) {// 子进程关闭监听套接字if (close(listenfd) == -1) {printf("child close listenfd error");return -1;           }str_echo(connfd);    // 子进程处理客户端请求exit(0);             // 清理描述符    }/* --------------------------------------------- *///5) 父进程关闭已连接套接字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;// 当遇到文件结束符或错误时,fgets函数将返回一个空指针,于是客户端处理循环终止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];// 从控制台读入一行文本while (Fgets(sendline, MAXLINE, fp) != NULL) {// 把该行文本发送给服务器if (write(sockfd, sendline, strlen(sendline)) != strlen(sendline)) {printf("writen error");return;             }// 从服务器读入回射行if (readline(sockfd, recvline, MAXLINE) < 0){printf("readline error");return;        }// 把它写到标准输出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) 创建一个TCP连接套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 指定服务器的IP地址和端口bzero(&servaddr, sizeof(servaddr));         // 初始化内存servaddr.sin_family = AF_INET;              // 地址族servaddr.sin_port   = htons(SERV_PORT);     // 时间获取服务器端口为13// 注意:此处的IP和端口是服务器的IP和端口// 把点分十进制的IP地址(如:206.168.112.96)转化为合适的格式if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {printf("inet_pton error for %s", argv[1]);return -1;}/* --------------------------------------------- *///3) 建立客户端(sockfd)与服务器(servaddr)的连接,TCP连接if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {printf("connect error");return -1;}// 完成剩余部分的客户端处理工作str_cli(stdin, sockfd);/* --------------------------------------------- *///5) 终止程序运行,关闭该进程打开的所有描述符和TCP套接字exit(0);
}

正常启动

1)启动TCP服务器程序

gcc -o tcpserv tcpserv.c 
gcc -o tcpcli tcpcli.c ./tcpserv &

服务器启动后,它调用socked、bind、listen和accept,并阻塞于accept调用。

2)启动TCP客户端程序

./tcpcli 127.0.0.1// 输入字符串
kaikaixinxinxuebiancheng

启动客户端程序并指定服务器主机的IP地址。客户端调用socket和connect,后者引起TCP三次握手过程。当三次握手完成后,客户端中的connect和服务器中的accept均返回,连接于是被建立。

接着发生步骤如下:

  1. 客户端调用str_cli函数,该函数将阻塞于fgets调用,因为我们还未曾键入过一行文本。
  2. 当服务器中的accept返回时,服务器调用fork,再由子进程调用str_echo。该函数调用readline,readline调用read,而read在等待客户端送入一行文本期间阻塞。
  3. 服务器父进程再次调用accept并阻塞,等待下一个客户端连接。

连接建立后,不论在客户端中输入什么,都会回射到它的标准输出中。

接着在终端输入EOF字符(Ctrl+D)以终止客户端。

此时如果立刻执行netstat命令,则将看到如下结果:

// 服务器本地端口为9877,客户端本地端口为42758
netstat -a | grep 9877

当前连接的客户端(它的本地端口号为42758)进入了TIME_WAIT状态,而监听服务器仍在等待另一个客户端连接。

正常终止

正常终止客户端与服务器步骤:

1)当键入EOF字符时,fgets返回一个空指针,于是str_cli函数返回。

2)当str_cli返回到客户端的main函数时,main通过调用exit终止。

3)进程终止处理的部分工作是关闭所有打开的描述符,因此客户端打开的套接字由内核关闭。这导致客户端TCP发送一个FIN给服务器,服务器则以ACK响应,这就是TCP连接终止序列的前半部分。至此,服务器套接字处于CLOSE_WAIT状态,客户端套接字则处于FIN_WAIT_2状态。

4)当服务器TCP接收FIN时,服务器子进程阻塞于read调用,于是read返回0,这导致str_echo函数返回服务器子进程的main函数。

5)服务器子进程通过调用exit来终止。

6)服务器子进程中打开的所有描述符(包括已连接套接字)随之关闭。子进程关闭已连接套接字时会引发TCP连接终止序列的最后两个分节:一个从服务器到客户端的FIN和一个从客户端到服务器的ACK。至此,连接完全终止,客户端套接字进入TIME_WAIT状态(允许老的重复分节在网络中消逝)。

7)进程终止处理的另一部分内容是:在服务器进程终止时,给父进程发送一个SIGCHLD信号,这一点在上述程序示例中发生了,但是没有在代码中捕获该信号,而信号的默认行为是被忽略。既然父进程未加处理,子进程于是进入僵死状态(僵尸进程)。可以通过ps命令进行验证:

// 查看当前终端编号
tty// 查看子进程状态
ps -t /dev/pts/0 -o pid,ppid,tty,stat,args,wchan

查看结果:

子进程状态表现为Z(表示僵死)。针对僵死进程(僵尸进程),必须清理。

POSIX信号处理

信号(signal)就是告知某个进程发生了某个事件的通知,有时也称为软件中断。信号通常是异步发生的,也就是说进程预先不知道信号的准确发生时刻。

注意:

1)信号可以由一个进程发给另一个进程(或自身)。

2)信号可以由内核发给某个进程。

上一小节提到的SIGCHLD信号就是由内核在任何一个进程终止时发给它的父进程的一个信号。

每个信号都有一个与之关联的处置,也称为行为。

SIGCHLD信号处理

思考:为什么必须要处理僵死进程?

答:因为僵死进程占用内核空间,最终可能导致耗尽进程资源。所以,无论何时针对fork出来的子进程都得使用wait函数处理它们,以防止它们变为僵死进程。

TCP服务器程序

#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信号处理函数,防止子进程变为僵死进程
void sig_chld(int signo)
{pid_t   pid;int     stat;// 等待子进程结束,并获取子进程的PID和退出状态pid = wait(&stat);// 在此处调用诸如printf这样的标准I/O是不合适的,此处只是作为查看子进程何时终止的诊断手段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_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 */// 捕捉指定信号并采取行动
Sigfunc *Signal(int signo, Sigfunc *func)    /* for our signal() function */
{Sigfunc *sigfunc;if ( (sigfunc = signal(signo, func)) == SIG_ERR) {printf("signal error");    }return(sigfunc);
}// 从客户端读入数据,并把它们回射给客户端
void str_echo(int sockfd) {ssize_t n;char    buf[MAXLINE];
again:// 从套接字读入数据// 套接字中接收缓冲区和发送缓冲区是分开的,因此读和写不会发生混淆while ((n = read(sockfd, buf, MAXLINE)) > 0)write(sockfd, buf, n);    // 把套接字中的内容回射给客户端// 如果n<0表示读取数据出错或到达文件末尾// 如果errno等于EINTR,表示读取操作被信号中断// 如果上述两个条件同时满足,则重新尝试读取数据if (n < 0 && errno == EINTR)goto again;// 如果表示文件描述符到达文件末尾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) 创建一个TCP连接套接字listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 把服务器对应端口绑定到套接字 bzero(&servaddr, sizeof(servaddr));     // 开辟内存servaddr.sin_family      = AF_INET;     // 地址族// 指定IP地址为INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任意网络接口上接受客户端连接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) 把套接字转换为监听套接字// LISTENQ表示系统内核允许在这个监听描述符上排队的最大客户端连接数if(listen(listenfd, LISTENQ) < 0) {printf("listen error");return -1;}// 捕捉指定信号并采取行动Signal(SIGCHLD, sig_chld);    /* must call waitpid() *//* --------------------------------------------- *///4) 接受客户端连接,发送应答for ( ; ; ) {clilen = sizeof(cliaddr);// connfd为已连接描述符,用于和客户端进行通信connfd = accept(listenfd, (SA *) &cliaddr, &clilen);if(connfd < 0) {if (errno == EINTR) {continue;     // 重启被中断的accept           } else {printf("accept error");return -1;           }}if ((childpid = fork()) == 0) {// 子进程关闭监听套接字if (close(listenfd) == -1) {printf("child close listenfd error");return -1;           }str_echo(connfd);    // 子进程处理客户端请求exit(0);             // 清理描述符    }/* --------------------------------------------- *///5) 父进程关闭已连接套接字if (close(connfd) == -1) {printf("parent close connfd error");return -1;}}
}

注意:如果connect函数返回EINTR,则不能重启,否则将立即返回一个错误。当connect被一个捕获的信号中断而且不自动重启时,必须调用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;// 当遇到文件结束符或错误时,fgets函数将返回一个空指针,于是客户端处理循环终止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];// 从控制台读入一行文本while (Fgets(sendline, MAXLINE, fp) != NULL) {// 把该行文本发送给服务器if (write(sockfd, sendline, strlen(sendline)) != strlen(sendline)) {printf("writen error");return;             }// 从服务器读入回射行if (readline(sockfd, recvline, MAXLINE) < 0){printf("readline error");return;        }// 把它写到标准输出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) 创建一个TCP连接套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 指定服务器的IP地址和端口bzero(&servaddr, sizeof(servaddr));         // 初始化内存servaddr.sin_family = AF_INET;              // 地址族servaddr.sin_port   = htons(SERV_PORT);     // 时间获取服务器端口为13// 注意:此处的IP和端口是服务器的IP和端口// 把点分十进制的IP地址(如:206.168.112.96)转化为合适的格式if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {printf("inet_pton error for %s", argv[1]);return -1;}/* --------------------------------------------- *///3) 建立客户端(sockfd)与服务器(servaddr)的连接,TCP连接if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {printf("connect error");return -1;}// 完成剩余部分的客户端处理工作str_cli(stdin, sockfd);/* --------------------------------------------- *///5) 终止程序运行,关闭该进程打开的所有描述符和TCP套接字exit(0);
}

执行流程

// 启动服务器程序
./tcpserv02 &// 启动客户端程序
./tcpserv02 127.0.0.1
hi there
hi there
^D                                        键入EOF字符
child 16942 terminated                    信号处理函数中的printf输出
accept error:Interrupted system call      main函数终止执行

具体各步骤如下:

1)键入EOF字符终止客户端。客户端发送一个FIN给服务器,服务器响应一个ACK。

2)收到客户端的FIN导致服务器TCP递送一个EOF给子进程阻塞中的readline,从而子进程终止。

3)当SIGCHLD信号递交时,父进程阻塞与accept调用。sig_chld函数(信号处理函数)执行,其wait调用渠道子进程的PID和终止状态,随后是printf调用,最后返回。

4)既然该信号是在父进程阻塞于慢系统调用(accept)时由父进程捕获的,内核就会使accept返回一个EINTR错误(被中断的系统调用)。父进程不处理该错误,于是父进程中止,无法接受新的连接。

wait和waitpid函数

问1:什么是孤儿进程?什么是僵尸进程?二者分别会带来什么危害?

答:

1)孤儿进程:如果父进程在子进程结束前退出,那么子进程就会成为孤儿进程。在这种情况下,父进程没有机会调用wait或waitpid函数。每当出现一个孤儿进程的时候,内核就把孤儿进程交给init进程管理。即init进程会代替该孤儿进程的父进程回收孤儿进程的资源,因此孤儿进程并不会有什么危害。

2)僵尸进程:如果子进程结束时,父进程未调用wait或waitpid函数回收其资源,那么子进程就会称为僵尸进程。如果释放僵尸进程的相关资源,其进程号就会被一致占用,但是系统所能使用的进程号是有限的,如果产生大量的僵尸进程,最终将会因为没有可用的进程号而导致系统不能产生新的进程,所以应该避免僵尸进程的产生。

问2:为什么父进程需要在fork之前调用wait或waitpid函数等待子进程退出?

答:父进程使用fork函数创建子进程是为了处理多个客户端连接。fork会创建一个与父进程几乎完全相同的子进程,包括内存空间、文件描述符等。这样做的好处是父进程可以继续监听新的连接请求,而子进程可以专注于处理已接受的连接。因此,父进程调用wait或waitpid函数主要是为了防止出现僵尸进程。

wait和waitpid函数:

#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);返回:若成功则返回已终止的进程ID,若出错则返回0或-1

函数wait和waitpid均返回两个值:已终止的进程ID号,以及通过statloc指针返回的子进程终止状态(一个整数)。

可以调用三个宏来检查终止状态,并辨别子进程是正常终止、由某个信号杀死还是仅仅由作业控制停止而已。另有些宏用于接着获取子进程的推出状态、杀死子进程的信号值或停止子进程的作业控制号值。

如果调用wait的进程没有已终止的子进程,不过有一个或多个子进程仍在执行,那么wait将阻塞到有子进程第一个终止为止。

wait和waitpid的区别

客户端程序

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;// 当遇到文件结束符或错误时,fgets函数将返回一个空指针,于是客户端处理循环终止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];// 从控制台读入一行文本while (Fgets(sendline, MAXLINE, fp) != NULL) {// 把该行文本发送给服务器if (write(sockfd, sendline, strlen(sendline)) != strlen(sendline)) {printf("writen error");return;             }// 从服务器读入回射行if (readline(sockfd, recvline, MAXLINE) < 0){printf("readline error");return;        }// 把它写到标准输出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) 创建一个TCP连接套接字sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 指定服务器的IP地址和端口bzero(&servaddr, sizeof(servaddr));         // 初始化内存servaddr.sin_family = AF_INET;              // 地址族servaddr.sin_port   = htons(SERV_PORT);     // 时间获取服务器端口为13// 注意:此处的IP和端口是服务器的IP和端口// 把点分十进制的IP地址(如:206.168.112.96)转化为合适的格式if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {printf("inet_pton error for %s", argv[1]);return -1;}/* --------------------------------------------- *///3) 建立客户端(sockfd)与服务器(servaddr)的连接,TCP连接if (connect(sockfd[i], (SA *) &servaddr, sizeof(servaddr)) < 0) {printf("connect error");return -1;}}// 完成剩余部分的客户端处理工作str_cli(stdin, sockfd[0]);/* --------------------------------------------- *///5) 终止程序运行,关闭该进程打开的所有描述符和TCP套接字exit(0);
}

客户端建立5个与服务器的连接,随后在调用str_cli函数时仅用第一个连接(sockfd[0])。建立多个连接的目的是从并发服务器上派生多个子进程,如下图所示:

当客户端终止时,所有打开的文件描述符由内核自动关闭(无需调用close,仅调用exit),且所有5个连接基本在同一时刻终止。这就引发了5个FIN,每个连接一个,它们反过来使服务器的5个子进程基本在同一时刻终止。这又导致差不多在同一时刻有5个SIGCHLD信号递交给父进程,如图所示:

注意:如上所述,由于调用了exit函数,5个连接几乎同时产生SIGCHLD信号,即多个SIGCHLD信号同时递交给服务器。

测试结果

./tcpserv &               启动服务器程序
./tcpcli 127.0.0.1        启动客户端程序
hello
hello
^D                        键入EOF字符
child 31591 terminated    服务器输出

从执行结果可以看出,只有一个printf输出而并非5个,即信号处理函数只处理了一个SIGCHLD信号,剩下四个子进程变为僵尸进程。

问1:为什么只处理了一个SIGCHLD信号?

答:建立一个信号处理函数并在其中调用wait并不足以防止出现僵尸进程。因为所有5个信号都在信号处理函数执行之前产生,而信号处理函数只执行一次,因为Unix信号一般不排队。更严重的是,本问题是不确定的。因为本实验是在同一个主机上,信号处理函数执行1次,留下4个僵尸进程。但是如果客户端程序和服务端程序不在同一个主机上,那么信号处理函数一般执行2次:一次是第一个产生的信号引起的,由于另外4个信号在信号处理函数第一次执行时发生,因此该处理函数仅仅再被调用一次,从而留下3个僵尸进程。不过有的时候,依赖于FIN到达服务器主机的时机,信号处理函数可能会执行3次甚至4次。

问2:如何让信号处理函数调用多次,以防止出现僵尸进程?

答:调用waitpid而不是wait函数。当在一个循环内调用waitpid,以获取所有已终止子进程的状态时,必须指定WNOHANG选项,它告知waitpid在有尚未终止的子进程在运行时不要阻塞。不能在循环内调用wait,因为没有办法防止wait在正运行的子进程尚有未终止时阻塞。

服务端程序

修改后的服务端程序:

#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信号处理函数,防止子进程变为僵死进程
void sig_chld(int signo)
{pid_t   pid;int     stat;// 等待子进程结束,并获取子进程的PID和退出状态while (pid = waitpid(-1, &stat, WNOHANG)) > 0) {// 在此处调用诸如printf这样的标准I/O是不合适的,此处只是作为查看子进程何时终止的诊断手段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_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 */// 捕捉指定信号并采取行动
Sigfunc *Signal(int signo, Sigfunc *func)    /* for our signal() function */
{Sigfunc *sigfunc;if ( (sigfunc = signal(signo, func)) == SIG_ERR) {printf("signal error");    }return(sigfunc);
}// 从客户端读入数据,并把它们回射给客户端
void str_echo(int sockfd) {ssize_t n;char    buf[MAXLINE];
again:// 从套接字读入数据// 套接字中接收缓冲区和发送缓冲区是分开的,因此读和写不会发生混淆while ((n = read(sockfd, buf, MAXLINE)) > 0)write(sockfd, buf, n);    // 把套接字中的内容回射给客户端// 如果n<0表示读取数据出错或到达文件末尾// 如果errno等于EINTR,表示读取操作被信号中断// 如果上述两个条件同时满足,则重新尝试读取数据if (n < 0 && errno == EINTR)goto again;// 如果表示文件描述符到达文件末尾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) 创建一个TCP连接套接字listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd < 0) {printf("socket error");return -1;}/* --------------------------------------------- *///2) 把服务器对应端口绑定到套接字 bzero(&servaddr, sizeof(servaddr));     // 开辟内存servaddr.sin_family      = AF_INET;     // 地址族// 指定IP地址为INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任意网络接口上接受客户端连接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) 把套接字转换为监听套接字// LISTENQ表示系统内核允许在这个监听描述符上排队的最大客户端连接数if(listen(listenfd, LISTENQ) < 0) {printf("listen error");return -1;}// 捕捉指定信号并采取行动Signal(SIGCHLD, sig_chld);    /* must call waitpid() *//* --------------------------------------------- *///4) 接受客户端连接,发送应答for ( ; ; ) {clilen = sizeof(cliaddr);// connfd为已连接描述符,用于和客户端进行通信connfd = accept(listenfd, (SA *) &cliaddr, &clilen);if(connfd < 0) {if (errno == EINTR) {continue;     // 重启被中断的accept           } else {printf("accept error");return -1;           }}if ((childpid = fork()) == 0) {// 子进程关闭监听套接字if (close(listenfd) == -1) {printf("child close listenfd error");return -1;           }str_echo(connfd);    // 子进程处理客户端请求exit(0);             // 清理描述符    }/* --------------------------------------------- *///5) 父进程关闭已连接套接字if (close(connfd) == -1) {printf("parent close connfd error");return -1;}}
}

小结

问:SIGCHLD信号是怎么产生的,有什么作用?

答:SIGCHLD 信号是由操作系统产生的,当一个子进程结束(无论是正常退出还是被终止)时,操作系统都会向父进程发送这个信号。这个信号的目的是通知父进程子进程的状态已经改变,父进程可以采取相应的行动,比如回收子进程使用的资源。

注意:父进程调用wait函数时会阻塞整个父进程的执行,直到某一个或几个子进程结束,才会结束阻塞。上述服务器程序是通过异步调用wait函数,所以看上去不是那么直观,非异步调用wait如下:

for ( ; ; ) {clilen = sizeof(cliaddr);// connfd为已连接描述符,用于和客户端进行通信connfd = accept(listenfd, (SA *) &cliaddr, &clilen);if(connfd < 0) {if (errno == EINTR) {continue;     // 重启被中断的accept           } else {printf("accept error");return -1;           }}if ((childpid = fork()) == 0) {// 子进程关闭监听套接字if (close(listenfd) == -1) {printf("child close listenfd error");return -1;           }str_echo(connfd);    // 子进程处理客户端请求exit(0);             // 清理描述符    }// 等待子进程结束并回收子进程资源int status;wait(&status);/* --------------------------------------------- *///5) 父进程关闭已连接套接字if (close(connfd) == -1) {printf("parent close connfd error");return -1;}
}

UNIX网络编程总结:

1)当fork子进程时,必须捕获SIGCHLD信号。

2)当捕获信号时,父进程必须处理被中断的系统调用,如accept函数。

3)SIGCHLD的信号处理函数必须正确书写,并使用waitpid函数以免留下僵尸进程。

如果需要代码包,请在评论区留言!!! 

如果需要代码包,请在评论区留言!!! 

如果需要代码包,请在评论区留言!!! 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/472085.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Kafka-Eagle的配置——kafka可视化界面

通过百度网盘分享的文件&#xff1a;kafka-eagle-bin-2.0.8.tar.gz 链接&#xff1a;https://pan.baidu.com/s/1H3YONkL97uXbLTPMZHrfdg?pwdsltu 提取码&#xff1a;sltu 一、界面展示 二、软件配置 1、关闭kafka集群 kf.sh stop 2、将该软件上传到/opt/modules下 cd /opt…

Uniapp踩坑input自动获取焦点ref动态获取实例不可用

前言 大家好我是没钱的君子下流坯&#xff0c;用自己的话解释自己的知识。很久很更新了&#xff0c;这几个月一直在加班&#xff0c;今天记录一个uniapp关于input中focus()方法自动获取焦点的坑。 案例 为了实现一个手机验证码的页面&#xff0c;验证码是五个输入框&#xf…

报错 No available slot found for the embedding model

报错内容 Server error: 503 - [address0.0.0.0:12781, pid304366] No available slot found for the embedding model. We recommend to launch the embedding model first, and then launch the LLM models. 目前GPU占用情况如下 解决办法: 关闭大模型, 先把 embedding mode…

AI大模型(二):AI编程实践

一、软件安装 1. 安装 Visual Studio Code VSCode官方下载&#xff1a;Visual Studio Code - Code Editing. Redefined 根据自己的电脑系统选择相应的版本下载 安装完成&#xff01; 2. 安装Tongyi Lingma 打开VSCode&#xff0c;点击左侧菜单栏【extensions】&#xff0c;…

MFC程序崩溃时生成dmp文件

#include “HiExceptionHandle.h” #include <string> #pragma once class HiExceptionHandle { public:HiExceptionHandle(void);~HiExceptionHandle(void); public:void RunCrashHandler();void SetWERDumpLocation(const std::wstring dumpFolderPath); protected:st…

释放高级功能:Nexusflows Athene-V2-Agent在工具使用和代理用例方面超越 GPT-4o

在不断发展的人工智能领域&#xff0c;Nexusflows 推出了 Athene-V2-Agent 作为其模型系列的强大补充。这种专门的代理模型设计用于在功能调用和代理应用中发挥出色作用&#xff0c;突破了人工智能所能达到的极限。 竞争优势 Athene-V2-Agent 不仅仅是另一种人工智能模型&…

Flutter:input输入框

输入框&#xff1a; // 是否显示关闭按钮 bool _showClear false; // 文字编辑控制器&#xff0c;监听搜索框的变化。 final TextEditingController _controller TextEditingController(); // 输入框发生变化事件 void _onChange(String value){if(value.length > 0){setS…

vue 项目使用 nginx 部署

前言 记录下使用element-admin-template 改造项目踩过的坑及打包部署过程 一、根据权限增加动态路由不生效 原因是Sidebar中路由取的 this.$router.options.routes,需要在计算路由 permission.js 增加如下代码 // generate accessible routes map based on roles const acce…

鸿蒙next ui安全区域适配(刘海屏、摄像头挖空等)

目录 相关api 团结引擎对于鸿蒙的适配已经做了安全区域的适配&#xff0c;也考虑到了刘海屏和摄像机挖孔的情况&#xff0c;在团结引擎内可以直接使用Screen.safeArea 相关api 团结引擎对于鸿蒙的适配已经做了安全区域的适配&#xff0c;也考虑到了刘海屏和摄像机挖孔的情况&am…

多端校园圈子论坛小程序,多个学校同时代理,校园小程序分展示后台管理源码

社团活动与组织 信息发布&#xff1a;系统支持社团发布活动信息、招募新成员等&#xff0c;方便社团进行线上线下活动的组织和管理。 增强凝聚力&#xff1a;通过系统&#xff0c;社团成员可以更好地交流和互动&#xff0c;增强社团的凝聚力和影响力。 生活服务功能 二手市场…

SpringCloud-使用FFmpeg对视频压缩处理

在现代的视频处理系统中&#xff0c;压缩视频以减小存储空间、加快传输速度是一项非常重要的任务。FFmpeg作为一个强大的开源工具&#xff0c;广泛应用于音视频的处理&#xff0c;包括视频的压缩和格式转换等。本文将通过Java代码示例&#xff0c;向您展示如何使用FFmpeg进行视…

MySQL-初识数据库

目录 一、数据库基础概念 1、SQL 2、数据&#xff08;Data&#xff09; 3、数据库&#xff08;DB&#xff09; 4、数据库管理系统DBMS 5、数据库系统DBS 6、关系模型&#xff08;Relational Model&#xff09; 7、E-R图 8、常见的数据库 9、数据库基本操作 一、数据库…

【C语言】实现二维数组按列排序

文章目录 代码实现代码解释注意事项 代码实现 下面是一个C语言程序&#xff0c;它读取用户输入的4行5列的二维数组&#xff0c;并按照列对数组进行排序。 #include <stdio.h>int main() {int a[4][5]; // 定义一个4行5列的二维数组// 读取用户输入的二维数组for (int i…

aws ses 设置发件人昵称

看到别人的发的都是有昵称的&#xff0c;自己发的就是直接展示noreply 其实很简单&#xff1a; 只需要把发件人改成“nickname<noreplyxxx.com>”就行了

51c大模型~合集42

我自己的原文哦~ https://blog.51cto.com/whaosoft/11859244 #猎户座 「草莓」即将上线&#xff0c;OpenAI新旗舰大模型曝光&#xff0c;代号「猎户座」 ChatGPT 要进化了&#xff1f; 本月初&#xff0c;OpenAI 创始人、CEO 山姆・奥特曼突然在 X 上发了一张照片&#xff0…

【算法】二分查找

基本内容 提高在有序的数组中查找满足某一条件的索引 二分查找的基本类型 ① 有多种情况满足条件&#xff0c;找到满足条件的最右索引&#xff0c;例如找到值为4的最右索引&#xff08;也可以换为小于5的最后一个元素&#xff09; ​ ② 有多种情况满足条件&#xff0c;找到满…

PCA 原理推导

针对高维数据的降维问题&#xff0c;PCA 的基本思路如下&#xff1a;首先将需要降维的数据的各个变量标准化&#xff08;规范化&#xff09;为均值为 0&#xff0c;方差为 1 的数据集&#xff0c;然后对标准化后的数据进行正交变换&#xff0c;将原来的数据转换为若干个线性无关…

Selective attention improves transformer详细解读

Selective attention improves transformer Google 2024.10.3 一句话&#xff1a;简单且无需额外参数的选择性注意力机制&#xff0c;通过选择性忽略不相关信息并进行上下文剪枝&#xff0c;在不增加计算复杂度的情况下显著提升了Transformer模型的语言建模性能和推理效率。 论…

卡尔曼滤波:从理论到应用的简介

卡尔曼滤波&#xff08;Kalman Filter&#xff09;是一种递归算法&#xff0c;用于对一系列噪声观测数据进行动态系统状态估计。它广泛应用于导航、控制系统、信号处理、金融预测等多个领域。本文将介绍卡尔曼滤波的基本原理、核心公式和应用案例。 1. 什么是卡尔曼滤波&#x…

tdengine学习笔记

官方文档&#xff1a;用 Docker 快速体验 TDengine | TDengine 文档 | 涛思数据 整体架构 TDENGINE是分布式&#xff0c;高可靠&#xff0c;支持水平扩展的架构设计 TDengine分布式架构的逻辑结构图如下 一个完整的 TDengine 系统是运行在一到多个物理节点上的&#xff0c;包含…