IPC进程间通信 —— 管道

一、前情回顾

        上一篇主要介绍了IPC进程间通信的信号,了解到信号主要由bash指令的 kill 来触发,c语言的信号处理函数 signal() 实际是一个 void (*sighandler_t) 类型的函数指针,而连续的同种信号发送会造成传统信号的丢失,而实时信号却不会,最后上篇还提出了一个问题:异种信号的连续发送会对传统信号和实时信号产生什么影响?

        传统信号:

        实时信号:

        经由上篇代码小改(此处不展示)便可得如上运行结果,交替运行两种信号发送程序两次,每运行一次程序发送三次该信号并传输相同数据1234,总计12次,最后将每次接收的情况进行打印。

        传统信号发送12次共收到2次,信号2和信号3各一种;实时信号发送12次共收到12次,信号40与信号41分隔打印。所以由此现象可得:

不同种传统信号每种只接收一个,其余丢弃;不同种实时信号按同种优先级排队依次处理

上篇链接如下,可相互跳转学习

IPC进程间通信 —— 信号icon-default.png?t=O83Ahttps://blog.youkuaiyun.com/qq_59030165/article/details/142651372?spm=1001.2014.3001.5502

二、管道概述

        上篇的IPC通信 信号 为单向一对多机制,模型为:

        而 管道 则为纯粹的单向一对一通信,模型为:

        管道是利用内存的一块缓冲区实现数据传输的IPC通信方式,主要分为匿名管道和命名管道,匿名管道只能用于有亲缘关系的进程之间进行通信,而命名管道则可以用于跨任意进程通信

三、匿名管道pipe

3.1 管道符 |

        匿名管道最常见的应用便是shell指令的 管道符 |,它通常与刷选搜索指令 grep 同时出现。将每个指令视为一个进程,前者指令的输入便可通过 管道符 |  输出到后者指令的输入

        指令运行 cat 指令查看打印文件内容;然后指令单独运行 grep 0 时由于指令没有输入而产生了阻断,CTRL + C 返回;指令3 利用 管道符 | 让 cat指令的输出作为 grep指令的输入刷选出指定内容,效果等同于指令4 直接给予输入文件。

        那既然有同等指令可以实现同样的效果,而且更短,那管道符 | 作用体现在哪里呢?

//tr 命令用于转换或删除文件中的字符, -s 选项为合并同类字符
//cut 命令用于剪切文件中的字节、字符和字段,-d 为自定义分隔符, -f 指定区域
who |grep /2 |tr -s ' ' |cut -d ' ' -f3,4

         当我想提取特定会话的创建时间信息时,如果一个个输入还需要加上指令输入的文件名,当指令一多起来未免过于重复和繁琐,而管道符 | 允许创建一个临时通道将所有指令的输入输出连接起来,提升效率

3.2 pipe() 函数

/*****************************************************************************
 函数名称  : 
			#include <unistd.h>
			int pipe(int fd[2]);

 功能描述  : 
			创建一个管道来进行两个亲缘进程的数据传输
 
 输入参数  : 
			fd:一个包含两个整数的数组,用于存储管道的两个端点的文件描述符
				fd[0] 用于读取数据,fd[1] 用于写入数据
            
 
 返 回 值  : 成功返回 0,失败返回 -1
*****************************************************************************/

         由于非亲缘进程拥有不同的进程ID和独立的地址空间,而且非亲缘进程间文件描述符符fd无法共享和传递,所以这里还需要另一个函数fork的协助,fork产生的子进程会继承父进程对应的文件描述符

