實際上,咱們在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臨時管道也被刪除了,因此經過有名命道就實現了一個數據拷貝的功能。