[TOC]
# 簡介
**Linux 操作系統支持的主要進程間通信的通信機制:**

# 無名管道
管道也叫無名管道,它是是 UNIX 系統 IPC(進程間通信) 的最古老形式,所有的 UNIX 系統都支持這種通信機制。
**管道有如下特點:**
1) 半雙工,數據在同一時刻只能在一個方向上流動。
2) 數據只能從管道的一端寫入,從另一端讀出。
3) 寫入管道中的數據遵循先入先出的規則。
4) 管道所傳送的數據是無格式的,這要求管道的讀出方與寫入方必須事先約定好數據的格式,如多少字節算一個消息等。
5) 管道不是普通的文件,不屬于某個文件系統,其只存在于**內存**中。
6) 管道在內存中對應一個緩沖區。不同的系統其大小不一定相同。
7) 從管道讀數據是一次性操作,數據一旦被讀走,它就從管道中被拋棄,釋放空間以便寫更多的數據。
8) 管道沒有名字,只能在具有公共祖先的進程(父進程與子進程,或者兩個兄弟進程,具有親緣關系)之間使用。
對于管道特點的理解,我們可以類比現實生活中管子,管子的一端塞東西,管子的另一端取東西。
管道是一種特殊類型的文件,在應用層體現為兩個打開的文件描述符。

## 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
~~~
下面我們寫這個一個例子,子進程通過無名管道給父進程傳遞一個字符串數據:
~~~
int main()
{
int fd_pipe[2] = { 0 };
pid_t pid;
?
if (pipe(fd_pipe) < 0)
{// 創建管道
perror("pipe");
}
?
pid = fork(); // 創建進程
if (pid == 0)
{ // 子進程
char buf[] = "I am mike";
// 往管道寫端寫數據
write(fd_pipe[1], buf, strlen(buf));
?
_exit(0);
}
else if (pid > 0)
{// 父進程
wait(NULL); // 等待子進程結束,回收其資源
char str[50] = { 0 };
?
// 從管道里讀數據
read(fd_pipe[0], str, sizeof(str));
?
printf("str=[%s]\n", str); // 打印數據
}
?
return 0;
}
~~~
## 讀寫特點
使用管道需要注意以下4種特殊情況(假設都是阻塞I/O操作,沒有設置O\_NONBLOCK標志):
1) 如果所有指向管道寫端的文件描述符都關閉了(管道寫端引用計數為0),而仍然有進程從管道的讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會返回0,就像讀到文件末尾一樣。
2) 如果有指向管道寫端的文件描述符沒關閉(管道寫端引用計數大于0),而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那么管道中剩余的數據都被讀取后,再次read會阻塞,直到管道中有數據可讀了才讀取數據并返回。
3) 如果所有指向管道讀端的文件描述符都關閉了(管道讀端引用計數為0),這時有進程向管道的寫端write,那么該進程會收到信號SIGPIPE,通常會導致進程異常終止。當然也可以對SIGPIPE信號實施捕捉,不終止進程。具體方法信號章節詳細介紹。
4) 如果有指向管道讀端的文件描述符沒關閉(管道讀端引用計數大于0),而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那么在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入數據并返回。
**總結:**
**讀管道:**
? 管道中有數據,read返回實際讀到的字節數。
? 管道中無數據:
u 管道寫端被全部關閉,read返回0 (相當于讀到文件結尾)
u 寫端沒有全部被關閉,read阻塞等待(不久的將來可能有數據遞達,此時會讓出cpu)
**寫管道:**
? 管道讀端全部被關閉, 進程異常終止(也可使用捕捉SIGPIPE信號,使進程終止)
? 管道讀端沒有全部關閉:
u 管道已滿,write阻塞。
u 管道未滿,write將數據寫入,并返回實際寫入的字節數。
## 設置非阻塞方法
~~~
//獲取原來的flags
int flags = fcntl(fd[0], F_GETFL);
// 設置新的flags
flag |= O_NONBLOCK;
// flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);
~~~
結論: 如果寫端沒有關閉,讀端設置為非阻塞, 如果沒有數據,直接返回-1。
## 查看管道緩沖區命令
可以使用ulimit -a 命令來查看當前系統中創建管道文件所對應的內核緩沖區大小。

