【IPC-UNIX网络编程】第4章管道和FIFO

本文详细介绍了进程间通信(IPC)的几种方式,包括简单的客户-服务器模型、管道(半双工和全双工)、popen和pclose函数、FIFO(有名管道)以及它们在实际场景中的应用。通过示例代码展示了如何创建和使用这些通信机制,如文件的读写、错误处理和多进程协作。此外,还提到了如何处理字节流和信息边界,以及如何实现实现单个服务器处理多个客户的需求。
摘要由CSDN通过智能技术生成

1. 一个简单的客户—服务器例子

  • Client从标准输入(stdin)读进一个路径名,并把它写入IPC通道。Server从该IPC通道读出这个路径名,并尝试打开其文件。若server能打开该文件,它能读出其中的内容,并写入(不一定同一个)IPC通道,以作为对客户的响应;否则响应一个错误信息。

在这里插入图片描述

2. 管道

管道由pipe函数创建,提供一个单向数据流。

  • 该函数返回2个文件描述符:fd[0]和fd[1]。前者读,后者写。【半双工】
  • 也可创建【全双工】IPC管道。(全双工:管道两端既可读又可写)
  • 【常见用途:shell】
#include <unistd.h>
int pipe(int fd[2]);  // 成功则返回0,否则返回-1
单进程中的管道与单进程内的管道
  • 单进程中的管道
    在这里插入图片描述
  • 单进程内的管道
    管道的典型用途是为两个不同进程(父进程、子进程)提供进程间的通信手段。
    1)一个进程(父进程)创建一个管道后调用fork派生一个子进程,接着父进程关闭这个管道的读出端,子进程关闭统一管道的写入端。

3 例子

  • main函数创建两个管道并用fork生成一个子进程。客户作为父进程运行,服务器作为子进程运行。第一个管道用于从客户向服务器发送路径名;第二个管道用于从服务器向客户发送该文件的内容。【每个文件描述符只负责读或写】
    在这里插入图片描述
#include "unpipc.h"
void client(int, int), server(int, int);

void client (int readfd, int writefd) {
	size_t len;
	ssize_t n;
	char buff[MAXLINE];
	Fgets(buff, MAXLINE, stdin);  // read pathname
	len = strlen(buff);  // fgets()以空字节作为其结尾
	if (buff[len-1] == '\n')
		len--;     // delete newline from fgets()
	Write(writefd, buff, len);  // write pathname to IPC channel
	while ( (n = Read(readfd, buff, MAXLINE)) > 0)
		Write(STDOUT_FILENO, buff, n);
}

void server (int readfd, int writefd) {
	int fd;
	ssize_t n;
	char buff[MAXLINE+1];
	// read pathname from IPC channel
	if ( (n = Read(readfd, buff, channel)) == 0)
		err_quit("end-of-file while reading pathname");
	buff[n] = '\0';

	// 打开所请求的文件读
	// 若出错则返回出错字符串
	if ( (fd = open(buff, O_RDONLY)) < 0) {
		snprintf(buff + n, sizeof(buff) - n, ": can't open, %s\n", strerror(errno));
		n = strlen(buff);
		Write(writefd, buff, n);
	} else {  // 若成功,则将文件内容复制到管道中
		// open succeeded: copy file to IPC channel
		while ( (n = Read(fd, buff, MAXLINE)) > 0)
			Write(writefd, buff, n);
		Close(fd);
	}
}

int main(int argc, char const *argv[])
{
	int pipe1[2], pipe2[2];
	pid_t childpid;
	Pipe(pipe1);  // create 2 pipes
	Pipe(pipe2);  // create 2 pipes

	if ( (childpid = Fork()) == 0) {  // child
		Close(pipe1[1]); // 子进程关闭pipe1的写端
		Close(pipe2[0]); // 子进程关闭pipe2的读端

	}
	// parent
	Close(pipe1[0]); // 父进程关闭pipe1的读端
	Close(pipe2[1]); // 父进程关闭pipe2的写端
	// 将pipe2[0]写进client,pipe1[0]读client
	client(pipe2[0], pipe1[1]);

	// 取得已终止子进程的终止状态
	Waitpid(childpid, NULL, 0);  // wait for child to terminate
	exit(0);

	return 0;
}

