博客 / 詳情

返回

Linux系統編程--(三)進程間通信

一.進程間通訊

1.1 什麼是進程間通信

我們運行起來的進程,相互之間資源是獨立的,不能在一個進程中直接訪問另一個進程的資源。
但是很多時候不同的進程需要進行信息的交互和狀態的傳遞等,譬如數據傳輸,一個進程需要將它的數據發送給另一個進程,或者多個進程間資源共享,或者一個進程需要控制另一個進程的執行,再或者,一個進程要給另一個進程發送消息等,就需要進程間通信( IPC:Inter Processes Communication )。

1.2 進程間通信的方式

進程間通信的方式有很多,文件、管道、信號、共享內存映射、消息隊列、套接字、命名管道等。這裏説管道pipe,命名管道fifo,共享內存映射。

二.管道PIPE

2.1 管道概述

管道是一種最基本的IPC機制,也稱匿名管道、無名管道,應用於有血緣關係的進程之間,完成數據傳遞。
所有的 UNIX 系統都支持這種通信機制。

管道的本質是一塊內核緩衝區,由兩個文件描述符引用,一個表示讀端,一個表示寫端。
管道是半雙工,規定數據從管道的寫端流入管道,從讀端流出,當兩個進程都終結的時候,管道也自動消失,管道的讀端和寫端默認都是阻塞的。

管道的數據一旦被讀走,便不在管道中存在,不可反覆讀取,數據只能在一個方向上流動,若要實現雙向流動,必須使用兩個管道。

只能在有血緣關係的進程間使用管道

創建管道非常簡單,調用pipe函數即可

2.2 pipe函數

#include <unistd.h>
​
int pipe(int pipefd[2]);
功能:創建無名管道。
​
參數:
    pipefd : 為 int 型數組的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。
    
    當一個管道建立時,它會創建兩個文件描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用於讀管道,而 fd[1] 固定用於寫管道。一般文件 I/O的函數都可以用來操作管道(lseek() 除外)。
​
返回值:
    成功:0
    失敗:-1

代碼示例

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

/* pipe函數演示 */
int main(){
    int fd[2];
    int ret = pipe(fd); //創建管道

    if(ret<0){ 
        //創建管道失敗
        perror("pipe");
        return -1;
    }

    pid_t pid = fork();
    if(pid < 0){
        // 創建進程失敗
        perror("fork");
        return -1;
    }else if(pid > 0){
        // 父進程 
        close(fd[0]);
        // 父進程寫入fd[1]
        write(fd[1],"hello",strlen("hello"));
        wait(NULL);

    }else {
        // 子進程讀取
        char buf[128];
        memset(buf,0,sizeof(buf));
        int n = read(fd[0],buf,sizeof(buf));
        printf("%s\n",buf);
    }
}

2.3 管道的讀寫行為注意事項

這裏假定都是阻塞I/O操作,我們使用管道需要注意以下4種特殊情況:
1) 如果所有指向管道寫端的文件描述符都關閉了,而仍然有進程從管道的讀端讀數據,那麼管道中剩餘的數據都被讀取後,再次read會返回0,就像讀到文件末尾一樣。

2) 如果有指向管道寫端的文件描述符沒關閉,而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那麼管道中剩餘的數據都被讀取後,再次read會阻塞,直到管道中有數據可讀了才讀取數據並返回。

3) 如果所有指向管道讀端的文件描述符都關閉了,這時有進程向管道的寫端write,那麼該進程會收到信號SIGPIPE,通常會導致進程異常終止。當然也可以對SIGPIPE信號實施捕捉,不終止進程。具體方法信號章節詳細介紹。

4) 如果有指向管道讀端的文件描述符沒關閉,而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那麼在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入數據並返回。

默認管道的讀寫兩端為阻塞的IO操作,但也可以設置為非阻塞,方法也很簡單,就是使用fcntl函數,設置O_NONBLOCK標誌標誌。

//獲取原來的flags
int flags = fcntl(fd[0], F_GETFL);
// 設置新的flags
flag |= O_NONBLOCK;
// flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);

