day-06 多进程服务器端 -- 进程间通信

这篇具有很好参考价值的文章主要介绍了day-06 多进程服务器端 -- 进程间通信。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一.多进程服务器端

(一)进程概念及应用

        利用之前学习到的内容,我们的服务器可以按照顺序处理多个客户端的服务请求。在客户端和服务时间增长的情况下,服务器就不足以满足需求了。

1.两种类型的服务器端

(1)普通服务器:当有100个客户端连接请求到来时,假设每个请求的受理时间为1s,那么第50个请求需要等待50s,第100个请求需要等待100s

(2)并发服务器:所有客户端的连接请求受理时间都不超过1s,单平均服务时间2-3s。

        很明显并发服务器处理高并发量的情况效率更高。

2.并发服务器端的实现方法

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

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

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

3.理解进程(Precoess)

进程(Process)是计算机中的一个术语,指的是正在运行中的程序实例。在操作系统中,每个进程都有自己独立的内存空间和资源,它们之间相互隔离,并且可以独立执行。

每个进程可以包含一个或多个线程,线程是进程内的执行单元,负责执行进程的指令。不同的进程之间可以并发执行,相互之间独立运行,彼此不会干扰。

进程有以下几个特点:

  1. 独立性:每个进程拥有自己的地址空间和资源,运行时相互独立,一个进程的崩溃不会影响其他进程。
  2. 并发性:多个进程可以同时运行,由操作系统进行调度和管理,利用多核处理器实现并行处理。
  3. 隔离性:不同进程之间的内存空间相互隔离,一个进程无法直接访问另一个进程的数据和资源,需要通过特定的机制进行通信和共享。
  4. 可抢占性:操作系统可以根据优先级和时间片轮转等策略,暂停当前进程的执行,并将CPU分配给其他进程,以实现公平调度和资源利用。

进程是操作系统中重要的概念,它为程序的执行提供了一个独立和可控的环境。通过进程,操作系统可以同时运行多个应用程序,实现资源的合理分配和管理。

生活中有许多例子可以说明进程的概念。下面是几个常见的例子

  1. 煮饭过程:将烹饪一顿饭比作一个进程。在煮饭的过程中,你需要准备食材、洗切处理、点火、加热、炒煮等一系列步骤。每个步骤都是相对独立的,但又相互关联,最终完成一道美味的饭菜。

  2. 打印文件:当你要打印一个文件时,你会选择打印命令并发送给打印机。打印机会创建一个打印进程,它负责从计算机接收数据、解析文件格式、生成打印页面,并将页面发送到打印机进行输出。同时,你可以进行其他操作,如编辑文档或浏览网页,这些操作与打印进程并行执行。

  3. 路上的交通:将路上的车辆比作进程。在拥挤的道路上,每辆车都是一个独立的进程,它们之间相互独立运行,但也受到交通规则和信号灯的控制。每辆车根据自己的路径和目的地进行行驶,通过调度和协调,交通系统实现了车辆的并发运行和道路资源的合理利用。

  4. 整个工业生产过程:在一个工厂中,生产线上的各个环节可以看作是不同的进程。例如,原材料的采购、加工制造、装配、质量检测等环节都是相对独立的进程,它们按照一定的顺序和流程进行,并最终完成产品的制造。

这些例子说明了生活中进程的存在和应用。无论是在计算机系统中还是在日常生活中,进程都扮演着协调和管理任务的重要角色,实现了多个任务之间的并发执行和资源的合理利用。

4.进程ID

进程ID(Process ID),也称为PID,是操作系统中用来唯一标识一个正在运行的进程的数字标识符。每个进程在创建时都会被分配一个独特的PID。

  • pa au 查看当前运行的所有进程

