进程间通信之有名管道

自信、冷静、专注。 —— TM熊的自我勉励

冲呀


上一章已经介绍过无名管道了,那你对管道多少都有些了解(上一章看完没印象的建议自闭)。

废话不多说,直接进入正题。

概述

有名管道又可以称为命名管道,意思是可以看见名字的管道,名字是啥呢?众所周知,Linux下一切皆文件,有名管道则是能够直观看见名字的管道文件(管道类型文件),名字则是文件名。
如果对于linux下的文件类型有哪些不清楚的同学,建议去了解下,这将对你学习linux编程有非常大的帮助。

特性:

  • 同样是管道,那么数据是单向的,半双工;
  • 有名字,那么可用于任意两个进程间通信,因为进程能通过管道文件名字对同一管道进行读写。

管道文件的创建

管道文件的文件标记为p,也就是pipe的简写,是不是很好记。

创建管道文件有两种方式,mkfifo命令和函数接口。

  1. 直接在命令行使用mkfifo命令:

    $ mkfifo haha-pipe # 这个文件名则是有名管道的名字
    $ ls -l haha-pipe
    
    prw-r--r-- 1 wotsen wotsen 0 Oct 26 22:55 haha-pipe # 前边的p就表示它是一个管道文件
    
  2. 使用mkfifo接口函数:

    #include <sys/types.h>
    #include <sys/stat.h>
    
    int mkfifo(const char *pathname, mode_t mode);
    
    • pathname为管道文件名称,符合linux路径规则即可;
    • mode为文件读写权限;
    • 成功返回0,失败返回-1。

有名管道数据读写

有名管道的编程操作接口及流程和普通文件是一致的:

在这里插入图片描述

  1. open打开文件;
  2. write/read读写文件;
  3. close关闭文件。

使用shell命令读写

也就是直接在命令行创建管道文件进行读写,来个直观的演示:

在这里插入图片描述

打开管道文件

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

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

参数说明:

  • pathname和打开普通文件是有区别的,必须是使用mkfifo创建的管道文件;
  • flags则是文件打开标记;
  • mode则是文件权限。

特别说明:

flags含义影响
O_CREAT创建不要使用,使用了也没啥用啊,如果你忘了创建管道文件时,不使用该标记还能返回错误,查起来更方便
O_RDWR读写前面说过管道是单向的,使用该标记的话会造成未定义行为,进程会读回自己写的输出
O_RDONLY只读open调用将被阻塞,直到有一个进程以只写方式打开同一个FIFO,否则它不会返回
O_RDONLY|O_NONBLOCK只读非阻塞则即使没有其他进程以写方式打开同一个FIFO文件,open调用将立即返回
O_WRONLY只写open调用将被阻塞,直到有一个进程以只读方式打开同一个FIFO文件为止
O_WRONLY|O_NONBLOCK只写非阻塞则即使没有其他进程以读方式打开同一个FIFO文件,open调用将立即返回

读写、阻塞等API

这个可以查看上一章无名管道原子性的说明,不再赘述。

通信模型,可用于多进程间:

在这里插入图片描述

加餐(不感兴趣无视即可)

linux是如何做到不同文件能够使用相同的函数接口进行操作,这里简单提一下:

  • 首先来点linux常识,用户层级对于文件的一般操作都是在VFS(虚拟文件系统)基础上的,屏蔽了底层实现;

  • 每次打开文件时都会在系统当中对应一个文件对象:

    // include/linux/fstable.h
    
    struct files_struct {
       // ...
       struct file __rcu * fd_array[NR_OPEN_DEFAULT];
    };
    
    // include/linux/fs.h
    
    struct file {
       // ...
       const struct file_operations	*f_op;
    };
    
    struct file_operations {
       struct module *owner;
       loff_t (*llseek) (struct file *, loff_t, int);
       ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
       ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
       // ...
       int (*open) (struct inode *, struct file *);
       int (*flush) (struct file *, fl_owner_t id);
       int (*fsync) (struct file *, loff_t, loff_t, int datasync);
       int (*fasync) (int, struct file *, int);
       // ...
    };
    

    在这里插入图片描述

  • 后续对文件的操作会进入系统调用,系统调用则是依据文件对象进行文件操作的,你发现struct file_operations结构体中的这一组回调接口名称非常眼熟,没错,最终的操作都是调用这些接口的,我们在应用中的接口调用都和它们一一对应;

  • 这些接口都是在哪实现的呢?实际上这些接口的实现都是由驱动程序定义的。所有的设备被映射成linux中的文件,那么操作这些文件也就是操作对应的设备;设备也都需要对应的驱动程序来定义这些设备该如何被操作,所以我们对文件的操作也就变相地执行了驱动程序接口,进而操作实际设备。实际上在编写驱动程序时的重点也就是实现struct file_operations结构体中的这组回调接口;

  • 那我们打开文件创建系统文件对象时,文件对象就初始化了对应的回调接口,而在应用层调用open/write/read时就不用区分底层是如何实现了;

  • 这样的方式是由linux内核的设计思想所决定,类似于面向对象中的抽象、多态,只不过C语言中使用函数指针、回调函数来实现;

