Part 1 start network programming:chapter eleven:进程间通信

第11章:进程间通信

上一章我们尝试了创建进程,本章讨论两个进程之间的交换数据的方法。

11.1 进程间通信的基本概念

进程间通信(Inter Process Communication)意味着两个不同进程之间可以交换数据,因此,需要操作系统中提供两个进程可以同时访问的内存空间。

11.1.1 进程间通信的理解

上一章中谈到,进程具有完全独立的内存结构,就连通过fork创建的子进程也不会与父进程共享内存空间,因此进程间的通信需要通过其他特殊方法完成,这就是—管道

11.1.2 通过管道实现进程间通信

下图是基于管道(PIPE)的进程间通信结构模型。
在这里插入图片描述
为了完成进程间的通信,需要创建管道,管道并非是进程的资源,而是和套接字一样,是操作系统的(意味着不能被fork函数复制)。所以两个进程通过操作系统提供的内存空间进行通信,下面介绍创建管道的函数。

#include <unistd.h>

int pipe(int filedes[2]);
-> 成功时返回0, 失败时返回-1

filedes[0]:	通过管道接收数据时使用的   文件描述符   ,即管道出口
filedes[1]:	通过管道传输数据时使用的   文件描述符   ,即管道入口

长度为2 的 int 型数组中,保存了两个文件描述符,他们将被用作管道的出口和入口。

什么是管道呢? 就是一个入口和一个出口的管子,管子里面装的啥?装的是数据呀~

当父进程调用函数创建管道时,同时获得对应于出入口的文件描述符,此时父进程可以读写同一管道。(当然可以,入口和出口我父进程都要了!我都自己用,有啥不行的)

但父进程的目的是与子进程进行数据交换,因此需要将入口或者出口中的一个文件描述符传给子进程
这里有一个问题:管道是单向的,只有一个入口和一个出口,可不可以用俩管道呢,这样就能实现父子进程之间的互相传输了~

下面我先来1个管道试一下~
pipe1.c

#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 30

int main(int argc, char* argv[])
{
	/* code */
	int fds[2];
	char str[] = "您好,有人在吗?";
	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);
		puts(buf);
	}
	return 0;
}

在这里插入图片描述

上面用的是单向的一个管道,但是父子进程明明都有管道两边的文件描述符,能不能一个管道,双向通信呢?

当然可以,看下面

11.1.3 通过管道进行进程间双向通信

直接看图!
在这里插入图片描述

是不是很简单,但是这里有一个小问题需要注意,看下面的代码

#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 30

int main(int argc, char* argv[])
{
	/* code */
	int fds[2];
	char str1[] = "您好,有人在吗?";
	char str2[] = "有啊,你谁啊?";
	char buf[BUF_SIZE];
	pid_t pid;

	pipe(fds);
	pid = fork();
	if(pid == 0){
		write(fds[1], str1, sizeof(str1));
		sleep(2);

		read(fds[0], buf, BUF_SIZE);
		printf("接收父回复:%s\n", buf);
	}
	else{
		read(fds[0], buf, BUF_SIZE);
		printf("接收到子进程消息:%s\n", buf);

		write(fds[1], str2, sizeof(str2));
		sleep(3);
	}
	return 0;
}
下方图1.

在这里插入图片描述
从上面的程序可以看到,
子进程先:传输数据 -》 sleep(2)-》读取数据
父进程: 读取数据-》 写入数据 -》 sleep(3)

问题:子进程和父进程中的两个sleep都是干嘛用的? 如果删了会怎样?

回答:删除子进程中的sleep(2)之后,子进程在执行完write函数之后,直接执行read,请问是由父进程read到我们的数据还是由子进程呢? 我们测试一下

下方图2.

在这里插入图片描述
上面是注释了子进程的sleep看起来。。好像没啥变化啊。。
下面是注释了父进程的sleep

下方图3.

在这里插入图片描述
子进程的注释好像并没有带来任何变化
但是如果我们在注释子进程sleep的同时,在主进程的read之前增加一个sleep(1) 会发生什么?看下图结果

下方图4、

在这里插入图片描述
程序一直没有结束。问题出在了哪里呢??

我们先说主进程的那个sleep其实有没有并无影响,最多是如上面图三那样,父进程先结束了,子进程仍在工作,但不影响啥啊,只是先弹出了命令提示符而已。