进程ID的作用有以下几个方面:

  1. 进程标识:通过PID,操作系统可以准确地标识和区分不同的进程。不同的进程具有不同的PID,使得操作系统可以对它们进行管理、调度和资源分配。

  2. 进程控制:操作系统可以使用PID来控制进程的创建、终止和暂停等操作。通过指定PID,可以准确地选择目标进程并执行相应的操作。

  3. 进程通信:在进程间进行通信时,PID常被用作目标进程的标识符。发送进程可以通过目标进程的PID将消息或数据传递给指定的进程。

  4. 资源管理:各个系统资源,如内存、文件、网络连接等,都与特定的进程相关联。通过PID,操作系统可以将资源与相应的进程关联起来,并进行有效的资源管理和保护。

需要注意的是,PID是动态分配的,当一个进程终止后,其PID可能会被重新分配给新创建的进程。因此,PID只在进程的生命周期内是唯一的。

5.通过 fork() 函数创建进程

        在操作系统中,可以使用fork()函数来创建一个新的进程。fork()是一个系统调用,其功能是复制当前进程(称为父进程),创建一个新的进程(称为子进程)。子进程是父进程的副本,它继承了父进程的代码、数据和资源。

具体使用方法如下:

  • 在程序中调用fork()函数。fork()函数没有参数,返回值为整型。
  • fork()函数的返回值不同于父进程和子进程。在父进程中,fork()返回子进程的PID(大于0);在子进程中,fork()返回0;如果fork()调用失败,返回一个负值表示错误。
  • 父进程和子进程之后的代码是完全独立执行的。根据fork()函数的返回值,可以在程序中使用条件语句或其他逻辑来区分父进程和子进程的执行路径。
  • 子进程可以通过修改自己的代码和数据,执行不同的任务。父进程和子进程之间共享打开的文件描述符和某些系统资源,但是它们有各自独立的运行环境和内存空间。
#include<iostream>
#include<unistd.h>
int val=10;
int main(){
	pid_t pid=fork();
	int index=25;
	val++,index+=5;
	if(pid==-1){//fork调用失败
		std::cout<<" fork调用失败"<<std::endl;
		return 1;
	}
	else if(pid==0)
		index+=10;
	else
		val+=2;

	if(pid==0)
		std::cout<<"子进程:"<<"val:"<<val<<"  index:"<<index<<std::endl;
	else
		std::cout<<"父进程:"<<"val:"<<val<<"  index:"<<index<<std::endl
;

	return 0;
}

day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

         可以看出,父子进程的变量都是单独区分开的,修改并不会相互影响。

通过fork()函数创建的子进程继承了父进程的大部分状态,包括变量值、打开的文件、进程优先级等。子进程可以独立执行其他任务,这样就实现了并发执行多个进程的能力。

需要注意的是,fork()函数的调用可能会导致操作系统创建新的进程和分配额外的资源。因此,在使用fork()函数时应该注意合理使用系统资源,避免过多创建进程导致系统负载过重。

(二)进程与僵尸进程

进程是操作系统中正在运行的程序的实例,它具有独立的执行环境和资源。当一个进程完成了它的任务,并且终止了,但其父进程尚未通过wait()或waitpid()等系统调用来获取该子进程的状态信息时,这个已经终止但尚未被回收的进程就成为僵尸进程。

僵尸进程是一种特殊的进程状态,其主要特点包括:

  1. 僵尸进程处于终止状态:即进程已经执行完毕,但它的进程描述符仍然存在于系统中。
  2. 父进程尚未对其进行处理:父进程还没有使用wait()或waitpid()等系统调用来获取子进程的退出状态信息。
  3. 僵尸进程不再执行任何代码:僵尸进程不再占用CPU时间片,也不再占用其他系统资源。

产生僵尸进程的常见情况是,父进程在创建子进程后,没有及时处理子进程的终止状态。这可能是因为父进程疏忽、崩溃或者被其他任务所占用而没有处理子进程。

虽然僵尸进程本身并不会导致系统性能问题,但过多的僵尸进程可能会浪费系统资源。因此,需要及时清理僵尸进程。父进程可以通过以下方式处理僵尸进程:

  1. 使用wait()或waitpid()等系统调用:父进程可以主动调用wait()或waitpid()等系统调用来获取子进程的退出状态信息,从而使子进程成为"终止"状态,释放其占用的系统资源。
  2. 使用信号处理机制:父进程可以通过注册SIGCHLD信号处理函数,当收到这个信号时,处理僵尸进程的终止状态。

