《TCP/IP网络编程》阅读笔记--并发多进程服务端的使用

1--并发服务器端

并发服务器端主要有以下三类:

多进程服务器:通过创建多个进程提供服务;

多路复用服务器:通过捆绑并统一管理I/O对象提供服务;

多线程服务器:通过生成与客户端等量的线程提供服务;

2--进程

2-1--进程的相关概念

进程的相关概念:

① 进程的定义如下:占用内存空间的正在运行的程序;

② 从操作系统的角度看,进程是程序流的基本单位,若创建多个进程,则操作系统将同时运行;

③ 对于 CPU 而言,核的个数与可同时运行的进程数相同;若进程数超过核数,进程将分时使用 CPU 资源;

④ 无论进程是如何创建的,所有进程都会从操作系统中分配到进程 ID,其值为大于 2 的整数;

2-2--fork()创建进程

cpp 复制代码
#include <unistd.h>
pid_t fork(void);

// 成功时返回进程 ID,失败时返回 -1

fork() 函数会复制 父进程的进程副本 给创建的子进程,两个进程都将执行 fork() 函数调用后的语句;具体执行的内容可根据 fork() 函数的返回值进行区分,对于父进程 fork() 函数返回子进程的进程 ID,对于子进程 fork() 函数返回 0;

cpp 复制代码
// gcc fork.c -o fork
// ./fork
#include <stdio.h>
#include <unistd.h>

int gval = 10;
int main(int argc, char *argv[]){
    __pid_t pid;
    int lval = 20;
    gval++, lval += 5;

    pid = fork();
    if(pid == 0){ // 对于子进程,fork返回0,因此执行以下内容
        gval += 2, lval += 2;
    }
    else{ // 对于父进程,执行以下内容
        gval -= 2, lval -= 2;
    }

    if(pid == 0){
        // 对于子进程,复制父进程的进程副本,则最初 gval = 11, lval = 25;
        // 执行 += 2 后,gval = 13, lval = 27;
        printf("Child Proc: [%d, %d] \n", gval, lval);
    }
    else{
        // 对于父进程,执行 -= 2后,gval = 9, lval = 23;
        printf("Parent Proc: [%d %d] \n", gval, lval);
    }
    return 0;
}

2-3--僵尸进程

一般进程完成工作后都应被立即销毁,但部分进行由于各种原因导致不能及时销毁,就成为了僵尸进程,其会占用系统中的重要资源;

终止僵尸进程的两种方式:① 传递参数给 exit 函数并调用 exit 函数 ;② main 函数中执行 return 语句并返回值;

向 exit 函数传递的参数值和 main 函数 return 语句的返回值都会传递给操作系统 ,而操作系统不会立即销毁子进程 ,直到把这些返回值传递给父进程 ;这种不会被立即销毁的子进程就是僵尸进程

此外,操作系统不会主动将返回值传递给父进程;只有父进程主动发起请求时,操作系统才会将子进程的返回值传递给父进程;因此,如果父进程未主动要求获得子进程的结束状态值,操作系统就不会销毁子进程,子进程就一直处于僵尸进程状态;

cpp 复制代码
// gcc zombie.c -o zombie
// ./zombie
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]){
    __pid_t pid = fork();
    if(pid == 0){
        puts("Hi, I am a child process");
    }
    else{
        // 父进程终止时,子进程也会被同时销毁
        // 本案例通过延缓父进程的终止时间,来让子进程进入僵尸进程状态
        printf("Child Process ID: %d \n", pid);
        sleep(30);
    }

    if(pid == 0){
        puts("End child process");
    }
    else{
        puts("End parent process");
    }
    return 0;
}

通过 ps au 可以观测到在父进程睡眠的时间里,子进程成为了僵尸进程(**Z+**状态);

2-4--wait()和waitpid()销毁僵尸进程

为了销毁僵尸子进程,父进程必须主动请求获取子进程的返回值;

父进程调用 wait() 函数 和 waitpid() 函数可以主动获取子进程的返回值;

cpp 复制代码
#include <sys/wait.h>

pid_t wait(int* statloc);
// 成功时返回终止的子进程 ID, 失败时返回 -1;
// 子进程的返回值会保存到 statloc 所指的内存空间

// WIFEXITED() 子进程正常终止时返回 true
// WEXITSTATUS() 返回子进程的返回值

父进程调用 wait() 函数时,如果没有已终止的子进程,则父进程 的程序将会阻塞,直至有子进程终止来返回值;