4 全双工管道的真正实现

  • 写入fd[1]的数据只能从fd[0]读出,写入fd[0]的数据只能从fd[1]读出。
    在这里插入图片描述

  • pipe(fd[2]),设置了client / server写入fd[1] / fd[0] 只能从 fd[0] / fd[1]读出。

    #include "unpipc.h"
    int main(int argc, char const *argv[])
    {
    	int fd[2], n;
    	char c;
    	pid_t childpid;
    	Pipe(fd);
    	if ( (childpid = Fork()) == 0 )	{
    		sleep(3);
    		if ( (n = Read(fd[0], &c, 1)) != 1) 
    			err_quit("child: read returned %d", n);
    		printf("child read %c\n", c);
    		Write(fd[0], "c", 1);
    		exit(0);			
    	}
    	Write(fd[1], "p", 1);
    	// 当server 并未往fd[0]写入数据时,此处发生阻塞
    	if ( (n = Read(fd[1], &c, 1)) != 1)
    		err_quit("child: read returned %d", n);
    	printf("parent read %c\n", c);
    	exit(0);
    	return 0;
    }
    

5 popen和pclose函数

popen函数创建一个管道并启动另外一个进程,该进程要么从该管道读出标准输入,要么往该管道写入标准输出

#include <stdio.h>
FILE *popen(const char *command, const char *type);  // 成功返回文件指针,否则返回NULL
int pclose(FILE *stream);  // 成功返回shell的终止状态,否则返回-1

command是shell命令行,PATH环境变量可用于定位command。popen在调用进程和所指定的命令之间创建一个管道。

  • 若type为r,则调用进程读进command的stdout
  • 若type为w,则调用进程写进command的stdin
例子:将客户-服务器使用popen()函数实现
#include "unpipc.h"
int main(int argc, char const *argv[])
{
	size_t n;
	char buff[MAXLINE], command[MAXLINE];
	FILE *fp;
	Fgets(buff, MAXLINE, stdin);
	n = strlen(buff);
	if (buff[n-1] == '\n')
		n--;
	snprintf(command, sizeof(command), "cat %s", buff);
	// command内容是路径名
	fp = Popen(command,"r");
	while (Fgets(buff, MAXLINE, fp) != NULL)
		Fputs(buff, stdout);
	Pclose(fp);
	exit(0);
}`在这里插入代码片`

6 FIFO

管道无名字,只能用于有一个共同祖先进程的各个进程之间,而无法在无亲缘关系的两个进程间创建一个管道并用作IPC通道。
FIFO称为有名管道,由mkfifo函数创建。

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);  // 成功则返回0,否则返回-1

mode参数指定文件权限位,类似于open的第二个参数

例子:使用两个FIFO代替两个管道

这个FIFO例子与之前的管道相比:

  • 创建并打开一个管道只需调用pipe。创建并打开一个FIFO则需在调用mkfifo后再调用open;
  • 管道在所有进程中最终都关闭它之后自动消失。FIFO的名字则只有通过调用unlink才从文件系统中删除。好处:FIFO在文件系统中有一个名字,该名字允许某个进程创建一个FIFO,与它无亲缘关系的另一个进程打开这个FIFO。
#include "unpipc.h"
// 在/tmp文件系统中创建2个FIFO。这2个FIFO事先存在与否无关紧要
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"

void client(int, int), server(int, int);
int main(int argc, char const *argv[])
{
	int readfd, writefd;
	pid_t childpid;

	// create 2 FIFOs; OK if they already exist
	if ( (mkfifo(FIFO1, FILE_MODE) < 0) && (errno != EEXIST)) 
		err_sys("can't create %s", FIFO1);
	if ( (mkfifo(FIFO2, FILE_MODE) < 0) && (errno != EEXIST)) {
		unlink(FIFO1);
		err_sys("can't create %s", FIFO2)
	}
	if ( (childpid = Fork()) == 0) {
		readfd = Open(FIFO1, O_RDONLY, 0);
		writefd = Open(FIFO2, O_WRONLY, 0);		
		server(readfd, writefd);
		exit(0);
	}

	writefd = Open(FIFO1, O_WRONLY, 0);
	readfd = Open(FIFO2, O_RDONLY, 0);
	client(readfd, writefd);
	Waitpid(childpid, NULL, 0);  // wait for child to terminate
	Close(readfd);
	Close(writefd);

	Unlink(FIFO1);
	Unlink(FIFO2);
	exit(0);
}

