# 管道,第 2 部分:管道編程秘密
> 原文:<https://github.com/angrave/SystemProgramming/wiki/Pipes%2C-Part-2%3A-Pipe-programming-secrets>
## 管道陷阱
這是一個完整的例子,不起作用!孩子一次從管道讀取一個字節并將其打印出來 - 但我們從未看到過該消息!你能明白為什么嗎?
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
int main() {
int fd[2];
pipe(fd);
//You must read from fd[0] and write from fd[1]
printf("Reading from %d, writing to %d\n", fd[0], fd[1]);
pid_t p = fork();
if (p > 0) {
/* I have a child therefore I am the parent*/
write(fd[1],"Hi Child!",9);
/*don't forget your child*/
wait(NULL);
} else {
char buf;
int bytesread;
// read one byte at a time.
while ((bytesread = read(fd[0], &buf, 1)) > 0) {
putchar(buf);
}
}
return 0;
}
```
父節點將`H,i,(space),C...!`字節發送到管道中(如果管道已滿,則可能會阻塞)。孩子一次開始讀取一個字節的管道。在上述情況下,子進程將讀取并打印每個字符。但它永遠不會離開 while 循環!當沒有剩下的字符可供讀取時,它只是阻塞并等待更多。
調用`putchar`將字符寫出,但我們從不刷新`stdout`緩沖區。即我們已將消息從一個進程轉移到另一個進程但尚未打印。要查看消息,我們可以刷新緩沖區,例如`fflush(stdout)`(如果輸出到達終端,則為`printf("\n")`)。更好的解決方案還可以通過檢查消息結束標記來退出循環,
```c
while ((bytesread = read(fd[0], &buf, 1)) > 0) {
putchar(buf);
if (buf == '!') break; /* End of message */
}
```
當子進程退出時,消息將被刷新到終端。
## 想使用 printf 和 scanf 的管道?使用 fdopen!
POSIX 文件描述符是簡單的整數 0,1,2,3 ...在 C 庫級別,C 用緩沖區和 printf 和 scanf 等有用的函數包裝它們,因此我們可以輕松地打印或解析整數,字符串等。如果您已經有文件描述符,那么您可以使用`fdopen`將其自己“包裝”到 FILE 指針中:
```c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
char *name="Fred";
int score = 123;
int filedes = open("mydata.txt", "w", O_CREAT, S_IWUSR | S_IRUSR);
FILE *f = fdopen(filedes, "w");
fprintf(f, "Name:%s Score:%d\n", name, score);
fclose(f);
```
對于寫入文件,這是不必要的 - 只需使用與`open`和`fdopen`相同的`fopen`但是對于管道,我們已經有了文件描述符 - 所以這是使用`fdopen`的好時機!
這是一個使用幾乎可以工作的管道的完整示例!你能發現錯誤嗎?提示:父母從不打印任何東西!
```c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main() {
int fh[2];
pipe(fh);
FILE *reader = fdopen(fh[0], "r");
FILE *writer = fdopen(fh[1], "w");
pid_t p = fork();
if (p > 0) {
int score;
fscanf(reader, "Score %d", &score);
printf("The child says the score is %d\n", score);
} else {
fprintf(writer, "Score %d", 10 + 10);
fflush(writer);
}
return 0;
}
```
請注意,一旦子項和父項都退出,(未命名的)管道資源將消失。在上面的例子中,子節點將發送字節,父節點將從管道接收字節。但是,沒有發送行尾字符,因此`fscanf`將繼續詢問字節,因為它正在等待行的結束,即它將永遠等待!修復是為了確保我們發送換行符,以便`fscanf`返回。
```c
change: fprintf(writer, "Score %d", 10 + 10);
to: fprintf(writer, "Score %d\n", 10 + 10);
```
## 那么我們也需要`fflush`嗎?
是的,如果您希望立即將您的字節發送到管道!在本課程開始時,我們假設文件流始終是 _ 行緩沖 _,即每次發送換行符時 C 庫都會刷新其緩沖區。實際上,這僅適用于終端流 - 對于其他文件流,C 庫嘗試通過僅在內部緩沖區已滿或文件關閉時進行刷新來提高性能。
## 我什么時候需要兩個管道?
如果需要異步向子級發送數據和從子級發送數據,則需要兩個管道(每個方向一個)。否則孩子會嘗試讀取自己的父母數據(反之亦然)!
## 關閉管道了
當沒有進程正在偵聽時,進程會收到信號 SIGPIPE!從管道(2)手冊頁 -
```
If all file descriptors referring to the read end of a pipe have been closed,
then a write(2) will cause a SIGPIPE signal to be generated for the calling process.
```
提示:請注意,只有編寫者(不是讀者)才能使用此信號。要通知讀者寫入器正在關閉管道的末尾,您可以編寫自己的特殊字節(例如 0xff)或消息(`"Bye!"`)
這是一個捕捉這個不起作用的信號的例子!你能明白為什么嗎?
```c
#include <stdio.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void no_one_listening(int signal) {
write(1, "No one is listening!\n", 21);
}
int main() {
signal(SIGPIPE, no_one_listening);
int filedes[2];
pipe(filedes);
pid_t child = fork();
if (child > 0) {
/* I must be the parent. Close the listening end of the pipe */
/* I'm not listening anymore!*/
close(filedes[0]);
} else {
/* Child writes messages to the pipe */
write(filedes[1], "One", 3);
sleep(2);
// Will this write generate SIGPIPE ?
write(filedes[1], "Two", 3);
write(1, "Done\n", 5);
}
return 0;
}
```
上面代碼中的錯誤是管道仍有讀卡器!孩子仍然打開管道的第一個文件描述符并記住規范?所有讀者都必須關閉。
在分叉時,_ 通常的做法是 _ 關閉子進程和父進程中每個管道的不必要(未使用)端。例如,父級可能會關閉讀取端,而子級可能會關閉寫入端(反之亦然,如果您有兩個管道)
## 什么填充管道?管道變滿后會發生什么?
當編寫器向管道寫入太多而沒有讀取器讀取任何管道時,管道會被填滿。當管道變滿時,所有寫入都會失敗,直到讀取發生。即使這樣,如果管道剩余一點空間但對整個消息還不夠,寫入可能會部分失敗。
為避免這種情況,通常會做兩件事。要么增加管道的尺寸。或者更常見的是,修復程序設計,以便不斷讀取管道。
## 管道過程安全嗎?
是!管道寫入是原子的,直到管道的大小。這意味著如果兩個進程嘗試寫入同一個管道,則內核具有內部互斥鎖,該管道將鎖定,執行寫入和返回。唯一的問題是管子即將變滿。如果兩個進程正在嘗試寫入并且管道只能滿足部分寫入,那么管道寫入不是原子的 - 請小心!
## 管道的使用壽命
未命名的管道(我們到目前為止看到的那種)存在于內存中(不占用任何磁盤空間),是一種簡單有效的進程間通信(IPC)形式,對流數據和簡單消息很有用。關閉所有進程后,將釋放管道資源。
_unamed_ 管道的替代品是使用`mkfifo`創建的名為管道的 _。_
## 命名管道
## 如何創建命名管道?
從命令行:`mkfifo`從 C:`int mkfifo(const char *pathname, mode_t mode);`
你給它路徑名和操作模式,它就準備好了!命名管道在磁盤上不占用空間。當你有一個命名管道時,操作系統基本上告訴你的是它將創建一個引用命名管道的未命名管道,就是這樣!沒有額外的魔力。這只是為了方便編程,如果進程是在沒有分叉的情況下啟動的(這意味著沒有辦法將文件描述符提供給未命名管道的子進程)
## 為什么我的煙斗掛了?
讀取和寫入掛在命名管道上,直到至少有一個讀取器和一個寫入器為止
```source-shell
1$ mkfifo fifo
1$ echo Hello > fifo
# This will hang until I do this on another terminal or another process
2$ cat fifo
Hello
```
在命名管道上調用任何`open`,內核將阻塞,直到另一個進程調用相反的 open。這意味著,回調調用`open(.., O_RDONLY)`但是阻塞直到 cat 調用`open(.., O_WRONLY)`,然后允許程序繼續。
## 具有命名管道的競爭條件。
以下程序有什么問題?
```c
//Program 1
int main(){
int fd = open("fifo", O_RDWR | O_TRUNC);
write(fd, "Hello!", 6);
close(fd);
return 0;
}
//Program 2
int main() {
char buffer[7];
int fd = open("fifo", O_RDONLY);
read(fd, buffer, 6);
buffer[6] = '\0';
printf("%s\n", buffer);
return 0;
}
```
由于競爭條件,這可能永遠不會打印你好。由于您在第一個進程中在兩個權限下打開了管道,因此您不會等待讀者,因為您告訴操作系統您是讀者!有時它看起來像是有效的,因為代碼的執行看起來像這樣。
| 過程 1 | 過程 2 |
| --- | --- |
| 開放(O_RDWR)&amp;寫() | |
| | 打開(O_RDONLY)&amp;讀() |
| close()&amp;出口() | |
| | print()&amp;出口() |
有時它不會
| Process 1 | Process 2 |
| --- | --- |
| open(O_RDWR) & write() | |
| close() & exit() | (命名管道被銷毀) |
| (無限期阻止) | 開(O_RDONLY) |
- UIUC CS241 系統編程中文講義
- 0. 簡介
- #Informal 詞匯表
- #Piazza:何時以及如何尋求幫助
- 編程技巧,第 1 部分
- 系統編程短篇小說和歌曲
- 1.學習 C
- C 編程,第 1 部分:簡介
- C 編程,第 2 部分:文本輸入和輸出
- C 編程,第 3 部分:常見問題
- C 編程,第 4 部分:字符串和結構
- C 編程,第 5 部分:調試
- C 編程,復習題
- 2.進程
- 進程,第 1 部分:簡介
- 分叉,第 1 部分:簡介
- 分叉,第 2 部分:Fork,Exec,等等
- 進程控制,第 1 部分:使用信號等待宏
- 進程復習題
- 3.內存和分配器
- 內存,第 1 部分:堆內存簡介
- 內存,第 2 部分:實現內存分配器
- 內存,第 3 部分:粉碎堆棧示例
- 內存復習題
- 4.介紹 Pthreads
- Pthreads,第 1 部分:簡介
- Pthreads,第 2 部分:實踐中的用法
- Pthreads,第 3 部分:并行問題(獎金)
- Pthread 復習題
- 5.同步
- 同步,第 1 部分:互斥鎖
- 同步,第 2 部分:計算信號量
- 同步,第 3 部分:使用互斥鎖和信號量
- 同步,第 4 部分:臨界區問題
- 同步,第 5 部分:條件變量
- 同步,第 6 部分:實現障礙
- 同步,第 7 部分:讀者編寫器問題
- 同步,第 8 部分:環形緩沖區示例
- 同步復習題
- 6.死鎖
- 死鎖,第 1 部分:資源分配圖
- 死鎖,第 2 部分:死鎖條件
- 死鎖,第 3 部分:餐飲哲學家
- 死鎖復習題
- 7.進程間通信&amp;調度
- 虛擬內存,第 1 部分:虛擬內存簡介
- 管道,第 1 部分:管道介紹
- 管道,第 2 部分:管道編程秘密
- 文件,第 1 部分:使用文件
- 調度,第 1 部分:調度過程
- 調度,第 2 部分:調度過程:算法
- IPC 復習題
- 8.網絡
- POSIX,第 1 部分:錯誤處理
- 網絡,第 1 部分:簡介
- 網絡,第 2 部分:使用 getaddrinfo
- 網絡,第 3 部分:構建一個簡單的 TCP 客戶端
- 網絡,第 4 部分:構建一個簡單的 TCP 服務器
- 網絡,第 5 部分:關閉端口,重用端口和其他技巧
- 網絡,第 6 部分:創建 UDP 服務器
- 網絡,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:遠程過程調用簡介
- 網絡復習題
- 9.文件系統
- 文件系統,第 1 部分:簡介
- 文件系統,第 2 部分:文件是 inode(其他一切只是數據...)
- 文件系統,第 3 部分:權限
- 文件系統,第 4 部分:使用目錄
- 文件系統,第 5 部分:虛擬文件系統
- 文件系統,第 6 部分:內存映射文件和共享內存
- 文件系統,第 7 部分:可擴展且可靠的文件系統
- 文件系統,第 8 部分:從 Android 設備中刪除預裝的惡意軟件
- 文件系統,第 9 部分:磁盤塊示例
- 文件系統復習題
- 10.信號
- 過程控制,第 1 部分:使用信號等待宏
- 信號,第 2 部分:待處理的信號和信號掩碼
- 信號,第 3 部分:提高信號
- 信號,第 4 部分:信號
- 信號復習題
- 考試練習題
- 考試主題
- C 編程:復習題
- 多線程編程:復習題
- 同步概念:復習題
- 記憶:復習題
- 管道:復習題
- 文件系統:復習題
- 網絡:復習題
- 信號:復習題
- 系統編程笑話