/*****************************************************************************
 函数名称  : 
			#include <unistd.h>
			pid_t fork(void);

 功能描述  : 
			创建一个与当前进程几乎完全相同的子进程,将当前进程的内存内容完整的复制到内存的另一个区域
			(fork()之后的代码为共同代码,父子进程均会执行)
				
 输入参数  : 
			无
            
 返 回 值  : 失败返回 -1; 成功子进程返回 0,父进程返回子进程PID
*****************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>

//信息发送函数
void writefunc(int fd)
{
	char *mes = "hellow world";
	pid_t self = getpid();
	char buff[1024];
	memset(buff, 0, sizeof(buff));
	snprintf(buff, sizeof(buff), "%s, I am %d", mes, self);
	if(write(fd, buff, sizeof(buff)) < 0)
	{
		perror("write() error\n");
		exit(EXIT_FAILURE);
	}
}

//信息接受函数
void readfunc(int fd)
{
	char buff[1024];
	memset(buff, 0, sizeof(buff));
	pid_t self = getpid();
	if (read(fd, buff, sizeof(buff)) < 0) 
	{
		perror("read() error\n");
		exit(EXIT_FAILURE);
	}
	printf("PID:%d; recv[%s] \n",self, buff);
}

int main() {
	
	int pipefd[2];
	if (pipe(pipefd) == -1) 
	{
		perror("pipe() error\n");
		exit(EXIT_FAILURE);
    }
	
	printf("Before fork Process id: %d\n", getpid());
	pid_t pid = fork();
	if (pid == -1) 
	{
        perror("fork() error\n");
        exit(1);
    }
	if (pid == 0)	//子进程
	{
		printf("After fork Child Process id: %d\n", getpid());
		readfunc(pipefd[0]);
		sleep(3);
		writefunc(pipefd[1]);
		
	}
	else{	//父进程
		printf("After fork Parent Process id: %d\n", getpid());
		writefunc(pipefd[1]);
		sleep(3);
		readfunc(pipefd[0]);
		wait(NULL);  //等待子进程结束回收,防止僵尸进程产生
	}
	
    return 0;
}

        父进程先给子进程发一个消息,子进程接收到之后打印消息,之后再给父进程发消息,父进程再打印从子进程接收到的消息。程序执行效果: 

 

         其中在子进程中的sleep()函数是不必要的,因为在管道中没有数据时,read()读取操作默认是阻塞的,而父进程的sleep()是为了防止父进程运行过快(内存调度失衡)导致父进程自写自读的情况发生,而这也是管道不推荐这种半双工写法的隐患之一,这种写法逻辑图为

        通常写法为通过在相应进程内 close(fd[0])或close(fd[1]) 实现一个进程只负责写或读的单工通信

3.3 匿名管道的读写规则

3.3.1 未设置O_NONBLOCK(默认)

        a.读快,写慢——>管道为空,读端阻塞;直到管道有数据才继续读取

        代码部分在原有的基础上小改一下sleep的间隔就可以了,呈现的效果如下(此为gif):

 

        b.写快,读慢——>管道满载,写端堵塞;直到管道有空位才继续写入

        代码在write后加个printf再改一下sleep即可,效果如下:

  

        c.写端关闭,读端一直读——>读端读完管道所有数据,然后read返回0

//信息接受函数
void readfunc(int fd)
{
	char buff[1024];
	memset(buff, 0, sizeof(buff));
	pid_t self = getpid();
	int ret = read(fd, buff, sizeof(buff));
	if (ret < 0) 
	{
		perror("read() error\n");
		exit(EXIT_FAILURE);
	}
	else if(ret == 0) 
	{ 
		printf("link down, read return 0\n");
	}
	else printf("PID:%d; recv[%s] \n",self, buff);
}

/******************************************************/
	else{	//父进程
		close(pipefd[0]);
		printf("After fork Parent Process id: %d\n", getpid());
		int count = 3;
		while(count > 0){
			writefunc(pipefd[1]);
			printf("write success, count = [%d]\n", count--);
			//sleep(3);
		}
		close(pipefd[1]);
		wait(NULL);  //等待子进程结束
	}

         代码改写也就原基础上信息接收函数的read多加几个判断,父进程加个循环结束条件,效果如下:

        d. 读端关闭,写端一直写——>两端进程中断,写端进程返回信号SIGPIPE

	if (pid == 0)	//子进程
	{
		close(pipefd[0]);
		printf("After fork Parent Process id: %d\n", getpid());
		int count = 0;
		while(1){
			writefunc(pipefd[1]);
			printf("write success, count = [%d]\n", count++);
			sleep(1);
		}
	}
	else{	//父进程
		close(pipefd[1]);
		printf("After fork Child Process id: %d\n", getpid());
		int count = 3;
		while(count > 0){
			readfunc(pipefd[0]);
			count--;
			sleep(2);
		}
		close(pipefd[0]);
		int status = 0;
		pid_t ret = waitpid(pid, &status, 0);//等待子进程结束并储存信号编号
		if(ret < 0){
			perror("waitpid() error\n");
			exit(1);
		}
		//0x7f 是一个掩码,用于提取状态的低 7 位
		printf("receve signal:%d\n", status & 0x7f);  
	}

         代码需要让子进程一直写,父进程进行读取,关闭,回收,不然父进程一直写入的话无法进行子进程的状态回收;waitpid()类似于wait()的pro版本,可以储存进程的退出状态或信号编号并指定 waitpid 调用的行为选项,效果如下:

3.3.2 设置O_NONBLOCK(非阻塞) 

