8、管道 - 看這一篇就夠了

八、管道

1、什麼是管道

  • 管道是Unix中最古老的進程間通信的形式。
  • 我們把從一個進程連接到另一個進程的一個數據流稱爲一個"管道"
  • 我們通常把是把一個進程的輸出連接或“管接”(經過管道來連接)到另一個進程的輸入。

2、在shell中使用管道

  • 鏈接shell命令:把一個進程的輸出直接饋入另一個的輸入,命令格式如下

    cmd1 | cmd2

#生成一個8位的隨機密碼
tr -dc A-Za-z0-9_ </dev/urandom | head -c 8 | xargs

#查看系統中所有的用戶名稱,並按字母排序
awk -F: '{print $1}' /etc/passwd | sort

#列出當前用戶使用最多的5個命令(print的列數根據實際情況而定)
history | awk '{print $2}' | sort | uniq -u | sort -rn | head -5

#查看系統中有哪些用戶的登陸shell時/bin/bash
cat /etc/passwd | grep "/bin/bash" | cut -d: -f1,6   
#cut -d: -f1,6 表示以:爲分隔符顯示第1和第6列的內容-d指定分隔符,-f指定列

#查看當前目錄的子目錄個數
ls -l | cut -c 1 | grep "d" | wc -l
#ls -l  長格式列出當前目錄的所有內容,每行的第一個字符表示文件的類
#cut -c 1 截取每行的第一個字符
#grep "d" 獲取文件類型是目錄的行
#wc -l  統計grep命令輸出的行數,即子目錄個數

#合併兩個文件的內容
cat 1.txt | paste -d: 2.txt -
#paste -d: 2.txt - 表示以:爲分割符合並兩個文件,合併時2.txt文件的內容在前 -代表1.txt文件

3、管道特點

  • 管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道
  • 只能用於父子進程或者兄弟進程之間(具有親緣關係的進程)進行通信;通常,一個管道由一個進程創建,然後該進程調用fork,此後父、子進程之間就可應用該管道。

4、pipe函數

  • 功能:創建一無名管道
  • 原型:
#include <unistd.h>

int pipe(int pipefd[2]);
  • 參數:

    • pipefd[0]:表示讀端
    • pipefd[1]:表示寫端
  • 返回值:成功返回0,失敗返回錯誤代碼

示例

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

int main()
{
    int pipefd[2];
    char buf[20];
    if(pipe(pipefd) == -1){
        printf("error\n");
    }
    int fd = fork();
    if(fd == 0){
        read(pipefd[0],buf,20);
        printf("%s\n",buf);
        close(pipefd[0]);
        close(pipefd[1]);
    }else if(fd > 0){
        write(pipefd[1],"hello world",20);
        close(pipefd[0]);
        close(pipefd[1]);
    }else{
        printf("error\n");
    }
    return 0;
}

5、管道讀寫規則

  • 如果試圖從管道寫端讀取數據,或者向管道讀端寫入數據都將導致錯誤發生

  • 當沒有數據可讀時,read調用就會阻塞,即進程暫停執行,一直等到有數據來到爲止。

  • 如果管道的另一端已經被關閉,也就是沒有進程打開這個管道並向它寫數據時,read調用就會阻塞

  • 如果管道的寫端不存在,則認爲已經讀到了數據的末尾,讀函數返回的讀出字節數爲0;

  • 當管道的寫端存在時,如果請求的字節數目大於PIPE_BUF,則返回管道中現有的數據字節數

  • 向管道中寫入數據時,linux將不保證寫入的原子性,管道緩衝區一有空閒區域,寫進程就會試圖向管道寫入數據。如果讀進程不讀走管道緩衝區中的數據,那麼寫操作將一直阻塞。

6、複製文件描述符dup、dup2 、fcntl

  • 複製文件描述符可以有三種辦法:
    • dup、dup2 、fcntl
  • dup系統調用從頭開始搜文件描述符數組,並且在找到第一個空閒文件描述符時完成它的複製。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd,int newfd);//dup2系統調用複製操作之前,如果newfd已被打開,先關閉。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KcFI94rg-1589279199747)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200512001427990.png)]