cpp 复制代码
// gcc wait.c -o wait
// ./wait
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char* argv[]){
    int status;
    __pid_t pid = fork();

    if(pid == 0){
        return 3; // 第一个子进程返回3
    }
    else{
        printf("Child PID: %d \n", pid); // 第一个子进程的 ID
        pid = fork(); // 创建第二个子进程
        if(pid == 0){
            exit(7); // 第二个子进程返回7
        }
        else{
            printf("Child PID : %d \n", pid); // 第二个子进程的 ID
            wait(&status); // 主动请求获取子进程的返回值
            if(WIFEXITED(status)){
                printf("Chile send one: %d \n", WEXITSTATUS(status));
            }
            wait(&status); // 主动请求获取子进程的返回值
            if(WIFEXITED(status)){
                printf("Child send two: %d \n", WEXITSTATUS(status));
            }
            sleep(30); // 这时候父进程选择睡眠,子进程也不会成为僵尸进程
        }
    }
    return 0;
}

wait() 函数会引起程序阻塞,而 waitpid() 函数不会引起阻塞;

cpp 复制代码
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int* statloc, int options);
// 成功时返回终止的子进程的ID(或0),失败时返回-1
// pid 表示等待终止的目标子进程的 ID,传递 -1 时与 wait() 相同,即可以等待任意子进程终止
// statloc 存放子进程返回结果的地址空间
// options 设置为 WNOHANG 时,即使没有终止的子进程,父进程也不会进入阻塞状态,而是返回 0 并结束函数
cpp 复制代码
// gcc waitpid.c -o waitpid
// ./waitpid
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]){
    int status;
    __pid_t pid = fork();
    if(pid == 0){
        sleep(15);
        return 24;
    }
    else{
        // 没有终止的子进程时,返回0,则一直循环调用waitpid()
        // 直到有终止的子进程来跳出循环
        while(!waitpid(-1, &status, WNOHANG)){
            sleep(3);
            puts("sleep 3sec.");
        }
        if(WIFEXITED(status)){
            printf("Child send %d \n", WEXITSTATUS(status));
        }
        return 0;
    }
}

3--信号处理

上述父进程调用 wait() 函数会阻塞,而调用 waitpid() 函数也必须不断调用(因为不知道子进程何时终止),这也同样会影响父进程的工作效率;

通过信号处理机制 ,可以解决上述问题;信号表示在特定事件发生时由操作系统向进程发送(通知)的消息

因此可以通过注册信号,当子进程终止时,让操作系统将子进程终止的消息发送给父进程,这时候父进程才请求获取子进程的返回值;

3-1--signal()函数

cpp 复制代码
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);

// 第一个参数 signo 表示特殊情况信息
// 第二个参数表示特殊情况发生后,要调用的函数的地址值(指针)

// 常见特殊情况
// 1. SIGALRM 表示已到调用 alarm 函数注册的时间
// 2. SIGINT 表示遇到了 CTRL+C 的情况
// 3. SIGCHLD 表示遇到了子进程终止的情况

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 返回 0 或以秒为单位的距离 SIGALRM 信号发生的所剩时间(即还剩下多长时间就会发生 SIGALRM 信号时间)
// 经过 seconds 秒后会发生 SIGALRM 信号事件

发生信号事件时,将会唤醒由于调用 sleep 函数而进入阻塞状态的进程;即:即使还没到 sleep 函数规定的事件也会被强制唤醒,而进程一旦唤醒后就不会再进入睡眠状态;

相关推荐
行走__Wz1 天前
【网工入门-eNSP模拟-01】ip地址配置
网络·tcp/ip·智能路由器
酉鬼女又兒1 天前
零基础入门计算机网络网际层核心:IP数据报发送与转发完整流程、静态路由配置方法、路由环路成因与解决方案及历年考研经典例题深度解析
网络·tcp/ip·计算机网络·考研·职场和发展
liulilittle1 天前
KCC: An Exploration Along the Lines of BBR
网络·tcp/ip·计算机网络·bbr·通信·拥塞控制·kcc
行走__Wz1 天前
【网工入门-eNSP模拟-02】dhcp动态主机配置ip地址
服务器·网络·tcp/ip
数据知道1 天前
指纹浏览器代理中台设计:为每个指纹环境绑定独立出口IP的架构实现
网络协议·tcp/ip·架构
数据知道1 天前
指纹浏览器:DNS 泄漏防范与 WebRTC 本地 IP 屏蔽的底层实现
爬虫·网络协议·tcp/ip·安全·webrtc·数据采集·指纹浏览器
ElevenS_it1882 天前
Nginx日志监控告警实战:access_log解析+5xx突增+慢请求+异常IP自动告警完整方案(Filebeat+Zabbix)
运维·网络·tcp/ip·nginx·zabbix
大草原的小灰灰2 天前
TCP/IP协议栈传输层介绍
网络协议·tcp/ip
我是一颗柠檬2 天前
【计算机网络全面教学】网络层与IP协议,子网划分到路由协议全掌握Day3(2026年)
网络协议·tcp/ip·计算机网络
阿米亚波2 天前
SSH+TCP流程及抓包说明
网络·笔记·网络协议·tcp/ip·计算机网络·wireshark·ssh