编程示例

name_pipe.cpp

#include <time.h>
#include <stddef.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdexcept>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

#define INVALID_FD -1
typedef int fifo_t;

/**
 * @brief 双向管道通信,可用于无亲缘关系的线程
 * 
 */
class Fifo {
public:
	Fifo(const char* path, const int mode);
	~Fifo();

	// 打开管道文件
	// mode:
	//   'r': read         open if exists
	//   'a': append       created if not exists
	//   'w': write        created if not exists, truncated if exists
	//   'm': modify       like 'w', but not truncated if exists
	//   'd': default      可读可写
	bool open(const char flags);
	// 设置阻塞,非阻塞
	bool set_block(const bool en);

	// 读取
	int read(void* data, const size_t len, const time_t wait=0);
	// 写入
	int write(const void* data, const size_t len, const time_t wait=0);
	// 关闭全部
	void close(void);

private:
	std::string path_;
	fifo_t fd_;
};

int trans_flags(const char flags) {
    if (flags == 'r') {
        return O_RDONLY;
    } else if (flags == 'a') {
        return O_WRONLY | O_CREAT | O_APPEND;
    } else if (flags == 'w') {
        return O_WRONLY | O_CREAT | O_TRUNC;
    } else if (flags == 'm') {
        return O_WRONLY | O_CREAT;
    } else if (flags == 'd') {
        return O_RDWR | O_CREAT | O_APPEND;
    } else {
        return -1;
    }
}

bool set_fd_block(const int fd, const bool en) {
	int flag = fcntl(fd, F_GETFL, 0);
	if (flag < 0) { return false; }

	if (en) {
		flag &= ~O_NONBLOCK;
	} else {
		flag |= O_NONBLOCK;
	}

	return fcntl(fd, F_SETFL, flag) == 0;
}

Fifo::Fifo(const char* path, const int mode) : path_(path), fd_(INVALID_FD) {
	if (!path < 0) {
		throw std::invalid_argument("path or mode error");
	}

	if (mkfifo(path, mode) < 0) {
		throw std::runtime_error("mkfifo error");
	}
}

Fifo::~Fifo() {
	close();
}

bool Fifo::open(const char flags) {
	// 当前描述符已打开
	if (fd_ >= 0) { return false; }

	if (trans_flags(flags) < 0) {
		return false;
	}

	fd_ = ::open(path_.c_str(), trans_flags(flags));
	if (fd_ < 0) {
		fd_ = INVALID_FD;
		return false;
	}

	return true;
}

void Fifo::close(void) {
	if (fd_ >= 0) {
		::close(fd_);
		fd_ = INVALID_FD;
	}
}

bool Fifo::set_block(const bool en) {
	if (set_fd_block(fd_, en)) {
		return true;
	}

	return false;
}

int Fifo::write(const void* data, const size_t len, const time_t wait) {
	if (fd_ < 0) { return -1; }

	return ::write(fd_, data, len);
}

int Fifo::read(void* data, const size_t len, const time_t wait) {
	if (fd_ < 0) { return -1; }

	return ::read(fd_, data, len);
}

int main(void) {
    Fifo p("/tmp/test_pipe", 0666);

	char buf[512] = "hello world";

    if (fork() > 0) {
        p.open('w');
	    printf("write len %d\n", p.write(buf, strlen(buf)));
    } else {
        p.open('r');
	    memset(buf, 0, sizeof(buf));
	    p.read(buf, 512);
	    printf("read : %s\n", buf);
    }

    printf("proccess exit.\n");

    return 0;
}

output:

write len 11
proccess exit.
read : hello world
proccess exit.

结束语

盛年不重来,一日难再晨。及时宜自勉,岁月不待人。—— 陶渊明

参考:

  • 《UNIX网络编程 卷二》

关注微信公众号:南极熊club

关注

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值