1. Linux下的多进程服务器
1.1 进程的概念及应用
并发服务器实现的模型和方法:
- 多进程服务器(通过创建多个进程提供服务)
- 多路复用服务器(通过捆绑并统一管理IO对象提供服务)
- 多线程服务器(通过创建多个线程提供服务)
进程定义:占用内存空间的正在运行的程序。例如在电脑上,同时打开文档编辑软件,聊天软件,以及MP3播放器,此时就是创建了三个进程,从操作系统的角度来看,进程是程序流的基本单位,若创建多个进程,操作系统将同时运行,有时一个进程运行的过程中也会产生多个进程。
注:拥有n个运算设备(运算器)的CPU称为n核CPU,核的个数与同时可运行的进程数量相同,若进程数超过核数,进程将分时使用CPU资源,但是由于CPU运算速度足够快,使得用户感觉到所有进程都是同时运行的。
1.2 进程ID
在创建进程的时候,所有进程都会从操作系统分配到对应得分ID,其值为大于2的整数,1是分配给操作系统启动后的首个进程(Linux系统启动后,创建的第一个进程就是init进程),可通过如下命令查看当前Linux下的所有进程:
ps au
// 指定au参数可列出进程的所有详细信息ps -ef | grep xxx
创建进程fork
#include pid_t fork(void);
// 成功时返回进程ID,失败时返回-1
fork函数创建的是调用它的进程的副本,即它是复制正在运行的,调用fork函数的进程,此外,在fork()函数返回后,两个进程都将执行fork()函数后面的语句。但是因为在fork()的时候,是通过同一个进程,复制相同的内存空间,因此fork()之后的程序需要根据fork()函数的返回值加以区分:
- 父进程:fork()函数返回子进程的ID
- 子进程:fork()函数返回0
代码实例:
main.cpp
#include
#include int gval = 10;
int main(int argc, char** argv)
{
pid_t pid;
int localVal = 20;
gval++;
localVal+=5;
pid= fork();
// # fork出一个新的进程if (pid == 0)// 子进程
{
/* code */
gval+=2;
localVal+=2;
printf("The child Process: %d, %d\n", gval, localVal);
}
else// 父进程
{
gval-=2;
localVal-=2;
printf("The father Process: %d, %d\n", gval, localVal);
}return 0;
}
输出结果:
文章图片
父进程在调用fork()函数的同事,复制出子进程,并且获取到fork函数的返回值,在复制前,父进程分别对局部变量和全局变量的值进行了修改,在这种状态下进行复制,子进程也将获取到修改后的值。复制完成后,根据fork()函数的返回值,区分父子进程。在父子进程中,修改变变量值不会相互影响,因为调用fork()函数进行复制之后,父子进程具有完全独立的内存结构,二者只是共享相同的代码而已。
1.3 僵尸进程 zombie
文件操作中,文件的打开和关闭同样重要。同样,进程的创建和销毁也同样重要,如果未成功销毁进程,他们将变成僵尸进程。进程在完成工作后(执行完main函数中的程序后)应该被销毁,但是有的进程会变成僵尸进程,占用系统中的重要资源,首先介绍僵尸进程产生的原因,和如何去销毁僵尸进程。
子进程运行结束时,向exit()函数传递的参数值或者return语句的返回值都会传递给操作系统,而操作系统在此时不会将这个值传递给给产生该子进程的父进程,也不会销毁子进程。此时处于这种状态下的进程就是僵尸进程,也就是说,正是操作系统自己,将子进程变成了僵尸进程。
销毁子进程的方法:
“将子进程exit()或者return的值传递个产生它的父进程,此时子进程会被销毁”
而操作系统不会主动把子进程的返回值传给父进程,只有父进程主动发起请求的时候,操作系统才会传递该值,此时自己成才会被销毁。如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存该值,且让子进程一直处于僵尸状态,即父进程负责回收自己产生的子进程。
通过一个代码实例来演示僵尸进程的产生:
#include
#include int main(int argc, char** argv)
{
pid_t pid = fork();
// 创建一个新的进程if (pid == 0)// 子进程
{
printf("This is child process: %d\n", pid);
}
else
{
printf("This is Father process: %d\n", pid);
sleep(30);
// 父进程延时30s
}if (pid == 0)
printf("Child process finished\n");
else
printf("Father process finished\n");
return 0;
}
运行结果:
文章图片
结果分析:可以看到在在30S以内查看进程,发现子进程为僵尸进程,当主进程到达30s而退出之后,处于僵尸状态的子进程将同时被销毁。
1.3.1 利用wait函数销毁僵尸进程
为了销毁子进程,父进程需要主动请求获取子进程的返回值,可用wait方法发起请求
#include /*
statloc: 包含子进程终止时传递回来的信息 (返回值,返回状态)
需要通过宏进行分离
子进程返回状态:WIFEXITED(statloc)
子进程的返回值:WEXITSTATUS(statloc)
返回值:成功时返回终止的子进程的ID,失败返回-1
*/
pid_t wait(int* statloc)
调用wait函数消灭僵尸进程的时候,如果没有已经终止的子进程,程序将阻塞直到有结束的子进程,因此在调用wait函数的时候需要谨慎。
代码示例:
#include
#include
#include
#include int main(int argc, char** argv)
{
int status;
pid_t pid = fork();
if (pid == 0)
{
// 子进程
return 3;
}
else
{
printf("Child pid: %d\n", pid);
pid = fork();
if (pid == 0)
{
// 子进程
exit(5);
}
else
{
printf("Child pid %d\n", pid);
// 调用wait函数结束子进程
wait(&status);
if (WIFEXITED(status))
{
printf("Child returned %d\n", WEXITSTATUS(status));
}wait(&status);
if (WIFEXITED(status))
{
printf("Child returned %d\n", WEXITSTATUS(status));
}
sleep(30);
}
}
return 0;
}
运行结果:
文章图片
可以看待在系统下运行的线程中不存在僵尸进程。
1.3.2 使用waitpid函数销毁僵尸进程
wait函数会造成阻塞问题,waitpid()函数也是一种销毁僵尸进程的方法,且能够避免发生阻塞。
#include /*
pid: 等待终止的目标子进程ID
statloc: 存储进程返回后的状态
options: 传递头文件sys/wait.h中声明的常量WNOHANG, 即使没有终止的进程也不会进入阻塞状态,
而实返回0并退出。
*/
pid_t waitpid(pid_t pid, int * statloc, int options);
代码示例:
#include
#include
#include
#include int main(int argc, char** argv)
{
int status;
pid_t pid = fork();
if (pid == 0)
{
// 子进程
sleep(15);
return 1;
}
else
{
while(waitpid(pid, &status, WNOHANG) == 0)
{
sleep(3);
printf("The child process %d is running.\n", pid);
}if (WIFEXITED(status))
{
printf("Child process exited with %d\n", WEXITSTATUS(status));
}
}
return 0;
}
运行结果:
文章图片
1.4 信号处理
父进程创建子进程之后,子进程何时终止,父进程往往同子进程一样繁忙,因此不能只通过调用waitpid函数等待子进程结束。因此需要寻找其他的解决方案:
子进程终止的识别主体是操作系统,若在子进程结束的时候,操作系统能将结束的信号告诉忙于处理其他业务的主进程,则将大大提高程序的运行效率。此时父进程将暂时放下其他的业务,来专门处理子进程终止的相关事宜,在Linux系统下,可以借助信操作系统的信号机制实现此想法。
Linux中的信号是一种消息处理机制,不同的信号使用不同的值表示,代表不同的含义,虽然信号结构简单,不能携带很大的信息量,但是信号在系统中的优先级很高,在Linux系统下,很多常规的操作,都会产生响应的信号:
- 键盘操作产生信号 :按下Ctrl+C,键盘输入一个硬件中断,产生一个信号,这个信号会杀死对应的某个进程
- 通过shell命令产生信号:kill -9 pid ,终止某个进程
- 函数调用产生信号:如进程中调用sleep()函数,进程收到相关信号,被迫挂起
- 对硬件进程了非法访问产生了信号:程序访问内存错误,或者段错误,进程退出
可通过kill -l 查看系统定义的信号列表:
文章图片
Linux中能够产生信号的函数有很多:
(1)kill发送指定的信号到指定的进程:
// 发送指定的信号到指定的进程
int kill(pid_t pid, int sig);
kill(getpid(), 9);
// 自己杀死自己
(2)raise给当前进程发送指定的信号
// 给自己发送某一个信号
#include
int raise(int sig);
// 参数就是要给当前进程发送的信号
(3)abort给当前进程发送一个固定信号 (SIGABRT)
// 这是一个中断函数, 调用这个函数, 发送一个固定信号 (SIGABRT), 杀死当前进程
#include
void abort(void);
(4)alarm用于单次定时,定时完成向当前进程发出一个信号
#include
unsigned int alarm(unsigned int seconds);
(5)setitimer用于周期定时,没触发一次定时器就会发出对应的信号
// 函数可实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号
#include struct itimerval {
struct timeval it_interval;
/* 时间间隔 */
struct timeval it_value;
/* 第一次触发定时器的时长 */
};
// 表示一个时间段: tv_sec + tv_usec
struct timeval {
time_ttv_sec;
/* 秒 */
suseconds_t tv_usec;
/* 微妙 */
};
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
// new_value : 输入值,为定时器设置的参数
// old_value :输出值,上一次为定时器设置的参数,如果不需要知到,传递NULL
// which:定时器的计数方式/*
which参数可选项:
ITIMER_REAL: 自然计时法, 最常用, 发出的信号为SIGALRM, 一般使用这个宏值,自然计时法时间 = 用户区 + 内核 + 消耗的时间(从进程的用户区到内核区切换使用的总时间)
ITIMER_VIRTUAL:只计算程序在用户区运行使用的时间,发射的信号为 SIGVTALRM
ITIMER_PROF:只计算内核运行使用的时间, 发出的信号为SIGPROF
*/
信号与signal函数:
进程首先需要告诉操作系统,在它创建的子进程结束之后,请求帮他调用zombie_handler函数,为了完成这一过程,进程首先需要向操作系统注册一个信号才能实现调用这个函数。操作系统调用的这个函数称为信号注册函数。
#include void (*signal(int signo, void(*func)(int)))(int);
函数名:signal
参数:int signo, void (*func)(int)
返回值:返回一个函数指针,这个函数指针指向的函数,具有一个int类型的参数,无返回值。
signal函数原型的理解:signal函数为带有两个参数的函数,一个参数为int类型的整数,另一个参数为函数指针,这个函数指针指向的函数原型没有返回值,具有一个int参数。signal函数执行完毕后,其返回值的也是一个函数指针,这个函数指针指向的是没有返回值,参数为int类型的函数。
在signal函数中可以注册的部分特殊情况和对应的常数值:
- SIGALARM已到了 通过调用alarm函数注册的时间
- SIGINT输入CTRL+C 程序中断
- SIGCHLD子进程终止
代码实例:
#include
#include
#include // 定义信号处理函数,这种函数称为信号处理器 Handler
void timeOut(int signo)
{
if (signo == SIGALRM)
{
// 到达alram注册的超时时间
printf("Time out\n");
}alarm(2);
}// 定义信号处理函数,这种函数称为信号处理器 Handler
void keyControl(int signo)
{
if (signo == SIGINT)
{
printf("CTRL+C pressed!\n");
}
}int main(int argc, char** argv)
{
// 注册信号
signal(SIGALRM, timeOut);
signal(SIGINT, keyControl);
alarm(2);
for (size_t i = 0;
i < 20;
i++)
{
/* code */
printf("Wait...\n");
sleep(30);
}return 0;
}
运行结果:
文章图片
发生信号时将会唤醒由于调用sleep而进入休眠状态的进程(即上述代码中,调用sleep之后,程序进入阻塞状态,当alarm(2)超时之后,会唤醒进程,直接的体现就是打印输出了"Wait")。调用函数的主体的确是操作系统,但是进程处于睡眠状态时无法调用函数,因此,产生信号的时候,为了调用信号处理函数,将唤醒由于调用sleep而处于阻塞状态的进程。而且,进程一旦被唤醒,就不会再进入休眠状态,即使还未到sleep中规定的睡眠时间。所以上述实例会很快运行结束。
利用sigaction函数进行信号处理
相比于signal函数,sigaction更加稳定,且具有通用性,因为signal在Unix系列的不同系统下,可能存在区别,但是sigaction函数完全相同。建议使用sigaction函数编写程序,以增强代码的可移植性。
#include int sigaction(int signo, const struct sigaction* act, const struct sigaction* oldact);
- signo与signal函数相同,用于传递信号信息
- act对应于第一个参数的信号处理函数信息
- oldact通过此参数获取之前注册的信号处理函数的函数指针,若不需要则传递0
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
}
- sa_handler用于保存信号处理函数的指针值
- sa_mask 所有位均初始化0即可(用于指定信号相关的选项和特性)
- sa_flags所有位均初始化位0即可(用于指定信号相关的选项和特性)
#include
#include
#include // 定义信号注册函数
void timeOut(int signo)
{
if (signo == SIGALRM)
{
// 到达alram注册的超时时间
printf("Time out\n");
}alarm(2);
}int main(int argc, char** argv)
{
// 注册信号
struct sigaction act;
act.sa_handler = timeOut;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGALRM, &act, 0);
alarm(2);
for (size_t i = 0;
i < 5;
i++)
{
/* code */
printf("Wait...\n");
sleep(50);
}return 0;
}
运行结果:
文章图片
利用信号处理技术消灭僵尸进程:
/*
利用信号机制处理僵尸进程
*/
#include
#include
#include
#include
#include // 定义信号处理函数
void readChildProc(int signo)
{
int status;
pid_t pid = waitpid(-1, &status, WNOHANG);
if (WIFEXITED(status))
{
printf("Terminated process %d\n", pid);
printf("Process return %d at termination\n", WEXITSTATUS(status));
}
}int main(int argc, char** argv)
{
struct sigaction sigact;
sigact.sa_handler = readChildProc;
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = 0;
// 注册信号
sigaction(SIGCHLD, &sigact, 0);
pid_t pid = fork();
// 创建进程if (pid == 0)
{
// 子进程
printf("I am child process.\n");
sleep(10);
return 5;
}
else
{
// 父进程
printf("Create child process %d\n", pid);
// 再创建一个子进程
pid = fork();
if (pid == 0)
{
// 子进程
printf("I am child process.\n");
sleep(20);
return 3;
}
else
{
// 父进程
printf("Create child process %d\n", pid);
for (size_t i = 0;
i < 5;
i++)
{
/* code */
printf("Wait.....\n");
sleep(15);
}}
}}
运行结果:
文章图片
通过上述信号机制处理进程,可以避免创建的子进程变成僵尸进程。
1.5 基于多进程的并发服务器
对回声服务器的例子进行扩展,使其可以向多个客户端同时提供服务。每当有客户端请求服务的时候,回升服务器端都会创建一个子进程以提供服务,此时的服务器端运行主流程如下:
- 回声服务器端(父进程)通过调用accept函数受理连接请求
- 此时获取的套接字文件描述符创建并传递给子进程
- 子进程利用传递来的文件描述符为客户端提供服务
服务端:
/*
多进程服务器
create_date: 2022-7-27
*/
#include
#include
#include
#include
#include
#include
#include
#include #define BUFF_SIZE 30// 缓冲区大小
#define PORT13100// 端口号void error_handler(char* msg)
{
printf("%s\n", msg);
exit(1);
}void read_child_proc(int signo)
{
int status;
pid_t pid = waitpid(-1, &status, 0);
printf("Removed process %d\n", pid);
}int main(int argc, char** argv)
{
int serverSocket;
int clientSocket;
struct sockaddr_in serverAddr;
struct sockaddr_in clientAddr;
char buffer[BUFF_SIZE];
socklen_t addrSize;
// 注册信号处理函数
struct sigaction sigact;
sigact.sa_handler = read_child_proc;
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = 0;
sigaction(SIGCHLD, &sigact, 0);
// 初始化服务端地址
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(PORT);
serverSocket = socket(PF_INET, SOCK_STREAM, 0);
// TCP socket// 为服务端socket绑定法地址
if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
{
close(serverSocket);
error_handler("Failed to bind server address");
}// 开始监听客户端
if (listen(serverSocket, 5) == -1)
{
close(serverSocket);
error_handler("Failed to listen client");
}while (true)
{
addrSize = sizeof(clientAddr);
printf("Successfully init server and wait for connect......\n");
clientSocket = accept(serverSocket, (sockaddr*)&clientAddr, &addrSize);
if (clientSocket == -1)
continue;
printf("Receive connection from %s : %d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
// 创建新的进程
pid_t pid = fork();
if (pid == -1)// 创建进程失败
{
close(clientSocket);
continue;
}if (pid != 0)
{
printf("Created new process for client.\n");
// 主进程中需要关闭客户端的socket,因为在创建进程的时候,这个socket会被复制到子进程中去重要!
close(clientSocket);
memset(&clientAddr, 0, sizeof(clientAddr));
continue;
}
else
{
// 在子进程创建的时候,父进程服务端的socket也会复制到子进程中去,而在子进程中,需要将其关闭重要!
close(serverSocket);
int str_len = 0;
memset(buffer, 0, BUFF_SIZE);
while ((str_len = read(clientSocket, buffer, BUFF_SIZE)) != 0)
{
/* code */
write(clientSocket, buffer, str_len);
memset(buffer, 0, BUFF_SIZE);
// 清一下缓冲区
}close(clientSocket);
printf("Disconnect %s: %d client from server.\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
return 0;
}
}close(serverSocket);
return 0;
}
客户端:
/*
客户端
create_date: 2022-7-29
*/#include
#include
#include
#include
#include
#include #define BUFF_SIZE30
#define ADDRESS"127.0.0.1"
#define PORT13100int main(int argc, char** argv)
{
int socket;
char buffer[BUFF_SIZE];
struct sockaddr_in serverAddr;
// 服务端地址
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(ADDRESS);
serverAddr.sin_port = htons(PORT);
memset(buffer, 0, BUFF_SIZE);
socket = ::socket(PF_INET, SOCK_STREAM, 0);
if (socket == -1)
{
printf("Failed to init socket.\n");
return -1;
}if (connect(socket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
{
printf("Failed to connect to server.\n");
return -2;
}printf("Successfully connect to the server.\n");
while (true)
{
fputs("Input message(Type Q(q) to quit): ", stdout);
fgets(buffer, BUFF_SIZE, stdin);
if (strncmp(buffer, "Q\n", 2) == 0 || strncmp(buffer, "q\n", 2) == 0)
break;
int write_len = write(socket, buffer, strlen(buffer));
int recv_len = 0;
while (recv_len < write_len)
{
int recv_count = read(socket, &buffer[recv_len], BUFF_SIZE-1);
recv_len += recv_count;
// printf("Received %d bytes data from sever.\n");
}printf("Receive data: %s", buffer);
memset(buffer, 0, BUFF_SIZE);
}close(socket);
return 0;
return 0;
}
运行结果:
服务端运行结果:
文章图片
客户端1运行结果:
文章图片
客户端2运行结果:
文章图片
查看运行的线程:
文章图片
问题:
在上述多进程服务器代码中,在调用fork函数创建子进程的过程中,父进程将两个套接字(一个服务端套接字,一个客户端套接字) 的文件描述符复制给了子进程,在这一过程中,是仅仅复制了文件描述符吗?是否对套接字也进行了复制?
调用fork()函数的时候,会复制父进程的所有资源,同理文件客户端,服务端的描述符也属于父进程资源同样会被复制,但是不会复制套接字。因为套接字属于操作系统的资源,,而文件描述符属于父进程的资源(假设套接字被复制了,那么将会出现同一端口对应多个套接字的情况)。
文章图片
调用fork函数复制文件描述符 如上图所示,如果一个套接字存在两个文件描述符的时候,只有当两个文件描述符都关闭之后,才能销毁套接字。如上图中所示,即使子进程销毁了与客户端连接的套接字的文件描述符,也无法完全销毁套接字。服务端套接字也是同样如此。因此在调用fork函数之后,需要将无关的套接字进行关闭。
if (pid != 0)
{
printf("Created new process for client.\n");
// 主进程中需要关闭客户端的socket,因为在创建进程的时候,这个socket会被复制到子进程中去重要!
close(clientSocket);
memset(&clientAddr, 0, sizeof(clientAddr));
continue;
}
else
{
// 在子进程创建的时候,父进程服务端的socket也会复制到子进程中去,而在子进程中,需要将其关闭重要!
close(serverSocket);
int str_len = 0;
....
1.6 分割TCP的IO程序
对上述的回声服务器程序进行改进,将客户端进行IO分割。在之前的客户端实现中,客户端首先向服务端发送数据,发送完成之后,无条件等待服务端回复(客户端中调用read),只有服务端回复之后,客户端才能进行下一次数据的发送。现在可利用多进程方法对客户端的程序进行改进,将接收与发送的逻辑放在两个不同的进程中进行。
设计方案:
- 客户端父进程负责接收数据
- 客户端子进程负责发送数据
客户端代码示例:
/*
客户端
create_date: 2022-7-29
*/#include
#include
#include
#include
#include
#include #define BUFF_SIZE30
#define ADDRESS"127.0.0.1"
#define PORT13100void readRoutine(int sock, char* buf);
void writeRoutine(int sock, char* buf);
int main(int argc, char** argv)
{
char buffer[BUFF_SIZE];
struct sockaddr_in serverAddr;
// 服务端地址
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(ADDRESS);
serverAddr.sin_port = htons(PORT);
memset(buffer, 0, BUFF_SIZE);
int socket = ::socket(PF_INET, SOCK_STREAM, 0);
if (socket == -1)
{
printf("Failed to init socket.\n");
return -1;
}if (connect(socket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
{
printf("Failed to connect to server.\n");
return -2;
}printf("Successfully connect to the server.\n");
pid_t pid = fork();
if (pid == 0)
{
// 子进程负责发送
writeRoutine(socket, buffer);
}
else
{
// 父进程负责接收
readRoutine(socket, buffer);
}close(socket);
return 0;
}void readRoutine(int sock, char* buf)
{
while (true)
{
memset(buf, 0, BUFF_SIZE);
int str_len = read(sock, buf, BUFF_SIZE);
if (str_len == 0)// EOF
{
return;
}printf("\nReceive data from server: %s\n", buf);
}}void writeRoutine(int sock, char* buf)
{
while (true)
{
fputs("Input message(Type Q(q) to quit): ", stdout);
fgets(buf, BUFF_SIZE, stdin);
if (strncmp(buf, "Q\n", 2) == 0 || strncmp(buf, "q\n", 2) == 0)
{
shutdown(sock, SHUT_WR);
return;
}write(sock, buf, strlen(buf));
}}
运行结果:
服务端:
文章图片
客户端1:
文章图片
客户端2:
文章图片
通过客户端数据结果可观察到,在提示输入之后,马上又会出现提示输入的语句,然后才出现服务端回复的内容,反映了在子进程中发送数据后,不用等待服务端回复,而又能马上发送数据,而父进程接收服务端回复数据会稍微慢于子进程发送数据,但是却不会对子进程的发送流程造成影响,适合客户端需要频繁发送数据的应用场景。
注:
在子进程中的发送流程中,有如下的代码:
fputs("Input message(Type Q(q) to quit): ", stdout);
fgets(buf, BUFF_SIZE, stdin);
if (strncmp(buf, "Q\n", 2) == 0 || strncmp(buf, "q\n", 2) == 0)
{
shutdown(sock, SHUT_WR);
return;
}
在用户输入Q/q结束发送流程时,客户端子进程会通过shutdown关闭客户端socket的写功能,此时调用shutdown,相当于向服务端传输EOF。在客户端调用完shutdown之后,继续调用后面的代码,也就是main的close和return
close(socket);
return 0;
此时只是将子进程中的客户端进行一次关闭。
接着服务端接收到客户端发送来的的EOF,并将其返回给客户端(此时客户端的接收功能并未关闭,还能正常接收服务端的EOF),客户端在判断接收到服务端发送来的EOF之后,结束接收流程,也同样调用main中后续的代码,执行close和return,此时客户端的socket又被关闭了一次,至此,客户端中的socket被成功关闭,子进程和父进程都结束。(可查看并没有出现僵尸进程,因为在子进程结束后,父进程马上结束)。
另一种情况:
如果没有在客户端发送流程中调用shutdown,而是直接return,此时客户端子进程不会发送任何内容便关闭socket结束了。此时服务端未收到任何内容,也就不会向客户端回复内容,此时客户端父进程将一致处于等待接收的状态而无法结束,且出现了僵尸进程。
文章图片
【网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器】 ---------------------------------The end---------------------------------------
推荐阅读
- 网络|开源linux_对技术的热爱导致Linux和开源
- c++|windows 下编译libcurl,通过url下载图片数据
- c++|C++通过url直接下载图片并保存
- 网络|计算BDP值和wnd值
- 网络|测试UDP端口连通性
- Docker容器+k8s|Docker网络详解
- 前端|一道面试题牵出12个前端硬核知识点,你知道几个((瀏覽器安全.網絡安全))
- 生活篇|Python pywifi 、Kali linux aircrack-n、Hashcat 【python、kali】破解无线WiFi密码(详细流程)
- 【学习笔记】