Unix網絡編程學習筆記_進程間通信IPC之管道通信

一、前言

1、在從事嵌入式軟件的相關工作中,也許會碰到許多編程技巧,比如,進程間的通信就算一種。大家可以用書本上的知識去指導下實踐,反過來也可以用實踐去檢驗書本上知識的正確性(6人還可以更深入地修改、完善參考書上的知識)。當然,筆者只是想整理和鞏固下相關知識,並以項目的實際應用,讓大家好對進程間的通信知識點重要性的定位,都是些基礎知識,大神可以直接略過。在某網絡編程之IPC參考書上,源碼把許多宏和函數的定義都放在某一個文件中,當運行程序時需要依賴某些庫文件;運筆者利用開源的思想,把每個案例所用到宏和函數的定義都提取出來,讓大家最終都能看到標準的系統調用,單獨編譯一個主程序即可運行調試。
2、 首先,介紹下我碰到的實際應用案列。我開發嵌入式設備的需求:看上去有點類似手機的拍照功能,但不同的是運行的是Linux操作系統。首先,ARM的顯存(Frame Buffer 0和Frame Buffer 1)有兩個部分:一個是用來顯示菜單部分的(Qt界面),另一個用來顯示CCD圖像,然後通過ARM 的疊加IP利用Overlay技術(Arm的 硬件模塊,根據手冊直接調用驅動接口即可實現),將兩層進行疊加顯示,大家就可以看到菜單界面的照相機了。
3、 那麼Qt在Linux是一個進程,我的理解即單獨運行的一個main函數,然後另一個進程就是CCD的預覽程序(用到了V4L2顯示架構的標準應用調用、DMA傳輸、線程同步、管道通信等技術)。如果想實現一個拍照工程,那麼就在Qt菜單上進行拍照操作時,然後告知CCD預覽進程並把要拍照的路徑名字傳遞過去,在這裏就用到了FIFO(有名管道),CCD預覽程序就會抓一幀底層的數據,再通過ARM 的JPEG模塊把拿到的一幀數據編碼成圖片。
4、 另外,加入在Qt界面還有其他的大量數據(從其他模塊得到),比如GPS、日期、天氣信息。然後我在拍照時,需要把這些數據添加到CCD照片中(以前聽說只要你上傳你的照片,就能查出你這張照片在何時何地拍出來的,這也沒什麼好驚訝的。照片包括了RGB圖像數據和一些地理、日期的信息字節流數據,不明白的可以去看https://blog.csdn.net/psy6653/article/details/79658144)。這時又要用到進程間的通信,那麼就不是用FIFO了,而是用的共享內存。

二、管道的特性以及開發環境

1、本節主要讓大家瞭解下管道(有名和無名管道),個別函數使用的介紹(fork、watipid ),以及無名管道的演示。後續章節筆者會針對FIFO、鎖和共享內存部分進行分析,我會用結合項目的代碼進行講解,並給大家共享源碼。
無名管道和有名管道(FIFO)在一般的Linux系統中都是半雙工通道,從一邊寫,從另一邊讀取,結構圖如下,
在這裏插入圖片描述
無名管道一般用在有親緣關係的父進程和子進程間(一個main函數中調用fork()創建子進程)的通信,而FIFO一般用於無親緣關係的進程間(Linux操作系統下運行的兩個main函數程序)的通信,FIFO往往是是用得最多的。

以下是有親緣關係的父、子進程利用管道進行通信的結構圖,用兩根管道把它們連接起來,某著名參考書上管道2的數據流好像畫反了。。。
在這裏插入圖片描述
管道1:父進程寫數據(fd1[1]),子進程讀數據(fd1[0]):
管道2:父進程讀數據(fd2[0]),子進程寫數據(fd2[1]):
所以fd1[1]和fd2[0]運行在父進程,fd1[0]和fd2[1]運行在子進程
在這裏插入圖片描述
平臺:X86 PC
系統:LInux 內核3.1.0(Fedora16)
編譯器:X86 gcc
繪圖軟件:Microsoft Office Visio
截圖軟件:FastStone_Capture

三、源碼的分析

以下是管道通信的源代碼
如果想查找某函數的頭文件以及函數功能、參數的詳細說明(標準系統調用),直接在終端用

`man 函數名`

客戶端的源碼程序運行在父進程,從管道1的fd1[1]端寫數據,從管道2的fd2[0]端讀數據,

void client(int readfd, int writefd)
{
        size_t  len;
        ssize_t n;
        char    buff[MAXLINE];

#ifdef RUN_STEP_DEBUG
        printf("[4]從控制檯(stdin)讀入輸入路徑\n");
#endif
		//等待終端輸入文件的絕對路徑
        fgets(buff, MAXLINE, stdin);/*stdin在<stdio.h>頭文件聲明*/
        len = strlen(buff);/* fgets()保證以字符串空(即null,ASCII值爲0)結束*/
        if (buff[len-1] == '\n')
                len--;/* 刪除輸入路徑中的換行符(即‘\n’,ASCII值爲10)*/

#ifdef RUN_STEP_DEBUG
        printf("[7]把路徑寫入進程間管道(fd1[1])\n");
#endif
        write(writefd, buff, len);

#ifdef RUN_STEP_DEBUG
        printf("[8]從進程間管道(fd2[0])讀出內容,並輸出到標準輸出控制檯\n");
#endif
        while ( (n = read(readfd, buff, MAXLINE)) > 0)
                write(STDOUT_FILENO, buff, n);/*STDOUT_FILENO在<unistd.h>頭文件聲明*/
}

客戶端需要用的strlen和open函數,man strlen命令即可查到,查找其他的函數類似,
這裏寫圖片描述
這裏寫圖片描述
服務端的源碼程序運行在子進程,從管道1的fd1[0]端讀數據,從管道2的fd2[1]端寫數據,源碼如下,

void server(int readfd, int writefd){
        int             fd;
        ssize_t n;
        char    buff[MAXLINE+1];
#ifdef RUN_STEP_DEBUG
        printf("[6]從進程間管道(fd1[0])讀入輸入路徑\n");
#endif
        if ( (n = read(readfd, buff, MAXLINE)) == 0)
                buff[n] = '\0';/*路徑以null結束 */

        if ( (fd = open(buff, O_RDONLY)) < 0) {
                #ifdef RUN_STEP_DEBUG
                        printf("[9]如果打開輸入路徑失敗,向管道(fd2[1])寫入提示原因告知客戶端\n");
                #endif
                snprintf(buff + n, sizeof(buff) - n, ": can't open, %s\n",strerror(errno));/*errno在<errno.h>頭文件聲明*/
                n = strlen(buff);
                write(writefd, buff, n);

        } else {
                #ifdef RUN_STEP_DEBUG
                        printf("[9]如果打開輸入路徑成功,向管道(fd2[1])寫入整個文件內容\n");
                #endif
                while ( (n = read(fd, buff, MAXLINE)) > 0)
                        write(writefd, buff, n);
                close(fd);
        }
}

子進程服務端需要用的snprintf()函數需要的頭文件如下,其作用是把數據按照一定的格式放在buff中
這裏寫圖片描述
main函數的源碼如下:

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

#define MAXLINE (4096)
#define RUN_STEP_DEBUG

void    client(int, int), server(int, int); /*客戶端(主進程)和服務端(子進程)函數聲明*/

int main(int argc, char **argv){
        int             fd1[2], fd2[2];
        pid_t   pid;/*pid_t類型在<sys/types.h>頭文件聲明*/

#ifdef RUN_STEP_DEBUG
        printf("[1]創建2個管道\n");
#endif
        pipe(fd1);
        pipe(fd2);

#ifdef RUN_STEP_DEBUG
        printf("[2]創建一個子進程\n");
#endif
        pid = fork();
        if (pid < 0){
                printf("error in fork!");

        }else if ( pid == 0) {
                close(fd1[1]);
                close(fd2[0]);
                #ifdef RUN_STEP_DEBUG
                        printf("[5]子進程執行服務端函數\n");
                #endif
                server(fd1[0], fd2[1]);
                exit(0);
        }else{
                close(fd1[0]);
                close(fd2[1]);

#ifdef RUN_STEP_DEBUG
                printf("[3]執行客戶端函數\n");
#endif
                client(fd2[0], fd1[1]);

                waitpid(pid, NULL, 0);/* 等待子進程結束 */
                exit(0);
        }
}

以下是pipe、fork、waitpid函數需要的頭文件信息。
這裏寫圖片描述
這裏寫圖片描述
其中fork函數的使用方法,
1)在父進程中,fork返回新創建子進程的進程ID;
2)在子進程中,fork返回0;
3)如果出現錯誤,fork返回一個負值;
這裏寫圖片描述
waitpid函數在次是阻塞當前進程,直到子進程結束,接收子進程的結束狀態值。根據某參考書上,在此解釋下waitpid函數的作用,在子進程調用exit(0)終止後(fork的子進程),但父進程仍然在運行,內核會馬上給父進程產生一個SIGCHLD信號,而父進程並沒有捕捉該信號(默認是被忽略的),此時已終止的子進程稱爲殭屍進程。當父進程的client函數從管道讀入最終數據返回,再調用waitpid函數獲取該殭屍進程的狀態。如果父進程沒有調用該函數,那麼子進程將託孤給init進程(操作系統的守護進程,一直都在運行)的孤兒進程,內核將向init進程發送另外一個SIGCHLD信號,讓守護進程取得該終止子進程的終止狀態。

makefile

OBJS=mainpipe.o
%.o:%.c
        gcc -c $< -o $@
all:$(OBJS)
        gcc $(OBJS) -o mainpipe
clean:
        rm *.o mainpipe

四、程序的編譯測試

這裏寫圖片描述
運行失敗的結果
這裏寫圖片描述
運行權限的問題
這裏寫圖片描述
修改文件夾wwww無讀、寫、執行權限;
這裏寫圖片描述

下圖是程序正常運行的結果:
在這裏插入圖片描述
免費分享演示代碼鏈接: https://pan.baidu.com/s/1JmuZGE0MFxC_aCFEHbuWHA 提取碼: dsje
在這裏插入圖片描述
在這裏插入圖片描述

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