2.4 查看緩衝區

管道本質是一個內核緩衝區,那麼如何查看緩衝區大小呢?
1)ulimit -a

(base) zhaow@zhaow-610:~$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 62944
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8 // 管道緩衝區大小
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 62944
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

2)fpathconf函數

#include <unistd.h>
​
long fpathconf(int fd, int name);
功能:該函數可以通過name參數查看不同的屬性值
參數:
    fd:文件描述符
    name:
        _PC_PIPE_BUF,查看管道緩衝區大小
        _PC_NAME_MAX,文件名字字節數的上限
返回值:
    成功:根據name返回的值的意義也不同。
    失敗: -1

示例

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

int main(){
    int fd[2];
    int ret = pipe(fd);
    if(ret <0){
        perror("pipe");
        return -1;
    }
    printf("pipe read size==[%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF));
    printf("pipe write size==[%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF));

    return 0;

}

三.命名管道(FIFO)

3.1 概述

管道(pipe)只能用於“有血緣關係”的進程間通信,為了彌補這個缺陷,提出了命名管道(FIFO),也叫有名管道、FIFO文件。

命名管道(FIFO)提供了一個路徑名與之關聯,以 FIFO 的文件形式存在於文件系統中,這樣即使與 FIFO 的創建進程不存在親緣關係的進程,只要可以訪問該路徑,就能夠彼此通過 FIFO 相互通信,因此,通過 FIFO 不相關的進程也能交換數據。

FIFO是Linux基礎文件類型中的一種(文件類型為p,可通過ls -l查看文件類型)。
FIFO文件在磁盤上沒有數據塊,僅僅用來標識內核中一條通道,進程打開這個文件進行read/write,實際上是在讀寫內核緩衝區。

3.2 創建fifo

1)命令mkfifo

(base) zhaow@zhaow-610:demo$ mkfifo myfifo
(base) zhaow@zhaow-610:demo$ ls -l
總用量 0
prw-rw-r-- 1 zhaow zhaow 0 8月  21 02:42 myfifo  //文件類型 p

2)函數mkfifo

#include <sys/types.h>
#include <sys/stat.h>
​
int mkfifo(const char *pathname, mode_t mode);
功能:
    命名管道的創建。
參數:
    pathname : 普通的路徑名,也就是創建後 FIFO 的名字。
    mode : 文件的權限,與打開普通文件的 open() 函數中的 mode 參數相同。(0666)
返回值:
    成功:0   狀態碼
    失敗:如果文件已經存在,則會出錯且返回 -1。

3.3 代碼示例

進程A 寫入

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

int main()
{
    //創建fifo文件
    // 1.檢查是否存在,不存在則創建
    int ret = access("./myfifo", F_OK);
    if(ret!=0)
    {        
        ret = mkfifo("./myfifo", 0777);
        if(ret<0)
        {
            perror("mkfifo error");
            return -1;
        }
    }

    //打開文件
    int fd = open("./myfifo", O_RDWR);
    if(fd<0)
    {
        perror("open error");
        return -1;
    }

    //寫fifo文件
    int i = 0;
    char buf[64];
    while(1)
    {
        memset(buf, 0x00, sizeof(buf));
        sprintf(buf, "%d:%s", i, "hello world");
        write(fd, buf, strlen(buf));
        sleep(1);

        i++;
    }

    
    close(fd);


    return 0;
}

進程B 讀取

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


int main(){
    // 1.檢查要創建的fifo文件是否存在,若不存在則創建
    int ret = access("./myfifo",F_OK);
    if(ret != 0){
        ret = mkfifo("./myfifo",0777);
        if(ret < 0){
            perror("mkfifo");
            return -1;
        }
    }

    // 打開fifo

    int fd = open("./myfifo",O_RDWR);
    if(fd<0){
        perror("open");
        return -1;
    }

    // 讀取
    int n;
    char buf[128];
    while (1)
    {
        memset(buf,0x00,sizeof(buf));
        n = read(fd,buf,sizeof(buf));
        printf("buf=[%s]\n",buf);
    }
    
    close(fd);

    return 0;
}