fcntl()函數

  • 功能:實現對文件描述符的多種控制
  • 原型:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int oldfd,F_DUPFD,int minifd);
  • 參數:

    • minifd:從指定的minifd開始搜索空閒文件描述符,找到時,複製oldfd,返回值爲新的文件描述符。

    • oldfd:要被複制的文件描述符

說明:另外fcntl還有其他的作用。

dup示例

//實現cat /etc/passwd | wc -l

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int pipefd[2];
    if(pipe(pipefd) == -1){
        fprintf(stderr,"error creating pipe\n");
        exit(1);
    }
    int fd = fork();
    if(fd == 0){
        close(1);//關閉標準輸出
        dup(pipefd[1]);
        close(pipefd[1]);
        close(pipefd[0]);
        execlp("cat","cat","/etc/passwd",0);
        fprintf(stderr,"error trying to exec ls\n");
		sleep(1);
        exit(1);
    }else if(fd > 0){
        close(0);//關閉標準輸入
        dup(pipefd[0]);
        close(pipefd[0]);
        close(pipefd[1]);
        execlp("wc","wc","-l",0);
        fprintf(stderr,"error trying to exec wc\n");
		exit(1);
    }else{
        fprintf(stderr,"error fork\n");
    }
    return 0;
}

7、popen函數

  • 作用:允許一個程序把另外一個程序當作一個新的進程來啓動,並能對它發送數據或接收數據
FILE* popen(const char *command,const char *open_mode);
  • 參數:

    • command:待運行程序的名字和相應的參數
    • open_mode:必須是“r”或“w”
  • 如果操作失敗,popen會返回一個空指針

  • 每個popen調用都必須指定“r”或“w”,在popen的標準實現裏不支持任何其他的選項

  • 如果open_mode是“r”,調用者程序利用popen返回的那個“FILE*”類型的指針用一般的stdio庫函數(比如fread)就可以讀這個文件流

  • 如果open_mode是“w”,調用者程序就可以使用fwrite向被調用命令發送數據

pclose函數

  • 作用:關閉popen打開的與之關聯的文件流
int pclose(FILE *stream_to_close);
  • pclose調用只有在popen啓動的進程結束之後才能返回。
  • 如果在調用pclose的時候它仍然在運行,pclose將等待該進程的結束

示例

//通過popen實現cat /etc/passwd | wc -l

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    FILE *read_fp;
    FILE *write_fp;
    char buffer[BUFSIZ + 1];//BUFSIZ的值等於一個常量值,這個值是8192,一般是會用來做數組的長度。
    int chars_read;
    memset(buffer,0,sizeof(buffer));
    read_fp = popen("cat /etc/passwd","r");
    if(read_fp != NULL){
        chars_read = fread(buffer,sizeof(char),BUFSIZ,read_fp);
        pclose(read_fp);
        if(chars_read > 0){
            write_fp = popen("wc -l","w");
            if(write_fp != NULL){
                fwrite(buffer,sizeof(char),strlen(buffer),write_fp);
				pclose(write_fp);
				exit(EXIT_SUCCESS);
            }
        }
    }
    exit(EXIT_FAILURE);
    return 0;
}

8、命名管道:FIFO文件

  • 管道應用的一個限制就是只能在相關的程序之間進行,這些程序是由一個共同的祖先進程啓動的。
  • 如果我們想在不相關的進程之間交換數據,可以使用FIFO文件來做這項工作,它經常被稱爲命名管道。
  • 命名管道是一種特殊類型的文件

創建一個命名管道

  • 命名管道可以從命令行上創建,推薦的命令行方法是使用下面這個命令:
    $ mkfifo filename
  • 命名管道也可以從程序裏創建,相關函數有:
int mkfifo(const char *filename,mode_t mode);
int mknod(const char *filename,mode_t mode | S_IFIFO,(dev_t) 0);

用mkfifo創建一個命名管道

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

int main()
{
    int res = mkfifo("/tmp/my_fifo",0777);
    if(res == 0)
        printf("FIFO created\n");
    exit(EXIT_SUCCESS);
    return 0;
}

9、命名管道(FIFO文件)的打開規則