## 查看管道緩沖區函數
~~~
#include <unistd.h>
?
long fpathconf(int fd, int name);
功能:該函數可以通過name參數查看不同的屬性值
參數:
fd:文件描述符
name:
_PC_PIPE_BUF,查看管道緩沖區大小
_PC_NAME_MAX,文件名字字節數的上限
返回值:
成功:根據name返回的值的意義也不同。
失敗: -1
~~~
~~~
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret == -1)
{
perror("pipe error");
exit(1);
}
?
long num = fpathconf(fd[0], _PC_PIPE_BUF);
printf("num = %ld\n", num);
?
return 0;
}
~~~
# 有名管道
管道,由于沒有名字,只能用于親緣關系的進程間通信。為了克服這個缺點,提出了命名管道(FIFO),也叫有名管道、FIFO文件。
命名管道(FIFO)不同于無名管道之處在于它提供了一個路徑名與之關聯,以 FIFO 的文件形式存在于文件系統中,這樣,即使與 FIFO 的創建進程不存在親緣關系的進程,只要可以訪問該路徑,就能夠彼此通過 FIFO 相互通信,因此,通過 FIFO 不相關的進程也能交換數據。
命名管道(FIFO)和無名管道(pipe)有一些特點是相同的,不一樣的地方在于:
1) FIFO 在文件系統中作為一個特殊的文件而存在,但 FIFO 中的內容卻存放在**內存**中。
2) 當使用 FIFO 的進程退出后,FIFO 文件將繼續保存在文件系統中以便以后使用。
3) FIFO 有名字,不相關的進程可以通過打開命名管道進行通信。
## 通過命令創建有名管道

## 通過函數創建有名管道
~~~
#include <sys/types.h>
#include <sys/stat.h>
?
int mkfifo(const char *pathname, mode_t mode);
功能:
命名管道的創建。
參數:
pathname : 普通的路徑名,也就是創建后 FIFO 的名字。
mode : 文件的權限,與打開普通文件的 open() 函數中的 mode 參數相同。(0666)
返回值:
成功:0 狀態碼
失敗:如果文件已經存在,則會出錯且返回 -1。
~~~
## 有名管道讀寫操作
一旦使用mkfifo創建了一個FIFO,就可以使用open打開它,常見的文件I/O函數都可用于fifo。如:close、read、write、unlink等。
FIFO嚴格遵循先進先出(first in first out),對管道及FIFO的讀總是從開始處返回數據,對它們的寫則把數據添加到末尾。**它們不支持諸如lseek()等文件定位操作。**
~~~
//進行1,寫操作
int fd = open("my_fifo", O_WRONLY);
?
char send[100] = "Hello Mike";
write(fd, send, strlen(send));
?
//進程2,讀操作
int fd = open("my_fifo", O_RDONLY);//等著只寫
?
char recv[100] = { 0 };
//讀數據,命名管道沒數據時會阻塞,有數據時就取出來
read(fd, recv, sizeof(recv));
printf("read from my_fifo buf=[%s]\n", recv);
~~~
## 注意事項
1) 一個為只讀而打開一個管道的進程會阻塞直到另外一個進程為只寫打開該管道
2)一個為只寫而打開一個管道的進程會阻塞直到另外一個進程為只讀打開該管道
**讀管道:**
? 管道中有數據,read返回實際讀到的字節數。
? 管道中無數據:
u 管道寫端被全部關閉,read返回0 (相當于讀到文件結尾)
u 寫端沒有全部被關閉,read阻塞等待
**寫管道:**
? 管道讀端全部被關閉, 進程異常終止(也可使用捕捉SIGPIPE信號,使進程終止)
? 管道讀端沒有全部關閉:
u 管道已滿,write阻塞。
u 管道未滿,write將數據寫入,并返回實際寫入的字節數。
# 共享存儲映射
存儲映射I/O (Memory-mapped I/O) 使一個磁盤文件與存儲空間中的一個緩沖區相映射。

