linux管道基本概念
管道基本概念及操作
管道是Unix
中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”,下面是一个简单的例子.
ls | wc -l ps –u wbm01|grep “aa”
** 管道的本质:**
固定大小的内核缓冲区
管道限制:
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork
,此后父、子进程之间就可应用该管道。
匿名管道pipe
功能:创建一无名管道
原型:
#include
int pipe(int fd[2]);
参数:
fd:文件描述符数组,其中fd[0]
表示读端, fd[1]
表示写端
返回值:成功返回0
,失败返回错误代码
管道创建后示意图
管道基本编程示例
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
//这个例子中,父进程如果读不到数据会阻塞到read函数那里
int main01(void)
{
int pipefd[2];
pid_t pid;
//创建管道文件,并打开
if (pipe(pipefd) == -1)
{
printf("pipe() err..\n");
return -1;
}
pid = fork();
if (pid == -1)
{
printf("fork err..\n");
return -1;
}
if (pid == 0)
{
close(pipefd[0]); //子进程关闭读管道
write(pipefd[1], "hello hello....", 6);//向管道中写数据
close(pipefd[1]);
printf("child .....quit\n");
}
else if (pid > 0)
{
int len = 0;
char buf[100] = { 0 };
close(pipefd[1]); //父进程关闭写管道
len = read(pipefd[0], buf, 100); //从管道中读数据
printf("len = %d, buf:%s \n", len, buf);
close(pipefd[0]);
}
wait(NULL);
printf("parent ..quit\n");
return 0;
}
//阻塞非阻塞场景
int main02(void)
{
int ret = 0;
int pipefd[2];
ret = pipe(pipefd);
if (ret < 0)
{
perror("ddd");
exit(0);
}
int pid;
pid = fork();
if (pid == -1)
{
perror("dd");
exit(0);
}
if (pid == 0) //子进程
{
close(pipefd[0]);
sleep(5);
write(pipefd[1], "childaaaaaaaaa", 6);
printf("chile quit\n");
close(pipefd[1]);
exit(0);
}
close(pipefd[1]);
char readbuf[1000] = { 0 };
int n = 0;
int flags = fcntl(pipefd[0], F_GETFL);
flags = flags | O_NONBLOCK;
ret = fcntl(pipefd[0], F_SETFL, flags); //用fcntl函数可以设置文件描述符阻塞或者非阻塞
if (ret == -1)
{
printf("fcntl err.\n");
exit(0);
}
n = read(pipefd[0], readbuf, sizeof(readbuf));
if (n < 0)
{
printf("read非阻塞返回。。。。。立马返回");
//errno
}
printf("%s\n", readbuf);
close(pipefd[0]);
wait(NULL);
printf("parent quit\n");
}
//当所有的写端被关闭,此时读的话会返回0
int main(void)
{
int ret = 0;
int pipefd[2];
ret = pipe(pipefd);
if (ret < 0)
{
perror("ddd");
exit(0);
}
int pid;
pid = fork();
if (pid == -1)
{
perror("dd");
exit(0);
}
if (pid == 0) //子进程
{
close(pipefd[0]);
close(pipefd[1]);//写端被关闭
exit(0);
}
close(pipefd[1]); //写端被关闭
char readbuf[1000] = { 0 };
int n = 0;
sleep(3);
n = read(pipefd[0], readbuf, sizeof(readbuf));
printf("所有的写端都关闭,此时 n = %d \n", n);
//当所有的写端被关闭,此时读的话会返回0
printf("%s\n", readbuf);
close(pipefd[0]);
wait(NULL);
printf("parent quit\n");
}
管道和文件描述符在一起
管道命令的编程实现:
ls | wc –w
从下面的这个例子里,我们可以更加深刻地领会|
符号的实现原理。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main21(void)
{
int pipefd[2];
pid_t pid;
if (pipe(pipefd) == -1)
{
printf("pipe() err..\n");
return -1;
}
pid = fork();
if (pid == -1)
{
printf("fork err..\n");
return -1;
}
if (pid == 0)//子进程
{
close(pipefd[0]);
//复制文件描述符pipefd[1],给标准输出,言外之意:execlp的ls命令输出到管道中
dup2(pipefd[1], STDOUT_FILENO);
//这里需要注意一点,那就是复制后,pipefd[1]的应用计数会增加,下面关闭pipefd[1]不会真正关闭该文件
close(pipefd[1]);//关闭写端
execlp("ls", "ls", NULL);
//如果替换新的进程印象失败,则会执行下面一句话
sprintf(stderr, "execute the cmd ls err..\n");
exit(0);
}
else if (pid > 0) //父进程
{
int len = 0;
char buf[100] = { 0 };
close(pipefd[1]);
//复制文件描述符pipefd[0],给标准输入,言外之意:execlp的wc命令从管道中读
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
//len = read(pipefd[0], buf, 100);
execlp("wc", "wc", "-w", NULL);//执行wc命令,相当于从
printf("len:%d, buf:%s \n", len, buf);
//close(pipefd[0]);
}
wait(NULL);
printf("parent ..quit\n");
return 0;
}
int main(int argc, char *argv[])
{
close(0); //关闭标准输入
open("makefile", O_RDONLY); //makefile文件变成标准输入
close(1);//关闭标准输出
open("makefile2", O_WRONLY | O_CREAT | O_TRUNC, 0644); //maifle2变成标准输出
execlp("cat", "cat", NULL); //替换进程印象后,执行cat命令
//cat命令 从标准输入中按行读,紧接着写到标准输出
return 0;
}
dup2
函数:
作用:
dup2
函数使得newfd
成为oldfd
,即将oldfd
的值赋给newfd
。
函数原型:
int dup2(int oldfd, int newfd);
参数:两个都为文件描述符
返回值:如果成功,返回新的描述符,否则返回-1
,设置errno
。
管道的本质是内核的一块缓冲区。
管道的读写规则
当没有数据可读时
O_NONBLOCK disable:read
调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read
调用返回-1
,errno
值为EAGAIN
。
当管道满的时候
O_NONBLOCK disable:write
调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1
,errno
值为EAGAIN
当管道不停的被写,写满的时候
O_NONBLOCK disable:write
调用阻塞
O_NONBLOCK enable:调用返回-1
,errno
值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read
返回0
如果所有管道读端对应的文件描述符被关闭,则write
操作会产生信号SIGPIPE
。也就是所谓的管道破裂,这在socket
编程中很常见。
当要写入的数据量不大于PIPE_BUF
时,linux
将保证写入的原子性。
当要写入的数据量大于PIPE_BUF
时,linux
将不再保证写入的原子性。
下面是一个例子:
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
#define TEST_SIZE 68*1024 //68K
int main(void)
{
char a[TEST_SIZE];
char b[TEST_SIZE];
memset(a, 'A', sizeof(a));
memset(b, 'B', sizeof(b));
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
ERR_EXIT("pipe error");
pid_t pid;
pid = fork();
if (pid == 0) //A子进程写68K数据A
{
close(pipefd[0]);
ret = write(pipefd[1], a, sizeof(a));
printf("apid=%d write %d bytes to pipe\n", getpid(), ret);
exit(0);
}
pid = fork();
if (pid == 0) //B子进程写68K数据B
{
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);
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
char buf[1024 * 4] = { 0 };
int n = 1;
while (1)
{
//父进程4k 4k的读数据,发现AB进程是交叉的写数据到管道。
//多个进程往管道里面,写数据。
ret = read(pipefd[0], buf, sizeof(buf));
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;
}
上面其实是一个很恐怖的东西,我用vim
处理了一下test.txt
我们来看一下这个文件中的数据。我做了如下处理:
每70
个A
为一行,后面加了换行符。每70
个B
为一行,后面加了换行符。
AB
之间加入换行符。
A
一直持续到了937行!
我们计算一下:936 * 70 + 16 = 65536
,一个字符一个字节,恰好64K
。
B
写入了(995 – 936 + 1) * 70 + 36 = 4236
,后面的写入就有一些乱了。
后面全部是B
。
从上面的结果可以看到,如果写入的数据量很大的话,其实挺乱的,这个东西。