四.共享存儲映射

4.1 概述

存儲映射I/O (Memory-mapped I/O) 就是使一個磁盤文件與存儲空間中的一個緩衝區相映射。
從緩衝區中取數據,就如同讀文件中的相應字節;同樣,將數據寫入緩衝區,則會將數據寫入文件。

4.2 存儲映射函數

1)mmap

#include <sys/mman.h>
​
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
1 用途:一個文件或者其它對象映射進內存)
2 參數説明:
    addr :  指定映射的起始地址, 通常設為NULL, 由系統指定
    length:映射到內存的文件長度
    prot:  映射區的保護方式, 常用的三種
        1) 讀:PROT_READ
        2) 寫:PROT_WRITE
        3) 讀寫:PROT_READ | PROT_WRITE
    flags:  映射區的特性, 可以是
        1) MAP_SHARED : 寫入映射區的數據會複製迴文件, 且允許其他映射該文件的進程共享。
        2) MAP_PRIVATE : 對映射區的寫入操作會產生一個映射區的複製(copy - on - write), 對此區域所做的修改不會寫回原文件。
    fd:由open返回的文件描述符, 代表要映射的文件。
    offset:以文件開始處的偏移量, 必須是4k的整數倍, 通常為0, 表示從文件頭開始映射
返回值:
    成功:返回創建的映射區首地址
    失敗:MAP_FAILED宏
​

2)munmap


#include <sys/mman.h>
​
int munmap(void *addr, size_t length);
功能:
    釋放內存映射區
參數:
    addr:使用mmap函數創建的映射區的首地址
    length:映射區的大小
返回值:
    成功:0
    失敗:-1

代碼示例 父子進程間通信

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

int main(){
    int fd = open("./ts.log",O_RDWR); //當映射文件大小為0時,不能創建映射區,所以,用於映射的文件必須要有實際大小;
    if(fd < 0){
        perror("open");
        return -1;
    }

    int len = lseek(fd,0,SEEK_END);

    void * addr = mmap(NULL,len,PROT_READ |PROT_WRITE,MAP_SHARED,fd,0);

    if(addr == MAP_FAILED){ // mmap創建映射區出錯概率非常高,一定要檢查返回值,確保映射區建立成功再進行後續操作
        perror("mmap");
        return -1;
    }

    close(fd); //映射區的釋放與文件關閉無關,只要映射建立成功,文件可以立即關閉。

    pid_t pid = fork();
    if(pid < 0){
        perror("fork");
        return -1;
    }else if(pid > 0){
        memcpy(addr,"hello",strlen("hello"));
        wait(NULL);
    }else{
        char buf[64];
        memset(buf,0x00,sizeof(buf));
        memcpy(buf,addr,5);
        printf("%s\n",buf);
    }

    return 0;
}

4.3 匿名映射實現父子進程通信

使用映射區來完成文件讀寫操作十分方便,父子進程間通信也較容易,但比較麻煩的是,每次創建映射區一定要依賴一個文件才能實現,為了建立映射區要open一個temp文件,創建好了再unlink、close掉,很麻煩。
Linux系統提供了創建匿名映射區的方法,無需依賴一個文件即可創建映射區。同樣需要藉助標誌位參數flags來指定。

int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
參數説明:
    4"隨意,該位置表示映射區大小,可依實際需要填寫。
    MAP_ANONYMOUS和MAP_ANON這兩個宏是Linux操作系統特有的宏。

代碼示例

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

int main(){
    void * addr = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);
    if(addr==MAP_FAILED)
    {
        perror("mmap");
        return -1;
    }

    pid_t pid  = fork();
    if(pid < 0){
        perror("fork");
        return -1;
    }else if(pid > 0){
        memcpy(addr,"hello",strlen("hello"));
        wait(NULL);
    }else{
        char buf[64];
        memset(buf,0x00,strlen(buf));
        memcpy(buf,addr,5);
        printf("%s\n",buf);
    }

    return 0;
}
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.