当前位置: 首页 > article >正文

linux系统编程(七)管道和FIFO

1、管道

使用系统调用pipe可以创建一个新管道:

#include <unistd.h>
int pipe(int filedes[2]);

成功的pipe调用会在数组filedes中返回两个打开的文件描述符,读取端为filedes[0],写入端为filedes[1]。我们可以使用read/write系统调用在管道上执行IO。管道上的read调用会读取的数据量为请求的字节数与管道中当前存在的字计数两者之间较小的那个。

在单个进程中管道的用途不多,一般来说是使用管道让两个进程进行通信。为了让两个进程通过管道进行连接,调用完pipe调用之后可以调用fork。fork之后一般会让其中一个进程立即关闭管道的写入段文件描述符,另一端关闭读取的文件描述符。

关闭pipe需要先关闭其写入端,这样读取端就可以读取到eos,这时候读取端就可以正常关闭了。

以下是一个简单示例:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>

#define MAX_BUF 1024

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

	size_t numRead = -1;
	size_t numWritten = -1;
	char buf[MAX_BUF];

	int ret = pipe(fds);
	if(ret != 0) {
		printf("create pipe fail, err:%s", strerror(errno));
		exit(1);
	}

	switch(fork()) {
		case 0:
		{
			close(fds[0]);
			while((numRead = read(STDIN_FILENO, buf, MAX_BUF-1)) > 0) {
				buf[numRead] = '\0';
				printf("child pid:%d write %s", getpid(), buf);
				write(fds[1], buf, numRead);
			}
			printf("child pid:%d close pipe of write\n", getpid());
			close(fds[1]);
			exit(0);
		}

		default:
		{
			close(fds[1]);

			while((numRead = read(fds[0], buf, MAX_BUF - 1)) > 0) {
				buf[numRead] = '\0';
				printf("parent pid:%d read %s", getpid(), buf);
			}

			printf("parent pid:%d close pipe of read\n", getpid());
            close(fds[0]);
			pid_t pid = wait(NULL);
			printf("parent recyle child:%d OK\n", pid);
			exit(0);
		}
	}

	return 0;
}

/*
hello world
child pid:5353 write hello world
parent pid:5352 read hello world
child pid:5353 close pipe of write
parent pid:5352 close pipe of read
parent recyle child:5353 OK
*/

管道还可以用来进程同步,做法是多个进程继承同一管道,其中一个进程关闭写,其他所有进程关闭读,当其他进程做完工作后关闭管道的写端口,剩余唯一一个读端口就可以关闭了,这时候就实现了进程同步。

接下来是一个使用管道连接ls和wc的示例:

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

#define MAX_BUF 1024

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

	size_t numRead = -1;
	size_t numWritten = -1;
	char buf[MAX_BUF];

	int ret = pipe(fds);
	if(ret != 0) {
		printf("create pipe fail, err:%s", strerror(errno));
		exit(1);
	}

	switch(fork()) {
		case 0:
		{
			close(fds[0]);
			dup2(fds[1], STDOUT_FILENO);	// redirect pipe write to stdout
			close(fds[1]);
			execlp("ls", "ls", (char *)NULL);       // ls会把结果输出到标准输出,实际上是输出到管道的写端了
			exit(0);
		}

		default:
			break;
	}

	switch(fork()) {
		case 0:
		{
			close(fds[1]);
			dup2(fds[0], STDIN_FILENO);
			close(fds[0]);
			execlp("wc", "wc", "-l", (char *)NULL);         // wc 是从标准输入读数据,所以实际上是从管道的读端读取的数据
			exit(0);
		}
		default:
			break;
	}

	close(fds[0]);
	close(fds[1]);

	wait(NULL);
	wait(NULL);

	return 0;
}

管道常见的用途是执行shell命令并读取输出,popen简化了这个任务:

#include <stdio.h>

FILE *popen(const char *command, const char *mode);
int pclose(FILE *stream);

对于管道的使用我们要注意,如果管道的读取端都被关闭,那么写入端写入数据的时候会发送SIGPIPE信号,默认会中止进程。如果堆SIGPIPE信号做特殊处理,那么write系统会返回-1,表示写入操作出现错误,errno会被设定为EPIPE。

2、FIFO

FIFO和管道类似,它们之间最大的区别在于FIFO在文件系统中拥有一个名称,并且打开方式和打开一个普通文件是一样的,这样就可以将FIFO用于非相关进程之间的通信。

使用mkfifo命令可以在shell中创建一个FIFO:

$ mkfifo [-m mode] pathname
// prw-r--r-- 1 may may     0 Dec 26 07:32 myfifo

mkfifo函数可以创建一个名为pathname的全新FIFO

#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

打开FIFO时应该一个进程设定O_RDONLY,另一个设置O_WRONLY。要避免设置O_RDWR,设置之后另一个写入端关闭后,读取的进程会无法看到eos。

接下来我们基于FIFO来实现一个简单的迭代式服务器。

首先有几个问题:
FIFO/PIPE中的数据是字节流,消息之间没有边界,这意味这多条消息被送到一个进程中时,发送者和接收者需要约定一种规则来分隔消息。有几种做法:

  1. 每条消息可以使用换行符之类的分隔字符结束;
  2. 每条消息包含一个固定大小的头,头中包含一个表示消息长度的字段,该字段表示消息中剩余部分的长度;
  3. 使用固定长度的消息,让服务器总是读取这个大小固定的消息。

接下来的示例将使用第三种。

什么是迭代式服务器?迭代表示服务器会在读取并处理完当前客户端之后才去处理下一个客户端。另一种设计方法是并发式服务器,使用单独的子线程/进程来处理客户端请求。