訪問一個FIFO文件

  • FIFO和通過pipe調用創建的管道不同,它是一個有名字的文件而表示一個打開的文件描述符。
  • 在對它進行讀或寫操作之前必須先打開它
  • FIFO文件也要用open和close函數來打開或關閉,除了一些額外的功能外,整個操作過程與我們前面介紹的文件操作是一樣的
  • 傳遞給open調用的是一個FIFO文件的路徑名,而不是一個正常文件的路徑名

用open打開FIFO文件

  • 在打開FIFO文件時需要注意一個問題:即程序不能以O_RDWR模式打開FIFO文件進行讀寫
  • 如果確實需要在程序之間雙向傳遞數據的話,我們可以同時使用一對FIFO或管道,一個方向配一個;還可以用先關閉再重新打開FIFO的辦法明確地改變數據流的方向

命名管道的打開規則

  • 如果當前打開操作是爲讀而打開FIFO時,若已經有相應進程爲寫而打開該FIFO,則當前打開操作將成功返回;否則,可能阻塞直到有相應進程爲寫而打開該FIFO(當前打開操作設置了阻塞標誌,即只設置了O_RDONLY),反之,如果當前打開操作沒有設置了非阻塞標誌,即O_NONBLOCK,則返回成功
  • 如果當前打開操作是爲寫而打開FIFO時,如果已經有相應進程爲讀而打開該FIFO,則當前打開操作將成功返回;否則,可能阻塞直到有相應進程爲讀而打開該FIFO(當前打開操作設置了阻塞標誌);或者,返回ENXIO錯誤(當前打開操作沒有設置阻塞標誌)。

10、命名管道(FIFO文件)讀寫規則

  • 對於沒有設置阻塞標誌的寫操作:
    • 當要寫入的數據量大於PIPE_BUF時,linux將不再保證寫入的原子性。在寫滿所有FIFO空閒緩衝區後,寫操作返回
    • 當要寫入的數據量不大於PIPE_BUF時,linux將保證寫入的原子性。如果當前FIFO空閒緩衝區能夠容納請求寫入的字節數,寫完後成功返回;如果當前FIFO空閒緩衝區不能夠容納請求寫入的字節數,則返回EAGAIN錯誤,提醒以後再寫。

示例

  • 先在終端:mkfifo myfifo
//fifo_r.cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	int fd = open("myfifo",O_RDONLY);
	if(fd == -1){
		perror("open fifo");
		exit(1);
	}
	char buf[1024]={0};
    int fd_r;
	while(1){
        memset(&buf,0,sizeof(buf));
		fd_r = read(fd,&buf,sizeof(buf));
		if(fd_r == -1){
			perror("read fifo");
		}
		printf("read:%s\n",buf);
        sleep(1);
	}
	return 0;
}
//fifo_w.cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#define MYFIFO "./myfifo"

int main()
{
    if(access(MYFIFO,F_OK) == -1){//檢查調用進程是否可以訪問文件路徑名
        int res = mkfifo(MYFIFO,0777);
        if(res < 0)
            exit(EXIT_FAILURE);
    }
	int fd = open(MYFIFO,O_WRONLY);
	if(fd == -1){
		perror("open fifo");
		exit(1);
	}
	char buf[1024]="hello world!";
    int fd_r;
	while(1){
		fd_r = write(fd,&buf,sizeof(buf));
		if(fd_r == -1){
			perror("write fifo");
		}
		printf("write:%s\n",buf);
        sleep(1);
	}
	return 0;
}
,0777);
        if(res < 0)
            exit(EXIT_FAILURE);
    }
	int fd = open(MYFIFO,O_WRONLY);
	if(fd == -1){
		perror("open fifo");
		exit(1);
	}
	char buf[1024]="hello world!";
    int fd_r;
	while(1){
		fd_r = write(fd,&buf,sizeof(buf));
		if(fd_r == -1){
			perror("write fifo");
		}
		printf("write:%s\n",buf);
        sleep(1);
	}
	return 0;
}

專欄 《linux網絡編程》 將持續更新中…
從linux 零基礎 到 高併發服務器架構

如果我的文章能夠幫到您,可以點個贊!
您的每次 點贊、關注、收藏 都是對我最大的鼓勵!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章