实际上,咱们在linux中常常会用到带"管道"的命令,如:linux
那管道的本质是什么呢?既然它是一个数据流,那就必定得要有一个缓冲区来保存数据, 因此说,咱们能够把管道当作是具备固定大小的一个内核缓冲区。shell
关于上面提到的第二点,为啥只能用于具备共同祖先的进程呢?须要先理解下面的函数才能明白,因此先学习下面的用法,回过头来再来理解这句话。vim
回到以前提出的问题来,为啥管道只能用于具备共同祖先的进程呢?缘由在于:管道的文件描述符其它进程是没有办法获取,只能经过子进程继承父进程得来了。 经过管道的这些文件描述符,咱们就能够实现进程间的通讯了,好比:子进程往管道的写入端点中写入数据,父进程能够从管道的读端点获取数据,下面就以实际代码来讲明一下父子进程的数据传递:api
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int pipefd[2];
if (pipe(pipefd) == -1)//建立一个管道以后,就会获得两个文件描述符
ERR_EXIT("pipe error");
pid_t pid;
pid = fork();//建立父子进程来演示数据通信的目的
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0)
{//子进程发送数据
close(pipefd[0]);//关闭子进程管道的读端,由于没有用
write(pipefd[1], "hello", 5);//向子进程管道的写端传入数据
close(pipefd[1]);//传递完以后将其关掉
exit(EXIT_SUCCESS);
}
//父进程读取数据
close(pipefd[1]);//关闭父进程的写端,由于没有用
char buf[10] = {0};
read(pipefd[0], buf, 10);//从管道的读端读入数据
close(pipefd[0]);//关闭管道的读端
printf("buf=%s\n", buf);
return 0;
}
复制代码
编译运行:bash
这个经过管道达到进程间传递数据的例子比较简单,下面用程序来模拟下面的这个shell命令的效果:函数
咱们能够用子进程来运行ls,父进程来运行wc -w命令,具体代码以下:学习
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int pipefd[2];
if (pipe(pipefd) == -1)
ERR_EXIT("pipe error");
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0)
{//子进程运行ls命令,
execlp("ls", "ls", NULL);
fprintf(stderr, "error execute ls\n");
exit(EXIT_FAILURE);
}
//父进程运行wc -w命令
execlp("wc", "wc", "-w", NULL);
fprintf(stderr, "error execute wc\n");//若是执行execlp运行失败了,才会执行到这
exit(EXIT_FAILURE);
}
复制代码
第二步,重定向文件描述符,这是实现的关键:ui
由于ls命令标准是输出到标准输出设备(屏幕)当中,wc命令是从标准输入设备获取数据,而如今,咱们但愿ls命令输出到管道的写端,而wc命令是从管道的读端获取数据,那该怎么办呢?文件描述符的复制既可达到这个目的,具体代码以下:spa
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int pipefd[2];
if (pipe(pipefd) == -1)
ERR_EXIT("pipe error");
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0)
{//子进程运行ls命令,
dup2(pipefd[1], STDOUT_FILENO);//将标准输出复制到管道的写端,也就是说标准输出定位到了管道的写端
close(pipefd[1]);//这时管道的读写端都没用了,将其关闭
close(pipefd[0]);
execlp("ls", "ls", NULL);//这时ls输出则为管道的写端了,因为文件描述符重定向了
fprintf(stderr, "error execute ls\n");
exit(EXIT_FAILURE);
}
//父进程运行wc -w命令
dup2(pipefd[0], STDIN_FILENO);//将标准输入重定向管道的读端,因此wc命令这时就会从管道的读端来获取数据喽
close(pipefd[0]);
close(pipefd[1]);
execlp("wc", "wc", "-w", NULL);
fprintf(stderr, "error execute wc\n");//若是执行execlp运行失败了,才会执行到这
exit(EXIT_FAILURE);
}
复制代码
编译运行:线程
下面再来看一个有关文件描述符复制的程序,先看效果,再来分析其原理:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
close(0);
open("Makefile", O_RDONLY);
close(1);
open("Makefile2", O_WRONLY | O_CREAT | O_TRUNC, 0644);
execlp("cat", "cat", NULL);
return 0;
}
复制代码
编译运行:
为啥能实现文件的拷贝效果呢?我们来分析一下程序:
默认状况下:
而下面这句代码事后:
close(0);
open("Makefile", O_RDONLY);
close(1);
open("Makefile2", O_WRONLY | O_CREAT | O_TRUNC, 0644);
而最后一句关键代码以下:
execlp("cat", "cat", NULL);
咱们一般用cat能够查看一个文件内容:
可是,若是cat不带参数,那是什么意思呢?
如效果所示,不带参数的cat命令其实是从标准输入获取数据,写入到标准输出当中,因此也就是从Makefile文件获取数据,写入到Makefile2文件当中,若是Makefile2文件不存在则会主动建立一个,因此就实现了一个cp命令喽,是否是颇有技巧。
对于管道,有必定的读写规则,因此这里主要是对它的规则进行探讨,具体规则以下:
下面用程序来验证下,仍是用上节学的子进程写数据,父进程读取数据的例子,只是基于这个程序进行修改来解释上面的理论,先看一下这个原程序:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int pipefd[2];
if (pipe(pipefd) == -1)
ERR_EXIT("pipe error");
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0)
{
close(pipefd[0]);
write(pipefd[1], "hello", 5);
close(pipefd[1]);
exit(EXIT_SUCCESS);
}
close(pipefd[1]);
char buf[10] = {0};
int ret = read(pipefd[0], buf, 10);
if (ret == -1)
ERR_EXIT("read error");
printf("buf=%s\n", buf);
return 0;
}
复制代码
编译运行:
先来验证第一条理论,"O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止",为了看到效果,咱们在子进程里写数据以前休眠3秒中,这样父进程在读取数据时就是一个没有数据状态,以下:
编译运行:
从运行效果来看,在没有发送数据以前的3秒,父进程读取状态是阻塞的,直到子进程写了数据,这是默认的行为。
下面,再来验证“O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN”这条理论,因此咱们将read的文件描述符设置为非阻塞模式(O_NONBLOCK),以下:
编译运行:
可见,在非阻塞模式下,read时会错误提示。
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int pipefd[2];
if (pipe(pipefd) == -1)
ERR_EXIT("pipe error");
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0)
{
close(pipefd[1]);//子进程关闭了管道对应的写端
exit(EXIT_SUCCESS);
}
close(pipefd[1]);//父进程关闭了管道对应的写端
sleep(1);
char buf[10] = {0};
int ret = read(pipefd[0], buf, 10);//这时看一下读端返回值
printf("ret = %d\n", ret);
return 0;
}
复制代码
编译运行:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int pipefd[2];
if (pipe(pipefd) == -1)
ERR_EXIT("pipe error");
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0)
{
close(pipefd[0]);//子进程关闭读端
exit(EXIT_SUCCESS);
}
close(pipefd[0]);//父进程也关闭读端
sleep(1);//休眠一秒也就是为了让子进程代码执行了close操做
int ret = write(pipefd[1], "hello", 5);//看下是否会产生SIGPIPE,而默认它的行为就是终止当前进程
if (ret == -1)
printf("write error\n");
return 0;
}
复制代码
编译运行:
从运行结果来看,确实wirte error没有执行到,是由于write操做产生了SIGPIPE信号,从这个运行结果可能不是很确实就是产生了这个信号,那咱们改变一下SIGPIPE的默认行为,就能知道了,修改代码以下:
编译运行:
先来验证第一条理论,"O_NONBLOCK disable: write调用阻塞,直到有进程读走数据",实验原理很简单,就是不断往管道里面写东西,由于默认就是阻塞模式,因此看一下当管道满的时候,是否阻塞了,具体代码以下:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int pipefd[2];
if (pipe(pipefd) == -1)
ERR_EXIT("pipe error");
int ret;
int count = 0;//计划一下管道的大小,每写一个字符进行累加
while (1)
{//不断往管道里面写东西
ret = write(pipefd[1], "A", 1);
if (ret == -1)
{
printf("err=%s\n", strerror(errno));
break;
}
count++;
}
printf("count=%d\n", count);
return 0;
}
复制代码
编译运行:
从运行结果来看,确实是阻塞了,咱们打印管道大小的语句也没打印出来,论证了第一条观点,接下来来论证第二个观点:"O_NONBLOCK enable:调用返回-1,errno值为EAGAIN",实验原理就是将管道的写端描述符改为非阻塞模式,看下此次还会阻塞么?若是不阻塞了,那管道的最大容量是多少呢?具体代码以下:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int pipefd[2];
if (pipe(pipefd) == -1)
ERR_EXIT("pipe error");
int ret;
int count = 0;//计划一下管道的大小,每写一个字符进行累加
int flags = fcntl(pipefd[1], F_GETFL);
fcntl(pipefd[1], F_SETFL, flags | O_NONBLOCK);//将写端改成非阻塞模式
while (1)
{//不断往管道里面写东西
ret = write(pipefd[1], "A", 1);
if (ret == -1)
{
printf("err=%s\n", strerror(errno));
break;
}
count++;
}
printf("count=%d\n", count);
return 0;
}
复制代码
编译运行:
此次,能够清晰地看到,当管道写满时是不会阻塞的,且返回了错误码为EAGAIN,并且能够看到管道的最大容量为65536个字符,也就是64K的容量,实际上,关于这个,能够在man帮助中查找到:
最后一个规则:
【说明】:上图中提示的"写入原子性"是指:若是写入的数据量不大于PIPE_BUF,假若有两个进程同时往管道中写入数据,意味着第一个进程写入的数据是连续的,也就是中途不会插入第二个进程写入的数据,有点相似于线程的同步机制,同理不保证写入的原子性也就明白了。另外PIPE_BUF的大小是多少呢?我们先来打印一下它的值:
运行:
关于这个规则要难理解一些,没事,下面会用实例代码来一一验证上面的观点:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
#define TEST_SIZE 68*1024//定义一个68K大小的缓冲区,以便撑爆管道的PIP_BUF大小的容量来看其是否保证原子性
int main(void)
{
char a[TEST_SIZE];
char b[TEST_SIZE];//定义了两个缓冲区,都是68K的大小
memset(a, 'A', sizeof(a));//将a缓冲区内容都初使化为A
memset(b, 'B', sizeof(b));//将b缓冲区内容都初使化为A
int pipefd[2];
int ret = pipe(pipefd);//建立一个管道
if (ret == -1)
ERR_EXIT("pipe error");
pid_t pid;
pid = fork();
if (pid == 0)
{//第一个子进程
close(pipefd[0]);
ret = write(pipefd[1], a, sizeof(a));//往管道中写入a缓冲区,看一下这个数据是连续的么?仍是被下面第二个进程的数据给穿插了
printf("apid=%d write %d bytes to pipe\n", getpid(), ret);
exit(0);
}
pid = fork();
if (pid == 0)
{
//第二个子进程式,代码跟第一个子进程的相似
close(pipefd[0]);
ret = write(pipefd[1], b, sizeof(b));
printf("bpid=%d write %d bytes to pipe\n", getpid(), ret);
exit(0);
}
close(pipefd[1]);//父进程,关闭写端
sleep(1);//休眠一秒的做用是为了让子进程都write数据至管道了
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);//将子进程写入管道的数据读到test.txt文件中进行查看
char buf[1024*4] = {0};
int n = 1;
while (1)
{
ret = read(pipefd[0], buf, sizeof(buf));//每次从管道中读取4个字节,以便进行观察原子性
if (ret == 0)
break;
printf("n=%02d pid=%d read %d bytes from pipe buf[4095]=%c\n", n++, getpid(), ret, buf[4095]);
write(fd, buf, ret);//而后再写入到文件中
}
return 0;
}
复制代码
编译运行:
再次运行:
关于小于PIPE_BUF这时就不演示了,这种状况确定是能保证原子性的,关于这些规则,实际上在man帮助里面均可以看到:
【注意】:管道的容量以前咱们已经验证过了是65536,而PIPE_BUF的大小为4096,也就是说这二者是不划等号的,须要注意。
因此说,咱们要知道命名管道的做用,能够进行毫无关系的两个进程间进行通信,这是匿名管道所没法实现的。
下面来用命令建立一下:
用程序来建立:
另外管道文件是一种特珠类型的文件,因此不能用vim去像文本文件去编辑
也能够能过man帮助来查看到:
下面用一个实际的例子来讲明下:
编译运行:
能够看到,此时运行已经被阻塞了,这时,咱们来写一个往有名管道中写数据的程序,看看是否能解除阻塞?
这时,两个程序都来运行,先运行读操做的,再运行写操做的,看效果:
当有写进程打开该管道时,那么读进程就会由阻塞返回,也就论证了“O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO”。
若是是非阻塞模式呢?下面来看下:
编译运行:
可见此次并无阻塞,而是直接打开成功了,就论证了:“O_NONBLOCK enable:马上返回成功 ”。下面来看一下写操做的规则:
对于第一条是跟读操做相关的,代码不用变,只是先运行写操做程序,再运行读操做程序: 读:
int main(int argc, char *argv[])
{
int fd;
fd = open("p1", O_RDONLY);
if (fd == -1)
ERR_EXIT("open error");
printf("open succ\n");
return 0;
}
复制代码
写:
int main(int argc, char *argv[])
{
int fd;
fd = open("p1", O_WRONLY);
if (fd == -1)
ERR_EXIT("open error");
printf("open succ\n");
return 0;
}
复制代码
编译运行:
效果跟先运行读操做同样,这就论证了第一条:“O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO”。下面来看一下非阻塞的状况:
编译运行:
因此就论证了:“O_NONBLOCK enable:马上返回失败,错误码为ENXIO”。
以上就对有名管道的打开规则进行了说明,下面以一个实例的例子来加深对有名管道用法的认识。由于有名管道是能够不相关的两个进程之间传递数据,因此下面的这个例子是一个进程往管道中写入文件Makefile,而后另一个进程从管道中读取Makefile并写入到Makefile2,也就变向的进行了文件的拷贝操做,具体代码以下:
写文件代码:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
mkfifo("tp", 0644);//建立一个管道文件
int infd;
infd = open("Makefile", O_RDONLY);//打开Makefile文件
if (infd == -1)
ERR_EXIT("open");
int outfd;
outfd = open("tp", O_WRONLY);//以写的方式打开管道,准备往里面写数据
if (outfd == -1)
ERR_EXIT("open");
char buf[1024];
int n;
while ((n=read(infd, buf, 1024))>0)//将Makefile文件的内容写入到有名管道中
{
write(outfd, buf, n);
}
close(infd);
close(outfd);
return 0;
}
复制代码
读文件并建立文件代码:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/time.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
int outfd;
outfd = open("Makefile2", O_WRONLY | O_CREAT | O_TRUNC, 0644);//本地建立一个Makefile2文件
if (outfd == -1)
ERR_EXIT("open");
int infd;
infd = open("tp", O_RDONLY);//以只读的方式打开本地有名管道
if (outfd == -1)
ERR_EXIT("open");
char buf[1024];
int n;
while ((n=read(infd, buf, 1024))>0)//将管道中的数据写入到新建立的Mkaefile2文件以变向实现了文件的拷贝操做
{
write(outfd, buf, n);
}
close(infd);
close(outfd);
unlink("tp");//删除建立的管道文件
return 0;
}
复制代码
先运行写端:
再运行读端,将管道中的文件读入到新的一个文件:
这时,来查看下结果:
而且能够看到,建立的tp临时管道也被删除了,因此经过有名命道就实现了一个数据拷贝的功能。