另外,操作系统也会提供一些机制来自动回收僵尸进程,例如Linux中的"init"进程(PID为1)会负责收养孤儿进程和回收僵尸进程。

在编写程序时,父进程应该及时处理子进程的退出状态,以避免过多的僵尸进程积累。

1.子进程的终止方式:

(1)正常退出:子进程可以在执行完任务后通过调用exit()函数来正常退出。exit()函数会终止当前进程,并将退出状态传递给父进程。

#include <stdlib.h>

int main() {
    // 子进程执行任务
    // ...

    // 正常退出
    exit(EXIT_SUCCESS);
}

(2)异常退出:子进程也可以通过调用abort()函数或触发一个信号来异常退出。abort()函数会立刻终止进程,触发SIGABRT信号,而信号处理程序则会默认终止进程。

#include <stdlib.h>

int main() {
    // 子进程执行任务
    // ...

    // 异常退出
    abort();
    // 或者
    raise(SIGABRT);
}

(3)返回值退出:子进程可以通过在main函数中返回一个整数值来退出。这个整数值会被传递给父进程作为退出状态码。

int main() {
    // 子进程执行任务
    // ...

    // 返回值退出
    return 0;
}

(4)exec()系列函数:子进程可以使用exec()系列函数来加载一个新的程序镜像,从而替换当前进程的内容。一旦调用exec()成功,子进程就会停止原有的代码执行,而是开始执行新程序的代码。这种方式不是直接终止进程,而是将子进程转变为新的程序。

#include <unistd.h>

int main() {
    // 子进程执行任务
    // ...

    // 加载新程序
    execl("/bin/ls", "ls", "-l", NULL);
}

无论子进程是通过哪种方式终止,父进程都可以通过使用 wait() 或 waitpid() 等系统调用来获取子进程的退出状态信息,并进行相应的处理。这样可以确保父进程及时清理僵尸进程,并释放相应的资源。

2.销毁僵尸进程
(1)wait
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
using namespace std;
int main(){
	int status;
	pid_t pid=fork();

	if(pid==-1){
		std::cout<<"父进程创建失败"<<std::endl;
		return 1;
	}
	else if(pid==0)
		return 3;//返回终止
	else{
		cout<<"child PID: "<<pid<<endl;
		pid=fork();
		if(pid==0)
			exit(7);//exit函数终止
		else{
			cout<<"child PID: "<<pid<<endl;
			wait(&status);//将之前终止的子进程相关信息保存到status变量,同时子进程完全销毁
			if(WIFEXITED(status))//判断是否正常终止,如果正常退出,下面子进程返回值
				cout<<"child send one:"<<WEXITSTATUS(status)<<endl;
			wait(&status);//第二个终止的子进程
			if(WIFEXITED(status))
                                cout<<"child send two:"<<WEXITSTATUS(status)<<endl;
			sleep(30);
		}
	}
	return 0;
}

day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

        return 或是 exit 都是把进程终止,但是子进程的系统资源还没有回收,父进程通过 wait 函数释放子进程所占据的资源。

注意:调用 wait 函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到子进程终止,因此需谨慎使用。

(2)waitpid

调用 waitpid 函数时,程序不会阻塞。

#include<iostream>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;
int main(){
	pid_t pid=fork();//创建子进程
	int status;

	if(pid==0){//子进程
		sleep(15);
		return 24;
	}
	else{//父进程
		while(!waitpid(-1,&status,WNOHANG)){//当没有子进程终止时,保持循环
			sleep(3);//休眠三秒
			cout<<"sleep 3sec"<<endl;
		}//子进程终止后,资源被waitpid回收


		if(WIFEXITED(status))//判断子进程是否正常退出			
            cout<<"child send :"<<WEXITSTATUS(status)<<endl;
	}
	return 0;
}

(三)信号处理

我们已经直到了进程创建和销毁方法,那么,子进程何时终止呢?父进程要一直等待吗?

1.利用信号处理技术消灭僵尸进程

(1)signal

#include <iostream>
#include <csignal>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