如果对换父进程中两个open调用的顺序,该程序不工作。其原因在于,若当前尚无任何进程打开某个FIFO写,则打开该FIFO读的进程将阻塞。此时,父子进程都读,二者均阻塞,发生死锁现象。

Note:

  • 最后删除所用的FIFO的是client而不是server,因为对这些FIFO执行最终操作的是客户。
  • 内核为管道和FIFO维护一个访问计数器,其值是同一个管道或FIFO的打开着的描述符的个数。有了访问计数器后,client或server就能成功地调用unlink。尽管该函数从文件系统中删除了所指定的路径名,先前已经打开该路径名、目前仍打开着的描述符不受影响

7 管道和FIFO的额外属性

下面是关于管道或FIFO的读出与写入的若干额外规则:

  • 若请求写入的数据的字节数小于或等于PIPE_BUF,则write操作保证是原子的。即,若有两个进程差不多同时往管道或FIFO写,则或者先写入来自第一个进程的所有数据,再写入来自第二个进程的所有数据,或者颠倒。 系统不会相互混杂来自这两个进程的数据。然而,若请求写入的数据的字节数大于PIPE_BUF,则write操作不能保证是原子的。

8 单个服务器,多个客户

在这里插入图片描述

/tmp/fifo.serv为众所周知的路径名,服务器以此创建一个FIFO,它将从这个FIFO读入客户的请求。每个客户在启动时创建创建自己的FIFO,所用的路径名含有自己的进程ID。每个客户把自己的请求写入服务器的众所周知FIFO中,该请求含有客户的进程ID,以及一个路径名,具有该路径名的文件就是客户希望服务器打开并发回的文件。

8.1 服务器程序
#include "fifo.h"
void server(int, int);
int main(int argc, char const *argv[])
{
	int readfifo, writefifo, dummyfd, fd;
	char *ptr, buff[MAXLINE+1], fifoname[MAXLINE];
	pid_t pid;
	ssize_t n;
	if ( (mkfifo(SERV_FIFO, FILE_MODE) < 0) && (errno != EEXIST))
		err_sys("can't create %s", SERV_FIFO);
	readfifo = Open(SERV_FIFO, O_RDONLY, 0);  
	dummyfd = Open(SERV_FIFO, O_WRONLY, 0);  //never used

	while ( (n = Readline(readfifo, buff, MAXLINE)) > 0) {
		if (buff[n-1] == '\n')
			n--;
		buff[n] = '\0';
		if ( (ptr = strchr(buff, ' ')) == NULL) {
			err_sys("bogus request: %s", buff);
			continue;
		}
		// ptr增1后即指向后跟的路径名的首字符
		*ptr++ = 0;
		pid = atol(buff);
		snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%ld", (long)pid);
		// 尝试打开客户请求的文件
		if ( (writefifo = open(fifoname, O_WRONLY, 0)) < 0) {
			err_msg("cannot open: %s", fifoname);
			continue;
		}
		if ( (fd = open(ptr, O_RDONLY)) < 0) {
			snprintf(buff + n, sizeof(buff)-n, ":can't open, %s\n", 
				    strerror(errno));
			n = strlen(ptr);
			Write(writefifo, ptr, n);
			Close(writefifo);
		} else {
			while ( (n = Read(fd, buff, MAXLINE)) > 0)
				Write(writefifo, buff, n);
			Close(fd);
			Close(writefifo);
		}
	}
	exit(0);
}
8.2 客户端程序
#include "fifo.h"
int main(int argc, char const *argv[])
{
	int readfifo, writefifo;
	ssize_t n;
	size_t len;
	char *ptr, buff[MAXLINE], fifoname[MAXLINE];
	pid_t pid;
	pid = getpid();
	snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%ld", (long)pid);
	if ( (mkfifo(fifoname, FILE_MODE) < 0) && (errno != EEXIST))
		err_sys("can't create %s", fifoname);
	// start buffer with pid and a blank
	snprintf(buff, sizeof(buff), "%ld", (long)pid);
	len = strlen(buff);
	ptr = buff + len;
	// read pathname
	Fgets(ptr, MAXLINE-len, stdin);
	len = strlen(buff);
	// open FIFO to server and write PID and pathname to FIFO 
	writefifo = Open(SERV_FIFO, O_WRONLY, 0);  
	Write(writefifo, buff, len);
	// now open our FIFO; blocks until server opens for writing
	readfifo = Open(fifoname, O_RDONLY, 0);
	// read from IPC, write to standard output
	while ( (n = Read(readfifo, buff, MAXLINE)) > 0)
		Write(STDOUT_FILENO, buff, n);
	Close(readfifo);
	Unlink(fifoname);
	exit(0);
}

