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()函数的返回值,可以在程序中使用条件语句或其他逻辑来区分父进程和子进程的执行路径。
  • 子进程可以通过修改自己的代码和数据,执行不同的任务。父进程和子进程之间共享打开的文件描述符和某些系统资源,但是它们有各自独立的运行环境和内存空间。
cpp 复制代码
#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;
}

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

通过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()函数会终止当前进程,并将退出状态传递给父进程。

cpp 复制代码
#include <stdlib.h>

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

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

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

cpp 复制代码
#include <stdlib.h>

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

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

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

cpp 复制代码
int main() {
    // 子进程执行任务
    // ...

    // 返回值退出
    return 0;
}

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

cpp 复制代码
#include <unistd.h>

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

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

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

2.销毁僵尸进程

(1)wait
cpp 复制代码
#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;
}

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

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

(2)waitpid

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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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
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个文件描述符都终止(销毁后),才能销毁套接字!

echo_mpclien.cpp
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
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函数创建子进程,子进程发送,父进程接收

二.进程间通信(管道)

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

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

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

cpp 复制代码
#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;
}

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

cpp 复制代码
#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;
}

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

为何不用单个管道:

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

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

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

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

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

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

(二)运用进程间通信

1.保存消息的回声服务器端
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>
#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.客户端
cpp 复制代码
#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);
}

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

(三)总结

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

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

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

要想实现IPC机制,需要共享的内存,但由于两个进程之间不共享内存,因此需要操作系统的帮助,也就是说,两进程共享的内存空间必须由操作系统来提供

3."管道"是典型的IPC技术。关于管道,请回答如下问题。
  • 管道是进程间交换数据的路径。如何创建该路径?由谁创建?
    • 管道是由pipe函数产生的,而实际产生管道的主体是操作系统
  • 为了完成进程间通信,2个进程需同时连接管道。那2个进程如何连接到同一管道?
    • pipe函数通过输入参数返回管道的输入输出文件描述符。这个文件描述符在fork函数中复制到了其子进程,因此,父进程和子进程可以同时访问同一管道。
  • 管道允许进行2个进程间的双向通信。双向通信中需要注意哪些内容?
    • 管道并不管理进程间的数据通信。因此,如果数据流入管道,任何进程都可以读取数据。因此,要合理安排共享空间的输入和读取。
相关推荐
JunLan~1 小时前
Rocky Linux 系统安装/部署 Docker
linux·docker·容器
南东山人2 小时前
一文说清:C和C++混合编程
c语言·c++
方竞2 小时前
Linux空口抓包方法
linux·空口抓包
sun0077003 小时前
ubuntu dpkg 删除安装包
运维·服务器·ubuntu
海岛日记3 小时前
centos一键卸载docker脚本
linux·docker·centos
oi774 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
AttackingLin4 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
Ysjt | 深4 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++