什么是通信?
为什么要通信?
如何做到通信?
管道是什么?
管道用来干什么?
管道如何实现通信?
匿名管道是什么?如何实现?
命名管道是什么?如何实现?
什么是文件引用计数?
本文,将会回答上述问题,并且带你书写相关代码
目录
一、通信
1、进程之间为什么通信?
2、进程如何通信?
3、什么是通信标准?
(1)通信标准
(2)常见通信手段
二、管道
1、管道理解和特征
(1)管道的四种情况
(2)管道5个特征
2、匿名管道
(1)进程以读、写同时打开一个文件
(2)子进程和父进程同时打开一个文件
(3)实现匿名管道通信
a、管道函数说明
b、创建管道示例
3、命名管道
(1)、命名管道理解
(2)实现命名管道
(1)相关函数
1. mkfifo
2. open
3. read
4. write
5. unlink
(2)代码实现命名管道通信
一、通信
1、进程之间为什么通信?
进程需要协同
2、进程如何通信?
a、进程间通信,成本高,因为进程独立
一直可以通信和通信一次是不一样的
b、进程件通信的前提:让不同进程见到同一份(操作系统)资源(内存)
(1)首先是某一个进程需要通信,让OS创建一个共享资源(OS是操作系统的意思)
(2)OS必须提供很多系统调用
因此,OS创建的共享资源的不同,系统调用接口的不同,就说明进程间通信会有不同的种类
3、什么是通信标准?
(1)通信标准
什么是标准?通俗理解,就是一件事情,要怎么做。例如说,一个硬件,要怎么做,如功率多大,尺寸多大、IO接口怎么设计等。那么这个标准,谁规定的?这个行业最叼的公司制定的;凭什么你规定?因为人家叼;你怎么证明你叼?我的发明专利最多,质量最好;我又为什么要听你的?因为你不按照我的做,你卖不出去。例如说,现在有一家手机生产商,手机的充电口是正方形的,你买吗?你不会买。而专利,是需要收钱的;你掌握了标准,你就掌握了市场控制权。所以,这也就是为什么老美跟华为死过不去的原因。一流的企业卖标准,二流的企业卖服务,三流的企业卖产品。
同时,小国家的专利是没有意义的;一个专利的推行,背后必定有一个强大的国家在支撑。有兴趣的同学可以去了解一下日本的半导体产业。再比如华为,如果华为在一个很弱的国家,基本就遭殃了。
两个标准:system、posix。System V 和 POSIX 是两个重要的操作系统标准,它们定义了系统接口和行为,以确保软件的可移植性和一致性。了解即可
(2)常见通信手段
a、信息队列
b、共享内存
c、信息量
二、管道
上述是一个进程打开一个文件的过程。当多个进程同时对一个文件操作时,并不会重新在内存重新加载一份文件,而是两个同时使用一个文件内存,因为数据、属性都是一样的,没有必要再搞一份一模一样的。而为了让两个进程能够通信交流,只能开辟两个进程都能同时访问的空间。而我们发现,当打开同一个文件时,这个文件就能被两个进程同时访问看见。所以,为了直接复用操作系统的特性(降低成本),而不是从头开始开发一套新的通信模式,所以就让两个进程通过打开同一份文件的方式进行通信。所以,这个提供通信的通道也叫做管道文件。
1、管道理解和特征
(1)管道的四种情况
1、管道是空,且write fd没有关闭,则无法读文件,因为没有数据,没有数据我读什么?所以读程序就会被阻塞,此时需要等待读取条件准备,即有数据写入管道,让管道里有数据,即具备了读的条件,此时读取程序开始读取
管道大小是固定的,ubantu大小是64kB
2、管道被写满,且read fd关闭,则无法写数据,因为管道被写满了,再写数据就会被所覆盖,所以写程序就会被阻塞,此时需要等待读条件准备,才能继续写数据,即管道数据被读取
3、管道一直在被读取,但是写入端被关闭,则读端返回值就会读到0,返回值为0意味着没有更多的数据可以读取。程序通常会检查这个返回值,以决定在何时终止读取操作。
4、rfd读端直接关闭,写端wfd一直在写入
这种管道叫做坏的管道,此时写端进程会被操作系统直接杀掉,发送SIGPIPE 13个信号
(2)管道5个特征
1、匿名管道:只能进行具有血缘关系进程之间通信,例如父子、爷孙,这种通信的本质是子进程继承父进程数据(内核级缓存区)
2、管道的同步机制:多执行流执行代码时,具有明显的顺序
例如,子进程向缓冲区写数据,父进程才读数据;子进程没有写数据到缓冲区,父进程要等待子进程写入
3、文件的生命周期事跟随进程的,而管道也是文件,所以生命周期也是跟随进程
4、管道文件在通信的时候,是面向文件字节流的:写入次数和读出的次数不是一致的,例如:写五次,读一次
管道的一端是写入,另一端是读取。管道就像是自来水厂的蓄水池,水厂负责写入;你只管用
当你使用水的时候,你只关心你怎么用水,并不关心水厂那边是怎么往蓄水池里注水和注多少
所以,数据就像是蓄水池里的水一样,从一端流向另一端,因此形象的称为字节流
5、管道的通信模式,是一种特殊的半双工模式(我跟你说话,你得听我说完,你说我听,我说你听;单向)
还有另一种叫全双工(类似于吵架,你问候我,我也问候你,同时进行)
2、匿名管道
(1)进程以读、写同时打开一个文件
当进程同时以读和写两种方式打开文件时:
先用读方式打开:
a、操作系统创建文件属性结构体,从磁盘中加载文件属性
b、添加fd到文件描述符表,建立对应的fd和文件对象映射
c、申请一块文件缓冲区,将磁盘中的文件数据加载进来
再用写方式打开:
并不会重复上述a、b、c步骤
而是仅仅建立对应的文件对象,即一个新的fd对象
在文件描述符中添加新的fd,
但是指向和读文件同一份文件资源
这就避免了重复资源浪费
(2)子进程和父进程同时打开一个文件
当创建子进程时,如果是同时打开一份文件
子进程同样不会重新进行上述a、b、c步骤
而仅仅继承父进程的文件属性结构体
其中继承的文件描述符表指向同一个文件对象
我们说过进程要独立,但是没说过文件也要独立
因此,多个进程就可以管理同一个文件,同时,多个进程都能指向操作系统提供的同一个内存资源
例如文件的缓冲区
这个文件的缓冲区,就叫做管道文件,是父子进程共享的
于是,两个进程就通过共同的缓冲区,可以进行通信
但是,管道只允许单向通信:即子进程->父进程 / 父进程->子进程
所以,父进程读,那么子进程就只能写,因为单向
于是,父进程就要释放写文件对象的fd
子进程就要释放读文件对象的fd
因为是单向,就像是管道一样,只能从一头到另一头
所以叫做管道通信
而在文件对象结构体内部,会有一个类似于引用计数的变量
有多少个进程指向文件,文件引用计数就为几
当引用计数为0时,意味着没有进程使用文件,于是文件资源被释放
(3)实现匿名管道通信
a、管道函数说明
pipe 函数是一个用于在进程间创建管道的系统调用函数。pipe 函数通过创建一个管道来实现这种通信,其中管道有两个端点:一个用于写入数据(写端),另一个用于读取数据(读端)。
int pipe(int pipefd[2]);
参数
pipefd:这是一个整型数组,必须至少包含两个元素。pipefd[0] 是读端文件描述符,pipefd[1] 是写端文件描述符。返回值
成功时,pipe 返回0。
失败时,返回-1,并且设置 errno 以指示错误原因。使用方法
创建管道:
调用 pipe(pipefd) 创建一个管道,并将读端和写端的文件描述符存储在 pipefd 数组中。读写数据:
在写端(pipefd[1])写入数据时,数据会流入管道中。
在读端(pipefd[0])读取数据时,可以从管道中取出之前写入的数据。关闭文件描述符:
使用完毕后,应关闭文件描述符。通常使用 close(pipefd[0]) 关闭读端,close(pipefd[1]) 关闭写端。
close
函数用于关闭打开的文件描述符,释放与之相关的资源。
int close(int fd);
参数
fd:需要关闭的文件描述符。返回值
成功时,返回0。
失败时,返回-1,并设置 errno 以指示错误原因。
exit
函数用于终止当前进程,并返回一个退出状态码给操作系统。头文件<stdlib.h>
void exit(int status);
参数
status:一个整数值,表示进程的退出状态。通常用来指示进程是正常退出还是出现了错误。常见的约定是:
0 表示正常退出。
非 0 的值表示异常退出,具体值可以用于表示不同的错误类型(这取决于程序的约定)。
waitpid 函数用于在父进程中等待特定的子进程结束,并获取其退出状态。它允许父进程在不阻塞的情况下等待某个特定的子进程或任何子进程的状态变化。
pid_t waitpid(pid_t pid, int *status, int options);
参数
pid:指定要等待的子进程的进程 ID。可以是以下值:
正整数:等待指定的子进程。
-1:等待任何子进程。
0:等待与调用进程具有相同进程组 ID 的任何子进程。
小于 -1:等待具有指定进程组 ID 的任何子进程。status:一个指向整数的指针,用于存储子进程的退出状态。如果为 NULL,则不保存状态。
options:指定等待行为的选项。常用的选项有:
0:默认行为,阻塞,直到子进程结束。
WNOHANG:非阻塞模式,如果没有子进程退出,则立即返回。
WUNTRACED:即使子进程被停止(例如由于信号),也返回其状态。返回值
成功:返回结束的子进程的进程 ID。
失败:返回 -1,并设置 errno 来指示错误原因。
进程退出状态:
位数 | 描述 | 说明 |
---|---|---|
31 | 终止信号标志 | 1 位,用于指示进程是否因信号终止。 |
30 | 保留位 | 1 位,通常为保留位。 |
29-23 | 停止信号编号 | 7 位,表示导致进程停止的信号编号(如 |
22-16 | 保留位 | 7 位,通常为保留位。 |
15-8 | 正常退出状态码 | 8 位,表示进程正常退出时的状态码。 |
7-0 | 信号编号 | 8 位,表示进程因信号终止时的信号编号。 |
read 函数是 Unix 和类 Unix 操作系统中用于从文件描述符中读取数据的系统调用。它的功能是从指定的文件描述符中读取数据到缓冲区中。
ssize_t read(int fd, void *buf, size_t count);
参数
fd
: 文件描述符。这个文件描述符通常是通过open
、socket
或pipe
函数获得的,指定了要读取的文件或设备。
buf
: 指向一个缓冲区的指针。read
函数会将读取到的数据存储在这个缓冲区中。
count
: 要读取的字节数。read
函数会尽可能读取count
字节的数据到缓冲区buf
中。返回值
成功: 返回实际读取的字节数。如果返回值小于
count
,可能是因为到达了文件末尾或文件描述符的缓冲区数据不足。失败: 返回 -1,并设置
errno
以指示错误原因。常见的错误包括:
EAGAIN
或EWOULDBLOCK
: 文件描述符设置为非阻塞模式,并且在该操作无法立即完成。
EBADF
: 文件描述符无效。
EFAULT
:buf
指针无效(指向不可访问的内存区域)。
EINTR
: 操作被信号中断。
b、创建管道示例
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <string.h>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
void SubProcessWrite(int wfd)
{
char ch = 'A';
int pipesize = 1;
while (1)
{
write(wfd, &ch, 1); // 往管道里写内容
ch++;
if(ch == 'G')
break;
}
std::cout << "child quit ..." << std::endl;
}
void FatherProcessRead(int rfd)
{
char inbuffer[1024];
while (1)
{
std::cout << "------------------------" << std::endl;
ssize_t n = read(rfd, inbuffer, sizeof inbuffer - 1);
if (n > 0) // 读取成功
{
inbuffer[n] = 0;
std::cout << "father get info is : " << inbuffer << std::endl;
}
else if (n == 0)
{
// 返回值为0,表示写端口关闭,读到文件末尾
std::cout << "client quit, father get return val : " << n << std::endl;
break;
}
else if (n < 0) // 读取错误
{
std::cout << "read error " << std::endl;
break;
}
}
}
int main()
{
// 创建管道
int pipefd[2];
int n = pipe(pipefd); // 创建成功,返回0;失败返回-1,并设置出错码
if (n != 0)
{
std::cout << "error: " << errno << ":"
<< "erroring : " << strerror(errno) << std::endl; // 用strerror函数获取错误码信息
return 1;
}
std::cout << "pipe[0]: " << pipefd[0] << " pipe[1]: " << pipefd[1] << std::endl;
std::cout << "pipe create sucessed!" << std::endl;
// 2、创建子进程
pid_t id = fork();
if (id == 0) // 返回0创建成功
{
// 关闭不需要的端口,加入让子进程写,读端口就要关闭
close(pipefd[0]);
std::cout << "child close read done, Get ready to write!" << std::endl;
// 子进程写数据
SubProcessWrite(pipefd[1]);
close(pipefd[1]);
std::cout << "child close wtite done!" << std::endl;
exit(0); // 子进程正常退出
}
// 3、父子进程读取,并关闭不需要的管道
sleep(2);
close(pipefd[1]);
std::cout << "Father close write fd done, Receiving information: " << std::endl;
FatherProcessRead(pipefd[0]);
close(pipefd[0]);
std::cout << "Father close done" << std::endl;
// 父进程等待子进程退出并回收
sleep(1);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) // 如果返回值大于0,返回成功(status退出状态是以32位图标识)
{
std::cout << "wait child process done, exit sigstop: " << (status & 0x7f) << std::endl;
std::cout << "wait child process done, exit cod(ign) :" << ((status >> 8) & 0xff) << std::endl;
}
return 0;
}
3、命名管道
(1)、命名管道理解
解决的是两个毫无相关进程的通信问题
当一个进程打开一个文件时,首先把文件加入到进程结构体中的文件描述符表(数组),下标就是文件fd,即inode编号
这个inode编号指向文件的结构体,这个结构体有文件的属性信息、操作集、文件内核缓冲区等
此时如果另一个进程也同时打开这个文件,也要创建文件结构体,但是文件结构体没有必要再创建
而是指向同一个文件结构体,此时,两个进程就有了共同的资源
此时一个进程读,一个进程写,就实现了通信
如何确保两个进程打开同一个文件?
有父子关系的进程,通过继承打开同一个文件
每一个文件的路径具有唯一性,所以通过路径找到同一个文件
这个文件数据没有必要同步到磁盘
因为只需要两个进程间数据交流即可,其他部分没必要拿到数据
所以,这个文件是一个特殊的文件,叫做命名管道
以p开头的文件是管道文件
读端关闭,写端会直接关闭,因为人读取,写了也没有用。
(2)实现命名管道
(1)相关函数
命名管道(Named Pipes),也称为 FIFOs(First In, First Out),是 Unix 和类 Unix 操作系统中用于进程间通信(IPC)的机制。它允许不同进程之间通过文件系统中的特殊文件进行数据交换。以下是与命名管道相关的主要函数及其用法:
1. mkfifo
函数原型:
int mkfifo(const char *pathname, mode_t mode);
参数:
pathname: 命名管道的路径。
mode: 管道的权限位(通常与 chmod 中使用的权限位类似)。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno。说明:
mkfifo 用于创建一个命名管道。如果管道已经存在,mkfifo 将失败。创建的管道在文件系统中具有指定的 pathname,可以用于不同进程之间的通信。
2. open
函数原型:
int open(const char *pathname, int flags, ...);
参数:
pathname: 命名管道的路径。
flags: 文件访问模式(例如 O_RDONLY、O_WRONLY、O_RDWR)。
...: 可选的模式位(仅在创建新文件时使用,如 O_CREAT)。返回值:
成功:返回文件描述符。
失败:返回 -1,并设置 errno。说明:
使用 open 函数打开命名管道时,可以指定不同的访问模式。如果以 O_WRONLY 打开管道,表示写入;如果以 O_RDONLY 打开管道,表示读取。
3. read
函数原型:
ssize_t read(int fd, void *buf, size_t count);
参数:fd: 命名管道的文件描述符。
buf: 指向缓冲区的指针,用于存放读取的数据。
count: 读取的字节数。返回值:
成功:返回实际读取的字节数。
失败:返回 -1,并设置 errno。说明:
从命名管道中读取数据时,read 函数会阻塞直到有数据可读或管道被关闭。
4. write
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd: 命名管道的文件描述符。
buf: 指向包含要写入数据的缓冲区的指针。
count: 写入的字节数。返回值:
成功:返回实际写入的字节数。
失败:返回 -1,并设置 errno。说明:
向命名管道中写入数据时,write 函数会阻塞直到有进程读取这些数据,或者管道被关闭。
5. unlink
函数原型:
int unlink(const char *pathname);
参数:
pathname: 要删除的命名管道的路径。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno。说明:
unlink 用于删除命名管道。即使管道文件被删除,只要仍有进程打开这个管道,管道仍然存在,直到所有进程关闭它为止。