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

2023-11-06

目录

1--并发服务器端

2--进程

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

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

2-3--僵尸进程

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

3--信号处理

3-1--signal()函数

3-2--sigaction()函数

3--3--利用信号处理技术消灭僵尸进程

4--基于多任务的并发服务器

5--分割 TCP 的 I/O 程序


1--并发服务器端

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

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

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

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

2--进程

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

进程的相关概念:

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

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

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

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

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

#include <unistd.h>
pid_t fork(void);

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

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

// 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 语句的返回值都会传递给操作系统,而操作系统不会立即销毁子进程,直到把这些返回值传递给父进程;这种不会被立即销毁的子进程就是僵尸进程

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

// 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() 函数可以主动获取子进程的返回值;

#include <sys/wait.h>

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

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

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

// 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() 函数不会引起阻塞;

#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 并结束函数
// 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()函数

#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 函数规定的事件也会被强制唤醒,而进程一旦唤醒后就不会再进入睡眠状态;

// gcc signal.c -o signal
// ./signal

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig){
    if(sig == SIGALRM){
        puts("Time out!");
    }
    alarm(2);
}

void keycontrol(int sig){
    if(sig == SIGINT){
        puts("CTRL+C pressed");
    }
}

int main(int argc, char *argv[]){
    int i;
    signal(SIGALRM, timeout);
    signal(SIGINT, keycontrol);
    alarm(2);

    for(i = 0; i < 3; i++){
        puts("wait...");
        sleep(100); // 不会真的睡眠 100s,因为alarm函数会产生SIGALRM信号事件,从而唤醒进程
    }
    return 0;
}

3-2--sigaction()函数

        sigaction() 函数的功能类似于 signal() 函数,但 sigaction() 更稳定;因为 signal() 函数在不同操作系统中可能存在区别,但 sigaction() 在不同 UNIX 系统中完全相同;

#include <signal.h>

int sigaction(int signo, const struct sigaction* act, struct sigaction* oldact);
// 成功时返回0,失败时返回 -1
// signo 用于传递信号信息
// act 对应于 signo 的信号处理函数
// oldact 获取之前注册的信号处理函数的指针,不用时传递0

struct sigaction{
    void (*sa_handler)(int); // 信号处理函数的指针
    sigset_t sa_mask; // 初始化为0
    int sa_flags; // 初始化为0
}
// gcc sigaction.c -o sigaction
// ./sigaction

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig){
    if(sig == SIGALRM){
        puts("Time out!");
    }
    alarm(2);
}

int main(int argc, char* argv[]){
    int i;
    struct sigaction act;
    act.sa_handler = timeout;
    sigemptyset(&act.sa_mask); // 调用sigemptyset()将sa_mask的所有位初始化为0
    act.sa_flags = 0; // sa_flags也初始化为0
    sigaction(SIGALRM, &act, 0);
    alarm(2);
    for(int i = 0; i < 3; i++){
        puts("wait...");
        sleep(100);
    }
    return 0;
}

3--3--利用信号处理技术消灭僵尸进程

// gcc remove_zombie.c -o remove_zombie
// ./remove_zombie

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void read_childproc(int sig){
    int status;
    pid_t id = waitpid(-1, &status, WNOHANG); // 等待任意子线程结束
    if(WIFEXITED(status)){ // 判断子线程是否正常终止
        printf("Remove proc id: %d \n", id);
        printf("Child send: %d \n", WEXITSTATUS(status)); // 打印子线程的返回值
    }
}

int main(int argc, char *argv[]){
    pid_t pid;
    struct sigaction act;
    act.sa_handler = read_childproc; // 设置信号处理函数
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, 0); // 调用sigaction(),当遇到子线程结束的信号时,调用信号处理函数

    pid = fork();
    if(pid == 0){ // 子线程执行区域
        puts("Hi! I'm child process");
        sleep(10);
        return 12;
    }
    else{
        printf("Child proc id: %d \n", pid);
        pid = fork();
        if(pid == 0){ // 另一个子线程执行区域
            puts("Hi! I'm child process");
            sleep(10);
            return 24;
        }
        else{
            int i;
            printf("Child proc id: %d \n", pid);
            for(int i = 0; i < 5; i++){
                puts("wait...");
                sleep(5);
            }
        }
    }
    return 0;
}

4--基于多任务的并发服务器

        每当有客户端请求服务时,回声服务器端都创建子进程以提供服务;

        使用 fork() 创建进程时,子进程会复制父进程拥有的所有资源,因此无需额外传递文件描述符;

// gcc echo_mpserv.c -o echo_mpserv
// ./echo_mpserv 9190

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30