接着我们说子进程的sleep的重要性!!!我们只删除这个sleep好像没什么影响,但是在不同的运行环境下,并不是都有这样的好运气,比如我们的图4,只要在主进程中稍稍慢一些运行read函数,子进程中write的东西就被子进程自己取走了!!竟然打印出了 str1 的内容。这就是问题所在!所以这个sleep的作用就是让子进程等等再read,别弄错了!

上面的示例我们可以看到,一个管道完成双向通信是有很大风险的,因为你无法预测并控制每种系统中不同的流程。

因此下面我们尝试用两个管道避免程序流程的预测或控制。

pipe3.c

#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 30

int main(int argc, char* argv[])
{
	/* code */
	int fds1[2], fds2[2];
	char str1[] = "您好,有人在吗?";
	char str2[] = "有啊,你谁啊?";
	char buf[BUF_SIZE];
	pid_t pid;

	pipe(fds1), pipe(fds2);
	pid = fork();
	if(pid == 0){			// 子进程区域

		write(fds1[1], str1, sizeof(str1));
		read(fds2[0], buf, BUF_SIZE);
		printf("子进程接收到父回复:%s\n", buf);

	}
	else{
		read(fds1[0], buf, BUF_SIZE);
		printf("父进程接收到子进程消息:%s\n", buf);

		write(fds2[1], str2, sizeof(str2));
		sleep(3);
	}
	return 0;
}

在这里插入图片描述

11.2 运用进程间通信

下面我们用上面的创建管道的方法+回声服务器端,实现一下新的功能。

11.2.1 保存消息的回声服务器端

在上一章的echo_mpserv.c程序中 添加如下功能
将回声客户端传输的字符串按序保存到文件中,将该任务委托给另外的进程

首先我们先复习一下之前的回声服务器进程

// echo_mpserv.c

#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);
void read_childproc(int sig);

int main(int argc, char *argv[])
{
	if(argc != 2){
		printf("usage: %s <port>\n", argv[0]);
		exit(1);
	}

	// 声明套接字相关变量
	int serv_sock, client_sock;
	struct sockaddr_in serv_addr, client_addr;
	socklen_t client_addr_sz;
	char buf[BUF_SIZE];
	int str_len;

	// 声明信号相关变量
	struct sigaction act; 				// 注册信号函数 sigaction的输入参数
	int sigaction_state;				// 接收注册信号函数 sigaction的 返回值(0 成功/ -1失败)

	// 初始化信号变量
	act.sa_handler = read_childproc;	// 设置一下信号处理函数
	act.sa_flags = 0;					// 设置其他两个暂时用不上的为0
	sigemptyset(&act.sa_mask);

	// 注册信号(配合处理 僵尸进程的 信号处理函数)
	sigaction_state = sigaction(SIGCHLD, &act, 0);

	// 初始化套接字、服务器地址啥的
	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if(serv_sock == -1){
		error_handling("socket() error");
	}
	memset(&serv_addr,0,sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port  = htons(atoi(argv[1]));

	// 服务器套接字一条龙
	if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
		error_handling("bind() error");
	}
	if(listen(serv_sock, 5) == -1){
		error_handling("listen error");
	}

	// 接收到服务器端信息后,开启子进程,在循环中不停的接收子进程的请求
	while(1)
	{

		client_addr_sz = sizeof(client_addr);
		client_sock = accept(serv_sock, (struct sockaddr*)&client_addr, &client_addr_sz);
		if(client_sock == -1){
			continue;			// 接收请求不能停
		}
		puts("new client connected......");

		//下面开启子进程,给这个请求干活~
		pid_t pid = fork();
		if(pid == -1){			// fork失败返回 -1
			puts("fork 失败..正在断开客户端连接...\n");
			close(client_sock);
		}

		else if(pid == 0){		// 子进程 区域
			// 子进程为客户端提供服务,因此在子进程中多余的服务器端套接字需要断开
			close(serv_sock);
			// 子进程中提供 回声 服务。
			while((str_len = read(client_sock, buf, BUF_SIZE))!= 0){
				write(client_sock, buf, str_len);
			}

			close(client_sock);
			puts("client端断开连接\n");

			return 0;
		}
		else{					// 主进程 区域
			// 由于主进程要接收其他客户端的服务需求,因此对于主进程来说,客户端的文件描述符是多余的,这里断开主进程中与客户机的连接
			close(client_sock);
		}
	}

	// while外,结束提供服务器的服务
	close(serv_sock);
	return 0;
}

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