// SIGCHLD信号处理函数
void handleSIGCHLD(int signum) {
    pid_t pid;
    int status;

    // 循环等待所有子进程退出,并处理其退出状态
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            std::cout << "子进程 " << pid << " 正常退出,退出状态:" << WEXITSTATUS(status) << std::endl;
        }
        else if (WIFSIGNALED(status)) {
            std::cout << "子进程 " << pid << " 异常退出,终止信号:" << WTERMSIG(status) << std::endl;
        }
    }
}

int main() {
    // 注册SIGCHLD信号处理函数
    signal(SIGCHLD, handleSIGCHLD);

    // 创建子进程
    pid_t childPid = fork();

    if (childPid == 0) {
        // 子进程代码
        sleep(2);
        return 0;
    }
    else if (childPid > 0) {
        // 父进程代码
        sleep(5);
        return 0;
    }
    else {
        // fork()失败
        std::cerr << "无法创建子进程" << std::endl;
        return 1;
    }

    return 0;
}

(2)sigaction

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void sigchld_handler(int signum) {
    pid_t pid;
    int status;

    // 循环等待所有子进程退出
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            std::cout << "Child process " << pid << " exited with status " << WEXITSTATUS(status) << std::endl;
        } else if (WIFSIGNALED(status)) {
            std::cout << "Child process " << pid << " terminated by signal " << WTERMSIG(status) << std::endl;
        }
    }
}

int main() {
    struct sigaction sa;

    // 设置 SIGCHLD 信号处理函数
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;

    if (sigaction(SIGCHLD, &sa, nullptr) == -1) {
        perror("Error setting signal handler");
        return 1;
    }

    // 创建子进程
    pid_t pid = fork();

    if (pid == -1) {
        perror("Error creating child process");
        return 1;
    } else if (pid == 0) {
        // 子进程执行某些任务
        // ...

        exit(0); // 子进程正常退出
    }

    // 父进程继续执行其他任务
    // ...

    // 父进程等待一段时间后结束
    sleep(10);

    return 0;
}

(3)二者区别

  1. signal 函数:

    • signal 函数是 C 标准库提供的函数,具有广泛的兼容性。
    • 优点:简单易用,适合进行基本的信号处理,不需要额外的结构体参数。
    • 缺点:在某些情况下,可能会出现可重入性问题,并且对于某些信号,可能无法修改其行为或使用高级功能。
  2. sigaction 函数:

    • sigaction 函数是 POSIX 标准提供的函数,提供了更多的信号处理选项和控制能力。
    • 优点:灵活性强,可以指定更复杂的信号处理行为,可以获取和保存之前的信号处理信息。
    • 缺点:相对于 signal 函数,使用 sigaction 函数可能需要编写更多的代码。

(四)基于多任务的并发服务器

 echo_mpserv.cpp
#include <iostream>
#include <cstdlib>
#include <cstring>
#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(const char* message);
void read_childproc(int sig);

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

    socklen_t adr_sz;
    int str_len, state;
    char buf[BUF_SIZE];

    if (argc != 2) {
        std::cout << "Usage : " << argv[0] << " <port>" << std::endl;
        exit(1);
    }

    // 准备及注册 sigaction
    struct sigaction act;
    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD, &act, nullptr);

    // socket
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);

    // bind
    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(std::atoi(argv[1]));
    if (bind(serv_sock, reinterpret_cast<struct sockaddr*>(&serv_adr),
             sizeof(serv_adr)) == -1) {
        error_handling("bind() error");
    }

    // listen
    if (listen(serv_sock, 5) == -1) {
        error_handling("listen() error");
    }

    while (true) {
        // accept
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, reinterpret_cast<struct sockaddr*>(&clnt_adr), &adr_sz);
        if (clnt_sock == -1) {
            continue;
        } else {
            std::cout << "New client connected..." << std::endl;
        }

        // fork
        pid = fork();
        if (pid == -1) {  // 子进程创建失败
            close(clnt_sock);
            continue;
        }
        if (pid == 0) {  // 子进程
            close(serv_sock);  // 关闭子进程服务器套接字

            // read
            while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0) {
                write(clnt_sock, buf, str_len);
            }

            close(clnt_sock);  // 关闭子进程客户端套接字
            std::cout << "Client disconnected..." << std::endl;
            return 0;
        } else {  // 父进程
            close(clnt_sock);  // 关闭父进程客户端套接字
        }
    }

    close(serv_sock);  // 关闭父进程服务器套接字
    return 0;
}