/*****************************************************************************
 函数名称  : 
			#include <fcntl.h>
			int fcntl(int fd, int cmd, ... /* arg */ );

 功能描述  : 
			获取或设置文件描述符的属性
 
 输入参数  : 
			fd:文件描述符,是一个非负整数,指向需要操作的文件或文件描述符。
			cmd:指令,指定要执行的操作。
			arg:一些 cmd 需要的额外参数,可以是指针或值。
				F_DUPFD:
					复制 fd 到一个新的文件描述符,并返回新文件描述符的最小值。
					参数:期望的新文件描述符的最小值
				
				F_GETFD:
					返回文件描述符的标志。
					无参数(下文无参数以不写代替)
					
				F_SETFD:
					设置文件描述符的标志。
					参数:要设置的标志值。
					
				F_GETFL:
					返回文件的状态标志,如 O_NONBLOCK(非阻塞) 或 O_APPEND(追加)。

				F_SETFL:
					设置文件的状态标志。
					参数:要设置的标志值。
					
				F_GETLK:
					返回对文件的区域加锁信息
					参数:指向 struct flock 结构的指针。
					
				F_SETLK:
					设置对文件的区域加锁,如果区域已被锁定且不兼容,则阻塞。
					参数:指向 struct flock 结构的指针。
				
				F_SETLKW:
					设置对文件的区域加锁,等待模式,如果区域已被锁定且不兼容,则等待。
					参数:指向 struct flock 结构的指针。
					
				F_GETOWN:
					获取进程或线程接收 SIGIO 和 SIGURG 信号的所有权。

				F_SETOWN:
					设置进程或线程接收 SIGIO 和 SIGURG 信号的所有权。
					参数:要设置的进程或线程 ID。
            
 返 回 值  : 成功返回非负值,失败返回 -1
*****************************************************************************/

         a.写快,读慢——>管道满载,写端write立刻返回 -1;读端不受影响

        代码的改进即在前面未设置O_NONBLOCK的基础上加上fcntl函数和判断,再将信息发送函数writefunc加上printf write的返回结果即可。

        当管道被写满后,再次调用write(),会直接返回 -1,读端不受影响,效果如下:

        b.读快,写慢——> 读端不关闭时,管道为空,读端read返回-1,写端不受影响

 

        这次我们读端设置非阻断只把时间调慢一点,不设置exit() 退出,为什么呢?大家可以猜一下ψ(`∇´)ψ。管道为空时,读端read() 返回-1 ,效果如下:

四、断言assert()

        在我上上一篇文章介绍了预定义宏,它对我们定位报错的代码文件,行数等位置十分有用S/C模型(下),利用UDP、TCP协议和多线程、多路复用实现server的局域网搜索响应与定向实时通信连接icon-default.png?t=O83Ahttps://blog.youkuaiyun.com/qq_59030165/article/details/142376828?spm=1001.2014.3001.5502这次则介绍底层仍是调用这些预定义宏但是更加方便的断言宏assert()

/*****************************************************************************
 函数名称  : 
			#include <assert.h>
			void assert(int expression);

 功能描述  : 
			检查一个表达式的值是否为真,如果为假(即为0),则终止程序的执行,并输出一条错误信息
 
 输入参数  : 
			expression:	需要检查的表达式
            
 返 回 值  : 无
*****************************************************************************/

        随便写个判断演示一下       

         这种判断同样可以用于那些write和read的文件读写,而且assert()更加方便的一点便是它自带一个全局开关宏NDEBUG,定义这个开关之后编译器就会禁用文件中所有的assert()语句,反之双斜杠注释一下即可,便于debug版和正式版的切换

五、命名管道FIFO

        命名管道与匿名管道底层逻辑是一样的,但是命名管道多了一个全局可见的管道文件,这样便允许非亲缘进程也能以文件读写的方式进行通信了。嗯❓❔❓那我直接用普通文件如 .txt 进行数据信息交换不就好了,为什么要弄命名管道呢?ψ(`∇´)ψ        这是因为命名管道多了同步机制(任意时间下文件数据的一致性)流控制(正确的时间,顺序,大小,阻塞等控制)

5.1 mkfifo与mknod命令

/// @name	mkfifo [OPTION]... NAME...
/// @brief	用于创建命名管道FIFO
/// @args	-m, –mode=MODE:设置创建的 FIFO 的权限模式,默认为 umask 值。
///			-Z, –context[=CTX]:设置上下文安全性标签。
///			--help:显示帮助信息并退出。
///			--version:显示版本信息并退出。

         演示:

        用 mkfifo 命令创建的管道文件标识为p(红框部分),初始权限umask值为644(rwx为111<二进制>==7<十进制>,表示读写执行,三个rwx分别代表用户,用户组,其他),创建的两个会话能实现通信“hello world” ,而 mknod 的 用法也与之类似