void read_childproc(int sig)
{
	int status;
	puts("进入信号处理器函数.......\n");
	if(sig == SIGCHLD){
		pid_t pid = waitpid(-1, &status, WNOHANG);
		if(WIFEXITED(status)){		// 子进程正常终止
			printf("终止pid:%d\t进程号:%d 的子进程", pid,WEXITSTATUS(status));
		}
	}
}

我们修改上面的程序,实现要求的功能
修改的部分已经用 // ************************************************************************
标出

// echo_mpserv.c

#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);
void read_childproc(int sig);

int main(int argc, char *argv[])
{
	if(argc != 2){
		printf("usage: %s <port>\n", argv[0]);
		exit(1);
	}

	int fds[2];

	// 声明套接字相关变量
	int serv_sock, client_sock;
	struct sockaddr_in serv_addr, client_addr;
	socklen_t client_addr_sz;
	char buf[BUF_SIZE];
	int str_len;

	// 声明信号相关变量
	struct sigaction act; 				// 注册信号函数 sigaction的输入参数
	int sigaction_state;				// 接收注册信号函数 sigaction的 返回值(0 成功/ -1失败)

	// 初始化信号变量
	act.sa_handler = read_childproc;	// 设置一下信号处理函数
	act.sa_flags = 0;					// 设置其他两个暂时用不上的为0
	sigemptyset(&act.sa_mask);

	// 注册信号(配合处理 僵尸进程的 信号处理函数)
	sigaction_state = sigaction(SIGCHLD, &act, 0);

	// 初始化套接字、服务器地址啥的
	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if(serv_sock == -1){
		error_handling("socket() error");
	}
	memset(&serv_addr,0,sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port  = htons(atoi(argv[1]));

	// 服务器套接字一条龙
	if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
		error_handling("bind() error");
	}
	if(listen(serv_sock, 5) == -1){
		error_handling("listen error");
	}


	// ************************************************************************************************************
	// 先初始化一下 
	pipe(fds);
	// 接着就可以先开一个子进程专门用来向文件里传输信息了, 信息的来源嘛·~ 当然是下面wihile中接受客户端信息的子进程中了~
	pid_t pid = fork();
	if(pid == 0)
	{
		FILE* fp = fopen("echomsg.txt","wt");
		char msgbuf[BUF_SIZE];
		int i , len;

		for(i = 0; i < 10; i++){
			len = read(fds[1], msgbuf, BUF_SIZE);
			fwrite((void*)msgbuf, 1, len, fp);
		}
		fclose(fp);
		return 0;
	}
	// ************************************************************************************************************




	// 接收到服务器端信息后,开启子进程,在循环中不停的接收子进程的请求
	while(1)
	{

		client_addr_sz = sizeof(client_addr);
		client_sock = accept(serv_sock, (struct sockaddr*)&client_addr, &client_addr_sz);
		if(client_sock == -1){
			continue;			// 接收请求不能停
		}
		puts("new client connected......");


		//下面开启子进程,给这个请求干活~
		pid = fork();
		if(pid == -1){			// fork失败返回 -1
			puts("fork 失败..正在断开客户端连接...\n");
			close(client_sock);
		}

		else if(pid == 0){		// 子进程 区域
			// 子进程为客户端提供服务,因此在子进程中多余的服务器端套接字需要断开
			close(serv_sock);
			// 子进程中提供 回声 服务。
			while((str_len = read(client_sock, buf, BUF_SIZE))!= 0){
				write(client_sock, buf, str_len);
	// ************************************************************************************************************
				write(fds[0], buf, str_len);
	// ************************************************************************************************************			
			}

			close(client_sock);
			puts("client端断开连接\n");

			return 0;
		}
		else{					// 主进程 区域
			// 由于主进程要接收其他客户端的服务需求,因此对于主进程来说,客户端的文件描述符是多余的,这里断开主进程中与客户机的连接
			close(client_sock);
		}
	}

	// while外,结束提供服务器的服务
	close(serv_sock);
	return 0;
}

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

void read_childproc(int sig)
{
	int status;
	puts("进入信号处理器函数.......\n");
	if(sig == SIGCHLD){
		pid_t pid = waitpid(-1, &status, WNOHANG);
		if(WIFEXITED(status)){		// 子进程正常终止
			printf("终止pid:%d\t进程号:%d 的子进程", pid,WEXITSTATUS(status));
		}
	}
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值