void read_childproc(int sig) {
    pid_t pid;
    int status;
    pid = waitpid(-1, &status, WNOHANG);  // 回收子进程
    std::cout << "Removed proc id: " << pid << std::endl;
}

void error_handling(const char* message) {
    std::cerr << message << std::endl;
    exit(1);
}

注意:fork()只复制了父进程两个套接字(服务器端套接字、客户端套接字)的文件描述符,调用fork()函数后,2个文件描述符指向同一套接字。

        只有当2个文件描述符都终止(销毁后),才能销毁套接字!

day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

echo_mpclien.cpp
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024

void error_handling(const char* message);

int main(int argc, char* argv[]) {
    int sock;
    int str_len, recv_len, recv_cnt;
    char message[BUF_SIZE];

    struct sockaddr_in serv_adr;

    if (argc != 3) {
        std::cout << "Usage : " << argv[0] << " <IP> <Port>" << std::endl;
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1) {
        error_handling("socket() error");
    }

    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(std::atoi(argv[2]));  // 端口号

    if (connect(sock, reinterpret_cast<struct sockaddr*>(&serv_adr), sizeof(serv_adr)) == -1) {
        error_handling("connect() error");
    } else {
        std::cout << "Connected......" << std::endl;
    }

    while (true) {
        std::cout << "Input Message(Q to quit): ";
        std::cin.getline(message, BUF_SIZE);
        if (std::strcmp(message, "q") == 0 || std::strcmp(message, "Q") == 0) {
            break;
        }
        str_len = write(sock, message, strlen(message));

        recv_len = 0;
        while (recv_len < str_len) {
            recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
            if (recv_cnt == -1) {
                error_handling("read() error");
            }
            recv_len += recv_cnt;
        }

        message[str_len] = '\0';
        std::cout << "Message from server: " << message << std::endl;
    }

    close(sock);
    return 0;
}

void error_handling(const char* message) {
    std::cerr << message << std::endl;
    exit(1);
}

(五)分割TCP的I/O模型

echo_mpclient.cpp
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30

void error_handling(const char* message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);

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

    if (argc != 3) {
        std::cout << "Usage : " << argv[0] << " <IP> <port>" << std::endl;
        exit(1);
    }

    // 创建套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    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(std::atoi(argv[2]));

    // 连接服务器
    if (connect(sock, reinterpret_cast<struct sockaddr*>(&serv_adr), sizeof(serv_adr)) == -1) {
        error_handling("connect() error!");
    }

    pid = fork();
    if (pid == 0) {
        write_routine(sock, buf);
    } else {
        read_routine(sock, buf); 
    }

    close(sock);
    return 0;
}

void read_routine(int sock, char *buf) {
    while (true) {
        int str_len = read(sock, buf, BUF_SIZE);
        if (str_len == 0) {
            return;
        }
        buf[str_len] = 0;
        std::cout << "Message from server: " << buf << std::endl;
    }
}

void write_routine(int sock, char *buf) {
    while (true) {
        std::cin.getline(buf, BUF_SIZE);
        if (std::strcmp(buf, "q") == 0 || std::strcmp(buf, "Q") == 0) {
            shutdown(sock, SHUT_WR);
            return;
        }
        write(sock, buf, strlen(buf));
    }
}

void error_handling(const char* message) {
    std::cerr << message << std::endl;
    exit(1);
}

通过fork函数创建子进程,子进程发送,父进程接收

day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

二.进程间通信(管道)

(一)进程间通信的基本概念

        进程间通信(Inter-Process Communication,IPC)是指不同进程之间进行数据交换和共享资源的方式和机制。常见的进程间通信方法包括管道、命名管道、信号量、消息队列、共享内存和套接字等。

