1625 字
4 分钟
TCP&IP网络编程学习札记_6
2026-08-05
浏览量 33 · 访客 4

壁纸链接

多进程服务器端#

10.1 进程概念及应用#

之前实现的迭代服务器端每次只能服务一个客户端。当前面的客户端尚未结束通信时,后续请求只能留在等待队列中。网络通信的等待时间通常大于CPUCPU运算时间,因此让多个客户端同时获得服务,也能更有效地利用系统资源。

常见的并发服务器端有以下几种实现方式:

  1. 通过创建多个进程提供服务。
  2. 通过I/OI/O复用统一管理多个I/OI/O对象。
  3. 通过创建多个线程提供服务。

本章主要介绍LinuxLinux下的多进程方式。WindowsWindows不支持同样的forkfork进程模型,因此没有对应的平台实现。

理解进程#

进程ProcessProcess)简单来说就是“占用内存空间且正在运行的程序”。保存在硬盘上但尚未运行的只是程序;当它被加载到内存并开始执行时,才成为进程。

操作系统会为每个进程分配唯一的进程IDID,即PIDPID。在LinuxLinux中可以通过psps命令查看正在运行的进程及其PIDPID

通过fork创建进程#

forkfork用于创建当前进程的副本:

#include <unistd.h>
pid_t fork(void);
//成功时在父进程中返回子进程ID,
//在子进程中返回0,失败时返回-1。

调用forkfork后,父子进程都会从函数返回位置继续执行,所以必须根据返回值区分两条执行流:

pid_t pid = fork();
if (pid == 0)
{
/* 子进程执行区域 */
}
else
{
/* 父进程执行区域,pid是子进程ID */
}

forkfork会复制父进程当时的内存状态,但复制完成后父子进程拥有彼此独立的内存空间。一方修改全局变量或局部变量,不会直接影响另一方。

10.2 进程和僵尸进程#

子进程执行完mainmainreturnreturn,或者调用exitexit后,会把终止状态交给操作系统。操作系统不会立即删除该进程,而是保留这些信息,等待父进程读取。在终止状态尚未被回收期间,子进程就处于僵尸进程Zombie ProcessZombie\ Process)状态。

僵尸进程已经不再执行业务代码,但仍会占用进程表等系统资源。因此,父进程需要主动获取子进程的终止状态。

利用wait销毁僵尸进程#

#include <sys/wait.h>
pid_t wait(int *statloc);
//成功时返回终止的子进程ID,失败时返回-1。

waitwait会把某个已终止子进程的状态写入statlocstatloc指向的内存,同时完成该子进程的回收。可以通过宏拆分其中的信息:

int status;
wait(&status);
if (WIFEXITED(status))
printf("Child return: %d\n", WEXITSTATUS(status));

WIFEXITEDWIFEXITED用于判断子进程是否正常终止,WEXITSTATUSWEXITSTATUS用于取出返回值。若尚无子进程结束,waitwait会阻塞等待,因此需谨慎调用。

利用waitpid避免阻塞#

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
//成功时返回终止的子进程ID,
//使用WNOHANG且暂无子进程结束时返回0。

pidpid1-1时表示等待任意子进程。第三个参数传入WNOHANGWNOHANG后,即使尚无子进程终止也不会阻塞,而是直接返回00

10.3 信号处理#

父进程通常还要继续受理连接,不能不断调用waitpidwaitpid轮询子进程是否终止。子进程结束时,操作系统会向父进程发送SIGCHLDSIGCHLD信号;父进程可以注册对应的处理函数,只在信号到来时回收子进程。

signal函数#

#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);

signosigno表示需要关注的信号,funcfunc是信号发生时由操作系统调用的处理函数。常见信号包括:

信号含义
SIGCHLD子进程终止
SIGALRMalarmalarm设置的时间到达
SIGINT用户输入Ctrl+CCtrl+C

例如,子进程终止时调用read_childprocread\_childproc

signal(SIGCHLD, read_childproc);

书中还通过alarmalarm演示了定时信号:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

调用后经过指定秒数会产生SIGALRMSIGALRM。信号到来时,还会唤醒因sleepsleep等函数处于阻塞状态的进程。

使用sigaction注册信号#

signalsignal在不同UNIXUNIX系统中可能存在行为差异,实际程序中更常使用sigactionsigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act,
struct sigaction *oldact);
//成功时返回0,失败时返回-1。

用于回收子进程的核心结构如下:

void read_childproc(int sig)
{
int status;
while (waitpid(-1, &status, WNOHANG) > 0);
}
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, 0);

SIGCHLDSIGCHLD产生时,处理函数通过非阻塞的waitpidwaitpid取走子进程终止状态,从而避免僵尸进程残留。

10.4 基于多任务的并发服务器#

多进程回声服务器端的基本逻辑是:父进程专门通过acceptaccept受理连接,每当得到一个客户端套接字后调用forkfork,由新建子进程为该客户端提供服务。

替换文本

父进程负责反复受理连接,子进程则独立处理已连接客户端。核心代码可以简化为:

while (1)
{
clnt_sock = accept(serv_sock,
(struct sockaddr *)&clnt_adr, &adr_sz);
if (clnt_sock == -1)
continue;
pid = fork();
if (pid == 0)
{
close(serv_sock);
while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock);
return 0;
}
else
{
close(clnt_sock);
}
}

fork与套接字文件描述符#

forkfork复制的是套接字文件描述符,而不是在操作系统中创建一个新套接字。父子进程中的描述符会指向同一套接字对象。

替换文本

如果父子进程都保留所有描述符,即使其中一方调用closeclose,套接字也不会立即被销毁。只有指向该套接字的相关描述符全部关闭后,操作系统才会真正销毁它。

替换文本

所以调用forkfork后应立即整理无关描述符:

  • 子进程不负责接受新连接,应关闭serv_sockserv\_sock
  • 父进程不负责当前客户端的数据交换,应关闭自己持有的clnt_sockclnt\_sock副本。

父进程继续调用acceptaccept,各子进程独立处理已连接客户端,便实现了同时向多个客户端提供服务。

10.5 分割TCP的I/O程序#

之前的回声客户端按照“发送一次,等待回声,再发送下一次”的顺序工作。利用forkfork可以把收发逻辑拆分到两个进程中:父进程只负责接收,子进程只负责发送。

替换文本

这样一方面可以简化代码,另一方面使数据发送不再必须等待前一次接收完成。如图所示,I/OI/O分割后客户端可以连续发送数据,对频繁交换数据的程序更有意义。

替换文本

客户端中的分工可以简化为:

pid = fork();
if (pid == 0)
write_routine(sock, buf); //子进程只发送
else
read_routine(sock, buf); //父进程只接收

发送进程遇到退出输入时,需要对输出流执行半关闭:

if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR);
return;
}

由于forkfork后父子进程都持有指向同一套接字的描述符,只在发送进程中调用closeclose并不能立即向服务器传递EOFEOF。调用shutdown(sock,SHUT_WR)shutdown(sock, SHUT\_WR)可明确关闭套接字的输出流,同时让另一进程继续接收服务器剩余的数据。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

TCP&IP网络编程学习札记_6
https://mkrari.cn/posts/tcp_ip_learn_6/
作者
Mkrari
发布于
2026-08-05
许可协议
CC BY-NC-SA 4.0