管道的概念
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。
有如下特质:
1. 其本质是一个伪文件(实为内核缓冲区)
2. 由两个文件描述符引用,一个表示读端,一个表示写端。
3. 规定数据从管道的写端流入管道,从读端流出。
管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性:
① 数据自己读不能自己写。
② 数据一旦被读走,便不在管道中存在,不可反复读取。
③ 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
④ 只能在有公共祖先的进程间使用管道。
常见的通信方式有,单工通信、半双工通信、全双工通信。
管道示图
PIPE(无名管道)
/* 参数: pipefd[0]:为读而打开 pipefd[1]:为写而打开 pipefd[1]的输出是pipefd[0]的输入. 返回值: 若成功,返回0 若出错,返回-1.并设置errno。 */ #include <unistd.h> int pipe(int pipefd[2]);//进程要关闭读或写端取决于数据的流向。
① 读管道:
1. 管道中有数据,read返回实际读到的字节数。
2. 管道中无数据:
(1) 管道写端被全部关闭,read返回0 (读到文件结尾)
(2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
② 写管道:
1. 管道读端全部被关闭, 进程异常终止(收到信号SIGPIPE)(也可使用捕捉SIGPIPE信号,使进程不终止)
2. 管道读端没有全部关闭:
(1) 管道已满,write阻塞。
(2) 管道未满,write将数据写入,并返回实际写入的字节数。
FIFO(有名管道)
/* FIFO特殊文件类似于管道,除了创建它以不同的方式。 而不是匿名通信通道,FIFO特殊文件被输入到文件系统中调用mkfifo()。 一旦你以这种方式创建一个FIFO专用文件,任何进程可以打开它进行阅读或写作,就像普通人一样文件。 但是,它必须在两端同时打开您可以继续执行任何输入或输出操作。 开盘用于读取正常块的FIFO,直到某个其他进程打开相同的FIFO用于写入,反之亦然。 */ #include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *filename, mode_t mode);//创建一个名为pathname的FIFO特殊文件。 /* 函数的运行方式与mkfifo()完全相同,除了这里描述的差异。 如果路径名中给出的路径名是相对的,则会被解释相对于文件描述符dirfd引用的目录 (而不是相对于当前工作目录的调用进程,如同相对路径名的mkfifo()所做的那样)。 如果pathname是relative,dirfd是特殊值AT_FDCWD,那么路径名相对于当前工作目录进行解释调用进程(如mkfifo())。 如果pathname是绝对的,那么dirfd将被忽略。 */ int mkfifoat(int dirfd, const char *pathname, mode_t mode);
FIFO参数说明
filname:文件名
mode:指定FIFO的权限。 它被进程修改 mask以通常的方式:创建的文件的权限是(模式&〜umask)。
注:
1、就是程序不能以O_RDWR(读写)模式打开FIFO文件进行读写操作,而其行为也未明确定义,因为如一个管道以读/写方式打开,进程就会读回自己的输出,同时我们通常使用FIFO只是为了单向的数据传递。
2、就是传递给open调用的是FIFO的路径名,而不是正常的文件。(如:const char *fifo_name = "/tmp/my_fifo"; )
3、第二个参数中的选项O_NONBLOCK,选项O_NONBLOCK表示非阻塞,加上这个选项后,表示open调用是非阻塞的,如果没有这个选项,则表示open调用是阻塞的。
FIFO的打开规则
- 如果以只读方式(O_RDONLY)打开的FIFO文件时
若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);
如果是非阻塞的,则即使没有其他进程以写方式打开同一个FIFO文件,都将立即成功返回(当前打开操作没有设置阻塞标志)。
- 如果以只写方式(O_WRONLY)打开的FIFO文件时
如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);
如果是非阻塞的,立即返回ENXIO错误(当前打开操作没有设置阻塞标志) 但如果没有其他进程以只读方式打开同一个FIFO文件,open调用将返回-1,并且FIFO也不会被打开。
总结:一旦设置了阻塞标志,调用mkfifo建立好之后,那么管道的两端读写必须分别打开,有任何一方未打开,则在调用open的时候就阻塞。
FIFO读写规则
- 当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
- 当管道满时
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
注:
- 如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
O_NONBLOCK对管道的影响图示
常见的阻塞问题
- 打开方式:写进程阻塞,读进程阻塞。
先运行写进程(被阻塞),再运行读进程,一切正常。
先运行读进程(被阻塞),再运行写进程,一切正常。
- 打开方式:写进程阻塞,读进程非阻塞。
就改一句代码 fd=open(FIFO_NAME,O_RDONLY | O_NONBLOCK),下面类似。
先运行写进程(被阻塞),再运行读进程,一切正常。
先运行读进程,程序直接崩掉(Segmentation fault (core dumped)),想想也挺自然的,没东西你还要读,而且不愿等。。。
- 打开方式:写进程非阻塞,读进程阻塞。
先运行写进程,open调用将返回-1,打开失败。
先运行读进程(被阻塞),再运行写进程,一切正常。
- 写进程非阻塞,读进程非阻塞。
其实就是上面2,3类各取一半不正常的情况。。
管道和FIFO的限制
- OPEN_MAX
一个进程在任意时刻打开的最大描述符数.
可通过sysconf函数获取.
- PIPE_BUF
可原子地写往一个管道或FIFO的最大数据量.
定义在<limlits.h>中,可在运行的时候调用pathconf或fpathconf取得.
fork,exec和exit对管道的影响
fork:子进程取得父进程的所有打开着的描述符副本
exec:所有打开着的描述符继续打开着,除非已设置描述符 FD_CLOEXEC 位
_exit:关闭所有打开着的描述符,最后一个关闭时删除管道或FIFO中残留的所有数据.
管道的其他应用方式
创建一个链接到另一个进程的管道,常用于获取shell命令的结果
popen函数原型
FILE *popen(const char *command, const char *type); //本身会调用fork()产生子进程,然后从子进程中调用 /bin/sh -c 来执行参数command的指令。 //根据参数type的值,popen函数会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。 //之后此进程可以通过此文件指针来读取子进程的输出或写入子进程的标准输入设备中。 int pclose(FILE *stream); //关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。
功能
创建一个管道,调用fork()产生一个子进程,接着关闭管道的不使用端,子进程执行cmd指向的应用程序或者命令。
执行完该函数后父进程和子进程之间生成一条管道,函数返回值为FILE结构指针,该指针作为管道的一端,为父进程所拥有。
子进程则拥有管道的另一端,该端口为子进程的stdin或者stdout。
这个流是单向的(只能用于读或写)。
向这个流写内容相当于写入该命令的标准输入,命令的标准输出和调用popen()的进程相同;
与之相反的,从流中读数据相当于读取命令的标准输出,命令的标准输入和调用popen()的进程相同。
参数
type参数:
只能是读或者写中的一种,得到的返回值(标准I/O流)也具有和type相应的只读或只写类型。
如果type=r,那么该管道的方向为:子进程的stdout到父进程的FILE指针,即连接到cmd的标准输出;
如果type=w,那么管道的方向为:父进程的FILE指针到子进程的stdin,即连接到cmd的标准输入。
cmd参数:
是一个指向以NULL结束的shell命令字符串的指针。这行命令将被传到bin/sh并使用-c标志,shell将执行这个命令。
可以通过这个管道执行标准输入输出操作。这个管道必须由pclose()函数关闭,
而不是fclose()函数(若使用fclose则会产生僵尸进程)。
pclose()函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。
如果shell不能被执行,则pclose()返回的终止状态与shell已执行exit一样。
popen示例
#include<stdio.h> int main(void) { FILE * fp; char buffer[80]; fp=popen(“cat /etc/passwd”,”r”); fgets(buffer,sizeof(buffer),fp); printf(“%s”,buffer); pclose(fp); return 0; }