嵌入式Linux基础——无名管道和有名管道

管道

在这里插入图片描述
管道是最早出现的进程间通信的手段。在shell中执行命令,经常会将上一个命令的输出作为下一个命令的输入,由多个命令配合完成一件事情。而这就是通过管道来实现的。

进程who的标准输出,通过管道传递给下游的wc进程作为标准输入,从而通过相互配合完成了一件任务。
在这里插入图片描述
管道的作用是在有亲缘关系的进程之间传递消息。所谓有亲缘关系,是指有一个共同的祖先。所以管道并非只能用于父子进程之间,也可以用在兄弟进程之间,还可以用于祖孙进程之间甚至是叔侄进程之间。

总而言之,只要共同的祖先曾经调用了pipe函数,打开的管道文件就会在fork之后,被各个后代进程所共享。打开的管道文件,就像是创建了一个家族私密场所,由远祖进程来创建,家族所有成员都知晓。家族成员可以将消息存放进该私密场所,等待另外一个接头的家族成员来取走消息,阅后即焚。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
管道实质是一个字节流。前面曾提到过,管道中的内容是阅后即焚的,这个特性指的是读取管道内容是消耗型的行为,即一个进程读取了管道内的一些内容之后,这些内容就不会继续在管道之中了。一般来讲管道是单向的。一个进程负责往管道里面写内容,另外一个进程读取管道里的内容。若两个有亲缘关系的进程发扬二杆子精神,都要往管道里面写,都要往管道里面读,自然也是可以的,但是管道中的内容可能会变得混乱,从而无法完成通信的任务。如果两个进程之间想双向通信怎么办?可以建立两个管道
在这里插入图片描述
管道是一种文件,可以调用read、write和close等操作文件的接口来操作管道。另一方面管道又不是一种普通的文件,它属于一种独特的文件系统:pipefs。管道的本质是内核维护了一块缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区内存的操作。

管道读写

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

pipe调用

通过这个函数在两个程序之间传递数据不需要启动一个shell来解释请求的命令。它同时还提供了对读写数据的更多控制。
在这里插入图片描述
成功调用pipe函数之后,会返回两个打开的文件描述符,一个是管道的读取端描述符pipefd[0],另一个是管道的写入端描述符pipefd[1]。管道没有文件名与之关联,因此程序没有选择,只能通过文件描述符来访问管道,只有那些能看到这两个文件描述符的进程才能够使用管道。那么谁能看到进程打开的文件描述符呢?只有该进程及该进程的子孙进程才能看到。这就限制了管道的使用范围。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到,调用pipe函数之后,系统给进程分配了两个文件描述符,即pipe函数返回的两个描述符。该进程既可以往写入端描述符写入信息,也可以从读取端描述符读出信息。可是一个进程管道,起不到任何通信的作用。这不是通信,而是自言自语。

如果调用pipe函数的进程随后调用fork函数,创建了子进程,情况就不一样了。fork以后,子进程复制了父进程打开的文件描述符,如下图所示
在这里插入图片描述
两条通信的通道就建立起来了。此时,可以是父进程往管道里写,子进程从管道里面读;也可以是子进程往管道里写,父进程从管道里面读。这两条通路都是可选的,但是不能都选。

原因前面介绍过,管道里面是字节流,父子进程都写、都读,就会导致内容混在一起,对于读管道的一方,解析起来就比较困难。常规的使用方法是父子进程一方只能写入,另一方只能读出,管道变成一个单向的通道,以方便使用

如下图所示,父进程放弃读,子进程放弃写,变成父进程写入,子进程读出,成为一个通信的通道。
在这里插入图片描述
父进程如何放弃读,子进程又如何放弃写?其实很简单,父进程把读端口pipefd[0]这个文件描述符关闭掉,子进程把写端口pipefd[1]这个文件描述符关闭掉就可以了,示例代码如下:
在这里插入图片描述
从内核的角度看,调用pipe之后,系统给进程分配了两个文件描述符,调用fork之后,子进程也就有了与管道对应的两个文件描述符。和普通文件不同,这两个文件描述符对应的是一块内存缓冲区域
在这里插入图片描述
父进程再次创建一个子进程B,子进程B就持有管道写入端,这时候两个子进程之间就可以通过管道通信了。父进程为了不干扰两个子进程通信,很自觉地关闭了自己的写入端。从此管道成为了两个子进程之间的单向的通信通道。在shell中执行管道命令就是这种情景,只是略有特殊之处,其特殊的地方是管道描述符占用了标准输入和标准输出两个文件描述符。

任何两个有亲缘关系的进程,只要共同的祖先打开了一个管道,总能够通过关闭不相关进程的某些管道文件描述符,来建立起两者之间单向通信的管道。

关闭未使用的管道文件描述符

前面提到过,用管道通信的两个进程,各持有一个管道文件描述符,不相干的进程应自觉关闭掉这些文件描述符。这么做不仅仅是为了让数据的流向更加清晰,也不仅仅是为了节省文件描述符,更重要的原因是:关闭未使用的管道文件描述符对管道的正确使用影响重大。