void error_handling(char *message){
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

void read_childproc(int sig){
    __pid_t pid;
    int status;
    pid = waitpid(-1, &status, WNOHANG);
    printf("remove proc id: %d \n", pid);
}

int main(int argc, char* argv[]){
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;

    __pid_t pid;

    struct sigaction act; // 信号
    socklen_t adr_sz;
    int str_len, state;
    char buf[BUF_SIZE];
    if(argc != 2){
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    // 防止僵尸进程
    act.sa_handler = read_childproc; //设置信号处理函数
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD, &act, 0);

    serv_sock = socket(PF_INET, SOCK_STREAM, 0); // 创建 tcp socket
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1){
        error_handling("bind() error"); 
    } 
    if(listen(serv_sock, 5) == -1){
        error_handling("listen() error");
    }

    while(1){
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
        if(clnt_sock == -1){
            continue;
        }
        else{
            puts("new client connected...");
        }
        // 每当有客户端请求服务时,clnt_sock 不为 -1
        // 因此会执行 fork() 函数创建子进程,并由子进程向客户端提供服务
        pid = fork(); 
        if(pid == -1){
            close(clnt_sock);
            continue;
        }
        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);
            puts("client disconnected...");
            return 0;
        }
        else{ // 父进程运行区域
            // 因为客户端的文件描述符已经复制到子进程中
            // 由子进程处理客户端的内容,因此父进程需要销毁客户端的文件描述符
            close(clnt_sock); 
        }
    }
    close(serv_sock);
    return 0;
}

5--分割 TCP 的 I/O 程序

        客户端通过 fork() 创建子进程,将 I/O 分割;客户端的父进程负责接收数据,额外创建的子进程负责发送数据;

        分割后,不同进程分别负责输入和输出,因此客户端是否从服务器端接收完数据都可以进行传输;

// gcc echo_mpclient.c -o echo_mpclient
// ./echo_mpclient 127.0.0.1 9190

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30

void error_handling(char *message){
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

void read_routine(int sock, char *buf){
    while(1){
        int str_len = read(sock, buf, BUF_SIZE);
        if(str_len == 0) return;
        buf[str_len] = 0;
        printf("Message from server: %s", buf);
    }
}

void write_routine(int sock, char* buf){
    while(1){
        fgets(buf, BUF_SIZE, stdin);
        if(!strcmp(buf, "q\n") || !strcmp(buf, "Q\n")){
            shutdown(sock, SHUT_WR); // 调用 shutdown 函数向服务器端传递 EOF
            return;
        }
        write(sock, buf, strlen(buf));
    }
}

int main(int argc, char *argv[]){
    int sock;
    pid_t pid;
    char buf[BUF_SIZE];
    struct sockaddr_in serv_adr;

    if(argc != 3){
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0); // 创建 tcp socket
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1){
        error_handling("connect() error!");
    }

    pid = fork(); // 创建子进程实现 I/O 分离
    if(pid == 0){ // 子进程写数据到服务器端
        write_routine(sock, buf);
    }
    else{ // 父进程从服务器端中读取数据
        read_routine(sock, buf);
    }
    close(sock);
    return 0;
}   

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • C++11静态断言static_assert

    C 11静态断言static assert 一 运行时断言 二 静态断言的需求 三 静态断言 四 单参数版本的静态断言 一 运行时断言 断言 assertion 是一种编程中常用的手段 在通常情况下 断言就是将一个返回值总是需要为真的判别式