于是當從緩沖區中取數據,就相當于讀文件中的相應字節。于此類似,將數據存入緩沖區,則相應的字節就自動寫入文件。這樣,就可在不適用read和write函數的情況下,使用地址(指針)完成I/O操作。
共享內存可以說是最有用的進程間通信方式,也是最快的IPC形式, 因為進程可以直接讀寫內存,而不需要任何數據的拷貝。
## 存儲映射函數
**mmap函數**
~~~
#include <sys/mman.h>
?
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:
一個文件或者其它對象映射進內存
參數:
addr : 指定映射的起始地址, 通常設為NULL, 由系統指定
length:映射到內存的文件長度
prot: 映射區的保護方式, 最常用的 :
a) 讀:PROT_READ
b) 寫:PROT_WRITE
c) 讀寫:PROT_READ | PROT_WRITE
flags: 映射區的特性, 可以是
a) MAP_SHARED : 寫入映射區的數據會復制回文件, 且允許其他映射該文件的進程共享。
b) MAP_PRIVATE : 對映射區的寫入操作會產生一個映射區的復制(copy - on - write), 對此區域所做的修改不會寫回原文件。
fd:由open返回的文件描述符, 代表要映射的文件。
offset:以文件開始處的偏移量, 必須是4k的整數倍, 通常為0, 表示從文件頭開始映射
返回值:
成功:返回創建的映射區首地址
失敗:MAP_FAILED宏
~~~
**關于mmap函數的使用總結:**
1) 第一個參數寫成NULL
2) 第二個參數要映射的文件大小 > 0
3) 第三個參數:PROT\_READ 、PROT\_WRITE
4) 第四個參數:MAP\_SHARED 或者 MAP\_PRIVATE
5) 第五個參數:打開的文件對應的文件描述符
6) 第六個參數:4k的整數倍,通常為0
**munmap函數**
~~~
#include <sys/mman.h>
?
int munmap(void *addr, size_t length);
功能:
釋放內存映射區
參數:
addr:使用mmap函數創建的映射區的首地址
length:映射區的大小
返回值:
成功:0
失敗:-1
~~~
## 注意事項
1) 創建映射區的過程中,隱含著一次對映射文件的讀操作。
2) 當MAP\_SHARED時,要求:映射區的權限應 <=文件打開的權限(出于對映射區的保護)。而MAP\_PRIVATE則無所謂,因為mmap中的權限是對內存的限制。
3) 映射區的釋放與文件關閉無關。只要映射建立成功,文件可以立即關閉。
4) 特別注意,當映射文件大小為0時,不能創建映射區。所以,用于映射的文件必須要有實際大小。mmap使用時常常會出現總線錯誤,通常是由于共享文件存儲空間大小引起的。
5) munmap傳入的地址一定是mmap的返回地址。堅決杜絕指針++操作。
6) 如果文件偏移量必須為4K的整數倍。
7) mmap創建映射區出錯概率非常高,一定要檢查返回值,確保映射區建立成功再進行后續操作
## 共享映射的方式操作文件
~~~
int fd = open("xxx.txt", O_RDWR); //讀寫文件
int len = lseek(fd, 0, SEEK_END); //獲取文件大小
?
//一個文件映射到內存,ptr指向此內存
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
?
close(fd); //關閉文件
?
char buf[4096];
printf("buf = %s\n", (char*)ptr); // 從內存中讀數據,等價于從文件中讀取內容
?
strcpy((char*)ptr, "this is a test");//寫內容
?
int ret = munmap(ptr, len);
if (ret == -1)
{
perror("munmap error");
exit(1);
}
~~~
## 共享映射實現父子進程通信
~~~
int fd = open("xxx.txt", O_RDWR);// 打開一個文件
int len = lseek(fd, 0, SEEK_END);//獲取文件大小
?
// 創建內存映射區
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
close(fd); //關閉文件
?
// 創建子進程
pid_t pid = fork();
if (pid == 0) //子進程
{
sleep(1); //演示,保證父進程先執行
?
// 讀數據
printf("%s\n", (char*)ptr);
}
else if (pid > 0) //父進程
{
// 寫數據
strcpy((char*)ptr, "i am u father!!");
?
// 回收子進程資源
wait(NULL);
}
?
// 釋放內存映射區
int ret = munmap(ptr, len);
if (ret == -1)
{
perror("munmap error");
exit(1);
}
~~~
## 匿名映射實現父子進程通信
通過使用我們發現,使用映射區來完成文件讀寫操作十分方便,父子進程間通信也較容易。但缺陷是,每次創建映射區一定要依賴一個文件才能實現。
通常為了建立映射區要open一個temp文件,創建好了再unlink、close掉,比較麻煩。 可以直接使用匿名映射來代替。
其實Linux系統給我們提供了創建匿名映射區的方法,無需依賴一個文件即可創建映射區。同樣需要借助標志位參數flags來指定。
使用**MAP\_ANONYMOUS (或MAP\_ANON)**。
`int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);`
* 4"隨意舉例,該位置表示映射區大小,可依實際需要填寫。
* MAP\_ANONYMOUS和MAP\_ANON這兩個宏是Linux操作系統特有的宏。在類Unix系統中如無該宏定義,可使用如下兩步來完成匿名映射區的建立。
~~~
// 創建匿名內存映射區
int len = 4096;
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
if (ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
?
// 創建子進程
pid_t pid = fork();
if (pid > 0) //父進程
{
// 寫數據
strcpy((char*)ptr, "hello mike!!");
// 回收
wait(NULL);
}
else if (pid == 0)//子進程
{
sleep(1);
// 讀數據
printf("%s\n", (char*)ptr);
}
?
// 釋放內存映射區
int ret = munmap(ptr, len);
if (ret == -1)
{
perror("munmap error");
exit(1);
}
~~~
- c語言
- 基礎知識
- 變量和常量
- 宏定義和預處理
- 隨機數
- register變量
- errno全局變量
- 靜態變量
- 類型
- 數組
- 類型轉換
- vs中c4996錯誤
- 數據類型和長度
- 二進制數,八進制數和十六進制數
- 位域
- typedef定義類型
- 函數和編譯
- 函數調用慣例
- 函數進棧和出棧
- 函數
- 編譯
- sizeof
- main函數接收參數
- 宏函數
- 目標文件和可執行文件有什么
- 強符號和弱符號
- 什么是鏈接
- 符號
- 強引用和弱引用
- 字符串處理函數
- sscanf
- 查找子字符串
- 字符串指針
- qt
- MFC
- 指針
- 簡介
- 指針詳解
- 案例
- 指針數組
- 偏移量
- 間接賦值
- 易錯點
- 二級指針
- 結構體指針
- 字節對齊
- 函數指針
- 指針例子
- main接收用戶輸入
- 內存布局
- 內存分區
- 空間開辟和釋放
- 堆空間操作字符串
- 內存處理函數
- 內存分頁
- 內存模型
- 棧
- 棧溢出攻擊
- 內存泄露
- 大小端存儲法
- 寄存器
- 結構體
- 共用體
- 枚舉
- 文件操作
- 文件到底是什么
- 文件打開和關閉
- 文件的順序讀寫
- 文件的隨機讀寫
- 文件復制
- FILE和緩沖區
- 文件大小
- 插入,刪除,更改文件內容
- typeid
- 內部鏈接和外部鏈接
- 動態庫
- 調試器
- 調試的概念
- vs調試
- 多文件編程
- extern關鍵字
- 頭文件規范
- 標準庫以及標準頭文件
- 頭文件只包含一次
- static
- 多線程
- 簡介
- 創建線程threads.h
- 創建線程pthread
- gdb
- 簡介
- mac使用gdb
- setjump和longjump
- 零拷貝
- gc
- 調試器原理
- c++
- c++簡介
- c++對c的擴展
- ::作用域運算符
- 名字控制
- cpp對c的增強
- const
- 變量定義數組
- 盡量以const替換#define
- 引用
- 內聯函數
- 函數默認參數
- 函數占位參數
- 函數重載
- extern "C"
- 類和對象
- 類封裝
- 構造和析構
- 深淺拷貝
- explicit關鍵字
- 動態對象創建
- 靜態成員
- 對象模型
- this
- 友元
- 單例
- 繼承
- 多態
- 運算符重載
- 賦值重載
- 指針運算符(*,->)重載
- 前置和后置++
- 左移<<運算符重載
- 函數調用符重載
- 總結
- bool重載
- 模板
- 簡介
- 普通函數和模板函數調用
- 模板的局限性
- 類模板
- 復數的模板類
- 類模板作為參數
- 類模板繼承
- 類模板類內和類外實現
- 類模板和友元函數
- 類模板實現數組
- 類型轉換
- 異常
- 異常基本語法
- 異常的接口聲明
- 異常的棧解旋
- 異常的多態
- 標準異常庫
- 自定義異常
- io
- 流的概念和類庫結構
- 標準io流
- 標準輸入流
- 標準輸出流
- 文件讀寫
- STL
- 簡介
- string容器
- vector容器
- deque容器
- stack容器
- queue容器
- list容器
- set/multiset容器
- map/multimap容器
- pair對組
- 深淺拷貝問題
- 使用時機
- 常用算法
- 函數對象
- 謂詞
- 內建函數對象
- 函數對象適配器
- 空間適配器
- 常用遍歷算法
- 查找算法
- 排序算法
- 拷貝和替換算法
- 算術生成算法
- 集合算法
- gcc
- GDB
- makefile
- visualstudio
- VisualAssistX
- 各種插件
- utf8編碼
- 制作安裝項目
- 編譯模式
- 內存對齊
- 快捷鍵
- 自動補全
- 查看c++類內存布局
- FFmpeg
- ffmpeg架構
- 命令的基本格式
- 分解與復用
- 處理原始數據
- 錄屏和音
- 濾鏡
- 水印
- 音視頻的拼接與裁剪
- 視頻圖片轉換
- 直播
- ffplay
- 常見問題
- 多媒體文件處理
- ffmpeg代碼結構
- 日志系統
- 處理流數據
- linux
- 系統調用
- 常用IO函數
- 文件操作函數
- 文件描述符復制
- 目錄相關操作
- 時間相關函數
- 進程
- valgrind
- 進程通信
- 信號
- 信號產生函數
- 信號集
- 信號捕捉
- SIGCHLD信號
- 不可重入函數和可重入函數
- 進程組
- 會話
- 守護進程
- 線程
- 線程屬性
- 互斥鎖
- 讀寫鎖
- 條件變量
- 信號量
- 網絡
- 分層模型
- 協議格式
- TCP協議
- socket
- socket概念
- 網絡字節序
- ip地址轉換函數
- sockaddr數據結構
- 網絡套接字函數
- socket模型創建流程圖
- socket函數
- bind函數
- listen函數
- accept函數
- connect函數
- C/S模型-TCP
- 出錯處理封裝函數
- 多進程并發服務器
- 多線程并發服務器
- 多路I/O復用服務器
- select
- poll
- epoll
- epoll事件
- epoll例子
- epoll反應堆思想
- udp
- socket IPC(本地套接字domain)
- 其他常用函數
- libevent
- libevent簡介