/// @name	mknod [OPTIONS] NAME TYPE [MAJOR MINOR]
/// @brief	用于创建设备文件节点
/// @args	[OPTIONS]:
///				-m:设置文件模式(权限)
///				-Z:设置安全上下文
///			TYPE:
///				b(块设备)或 c(字符设备)或 p(管道FIFO)
///			[MAJOR MINOR]:
///				主次设备号 

         此处只演示mknod的管道创建:

5.2  mkfifo() 与 mknod() 函数

/*****************************************************************************
 函数名称  : 
			#include <sys/types.h>
			#include <sys/stat.h>
			int mkfifo(const char *pathname, mode_t mode);
			
 功能描述  : 
			创建一个命名管道
 
 输入参数  : 
			pathname:	要创建的命名管道的路径名
			mode:		设置管道的权限模式
            
 
 返 回 值  : 成功返回 0,失败返回 -1
*****************************************************************************/
/*****************************************************************************
 函数名称  : 
			#include <sys/types.h>
			#include <sys/stat.h>
			int mknod(const char *pathname, mode_t mode, dev_t dev);
			
 功能描述  : 
			创建一个设备文件
 
 输入参数  : 
			pathname: 要创建的文件的路径名。
			mode: 文件的权限和类型。
                S_IFMT:文件类型掩码,用于提取文件类型。
                S_IFREG:常规文件。
                S_IFDIR:目录。
                S_IFCHR:字符设备文件。
                S_IFBLK:块设备文件。
                S_IFIFO:FIFO 或命名管道。
                S_IFLNK:符号链接。
                S_IFSOCK:套接字
			dev: 设备特定的标识符。(组合主次设备号dev_t dev = makedev(major, minor))
            
 
 返 回 值  : 成功返回 0,失败返回 -1
*****************************************************************************/
//服务端111.c ,负责管道创建,信息接受和管道清除
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>

//#define NDEBUG

//信息接受函数
void readfunc(int fd)
{
	char buff[1024];
	memset(buff, 0, sizeof(buff));
	pid_t self = getpid();
	assert(read(fd, buff, sizeof(buff)) >= 0); 
	printf("PID:%d; recv[%s] \n",self, buff);
}

int main(int argc, char *argv[])
{
	assert(argc == 2);//运行时输入要创建的管道名
	assert(mkfifo(argv[1], 0600) >= 0);//0600的第一个0代表十进制
	//assert(mknod(argv[1], 0600|S_IFIFO, 0) >= 0);//可等效替代
	
	int fifofd = open(argv[1], O_RDONLY);
	assert(fifofd >= 0);
	
	for(int i = 1; i <= 3; i++)
		readfunc(fifofd);
	
	close(fifofd);
	int m = unlink(argv[1]);	//删除命名管道
	assert(m == 0);
	return 0;
}
//客户端112.c, 负责管道信息发送
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>

//#define NDEBUG

//信息发送函数
void writefunc(int fd, char *mess)
{
	char *mes = mess;
	pid_t self = getpid();
	char buff[1024];
	memset(buff, 0, sizeof(buff));
	snprintf(buff, sizeof(buff), "%s, I am %d", mes, self);
	assert(write(fd, buff, sizeof(buff)) >= 0);
}

int main(int argc, char *argv[])
{
	assert(argc == 3);//运行时输入创建的管道名和发送的信息
	int fifofd = open(argv[1], O_WRONLY);
	assert(fifofd >= 0);
	
	for(int i = 1; i <= 3; i++)
	{
		writefunc(fifofd, argv[2]);
		sleep(5);//此时可以ps -al查看进程PID
	}
	
	close(fifofd);

	return 0;
}

        运行展示:

        这里我打开了三个会话窗口,一个运行管道服务端111.c创建管道接受信息,一个运行管道客户端112.c发送信息,一个运行ps查看两个程序的进程id;利用上上篇讲的main函数参数实现自定义管道名为123,自定义传送信息为helloworld, 同时也用刚介绍的断言assert() 对一些判断做了优化替换。

六、总结

        本篇首先对上一篇异种信号的连续发送作了补充;接着进入正题,主要对匿名管道pipe、命名管道FIFO的Linux指令、对应函数以及不同情况的读写规则进行了描述,中间还穿插了断言机制用于预定义宏的替换与debug的优化;最后回答 3.3.2 提出的问题为什么非阻断读快写慢时不设置读取错误时退出读端?因为设置exit() 后就与 未设置O_NONBLOCK的情况是一样的,读端(子)退出后,写端进程(父)返回SIGPIPE,两进程均终止,即便此时换成父读子写也是如此;怎样,与你所想一致嘛\( ̄︶ ̄*\))

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值