1.通过管道实现进程间通信

day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

#include<iostream>
#include<unistd.h>
#define BUF_SIZE 30
using namespace std;

int main(int argc,char *argv[]){

	int fds[2];
	char str[]="who are you?";
	char buf[BUF_SIZE];
	pid_t pid;
	
	pipe(fds);//创建管道
	pid=fork();//创建子进程
	if(pid==0){
		write(fds[1],str,sizeof(str));//子进程写入
	}
	else{
		read(fds[0],buf,BUF_SIZE);//父进程读出
		cout<<buf<<endl;
	}
	return 0;
}

day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

 2.通过管道进程进程间双向通信(创建两个管道)

day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

#include<iostream>
#include<unistd.h>
#define BUF_SIZE 30
using namespace std;

int main(int argc,char *argv[]){
	int fds[2],fds1[2];
	char str[]="你好我是进程A!";
	char str1[]="你好我是进程B!";
	char buf[BUF_SIZE];
	pid_t pidB;

	pipe(fds);//创建两个管道
	pipe(fds1);
	pidB=fork();
	if(pidB==0){
		write(fds1[1],str1,sizeof(str1));
		read(fds[0],buf,BUF_SIZE);
		cout<<"子进程:"<<getpid()<<"   "<<buf<<endl;
	}
	else{
		read(fds1[0],buf,BUF_SIZE);
		cout<<"父进程:"<<getpid()<<"   "<<buf<<endl;
		cout<<"子进程:"<<pidB<<"   "<<buf<<endl;
		write(fds[1],str,sizeof(str));
		sleep(1);
	}
	return 0;
}

 day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

 注意:getpid获取当前进程ID号

为何不用单个管道:

在进行双向通信时,为了实现双向数据传输,需要创建两个管道。每个管道负责一个方向的数据传输。

使用两个管道的主要原因是,管道是单向的,即数据只能在一个方向上流动。如果只使用一个管道进行双向通信,那么会出现以下问题:

  • 阻塞:当一个进程读取管道中的数据时,如果另一个进程也想写入数据,但在同一时间被阻塞(因为管道已被占用),则会导致进程之间的通信阻塞。

  • 死锁:假设两个进程同时尝试读取和写入同一个管道,由于管道是单向的,它们可能会陷入相互等待对方完成操作的死锁状态。

通过创建两个管道,可以解决上述问题。一个管道用于父进程向子进程传递数据,另一个管道则用于子进程向父进程传递数据。这样,每个进程都有自己独立的读取端和写入端,从而避免了阻塞和死锁的问题。

总结起来,创建两个管道可以实现双向通信,确保数据在两个方向上的正常流动,避免了阻塞和死锁的问题。

(二)运用进程间通信

1.保存消息的回声服务器端
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fstream>

#define BUF_SIZE 100

void error_handling(const char *message);
void read_childproc(int sig);

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

    pid_t pid;
    struct sigaction act;
    socklen_t adr_sz;
    int str_len, state;
    char buf[BUF_SIZE];

    if (argc != 2) {
        std::cout << "Usage : " << argv[0] << " <port>" << std::endl;
        exit(1);
    }

    // Setup signal handling for child processes
    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD, &act, 0);

    // Create socket
    serv_sock = socket(AF_INET, SOCK_STREAM, 0);

    // Bind
    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, reinterpret_cast<struct sockaddr *>(&serv_adr), sizeof(serv_adr)) == -1)
        error_handling("bind() error");

    // Listen
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    pipe(fds);

    pid = fork();
    if (pid == 0) { // Child process for saving messages
        std::ofstream fp("echomsg.txt", std::ios::out | std::ios::binary);
        char msgbuf[BUF_SIZE];
        int i, len;

        for (i = 0; i < 10; i++) {
            len = read(fds[0], msgbuf, BUF_SIZE);
            fp.write(msgbuf, len);
        }
        fp.close();
        return 0;
    }

    while (1) {
        // Accept
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, reinterpret_cast<struct sockaddr *>(&clnt_adr), &adr_sz);
        if (clnt_sock == -1)
            continue;
        else
            std::cout << "New client connected..." << std::endl;

        pid = fork();
        if (pid == 0) { // Child process
            close(serv_sock);
            while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0) {
                write(clnt_sock, buf, str_len);
                write(fds[1], buf, str_len);
            }

            close(clnt_sock);
            std::cout << "Client disconnected..." << std::endl;
            return 0;
        } else { // Parent process
            close(clnt_sock);
        }
    }

    close(serv_sock);
    return 0;
}