管道有如下三条性质:

·只有当所有的写入端描述符都已关闭,且管道中的数据都被读出,对读取端描述符调用read函数才会返回0(即读到EOF标志)。

·如果所有读取端描述符都已关闭,此时进程再次往管道里面写入数据,写操作会失败,errno被设置为EPIPE,同时内核会向写入进程发送一个SIGPIPE的信号。

·当所有的读取端和写入端都关闭后,管道才能被销毁。

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

int main()
{
        int pipe_fd[2];
        pid_t pid;

        char r_buf[4096];
        char w_buf[4096];
        int writenum;
        int rnum;

        memset(r_buf, 0, sizeof(r_buf));
        if(pipe(pipe_fd) < 0)
        {
                printf("[PARENT] pipe create error\n");
                return -1;
        }
        //创建子进程
		if((pid = fork()) == 0)
        {
        		//关闭子进程的写入端
                close(pipe_fd[1]);
                while(1)
                {
                        rnum = read(pipe_fd[0], r_buf, 1000);
                        printf("[CHILD] readnum is %d\n", rnum);
                        if(rnum == 0)
                        {
                                printf("[CHILD] all the writer of pipe are closed. break and exit.\n");
                                break;
                        }

                }
                close(pipe_fd[0]);
                exit(0);
        }
        else if(pid > 0)
        {
        		//父进程关闭了读入端
                close(pipe_fd[0]);
				memset(w_buf, 0, sizeof(w_buf));
                if((writenum = write(pipe_fd[1], w_buf, 1024)) == -1)
                {
                        printf("[PARENT] write to pipe error\n");
                }
                else
                {
                        printf("[PARENT] the bytes write to pipe is %d\n", writenum);
                }
                sleep(15);
                printf("[PARENT] I will close the last write end of pipe.\n");
                close(pipe_fd[1]);
                sleep(2);
                return 0;
        }

在这里插入图片描述
在上面的例子中,父子进程通过管道进行通信,父进程关闭了管道的读取端,子进程关闭了管道的写入端。父进程写入了1024字节,子进程则在循环体中调用read,每次尝试读取1000字节。子进程很快就读完了父进程生产的1024字节。但是父进程并没有立刻关闭管道的写入端,而是睡眠了15秒后,才关闭管道写入端。从子进程读完父进程生产的1024字节开始,到父进程关闭管道写入端这段接近15秒的时间内,子进程实际上是阻塞在read函数上的。当父进程关闭管道写入端,子进程调用的read函数才得以返回,返回值是0。子进程看到返回值0后,意识到硕果仅存的管道写入端也不复存在了,所以它没必要再继续read了,于是子进程就跳出了循环体。

write()与read()

write()
函数定义:ssize_t write (int fd, const void * buf, size_t count);
函数说明:write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。
返回值:如果顺利write()会返回实际写入的字节数(len)。当有错误发生时则返回-1,错误代码存入errno中。

read()
函数定义:ssize_t read(int fd, void * buf, size_t count);
函数说明:read()会把参数fd所指的文件传送count 个字节到buf 指针所指的内存中。
返回值:返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据。若参数count 为0, 则read()不会有作用并返回0。
另外,以下情况返回值小于count:
读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有50个字节而请求读100个字节,则read返回50,下次read将返回0。

父子进程配合地珠联璧合,但是如果子进程忘记关闭管道的写入端,(删除上面示例代码中加粗的一行)结局就大相径庭了。纵然父进程关闭了管道的写入端,但是因为管道仍然存在一个写入端,所以子进程的read函数依旧会阻塞,无法返回。这显然不是我们期待的结果。

open()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

无名管道实验

在这里插入图片描述

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

int main()
{
        int pipe_fd[2]; //pipe_fd[0] and pipe_fd[1]
        pid_t pid;      //pid
        char buf_r[100];
        int r_num;

        memset(buf_r, 0, sizeof(buf_r));
		//pipe调用
        if(pipe(pipe_fd) < 0)
        {
                printf("pipe create error\n");
                return -1;
        }
        //创建子进程
		if((pid = fork()) == 0)
        {
                printf("\n");
                //关闭写端
                close(pipe_fd[1]);
                sleep(2);
                if((r_num = read(pipe_fd[0], buf_r, 100)) > 0)
                {
                        printf("%d numbers read from the pipe is %s\n", r_num, buf_r);
                }
                //关闭读端
                close(pipe_fd[0]);
                exit(0);
        }
        else if(pid > 0)
        {
        		//父进程关闭读端
                close(pipe_fd[0]);
                if(write(pipe_fd[1], "Hello", 5) != -1)
                {
                        printf("parent write1 Hello!\n");
                }
                if(write(pipe_fd[1], "Pipe", 5) != -1)
				{
                        printf("parent write2 Pipe!\n");
                }
                //关闭写端
                close(pipe_fd[1]);
                //等待子进程
                waitpid(pid, NULL, 0);
                exit(0);
        }
        return 0;
}
                             

在这里插入图片描述

关于write函数的说明

在这里插入图片描述

命名管道FIFO

这种管道因为没有实体文件与之关联,靠的是世代相传的文件描述符,所以只能应用在有共同祖先的各个进程之间。对于没有亲缘关系的任意两个进程之间,无名管道就爱莫能助了。

命名管道就是为了解决无名管道的这个问题而引入的。FIFO与管道类似,最大的差别就是有实体文件与之关联。由于存在实体文件,不相关的没有亲缘关系的进程也可以通过使用FIFO来实现进程之间的通信。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

mkfifo函数

在这里插入图片描述
mkfifo()会根据参数建立特殊的有名管道文件,该文件必须不存在,而参数mode为该文件的权限,mkfifo()建立的FIFO文件的其他进程都可以用读写一般文件的方式存取。当使用open()函数打开FIFO文件时,O_NONBLOCK会有影响。如果执行成功将返回0,否则返回-1,失败原因存储于errno中。

mkfifo_r.c

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

#define P_FIFO "/tmp/p_fifo"

int main(int argc, char **argv)
{
        char cache[100];
        int fd;
        memset(cache, 0, sizeof(cache));

        if(mkfifo(P_FIFO, 0777) < 0 && errno != EEXIST)
        {
                printf("create named pipe failed.\n");
        }

        fd = open(P_FIFO, O_RDWR|O_NONBLOCK);
        while(1)
        {
				memset(cache, 0, sizeof(cache));
                if((read(fd, cache, 100)) == 0)
                {
                        printf("nodata:\n");
                }
                else
                {
                        printf("getdata:%s\n", cache);
                }
                sleep(1);
        }
        close(fd);
        return 0;
}

mkfifo_w.c

#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>

#define P_FIFO "/tmp/p_fifo"
int main(int argc, char **argv)
{
        int fd;
        if(argc < 2)
        {
                printf("please input the write data.\n");
        }
        fd = open(P_FIFO, O_WRONLY|O_NONBLOCK);
        write(fd, argv[1], 100);
        close(fd);
        return 0;
}

在这里插入图片描述

常用控制字

O_RDONLY 只读打开。

O_WRONLY 只写打开。

O_RDWR 读、写打开。

O_APPEND 每次写时都加到文件的尾端。

O_CREAT 若此文件不存在则创建它。使用此选择项时,需同时说明第三个参数mode,用其说明该新文件的存取许可权位。

O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则出错。这可测试一个文件是否存在,如果不存在则创建此文件成为一个原子操作。

O_TRUNC 如果此文件存在,而且为只读或只写成功打开,则将其长度截短为0。

O_NOCTTY 如果pathname指的是终端设备,则不将此设备分配作为此进程的控制终端。

O_NONBLOCK 如果pathname指的是一个F I F O、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I / O操作设置非阻塞方式。

有名管道

fifo_write.c

#include<sys/types.h>
#include<sys/stat.h>
#include<errno.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define FIFO_SERVER "/tmp/myfifo"

int main(int argc, char **argv)
{
        int fd;
        char w_buf[100];
        int nwrite;
		//创建有名管道
        if((mkfifo(FIFO_SERVER, O_CREAT|O_EXCL|O_RDWR) < 0) && (errno != EEXIST))
        {
                printf("cannot create fifoserver\n");
        }
		//打开管道
		//可读可写打开且非阻塞方式
        fd = open(FIFO_SERVER, O_RDWR|O_NONBLOCK, 0);
        if(fd == -1)
		{
                perror("open");
                exit(1);
        }
        //入参检测
        if(argc == 1)
        {
                printf("Please send something\n");
                exit(-1);
        }
       //把传入的参数复制到w_buf数组里
        strcpy(w_buf, argv[1]);
		//向管道写入数据
        if((nwrite = write(fd, w_buf, 100)) == -1)
        {
          		printf("the FIFO write failed\n");
        }
        else
        {
                printf("write %s to the FIFO\n", w_buf);
		}
        close(fd);
        return 0;
}

fifo_read.c

#include<sys/types.h>
#include<sys/stat.h>
#include<errno.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define FIFO "/tmp/myfifo"

int main(int argc, char ** argv)
{
        char buf_r[100];
        int fd;
        int nread;

        printf("Preparing for reading bytes...\n");
        memset(buf_r, 0, sizeof(buf_r));
		//只读且非阻塞方式打开管道
        fd = open(FIFO, O_RDONLY|O_NONBLOCK,0);
        if(fd == -1)
        {
                perror("open");
                exit(1);
		 }
        while(1)
        {
                memset(buf_r, 0, sizeof(buf_r));
                if((nread = read(fd, buf_r,100)) == -1)
                {
                        if(errno == EAGAIN)
                        {
                                printf("no data yet\n");
                        }
                }
                printf("read %s from FIFO\n", buf_r);
                sleep(1);
        }
        close(fd);
        return 0;
}

在这里插入图片描述
在这里插入图片描述

如果喜欢我的文章,请记得三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持,下期更精彩!!!

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值