随机推荐

  • vue3 组合式api中 ref 和$parent 的使用

    ref 的使用 vue3中 在 组件中添加一个 component ref xxx 就可以在父组件中得到 子组件的 dom 对象 以及 虚拟的 dom 对象 有了虚拟 dom 我们就可以在父组件中控制子组件的显示了 ref 的使用方法 vu
  • vue中的富文本框的使用(vue-quill-editor)

    一 安装 vue quill editor npm install vue quill editor 二 在main js中引入 import VueQuillEditor from vue quill editor import quil
  • Android Widget:DrawerLayout配合Toolbar的使用及常见问题

    前言 最近在仿写网易云音乐安卓端界面 记录下所遇到的问题及解决方案 DrawerLayout的基础使用 DrawerLayout经常配合NavigationView及Toolbar使用 编写侧滑栏 1 首先添加支持 implementati
  • MapReduce处理csv

    MapReduce处理csv csv是由逗号 来分割的文件 在编写Mapper类的时候需要以 分割成一个个的数据 查看一下csv数据 以上是为了测试做的数据 要处理的结果就是经过mapreduce再原封不动的出来 因为是测试 所以内容不做任
  • 搭建虚拟专网络服务器

    搭建虚拟专网络服务器 准备环境 一台windows server2008R2 服务器 没有安装ad域服务的 IP地址为 192 168 4 92 一台物理机windows 10 家庭版 1 首先安装 网络策略和访问服务 第二步 勾选上远程访
  • 为什么普通人赚钱这么难?普通人的赚钱之路在哪里

    前几天听一个老家的朋友说 辛辛苦苦一整年 发现并没有赚到什么钱 付出与收入不成正比 首先要知道勤奋 努力并不一定就能够赚到钱 像送外卖的 工地上班的 厂里上班的哪个不勤奋 但他们即使非常努力工作一个月 扣除基本开支 也存不了多少钱 那普通人
  • GNS3-GREvpn

    GREvpn 发一下这些日忙的东西 实验拓扑 以R2为界限的左半边运行ospf 各个链路已经ping通 用R7模拟PC1 PC1和R2的f0 0来回链路没问题 右半边同左半边做相应的配置也成功ping通 在R2和R3之间建立vpn隧道 网段
  • 利用Matlab绘制图像中的某一行或者某一列的灰度曲线

    filename C Users Administrator Desktop 透视变换 包含裂缝的整个图片 123 jpg imgData imread filename imshow imgData 该函数可以用来显示已经读入的数据 A
  • 解决AD中pcb原件移动提示绿色报错问题

    有可能以下三个原因之一所导致的 1 不符合DRC规则 比如原件之间距离过近 就是报错 2 右下角ROOM没有删除 右键清楚就可以啦 3 如果以上两个确证都是正常的 还是报错的话 终极解决方案 step1 在pcb界面下 点击design n
  • 主线3.1DeepFM模型论文阅读:DeepFM: A Factorization-Machine based Neural Network for CTR Prediction

    文章目录 一 摘要 二 模型演变和各模型间的对比 1 CTR的任务要求 2 DeepFM模型的引入 3 各模型间的对比 4 DeepFM优势 三 DeepFM模型介绍 1 FM部分 2 Deep部分 一 摘要 对于一个基于CTR预估的推荐系
  • selenium小项目实践

    1 斗鱼爬虫 爬取斗鱼直播平台的所有房间信息 游戏直播 全部游戏直播 斗鱼直播 1 1 思路分析 数据的位置 每个房间的数据都在id live list contentbox的ul中 实现翻页和控制程序结束 selenium中通过点击就可以
  • 基于麻雀搜索算法(SSA)优化长短期记忆神经网络参数SSA-LSTM冷、热、电负荷预测(Python代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 2 1 冷负荷 2 2 热负荷 2 3 电负荷 3 参考文献 4 Python代码 数据 1
  • 虚拟滚动之原理及其封装

    本文分享自微信公众号 一Li小麦 gh c88159ec1309 作者 一li小麦 目前GitHub上只放出demo的版本 将在 https github com dangjingtao vList git 持续更新 前端的业务开发中会遇到
  • python输入输出+文件+OS

    声明 本人的所有博客皆为个人笔记 作为个人知识索引使用 因此在叙述上存在逻辑不通顺 跨度大等问题 希望理解 分享出来仅供大家学习翻阅 若有错误希望指出 感谢 Python输入和输出 输出格式美化 Python三种输出值的方式 表达式语句 p
  • BSN区块链服务网络底十六章

    1 1 简介 服务网络的设计和建设理念完全借鉴互联网 互联网是由TCP IP协议将所有数据中心连接而形成的 服务网络是通过建立一套区块链运行环境协议将所有数据中心连接而组成 与互联网一样 服务网络也是跨云服务 跨门户 跨底层框架的全球性基础
  • MPEG I,B,P的顺序

    MPEG视频压缩算法的特点 数字化后的数据量之大非常惊人 如果不对原始视频图像数据进行压缩 则在与VCD 相同的光盘上只能存储20秒钟的中等分辨率 彩色视频图像 和JVC公司在制定VCD标准时采用了MPEG 1数字图像压缩编码国际标准 IS
  • 【Matlab】矩阵操作

    矩阵操作 生成矩阵 生成行矩阵的方式 冒号表达式 x 1 1 5 1 2 3 4 5 以步长为1 从1到5生成数值 构成行矩阵 若不设置步长 则默认步长为1 linspace a b n linspace 1 5 5 1 2 3 4 5 从
  • JavaScript typeof操作符确认数据类型

    返回 number typeof 1 typeof new Date getTime 返回 undefiend typeof undefined typeof console log 1 先打印1 再打印 undefined 返回 stri
  • Xilinx ISE、MicroBlaze系列教程

    本文是Xilinx MicroBlaze系列教程的第0篇文章 这个系列文章是我个人最近两年使用Xilinx MicroBlaze软核的经验和笔记 以Xilinx ISE 14 7和Spartan 6 Vivado 2018 3和Artix
  • 《TCP/IP网络编程》阅读笔记--并发多进程服务端的使用

    目录 1 并发服务器端 2 进程 2 1 进程的相关概念 2 2 fork 创建进程 2 3 僵尸进程 2 4 wait 和waitpid 销毁僵尸进程 3 信号处理 3 1 signal 函数 3 2 sigaction 函数 3 3 利