void read_childproc(int sig) {
    pid_t pid;
    int status;
    pid = waitpid(-1, &status, WNOHANG);
    std::cout << "Removed proc id: " << pid << std::endl;
}

void error_handling(const char *message) {
    std::cerr << message << std::endl;
    exit(1);
}
2.客户端
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30

void error_handling(const char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);

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

    if (argc != 3) {
        std::cout << "Usage : " << argv[0] << " <IP> <port>" << std::endl;
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    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, reinterpret_cast<struct sockaddr *>(&serv_adr), sizeof(serv_adr)) == -1)
        error_handling("connect() error!");

    pid = fork();
    if (pid == 0)
        write_routine(sock, buf);
    else
        read_routine(sock, buf);

    close(sock);
    return 0;
}

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';
        std::cout << "Message from server: " << buf << std::endl;
    }
}

void write_routine(int sock, char *buf)
{
    while (1)
    {
        std::cin.getline(buf, BUF_SIZE);
        if (strcmp(buf, "q") == 0 || strcmp(buf, "Q") == 0)
        {
            shutdown(sock, SHUT_WR);
            return;
        }
        write(sock, buf, strlen(buf));
    }
}

void error_handling(const char *message)
{
    std::cerr << message << std::endl;
    exit(1);
}

day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++ day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

 day-06 多进程服务器端 -- 进程间通信,# 网络编程,服务器,linux,c++

注意:服务器端设置了只保存10个字符串。

(三)总结

1.什么是进程间通信?分别从概念和内存的角度进行说明

概括性地说,进程间通信是指两个进程之间交换数据。但是从内存的角度看,可以理解为两个进程共有内存。因为共享的内存区域存在,可以进行数据交换

2.进程间通信需要特殊的IPC机制,这是由操作系统提供的。进程间通信时为何需要操作系统的帮助?

要想实现IPC机制,需要共享的内存,但由于两个进程之间不共享内存,因此需要操作系统的帮助,也就是说,两进程共享的内存空间必须由操作系统来提供文章来源地址https://www.toymoban.com/news/detail-693762.html

3.“管道”是典型的IPC技术。关于管道,请回答如下问题。
  • 管道是进程间交换数据的路径。如何创建该路径?由谁创建?
    • 管道是由pipe函数产生的,而实际产生管道的主体是操作系统
  • 为了完成进程间通信,2个进程需同时连接管道。那2个进程如何连接到同一管道?
    • pipe函数通过输入参数返回管道的输入输出文件描述符。这个文件描述符在fork函数中复制到了其子进程,因此,父进程和子进程可以同时访问同一管道。
  • 管道允许进行2个进程间的双向通信。双向通信中需要注意哪些内容?
    • 管道并不管理进程间的数据通信。因此,如果数据流入管道,任何进程都可以读取数据。因此,要合理安排共享空间的输入和读取。

