网络编程 25_阻塞 I/O 进程模型
目标
为每一个连接创建一个独立的进程去服务
一、父子进程
进程是程序执行的最小单位,创建进程使用 fork 函数
1.1 fork 创建进程
pid_t fork(void);
调用一次,在父子进程中各返回一次,在父进程中返回进程 ID 号,在子进程中返回 0,只能通过返回值判断当前执行的进程是父进程还是子进程
if (fork() == 0) {
// 子进程
} else {
// 父进程
}
在 Linux 下僵尸进程会被挂到进程号为 1 的 init 进程上。由父进程派生的子进程,必须由父进程负责回收,否则子进程会变成僵尸进程(占用不必要的内存空间,数量达到一定数量级,会耗尽系统资源)
1.2 回收进程资源
wait 和 waitpid
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
wait 系统调用暂停调用进程的执行,直到其子进程之一终止
waitpid 系统调用暂停调用进程的执行,直到由 pid 参数指定的子进程更改了状态。默认情况下,waitpid 只等待终止的子项,但此行为可通过 options 参数修改
SIGCHILD 信号
注册信号处理函数,捕捉信号 SIGCHILD 信号,在信号处理函数中调用 waitpid 函数完成子进程的资源回收
// void ( *signal(int signum, void (*handler)(int)) ) (int);
// 第二个参数格式:void (*handler)(int)
signal(SIGCHLD, sigchld_handler); // sigchld_handler:自定义子进程回收资源的函数指针,满足第二个参数格式即可
二、阻塞 I/O 进程模型
服务器监听在连接套接字 listenfd 上,客户端发起连接请求时,服务器产生连接套接字,同时派生出一个子进程,服务器在子进程中使用连接套接字处理和客户端的通信
- 父进程只关心监听套接字,不关心连接套接字
- 子进程只关心连接套接字,不关心监听套接字
服务端
fork_server.c
#include "common.h"
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) {
return c + 13;
} else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) {
return c - 13;
}
return c;
}
void child_run(int fd) {
char outbuf[512];
int i;
ssize_t result;
while (1) {
result = recv(fd, &outbuf, sizeof(outbuf), 0);
if (result == 0) {
break;
} else if (result == -1) {
perror("read");
break;
}
for (i = 0; i < result; i++) {
outbuf[i] = rot13_char(outbuf[i]);
if (outbuf[i] == '\n') {
send(fd, outbuf, result, 0);
break;
}
}
}
}
void sigchld_handler(int sig) {
// 0:meaning wait for any child process whose process group ID is equal to that of the calling process
// WNOHANG 即使没有子进程退出,也立即返回
while (waitpid(-1, 0, WNOHANG) > 0) {}
return;
}
int main(int argc, char **argv) {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
// 端口复用
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
socklen_t servlen = sizeof(servaddr);
int bind_rt = bind(listenfd, (struct sockaddr *)&servaddr, servlen);
if (bind_rt < 0) {
error(1, errno, "bind failed");
}
int listen_rt = listen(listenfd, LISTENQ);
if (listen_rt < 0) {
error(1, errno, "listen failed");
}
// 注册信号处理函数,回收子进程资源
// sigchld_handler:函数指针,格式对应 void (*handler)(int)
signal(SIGCHLD, sigchld_handler);
while (1) {
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listenfd, (struct sockaddr *)&ss, &slen);
if (fd < 0) {
error(1, errno, "accept failed");
exit(1);
}
// fork 创建子进程,描述符会复制一份,即监听套接字 listenfd 和连接套接字 fd 引用计数都加 1
if (fork() == 0) { // 子进程
// close 函数会将引用计数减 1
close(listenfd); // 将监听套接字 listenfd 引用计数减 1,减到 0 时会将套接字资源回收
child_run(fd);
exit(0);
} else { // 父进程
close(fd);
}
}
}
头文件 common.h
#ifndef CHAP_25_COMMON_H
#define CHAP_25_COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h> /* basic socket definitions */
#include <netinet/in.h> /* sockaddr_in{} and other Internet defns */
#include <arpa/inet.h> /* inet(3) functions */
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void error(int status, int err, char *fmt, ...);
#define SERV_PORT 43211
#define LISTENQ 1024
#endif //CHAP_25_COMMON_H
三、CMake 管理当前项目
① 代码组成
-CMakeLists.txt
-include:存放头文件
-src:存放源代码
CMakeLists.txt
CMAKE_MINIMUM_REQUIRED(VERSION 3.1)
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/include)
ADD_SUBDIRECTORY(src)
include 目录:include/common.h(common.h 上面有)
src 目录(fork_server.c 上面有)
src/CmakeLists.txt
ADD_EXECUTABLE(fork_server fork_server.c)
TARGET_LINK_LIBRARIES(fork_server)
② 创建并进入 build 目录
mkdir build && cd build
③ 外部编译
cmake .. && make
四、测试
可以使用一个或多个 telnet 客户端连接服务器,检验交互是否正常
测试步骤
① 打开三个命令行窗口
② 其中一个窗口先执行服务器命令,输入命令 ./nonblockingserver
后回车
③ 其余窗口执行客户端命令,输入命令 ./telnet-client 127.0.0.1 43211
后回车
左:服务端;右上、右下:客户端
上面的阻塞 I/O 进程模型的服务端程序,可以并发处理多个不同的客户端连接,互不干扰
总结
使用阻塞 I/O + 进程的方式,为每一个连接创建一个独立的子进程,服务器在子进程中使用连接套接字处理和客户端的通信
- 及时关闭套接字
服务器端 fork 子进程,套接字引用计数加 1,close 使套接字引用计数减 1,引用计数减为 0 时会将套接字资源回收,避免服务器端资源泄漏 - 及时回收子进程资源,避免出现僵尸进程