// fifo_demo.h
#ifndef FIFO_DEMO_H
#define FIFO_DEMO_H

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SERVER_FIFO "./seqnum_sv"
#define CLIENT_FIFO_TEMPLATE "./seqnum_cl.%d"
#define CLIENT_FIFO_NAME_LEN (sizeof(CLIENT_FIFO_TEMPLATE) + 20)

struct request {
	pid_t pid;
	int seqLen;
};

struct response {
	int seqNum;
};

#endif

// server.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include "03_fifo_demo.h"
#include <signal.h>

int main(int argc, char **argv) {
	int ret = mkfifo(SERVER_FIFO, S_IRUSR | S_IWUSR | S_IWGRP);
	if(ret != 0 && errno != EEXIST) {
		printf("server create fifo fail, err:%s\n", strerror(errno));
		exit(1);
	}

	int serverFd = open(SERVER_FIFO, O_RDONLY);
	int seqNum = 0;
	if(serverFd == -1) {
		printf("server open fifo fail, err:%s\n", strerror(errno));
		exit(1);
	} else {
		printf("server open server fifo %s success\n", SERVER_FIFO);
	}


	int dummyFd = open(SERVER_FIFO, O_WRONLY);

	signal(SIGPIPE, SIG_IGN);

	struct request req;
	struct response resp;
	char clientFIFO[CLIENT_FIFO_NAME_LEN];
	int clientFd;
	while(1) {
		if(read(serverFd, &req, sizeof(struct request)) != sizeof(struct request)) {
			continue;
		}

		snprintf(clientFIFO, CLIENT_FIFO_NAME_LEN, CLIENT_FIFO_TEMPLATE, req.pid);
		printf("server get request from client:%s, seqLen:%d\n", clientFIFO, req. seqLen);

		clientFd = open(clientFIFO, O_WRONLY);
		printf("server open client fifo %s success\n", clientFIFO);
		
		resp.seqNum = seqNum;

		if(write(clientFd, &resp, sizeof(struct response)) != sizeof(struct response)) {
			printf("error write to client fifo: %s\n", clientFIFO);
		}

		close(clientFd);

		seqNum++;
	}

	return 0;
}


// client.c

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include "03_fifo_demo.h"

int main(int argc, char **argv) {
	int serverFd, clientFd;
	struct request req;
	struct response resp;

	char clientFIFO[CLIENT_FIFO_NAME_LEN];
	snprintf(clientFIFO, CLIENT_FIFO_NAME_LEN, CLIENT_FIFO_TEMPLATE, getpid());

	mkfifo(clientFIFO, S_IRUSR | S_IWUSR | S_IWGRP);

	req.pid = getpid();
	req.seqLen = atoi(argv[1]);

	serverFd = open(SERVER_FIFO, O_WRONLY);
	printf("client:%d open server OK!\n", getpid());
	write(serverFd, &req, sizeof(struct request));

	clientFd = open(clientFIFO, O_RDONLY);
	printf("client:%d open client fifo:%s OK!\n", getpid(), clientFIFO);
	
	read(clientFd, &resp, sizeof(struct response));

	printf("get response from server, seqNum:%d\n", resp.seqNum);

	close(clientFd);
	unlink(clientFIFO);
	return 0;
}

关于FIFO还有一些注意事项,当进程打开FIFO的一端时,如果FIFO的另一端还没有被打开,那么该进程会被阻塞。

以上面的代码为例,服务打开之后会有阻塞的情况出现,等到第一个client进行连接之后open才能结束阻塞。这不是我们预期的行为,我们可以在open时添加O_NONBLOCK来标记非阻塞打开。

添加O_NONBLOCK之后,会有以下情况:

  • 如果FIFO是为了读取,当前FIFO的写入端已经被打开,那么open会立即成功。
  • 如果FIFO是为了写入,FIFO的读取端还没有打开,那么open会调用失败,errno被设置为ENXIO

O_NONBLOCK还会影响到read和write的操作:当管道中没有数据时,read将会返回EAGAIN。当写入失败时,write会返回EAGAIN。


http://www.kler.cn/a/463762.html

相关文章:

  • ubuntu24.04使用open-vm-tools无法在主机和虚拟机之间拖拽文件夹
  • 【74HC192减法24/20/72进制】2022-5-17
  • “善弈者”也需妙手,Oclean欧可林:差异化不是说说而已
  • 弧形导轨如何避免生锈?
  • AWS K8s 部署架构
  • 《解密奖励函数:引导智能体走向最优策略》
  • 【MySQL】之Centos7.6 Yum方式安装部署安装Mysql 5.7版本
  • IPsec协议,网络安全的秘密
  • CentOS Stream 9 搭建单机版Clickhouse
  • Spring Boot 日志系统选择、日志系统选择、日志系统选择技术选型
  • 【GeekBand】C++设计模式笔记20_Composite_组合模式
  • 将广播发送和接收端实现一遍,完成一个发送端发送信息,对应多个接收端接收信息实验。
  • 怎么在 Linux 下运行 smart_rtmpd
  • 多模态论文笔记——CogVLM和CogVLM2(副)
  • [Linux]操作系统概念
  • Postman[4] 环境设置
  • ThinkPHP 8高效构建Web应用-第一个简单的MVC应用示例
  • Java Map 集合详解:基础用法、常见实现类与高频面试题解析
  • opencv图像直方图
  • 运动健康小程序SpringBoot+论文源码调试讲解
  • RabbitMQ-TTL机制
  • 缓存菜品的业务代码
  • 政策助力数字金融,CES Asia 2025展望科技新未来
  • AI 自动化编程对编程教育的影响
  • 每天五分钟机器学习:凸集
  • uni-app tab 双击事件监听