9 字节流与信息

  • 字节流I/O模型对读写操作不进行数据检查,例如,从某个FIFO中读出100字节的进程无法判断次100字节是执行了单个100字节的写操作,还是5个20字节的写操作,或者其他。
  • 有时候应用希望对所传送的数据加上某种结构,以知道获得信息的边界。下述3种技巧可用于这种目的:
    • 内特殊终止序列 :写进程给每个信息添加一个换行符,读进程则每次读出一行。
    • 显式长度;
  • mymesg结构
#include "unpipc.h"
// want sizeof(struct mymesg) <= PIPE_BUF
#define MAXMESGDATA (PIPE_BUF - 2*sizeof(long))
#define MESGHDRSIZE (sizeof(struct mymesg) - MAXMESGDATA)
struct mymesg {
	long mesg_len;   // can be 0
	long mesg_type;  // must be > 0
	char mesg_data[MAXMESGDATA];
};

ssize_t mesg_send(int, struct mymesg *);
void Mesg_send(int, struct mymesg *);
ssize_t mesg_recv(int, struct mymesg *);
ssize_t Mesg_recv(int, struct mymesg *);
  • mesg_send函数
ssize_t mesg_send(int fd, struct mymesg *mptr)
{
	return (write(fd, mptr, MESGHDRSIZE + mptr -> mesg_len));
}
  • mesg_recv函数
ssize_t mesg_recv(int fd, struct mymesg *mptr)
{
	size_t len;
	ssize_t n;

	// read message header first, to get len of data that follows
	if ( (n = Read(fd, mptr, MESGHDRSIZE)) == 0)
	// Note:return(0)是返回值为0, return 0 是终止程序
		return(0);
	else if (n != MESGHDRSIZE)
		err_quit("message header: expected %d, got%d", MESGHDRSIZE, n);

	if ( (len = mptr->mesg_len) > 0)
		if ( (n = Read(fd, mptr->mesg_data, len)) != len)
			err_quit("message data: expected %d, got %d", len, n);
	return(len);
}

10 习题

  • 4.2 我们一般是先调用mkfifo,检查是否返回EEXIST错误,若是则调用open。若先调用open,再调用mkfifo,情况又如何?

    如果调用关系反转了,另外某个进程就有可能在本进程的open和mkfifo两个调用之间创建本进程想要创建的FIFO,结果导致本进程的mkfifo调用失败。

  • 4.5 当服务器启动后,它阻塞在自己的第一个open调用中,直到客户的第一个open打开同一个FIFO用于写为止。我们怎样才能绕过这样的阻塞,使得两个open都立刻返回,转而阻塞正在首次调用readline上?

    把第一个open调用改为指定非阻塞标志 :
    readfifo = Open(SERV_FIFO, O_RDONLY | O_NONBLOCK, 0); 该调用将立即返回,接下去的open调用(用于只写)也立即返回,因为它要打开的FIFO已经由第一个open调用打开用于读。但是为了避免从readline返回错误,描述符readfifo的O_NONBLOCK标志必须在调用readline之前关掉。

  • 4.7 为什么在读进程关闭管道或FIFO之后给写进程产生一个信号,而不会在写进程关闭管道或FIFO之后给读进程产生一个信号?

    写进程关闭管道或FIFO的信息通过文件结束符传递给读进程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值