到了这里,关于day-06 多进程服务器端 -- 进程间通信的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 多进程并发TCP服务器模型(含客户端)(网络编程 C语言实现)

    摘要 :大家都知道不同pc间的通信需要用到套接字sockte来实现,但是服务器一次只能收到一个客户端发来的消息,所以为了能让服务器可以接收多个客户端的连接与消息的传递,我们就引入了多进程并发这样一个概念。听名字就可以知道--需要用到进程,当然也有多线程并发

    2024年02月17日
    浏览(46)
  • Linux网络编程:Socket服务器和客户端实现双方通信

    目录 一,什么是网络编程 二,为什么使用端口号 三,TCP协议与UDP协议 ①TCP(传输控制协议) ②UDP(用户数据报协议,User Data Protocol) ③总结归纳 四,Socket服务器和客户端的开发流程 五,服务器和客户端相关API说明 ①socket()函数 ②bind()函数 ③listen()函数 ④accept()函数 ⑤客户端

    2024年02月11日
    浏览(53)
  • Socket网络编程(TCP/IP)实现服务器/客户端通信。

    一.前言 回顾之前进程间通信(无名管道,有名管道,消息队列,共享内存,信号,信号量),都是在同一主机由内核来完成的通信。 那不同主机间该怎么通信呢? 可以使用Socket编程来实现。 Socket编程可以通过网络来实现实现不同主机之间的通讯。 二.Socket编程的网络模型如

    2024年02月08日
    浏览(68)
  • Flask服务器与客户端基本通信【Unity网络编程(三)】

    这里先演示基于Python的客户端和服务器的登录实现。 1.Flask服务器端(GET和POST请求接收实现) 1.GET和POST区别:(简单理解) 1.GET因为是读取,就可以对GET请求的数据 做缓存 (浏览器)。不能随意多次执行。POST不能缓存(所以如果重新执行POST请求,浏览器会弹框提示你可能对丢

    2023年04月09日
    浏览(37)
  • [Linux] 网络编程 - 初见TCP套接字编程: 实现简单的单进程、多进程、多线程、线程池tcp服务器

    网络的上一篇文章, 我们介绍了网络变成的一些重要的概念, 以及 UDP套接字的编程演示. 还实现了一个简单更简陋的UDP公共聊天室. [Linux] 网络编程 - 初见UDP套接字编程: 网络编程部分相关概念、TCP、UDP协议基本特点、网络字节序、socket接口使用、简单的UDP网络及聊天室实现…

    2024年02月16日
    浏览(52)
  • 【100天精通python】Day47:python网络编程_Web开发:web服务器,前端基础以及静态服务器

    目录 1  网络编程与web编程 1.1 网络编程 1.2 web编程  1.3 前后端交互的基本原理/

    2024年02月11日
    浏览(36)
  • GEC6818网络编程——服务器端与客户端tcp的双向通信

    网络编程之实现服务器和客户端的tcp双向通信,前面是双向通信的详细流程介绍,后面附上完整的代码o(  ̄▽ ̄ )ブ 1.1 服务器端双向通信的详细流程叙述 创建TCP套接字 : 使用 socket 函数创建一个TCP套接字。这里使用了IPv4地址族 AF_INET 和流式套接字 SOCK_STREAM 。 绑定IP和端口

    2024年02月21日
    浏览(34)
  • 【Java网络编程】基于UDP-Socket 实现客户端、服务器通信

    ​ 哈喽,大家好~我是你们的老朋友: 保护小周ღ   本期为大家带来的是网络编程的 UDP Socket 套接字,基于 UDP协议的 Socket 实现客户端服务器通信 ,Socket 套接字可以理解为是,传输层给应用层提供的一组 API,如此程序,确定不来看看嘛~~ 本期收录于博主的专栏 : JavaEE_保

    2024年02月02日
    浏览(46)
  • Linux网络编程:socket、客户端服务器端使用socket通信(TCP)

    socket(套接字),用于网络中不同主机间进程的通信。 socket是一个伪文件,包含读缓冲区、写缓冲区。 socket必须成对出现。 socket可以建立主机进程间的通信,但需要协议(IPV4、IPV6等)、port端口、IP地址。          (1)创建流式socket套接字。                 a)此s

    2024年02月11日
    浏览(45)
  • 计算机网络套接字编程实验-TCP多进程并发服务器程序与单进程客户端程序(简单回声)

    1.实验系列 ·Linux NAP-Linux网络应用编程系列 2.实验目的 ·理解多进程(Multiprocess)相关基本概念,理解父子进程之间的关系与差异,熟练掌握基于fork()的多进程编程模式; ·理解僵尸进程产生原理,能基于|sigaction()或signal(),使用waitpid()规避僵尸进程产生; ·

    2024年02月12日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包